OnPaint、Invalidate、Clipping 和 Region 的最佳实践
我有一个用户控件,其中包含许多自行绘制的对象的完全自定义绘制的图形(从 OnPaint 调用),背景是一个大位图。我内置了缩放和平移功能,并且画布上绘制的对象的所有坐标均采用位图坐标。
因此,如果我的用户控件是 1000 像素宽,位图是 1500 像素宽,并且我以 200% 缩放,那么在任何给定时间我只会查看位图宽度的 1/3。如果您滚动到最左侧,则具有从位图上的点 100,100 开始的矩形的对象将出现在屏幕上的点 200,200 处。
基本上我需要做的是创建一种有效的方法来仅重绘需要重绘的内容。例如,如果我移动一个对象,我可以将该对象的旧剪辑矩形添加到一个区域,并将该对象的新剪辑矩形合并到同一区域,然后调用 Invalidate(region) 来重绘这两个区域。
然而,这样做意味着我必须不断地将对象位图坐标转换为屏幕坐标,然后再将它们提供给 Invalidate。当其他窗口使我的窗口无效时,我必须始终假设 PaintEventArgs 中的 ClipRectangle 位于屏幕坐标中。
有没有办法可以利用 Region.Transform 和 Region.Translate 功能,这样我就不需要从位图转换为屏幕坐标?在某种程度上,它不会干扰在屏幕坐标中接收 PaintEventArgs?我应该使用多个区域还是有更好的方法来完成这一切?
我现在正在做的示例代码:
invalidateRegion.Union(BitmapToScreenRect(SelectedItem.ClipRectangle));
SelectedItem.UpdateEndPoint(endPoint);
invalidateRegion.Union(BitmapToScreenRect(SelectedItem.ClipRectangle));
this.Invalidate(invalidateRegion);
在 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();
}
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();
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
由于很多人都在看这个问题,我将继续以我目前所知的最佳方式回答它。
PaintEventArgs 提供的 Graphics 类始终被无效请求硬剪辑。这通常由操作系统完成,但也可以由您的代码完成。
您无法重置此剪辑或逃离这些剪辑边界,但您不需要这样做。绘画时,您通常不应该关心它是如何被剪切的,除非您迫切需要最大化性能。
图形类使用一堆容器来应用剪切和转换。您可以使用 Graphics.BeginContainer 和 Graphics.EndContainer 自行扩展此堆栈。每次启动容器时,对变换或剪辑所做的任何更改都是临时的,并且它们会在 BeginContainer 之前配置的任何先前变换或剪辑之后应用。因此,本质上,当您收到 OnPaint 事件时,它已经被剪切,并且您位于一个新容器中,因此您看不到剪辑(您的 Clip 区域或 ClipRect 将显示为无限),并且您无法摆脱这些剪辑剪辑边界。
当可视对象的状态发生变化(例如,鼠标或键盘事件或对数据更改做出反应)时,通常只需调用 Invalidate() 就可以了,它将重新绘制整个控件。 Windows 将在 CPU 使用率较低时调用 OnPaint。每次调用 Invalidate() 通常并不总是对应于 OnPaint 事件。 Invalidate 可以在下一次绘制之前多次调用。因此,如果数据模型中的 10 个属性同时更改,您可以在每次属性更改时安全地调用 Invalidate 10 次,并且您可能只会触发一个 OnPaint 事件。
我注意到您应该小心使用 Update() 和 Refresh()。这些会立即强制同步 OnPaint。它们对于在单线程操作期间进行绘制(可能更新进度条)很有用,但在错误的时间使用它们可能会导致过度和不必要的绘制。
如果您想在重新绘制场景时使用剪辑矩形来提高性能,则无需自己跟踪聚合剪辑区域。 Windows 会为您完成此操作。只需使一个矩形或需要失效的区域失效并正常绘制即可。例如,如果您正在绘制的对象被移动,则每次您想要使其旧边界和新边界无效时,以便除了在新位置绘制背景之外,还可以在其原始位置重新绘制背景。您还必须考虑笔划大小等。
正如 Hans Passant 提到的,始终使用 32bppPArgb 作为高分辨率图像的位图格式。以下是有关如何以“高性能”方式加载图像的代码片段:
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":