OnPaint、Invalidate、Clipping 和 Regions 的最佳实践

Posted

技术标签:

【中文标题】OnPaint、Invalidate、Clipping 和 Regions 的最佳实践【英文标题】:Best practice for OnPaint, Invalidate, Clipping and Regions 【发布时间】:2011-11-22 09:12:58 【问题描述】:

我有一个用户控件,其中包含许多自己绘制的对象(从 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();

【问题讨论】:

你正在做不需要做的工作。 Windows 在剪辑方面已经非常高效,您无需提供帮助。如果您有性能问题,请关注位图的像素格式。 32bppPArgb 比其他任何速度快十倍。 我认为您不了解剪贴画的工作原理。我正在使用 GDI+ 绘制可以移动、调整大小、旋转等的对象。我需要重绘 MouseMove 等事件,并且我需要确保只重绘实际需要更新的部分。例如,如果我更改一个对象,它不会自行重绘,我必须调用 Invalidate()。如果我没有指定要剪辑到的剪辑区域,那么它将重绘整个场景。例如,您不想在 MouseMove 事件上重绘整个场景。 哇!告诉 Hans Passant 他不了解窗口剪辑的工作原理就像告诉 Jon Skeet 他不了解 C# 的工作原理! :-) 很公平。我不知道 Windows 根据之前的失效预设了 e.Graphics.Clip 区域。我以为它给了你 e.ClipRectangle,你必须用它来手动剪辑。尽管如此,为我说 Windows 剪辑并不能告诉我如何有效地使无效:) 【参考方案1】:

由于很多人都在查看这个问题,我将继续并尽我所能回答。

PaintEventArgs 提供的 Graphics 类总是被无效请求硬裁剪。这通常由操作系统完成,但也可以由您的代码完成。

您无法重置此剪辑或逃离这些剪辑边界,但您不需要这样做。绘画时,您通常不应该关心它是如何被剪裁的,除非您迫切需要最大限度地提高性能。

图形类使用一堆容器来应用剪切和变换。您可以使用 Graphics.BeginContainer 和 Graphics.EndContainer 自行扩展此堆栈。每次启动容器时,对 Transform 或 Clip 所做的任何更改都是临时的,它们将在 BeginContainer 之前配置的任何先前的 Transform 或 Clip 之后应用。所以本质上,当你得到一个 OnPaint 事件时,它已经被剪辑并且你在一个新容器中,所以你看不到剪辑(你的剪辑区域或 ClipRect 将显示为无限)并且你不能打破那些剪辑边界。

当您的可视对象的状态发生变化时(例如,鼠标或键盘事件或对数据变化做出反应),通常只需调用 Invalidate() 即可重新绘制整个控件。 Windows 将在 CPU 使用率较低的时候调用 OnPaint。对 Invalidate() 的每次调用通常并不总是对应于 OnPaint 事件。在下一次绘制之前,可以多次调用 Invalidate。因此,如果您的数据模型中的 10 个属性一次全部更改,您可以安全地在每次属性更改时调用 Invalidate 10 次,并且您可能只会触发一个 OnPaint 事件。

我注意到您应该小心使用 Update() 和 Refresh()。这些会立即强制同步 OnPaint。它们对于在单线程操作期间进行绘制很有用(也许是更新进度条),但在错误的时间使用它们可能会导致过度和不必要的绘制。

如果您想在重新绘制场景时使用剪辑矩形来提高性能,您无需自己跟踪聚合的剪辑区域。 Windows 将为您执行此操作。只需使需要失效的矩形或区域失效并正常绘制即可。例如,如果您正在绘制的对象被移动,则每次您想要使其旧边界和新边界无效时,除了在其新位置绘制它之外,您还可以在它原来的位置重新绘制背景。您还必须考虑笔画大小等。

正如 Hans Passant 所说,始终使用 32bppPArgb 作为高分辨率图像的位图格式。这是关于如何将图像加载为“高性能”的代码 sn-p:

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;

【讨论】:

是否需要设置分辨率? 如果您希望将原始位图中的 DPI 信息传输到新位图中,则需要设置分辨率。否则任何 DPI 信息都会丢失。不需要只复制原始图像本身(像素阵列)。

以上是关于OnPaint、Invalidate、Clipping 和 Regions 的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

在vc++中 OnDraw()函数和 Invalidate()函数起啥作用

从OnPaint中的e.ClipRectangle绘制

Invalidate(TRUE)与Invalidate(FALSE)区别(前者会发送WM_ERASEBKGND消息全部刷新,然后使用WM_PAINT消息绘制,而后者只发送WM_PAINT消息)(示例代

MFC中啥时候调用OnDraw()函数?

MFC OnPaint()函数中最先调用CDialog::OnPaint()和最后调用CDialog::OnPaint()的巨大区别

通过单击按钮调用 onPaint()