views:

391

answers:

5

I am programming in Visual Studio .Net and using C#.

I am creating my own control that draws a wave based on values I get from an analog to digital converter (ADC). I take the incoming points and convert them into X and Y points to properly draw the graph in my control.

I have a loop inside my OnPaint method that goes through all the points and calls the DrawLine method between the current point and the next point.

However, this is very inefficient as some of these graphs have 8192 points and the system actually has nine ADCs that I would like to show at the same time. Every time the page redraws it takes almost a second for all graphs to redraw (especially during debug).

On top of that, I have functionality that allows you to zoom in and pan across the waves to get a better view (acts a lot like google maps does) and all 9 waves zoom in and pan together.

All of this functionality is very "jerky" because I am calling invalidate on mousewheel and mousemove. Basically, it all works but not as smoothly as I would like.

I was wondering if there were a way to create a predrawn object from the data and then just draw a dilated and translated version of the picture in the draw area.

Any help would be greatly appreciated even if it is just pointing me in the right direction.

+3  A: 

You might set DoubleBuffered to true on your control / form. Or you could try using your own Image to create a double buffered effect.

My DoubleBufferedGraphics class:

public class DoubleBufferedGraphics : IDisposable
{
    #region Constructor
    public DoubleBufferedGraphics() : this(0, 0) { }

    public DoubleBufferedGraphics(int width, int height)
    {
        Height = height;
        Width = width;
    }
    #endregion

    #region Private Fields
    private Image _MemoryBitmap;
    #endregion

    #region Public Properties
    public Graphics Graphics { get; private set; }

    public int Height { get; private set; }

    public bool Initialized
    {
        get { return (_MemoryBitmap != null); }
    }

    public int Width { get; private set; }
    #endregion

    #region Public Methods
    public void Dispose()
    {
        if (_MemoryBitmap != null)
        {
            _MemoryBitmap.Dispose();
            _MemoryBitmap = null;
        }

        if (Graphics != null)
        {
            Graphics.Dispose();
            Graphics = null;
        }
    }

    public void Initialize(int width, int height)
    {
        if (height > 0 && width > 0)
        {
            if ((height != Height) || (width != Width))
            {
                Height = height;
                Width = width;

                Reset();
            }
        }
    }

    public void Render(Graphics graphics)
    {
        if (_MemoryBitmap != null)
        {
            graphics.DrawImage(_MemoryBitmap, _MemoryBitmap.GetRectangle(), 0, 0, Width, Height, GraphicsUnit.Pixel);
        }
    }

    public void Reset()
    {
        if (_MemoryBitmap != null)
        {
            _MemoryBitmap.Dispose();
            _MemoryBitmap = null;
        }

        if (Graphics != null)
        {
            Graphics.Dispose();
            Graphics = null;
        }

        _MemoryBitmap = new Bitmap(Width, Height);
        Graphics = Graphics.FromImage(_MemoryBitmap);
    }

    /// <summary>
    /// This method is the preferred method of drawing a background image.
    /// It is *MUCH* faster than any of the Graphics.DrawImage() methods.
    /// Warning: The memory image and the <see cref="Graphics"/> object
    /// will be reset after calling this method. This should be your first
    /// drawing operation.
    /// </summary>
    /// <param name="image">The image to draw.</param>
    public void SetBackgroundImage(Image image)
    {
        if (_MemoryBitmap != null)
        {
            _MemoryBitmap.Dispose();
            _MemoryBitmap = null;
        }

        if (Graphics != null)
        {
            Graphics.Dispose();
            Graphics = null;
        }

        _MemoryBitmap = image.Clone() as Image;

        if (_MemoryBitmap != null)
        {
            Graphics = Graphics.FromImage(_MemoryBitmap);
        }
    }
    #endregion
}

Using it in an OnPaint:

protected override void OnPaint(PaintEventArgs e)
{
    if (!_DoubleBufferedGraphics.Initialized)
    {
        _DoubleBufferedGraphics.Initialize(Width, Height);
    }

    _DoubleBufferedGraphics.Graphics.DrawLine(...);

    _DoubleBufferedGraphics.Render(e.Graphics);
}

ControlStyles I generally set if I'm using it (you may have different needs):

SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);

Edit:

Ok since the data is static you should paint to an Image (before your OnPaint) and then in the OnPaint use the Graphics.DrawImage() overload to draw the correct region of your source image to the screen. No reason to redraw the lines if the data isn't changing.

Cory Charlton
Double-buffering doesn't make things redraw faster - it just removes the flicker. What needs to be done here is allowing the data to be panned, scaled etc. without re-rendering it all.
Anon.
According to the OP the data is changing as new data comes from the ADC so a static image isn't the solution either. Double buffering will solve the jerkiness he describes and can result in faster drawing.
Cory Charlton
Cory, the data is not streaming in. Sorry if I was not clear about that.
EatATaco
10-4, see my edit. Using a static image, drawn once, and then drawing only the part of the original image you want should be more efficient. Will eliminate the time it takes to (re-)draw the lines when you pan/zoom.
Cory Charlton
+5  A: 

Create a Bitmap object, and draw to that.

In your Paint handler, just blit the Bitmap to the screen.

That will allow you decouple changing the scale, from re-rendering the data.

Anon.
Good for Panning but how does this work with Zooming (and text labeling at the same time).
Henk Holterman
When you zoom, you can re-render at the new zoom level in the background. The way Google Maps shows you the fuzzy, blown-up version of your old view until it streams in the images at the new zoom level.
Anon.
A: 

You could draw a multiline. I'm not sure what that looks like in C#, but it has to be there (Its a GDI/GDI+ based API). That allows you specify all the points in one go, and allows Windows to optimize the call a bit (less stack push/pop to stay inside of the draw algorithm rather than returning to your code for each new point).

EDIT: but if your data is static, then using a double buffered / cached image of your output is more efficient than worrying about the initial drawing of it.

Here's a link: http://msdn.microsoft.com/en-us/library/system.windows.shapes.polyline.aspx

Mordachai
+1  A: 

I have two points to add:

  1. You say you have 8192 points. Your drawing area probably has no more then 1000. I suppose you could "lower the resolution" of your graph by adding only every 10th or so line.
  2. You could use GraphicsPath class to store all required lines and draw them all at once with Graphics.DrawPath

This way you'll avoid using static bitmap (to allow zooming) while still getting some performance improvements.

Hugo Riley
A: 

Just calculate the visible range and draw only these points. Use double buffering. And finally you can create you own realizaion of multiline drawing using raw bitmap data e.g. use LockBits and write pixel colors directly into bytes forming the picture.Use InvalidateRect(..) to redraw a portion of the window.

alexm