WIN32 剪裁区域指南






4.90/5 (45投票s)
本指南旨在帮助您理解三种不同的裁剪区域类型及其与设备上下文的关系
引言
裁剪是一项非常有用的技术,可用于为任何类型的计算机图形显示绘制复杂的 UI。在计算机图形学中,裁剪有两种类型:一种用于提高性能,另一种用于产生视觉效果。本文将重点介绍裁剪如何在 WIN32 API 中实现,尤其是在设备上下文 (DC) 内。
虽然本教程中的一些讨论可能只对寻求 WIN32 裁剪区域高级知识的开发人员感兴趣,但本指南的大部分内容都设定在中级水平。此外,了解裁剪区域结构和组织的许多秘密,将使您能够做出设计决策,从而更轻松地利用这些功能。
本文的大部分知识来源于 Feng Yuan 的著作《Windows Graphics Programming》。文中的示例也受到了该书演示程序的启发。该演示程序是在 Windows SDK 中开发的,因为它涉及的代码非常简单。
设备上下文及其区域
从功能上看,DC 就是一个矩形像素阵列。当没有任何限制时,可以写入这些像素中的任何一个。但是,DC 有三种区域,它们会限制对这些像素的访问(裁剪)。随着这些 DC 中的每一个被修改,DC 的交集将创建最终的裁剪区域,从而允许对 DC 进行绘图访问。
系统区域
系统区域是设备上下文的基础区域。此区域也可以称为可见区域,因为它只允许显示窗口的可见部分。最初,它将包含整个 DC。系统通常负责定义和维护此区域,但是,应用程序可以访问此区域,甚至可以为设备上下文设置初始的系统区域。此区域与创建 DC 的目标窗口密切相关。系统区域也是从许多不同因素计算得出的。这些因素包括:
- 如果目标窗口矩形或区域被
SetWindowRgn
修改。 - 如果设置了
CS_CLIPCHILDREN
类样式,则子窗口的窗口区域将从系统区域中移除。 - Z 顺序中位于目标窗口上方的所有窗口的窗口区域也将被移除。
- 如果使用了
BeginPaint
来创建 DC,则窗口的更新区域将与剩余的系统区域相交。
裁剪区域
此区域由应用程序定义,是开发人员实现裁剪最常用的区域。创建 DC 时,初始裁剪区域被设置为 DC 的整个设备表面。在 DC 的生命周期中,应用程序可以更改、操作甚至清除此区域。
元文件区域
此区域与裁剪区域非常相似,但在 MSDN 中文档记录得不太充分。此区域的主要目的是为元文件在绘制到绘图图面时设置裁剪区域,同时保持裁剪区域不变。此区域也可以看作是第二个裁剪区域。
在详细解释了操作这些区域的 API 之后,将给出更多关于这些裁剪区域可能用法的细节。
系统区域函数
只有两个函数可以访问系统区域。这两个函数都将得到完整详细的描述,以及其中一些未公开的特性。
GetRandomRgn
GetRandomRgn
函数将指定设备上下文的系统裁剪区域复制到特定区域。您可能需要获取最新版本的 Windows 头文件才能访问此函数,它最近才在 MSDN 中得到文档记录,但很早以前就已从 GDI32.dll 导出。在 iNum
参数中,唯一记录的值是 SYSRGN
,它在 wingdi.h 中定义为值 4
。从 MSDN 提取的描述可以看出,iNum
必须设置为 SYSRGN
。但这并不完全正确。还有三个未公开的值可以合法地传递给此函数。
int GetRandomRgn(
HDC hdc, // handle to DC
HRGN hrgn, // handle to region
INT iNum // must be SYSRGN
);
以下是四个有效值及其在 GetRandomRgn
的 iNum
参数中设置时的效果:
- 值 = 1:此**未公开**的值将返回 DC 中当前的裁剪区域。
- 值 = 2:此**未公开**的值将返回 DC 中当前的元文件区域。
- 值 = 3:此**未公开**的值将返回裁剪区域和元文件区域的交集。Feng Yuan 将此区域称为 Rao 区域,据称是以一位微软工程师的名字命名的,他劝说开发团队缓存这个交集区域以提高性能。姑且听之。
- 值 = 4, SYSRGN:此函数唯一已公开并定义的值。此值返回前面描述的系统区域。
当从 GetRandomRgn
检索到区域时,还有一件事需要注意:在 Windows 9x 操作系统上,区域以窗口坐标返回;而在 Windows NT 机器上,区域以屏幕坐标返回。因此,要在 Windows NT 机器上的窗口中使用该区域,需要对其进行偏移。以下是一段可用于转换 Windows NT 机器上区域的代码。
POINT pt = {0,0};
::MapWindowPoints(NULL, hWnd, &pt, 1);
::OffsetRgn(hRgn, pt.x, pt.y);
GetDCEx
GetDCEx
函数检索指定窗口或整个屏幕的 DC 的句柄。此函数允许开发人员为新创建的 DC 指定初始裁剪区域。但是,这个说法有点令人误解。当指定裁剪区域时,实际设置的区域是系统区域。这是开发人员设置 DC 系统区域的后门。但是,一旦设置了区域并创建了 DC,应用程序就无法直接操作系统区域了。
HDC GetDCEx(
HWND hWnd, // handle to window
HRGN hrgnClip, // handle to clipping region
DWORD flags // creation options
);
为了设置区域,必须使用以下两个标志之一来指示如何使用该区域:
DCX_EXCLUDERGN
:由hrgnClip
标识的裁剪区域将从返回的 DC 的可见区域中排除。DCX_INTERSECTRGN
:由hrgnClip
标识的裁剪区域将与返回的 DC 的可见区域相交。这将起到重新允许已为子窗口裁剪和 Z 顺序裁剪而裁剪掉的区域进行绘制的作用。
在使用此函数传递有效区域时,有一点需要注意:此函数假定对该区域拥有所有权。因此,在调用此函数后,绝对不能删除或使用此区域。
存储在 DC 中的区域在 Windows 9x 机器上以窗口坐标存储,在 Windows NT 机器上以屏幕坐标存储。这意味着,如果要在 Windows NT 机器上的 GetDCEx
中设置区域,那么在调用 GetDCEx
之前,需要将该区域转换为相对于窗口的屏幕坐标。下面是一个演示如何执行此操作的小代码示例:
POINT pt = {0,0};
::MapWindowPoints(hWnd, NULL, &pt, 1);
::OffsetRgn(hRgn, pt.x, pt.y);
裁剪区域函数
裁剪区域拥有一系列最丰富的函数来操作其数据。这些函数允许直接设置区域,通过布尔交集进行修改,甚至进行平移。下面将对每个函数进行描述。
GetClipBox
GetClipBox
函数检索可围绕设备上当前可见区域绘制的最紧密边界矩形的尺寸。可见区域由当前的裁剪区域或裁剪路径以及任何重叠的窗口定义。
int GetClipBox(
HDC hdc, // handle to DC
LPRECT lprc // rectangle
);
此函数返回的尺寸是当前 DC 的逻辑坐标。返回值将指示当前存储在裁剪区域中的 区域类型。
GetClipRgn
GetClipRgn
函数检索标识指定设备上下文当前应用程序定义的裁剪区域的句柄。返回的区域将是设备坐标,而不是逻辑坐标。这与 GetClipBox
的行为不同。很可能此函数可以通过将 1
传递给 iNum
参数来通过 GetRandomRgn
实现。
int GetClipRgn(
HDC hdc, // handle to DC
HRGN hrgn // handle to region
);
SelectClipRgn
SelectClipRgn
函数将一个区域选定为指定设备上下文的当前裁剪区域。这是设置设备上下文裁剪区域的最简单方法。
int SelectClipRgn(
HDC hdc, // handle to DC
HRGN hrgn // handle to region
);
此函数返回的尺寸是当前 DC 的设备坐标。返回值将指示当前存储在裁剪区域中的 区域类型。
ExtSelectClipRgn
ExtSelectClipRgn
函数使用指定的模式将指定区域与当前裁剪区域合并。如果您想了解不同模式如何影响当前裁剪区域的更多详细信息,请参阅本指南:WIN32 区域指南。
int ExtSelectClipRgn(
HDC hdc, // handle to DC
HRGN hrgn, // handle to region
int fnMode // region-selection mode
);
此函数返回的尺寸是当前 DC 的设备坐标。返回值将指示当前存储在裁剪区域中的 区域类型。
IntersectClipRect
IntersectClipRect
函数根据当前裁剪区域与指定矩形的交集创建一个新的裁剪区域。除非调用了其他裁剪函数来修改初始裁剪区域,否则此函数将不起作用,因为默认裁剪区域包含整个 DC。
int IntersectClipRect(
HDC hdc, // handle to DC
int nLeftRect, // x-coord of upper-left corner
int nTopRect, // y-coord of upper-left corner
int nRightRect, // x-coord of lower-right corner
int nBottomRect // y-coord of lower-right corner
);
ExcludeClipRect
ExcludeClipRect
函数创建一个新的裁剪区域,该区域由现有裁剪区域减去指定矩形组成。
int ExcludeClipRect(
HDC hdc, // handle to DC
int nLeftRect, // x-coord of upper-left corner
int nTopRect, // y-coord of upper-left corner
int nRightRect, // x-coord of lower-right corner
int nBottomRect // y-coord of lower-right corner
);
OffsetClipRgn
OffsetClipRgn
函数根据指定的偏移量移动设备上下文的裁剪区域。
int OffsetClipRgn(
HDC hdc, // handle to DC
int nXOffset, // offset along x-axis
int nYOffset // offset along y-axis
);
SelectClipPath
SelectClipPath
函数将当前路径选定为设备上下文的裁剪区域,并使用指定的模式将新区域与任何现有裁剪区域合并。
BOOL SelectClipPath(
HDC hdc, // handle to DC
int iMode // clipping mode
);
元文件区域函数
乍一看,元文件区域函数集可能显得有点少,只有两个。但是,当设置元文件区域时,它将合并当前元文件区域与当前裁剪区域的交集。然后,裁剪区域将被重置为整个 DC。实际上,这使得元文件区域可以使用所有裁剪区域函数来创建自身。很可能此函数可以通过将 2
传递给 iNum
参数来通过 GetRandomRgn
实现。
然而,在使用 SaveDC
保存 DC 状态后再设置元文件区域非常重要。因为当创建元文件区域时,它会与当前元文件区域相交。一旦元文件区域从默认的客户端矩形缩小,就无法将其恢复到原始状态,除非恢复先前保存的 DC 上下文。
GetMetaRgn
GetMetaRgn
函数检索指定设备上下文的当前元文件区域。
int GetMetaRgn(
HDC hdc, // handle to DC
HRGN hrgn // handle to region
);
SetMetaRgn
SetMetaRgn
函数将指定设备上下文的当前裁剪区域与当前元文件区域相交,并将组合区域保存为指定设备上下文的新元文件区域。裁剪区域将被重置为“空”区域。重置元文件区域到其原始状态的唯一方法是使用 SaveDC
返回到先前保存的 DC 版本。
int SetMetaRgn(
HDC hdc // handle to DC
);
DC 区域的应用
DC 所包含的区域得到了广泛的应用。很多时候,应用程序在使用这些区域时甚至没有意识到。通过了解所使用的区域及其功能,开发人员可以围绕这些功能设计他们的系统,以创建更高效的程序。当应用程序或控件的实现不遵循 WIN32 开发的常规设计时,这一点尤其适用。
BeginPaint
这可能是 DC 区域最常见的用途。在内部,BeginPaint
调用 GetDCEx
来创建最终使用的 DC。目标窗口的当前更新区域被用作 GetDCEx
中的裁剪区域。最终结果是,用于绘图 DC 的系统区域等同于窗口的更新区域。当应用程序尝试在绘图 DC 上绘图时,只有已失效的区域会被重新绘制。
此时,应用程序可以调用 GetRandomRgn
来获取需要重新绘制的区域,并剔除所有不必要的绘图,从而提高绘图例程的效率。即使应用程序在每次绘图消息时都将整个窗口发送以进行重绘,由于系统区域的自动裁剪,显示最终也会减少闪烁。为了看到此功能缺失可能导致的闪烁演示,请在你的绘图处理程序中 BeginPaint
之前调用 InvalidateRect
。
InvalidateRect(hWnd, NULL, TRUE);
这将导致整个目标窗口失效,并消除系统区域提供的任何好处。
为效果进行的裁剪
使用裁剪区域创建蒙版,只允许 DC 的特定部分进行绘制。这可能是 WIN32 开发中“裁剪”一词最常被联想到的用途。
元文件
元文件是一种允许应用程序或艺术家将一组矢量绘图命令存储在文件中,以便稍后在图面上绘制的方法。元文件之所以很棒,是因为它们是矢量基础的,这意味着它们可以很好地缩放到几乎任何显示尺寸。元文件的绘图尺寸仅限于窗口 GDI 中使用的值的分辨率。
如果应用程序使用元文件来绘制显示的一部分,则可能需要设置一个裁剪区域来限制恶意元文件可以绘制的区域。这就是元文件区域主要用于的地方。应用程序可以在元文件区域中设置一个边界,这个边界不能被绘制到,而且可能与裁剪区域不同。这实际上是元文件区域的初衷。
库
元文件区域的另一个可能用途是在导入或导出的库代码中,这些代码会绘制到 DC。如果导入了一个库函数,并且该函数通过使用裁剪区域来限制其绘制,那么它将使宿主应用程序难以进一步裁剪绘图,特别是当库函数没有将当前裁剪区域合并到它创建的新裁剪区域中时。元文件区域可以很好地解决这个问题。
另一方面,如果绘制代码被编写为在库中导出,并且裁剪区域是开发人员关心的问题,那么库就可以使用元文件区域来设置适当的裁剪区域,并允许库的用户获得进一步的裁剪权限。
演示
提供的演示程序受到了 Feng Yuan 的著作《Windows Graphics Programming》中一个演示程序的启发。DC 的三种区域将分别得到演示。
此应用程序是为 Windows SDK 编写的。由于使用了 GetRandomRgn
函数,应使用最新的头文件。此函数最近才在头文件中公开。以防万一,我包含了一个 wingdi.h 副本,其中包含 GetRandomRgn
的定义,供头文件过时的开发人员使用。只有在您当前没有最新头文件时,才建议使用此文件来构建应用程序。如果您想开发使用此函数的应用程序,我建议下载包含最新头文件的 SDK。
此应用程序将允许用户分别配置 DC 中的三种区域。以下是对应用程序中三种区域表示方式的描述:
- 系统区域:此区域由区域边界周围的红色边框表示。将使用
FrameRgn
来绘制此边界。系统区域是应用程序允许用户手动设置的唯一区域。
- 裁剪区域:此区域将由垂直深紫色线表示。激活此区域时,它将自动计算为垂直填充顶部和底部客户端边界,宽度为窗口宽度的 2/3 的椭圆。
- 元文件区域:此区域将由水平蓝色线表示。激活此区域时,它将自动计算为水平填充左侧和右侧客户端边界,高度为窗口高度的 2/3 的椭圆。
- 交集:所有三个区域的交集将始终处于活动状态,并由浅粉色填充表示。这表示如果按图示设置这三个区域,应用程序可用于绘制的区域。
以下是激活所有三个区域时的显示示例:
有四个菜单设置可以勾选以更改显示。下面列出了这五个设置中的每一个的说明,以及可能需要额外解释的任何代码才能使该功能正常工作。
-
默认系统区域:将显示 Windows 计算出的系统区域。当整个窗口刷新时,整个客户端区域将显示为系统区域。但是,当只更新窗口的一小部分时,系统区域将设置为窗口的更新区域。
将使用此调用从 DC 中提取系统区域:
::GetRandomRgn(hdc, hSystemRgn, SYSRGN /*4*/);
-
手动系统区域:用户可以通过绘制所需系统区域的路径来设置系统区域。系统区域将使用此调用设置到 DC 中:
//C: Create a region that is the inverse of the User region. HRGN hRgnExclude = ::CreateRectRgnIndirect(&rClient); ::CombineRgn(hRgnExclude, hRgnExclude, g_hUserRgn, RGN_DIFF); ... hdc = ::GetDCEx(hWnd, hRgnExclude, DCX_CACHE | DCX_EXCLUDERGN); //C: Since BeginPaint was not used to create the DC, WM_ERASEBKGND // has to be generated manually. ::SendMessage(hWnd, WM_ERASEBKGND, (WPARAM)hdc, NULL); //C: Validate the update region for this window to prevent // other calls to WM_PAINT. ::ValidateRect(hWnd, NULL);
请注意用于创建 DC 的额外代码。WM_ERASEBKGND
消息和ValidateRect
的调用。这是因为GetDCEx
用于创建 DC,而不是BeginPaint
。这些操作通常在BeginPaint
中执行。 -
裁剪区域:这将激活或停用裁剪区域。
-
元文件区域:这将激活或停用元文件区域。
演示程序非常简单,但它确实显示了 DC 中存在三个不同的区域,以及它们在确定所有裁剪后如何决定最终的绘图图面。
结论
裁剪是一项经常被忽略的技术。然而,在特定情况下,它可能非常有益。WIN32 在其 DC 中提供了三个不同的区域,当应用裁剪时,它们都对最终结果有所贡献。有些区域需要额外的努力,例如裁剪区域和元文件区域,而其他区域,如系统区域,则作为额外的优化添加,无需开发人员付出额外的工作。
本指南中提供的信息可能比您了解的 Windows 裁剪区域的知识要多得多。然而,这些知识将使您能够围绕这些功能设计您的程序,并在未来的特殊编程项目中加以利用。