4
votes

I have a User Control with completely custom drawn graphics of many objects which draw themselves (called from OnPaint), with the background being a large bitmap. I have zoom and pan functionality built in, and all the coordinates for the objects which are drawn on the canvas are in bitmap coordinates.

Therefore if my user control is 1000 pixels wide, the bitmap is 1500 pixels wide, and I am zoomed at 200% zoom, then at any given time I would only be looking at 1/3 of the bitmap's width. And an object which has a rectangle starting at point 100,100 on the bitmap, would appear at point 200,200 on the screen provided you were scrolled to the far left.

Basically what I need to do is create an efficient way of redrawing only what needs to be redrawn. For example, if I move an object, I can add the old clip rectangle of that object to a region, and union the new clip rectangle of that object to that same region, then call Invalidate(region) to redraw those two areas.

However doing it this way means I have to constantly convert the objects bitmap coordinates into screen coordinates before supplying them to Invalidate. I have to always assume that the ClipRectangle in PaintEventArgs is in screen coordinates for when other windows invalidate mine.

Is there a way that I can make use of the Region.Transform and Region.Translate capabilities so that I do not need to convert from bitmap to screen coordinates? In a way that it won't interfere with receiving PaintEventArgs in screen coordinates? Should I be using multiple regions or is there a better way to do all this?

Sample code for what I'm doing now:

invalidateRegion.Union(BitmapToScreenRect(SelectedItem.ClipRectangle));

SelectedItem.UpdateEndPoint(endPoint);

invalidateRegion.Union(BitmapToScreenRect(SelectedItem.ClipRectangle));

this.Invalidate(invalidateRegion);

And in the OnPaint()...

protected override void OnPaint(PaintEventArgs e)
{
    invalidateRegion.Union(e.ClipRectangle);

    e.Graphics.SetClip(invalidateRegion, CombineMode.Union);
    e.Graphics.Clear(SystemColors.AppWorkspace);

    e.Graphics.TranslateTransform(AutoScrollPosition.X + CanvasBounds.X, AutoScrollPosition.Y + CanvasBounds.Y);

    DrawCanvas(e.Graphics, _ratio);

    e.Graphics.ResetTransform();

    e.Graphics.ResetClip();

    invalidateRegion.MakeEmpty();
}
1
You are doing work that doesn't need to be done. Windows is already very efficient at clipping, you don't have to help. If you have a perf problem then focus on the pixel format of the bitmap. 32bppPArgb is ten times faster than any others.Hans Passant
I don't think you understand how paint clipping works. I am using the GDI+ to draw objects that can be moved, resized, rotated, etc. I need to redraw on events like MouseMove, and I need to make sure to only redraw the portions that actually need updating. For example if I change an object it will not redraw on its own, I have to call Invalidate(). If I don't specify a clip region to clip to, then it will redraw the entire scene. You don't want to redraw the entire scene on MouseMove events for example.Trevor Elliott
Wow! Telling Hans Passant that he doesn't understand how windows clipping works is like telling Jon Skeet that he doesn't understand how C# works! :-)Dan Byström
Fair enough. I didn't know that Windows presets the e.Graphics.Clip region based on previous invalidations. I thought it gave you the e.ClipRectangle and you had to use that to clip manually. Nevertheless, saying Windows clips for me doesn't tell me how to invalidate efficiently :)Trevor Elliott

1 Answers

10
votes

Since a lot of people are viewing this question I will go ahead and answer it to the best of my current knowledge.

The Graphics class supplied with PaintEventArgs is always hard-clipped by the invalidation request. This is usually done by the operating system, but it can be done by your code.

You can't reset this clip or escape from these clip bounds, but you shouldn't need to. When painting, you generally shouldn't care about how it's being clipped unless you desperately need to maximize performance.

The graphics class uses a stack of containers to apply clipping and transformations. You can extend this stack yourself by using Graphics.BeginContainer and Graphics.EndContainer. Each time you begin a container, any changes you make to the Transform or the Clip are temporary and they are applied after any previous Transform or Clip which was configured before the BeginContainer. So essentially, when you get an OnPaint event it has already been clipped and you are in a new container so you can't see the clip (your Clip region or ClipRect will show as being infinite) and you can't break out of those clip bounds.

When the state of your visual objects change (for example, on mouse or keyboard events or reacting to data changes), it's normally fine to simply call Invalidate() which will repaint the entire control. Windows will call OnPaint during moments of low CPU usage. Each call to Invalidate() usually will not always correspond to an OnPaint event. Invalidate could be called multiple times before the next paint. So if 10 properties in your data model change all at once, you can safely call Invalidate 10 times on each property change and you'll likely only trigger a single OnPaint event.

I've noticed you should be careful with using Update() and Refresh(). These force a synchronous OnPaint immediately. They're useful for drawing during a single threaded operation (updating a progress bar perhaps), but using them at the wrong times could lead to excessive and unnecessary painting.

If you want to use clip rectangles to improve performance while repainting a scene, you need not keep track of an aggregated clip area yourself. Windows will do this for you. Just invalidate a rectangle or a region that requires invalidation and paint as normal. For example, if an object that you are painting is moved, each time you want to invalidate it's old bounds and it's new bounds, so that you repaint the background where it originally was in addition to painting it in its new location. You must also take into account pen stroke sizes, etc.

And as Hans Passant mentioned, always use 32bppPArgb as the bitmap format for high resolution images. Here's a code snippet on how to load an image as "high performance":

public static Bitmap GetHighPerformanceBitmap(Image original)
{
    Bitmap bitmap;

    bitmap = new Bitmap(original.Width, original.Height, PixelFormat.Format32bppPArgb);
    bitmap.SetResolution(original.HorizontalResolution, original.VerticalResolution);

    using (Graphics g = Graphics.FromImage(bitmap))
    {
        g.DrawImage(original, new Rectangle(new Point(0, 0), bitmap.Size), new Rectangle(new Point(0, 0), bitmap.Size), GraphicsUnit.Pixel);
    }

    return bitmap;
}