Efficient use of OnPaint
I am programming in Visual Studio .Net and using C#.
I am creating my own control that draw开发者_JAVA百科s 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.
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.
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.
I have two points to add:
- 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.
- 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.
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
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.
精彩评论