前言:
学习笔记,随时更新。如有谬误,欢迎指正。
说明:
- 红色字体为较为重要部分。
- 绿色字体为个人理解部分。
原文链接:https://learn.microsoft.com/en-us/windows/win32/gdi/painting-and-drawing
12 绘画和绘制
12.1 关于绘画和绘制
-
几乎所有的应用程序都使用屏幕来显示它们操作的数据。应用程序绘制图像、绘制图形和写入文本,以便用户可以在创建、编辑和打印数据时查看数据。 Windows 为绘画和绘制提供了丰富的支持,但是,由于多任务操作系统的特性,应用程序在访问屏幕时必须相互协作。
-
为了保持所有应用程序的平稳和协作,系统管理所有到屏幕上的输出。应用程序使用窗口作为其主要输出设备,而不是屏幕本身。系统提供与窗口唯一对应的显示设备上下文。应用程序使用显示设备上下文将它们的输出定向到指定的窗口。在窗口中绘制(将输出定向到窗口)可以防止应用程序干扰其他应用程序的输出,并允许应用程序彼此共存,同时仍然充分利用系统的图形功能。
12.1.1 何时在窗口中绘制
-
应用程序在不同的时间绘制窗口:当第一次创建窗口时,当改变窗口的大小时,当从另一个窗口后面移动窗口时,当最小化或最大化窗口时,当显示打开的文件中的数据时,以及当滚动,更改或选择显示数据的一部分时。
-
系统管理诸如移动和调整窗口大小之类的操作。如果一个操作影响了窗口的内容,系统将窗口的受影响部分标记为准备更新,并在下一个时机,向窗口的窗口程序发送一个 WM_PAINT 消息。消息是给应用程序的一个信号,用于确定必须更新的内容并执行必要的绘制。
-
一些操作由应用程序管理,例如显示打开的文件和选择显示的数据。对于这些操作,应用程序可以标记为更新受该操作影响的窗口部分,从而在下次时机发送 WM_PAINT 消息。如果一个动作需要即时反馈,应用程序可以在动作发生时进行绘制,而不需要等待 WM_PAINT 。例如,典型的应用程序突出显示用户选择的区域,而不是等待下一个 WM_PAINT 消息来更新该区域。
-
在所有情况下,应用程序都可以在创建窗口后立即绘制窗口。要在窗口中绘制,应用程序必须首先获取窗口的显示设备上下文的句柄。理想情况下,应用程序在处理 WM_PAINT 消息期间执行大部分绘制操作。在这种情况下,应用程序通过调用 BeginPaint 函数来获取显示设备上下文。如果应用程序在任何其他时间绘制,例如在 WinMain 中或在处理键盘或鼠标消息期间,它调用 GetDC 或 GetDCEx 函数来获取显示 DC 。
12.1.2 WM_PAINT 消息
-
通常,应用程序在响应 WM_PAINT 消息时绘制一个窗口。当对窗口的更改改变了客户端区域的内容时,系统将此消息发送给窗口程序。只有当应用程序消息队列中没有其他消息时,系统才会发送该消息。
-
在接收到 WM_PAINT 消息后,应用程序可以调用 BeginPaint 来获取客户端区域的显示设备上下文,并在调用 GDI 函数时使用它来执行更新客户端区域所需的任何绘制操作。完成绘制操作后,应用程序调用 EndPaint 函数来释放显示设备上下文。
-
在 BeginPaint 返回显示设备上下文之前,系统为指定的窗口准备好设备上下文。它首先将设备上下文的裁剪区域设置为等于需要更新的窗口部分和用户可见的部分的交集。只有那些已经改变的窗口部分才会被重新绘制。尝试在此区域之外绘制时将被裁剪,并且不会显示在屏幕上。
-
系统还可以在 BeginPaint 返回之前向窗口程序发送 WM_NCPAINT 和 WM_ERASEBKGND 消息。这些消息指示应用程序绘制非客户端区域和窗口背景。非客户端区域是客户端区域之外的窗口部分。该区域包括标题栏、窗口菜单(也称为系统菜单)和滚动条等功能。大多数应用程序依赖默认窗口函数 DefWindowProc 来绘制该区域,因此将 WM_NCPAINT 消息传递给该函数。窗口背景是在其他绘制操作开始之前填充窗口的颜色或图案。背景覆盖以前在窗口中或窗口下屏幕上的任何图像。如果窗口属于具有类背景刷的窗口类,则 DefWindowProc 函数会自动绘制窗口背景。
-
BeginPaint 用要更新的窗口部分的尺寸和指示窗口背景是否已绘制的标志等信息填充 PAINTSTRUCT 结构。应用程序可以使用这些信息来优化绘制。例如,它可以使用由 rcPaint 成员指定的更新区域的尺寸来限制只绘制需要更新的窗口部分。如果应用程序有非常简单的输出,它可以忽略更新区域并绘制整个窗口,依靠系统丢弃(裁剪)任何不需要的输出。因为系统裁剪扩展到裁剪区域之外的绘制,所以只有在更新区域中的绘制是可见的。
-
BeginPaint 将窗口的更新区域设置为 NULL 。这将清除该区域,防止它产生后续的 WM_PAINT 消息。如果一个应用程序处理了一个 WM_PAINT 消息,但是没有调用 BeginPaint 或者清除更新区域,那么只要该区域不是空的,应用程序就会继续接收 WM_PAINT 消息。在所有情况下,应用程序必须在从 WM_PAINT 消息返回之前清除更新区域。
-
在应用程序完成绘制后,它应该调用 EndPaint 。对于大多数窗口, EndPaint 释放显示设备上下文,使其可用于其他窗口。如果插入符号之前被 BeginPaint 隐藏( BeginPaint 会隐藏插入符号以防止绘制操作损坏它),则 EndPaint 会显示它。
12.1.2.1 更新区域
-
更新区域标识窗口中过期或无效且需要重绘的部分。系统使用更新区域为应用程序生成 WM_PAINT 消息,并最大限度地减少应用程序将其窗口内容更新到最新的时间。系统只将窗口的无效部分添加到更新区域,只需要绘制该部分。
-
当系统确定窗口需要更新时,它将更新区域的尺寸设置为窗口的无效部分。设置更新区域不会立即导致应用程序绘制。相反,应用程序将继续从应用程序消息队列中查询消息,直到没有消息剩余。然后系统检查更新区域,如果该区域不是空的(非 NULL ),它发送一个 WM_PAINT 消息给窗口程序。
-
应用程序可以使用更新区域来生成 WM_PAINT 消息。例如,从打开的文件中加载数据的应用程序通常在加载时设置更新区域,以便在下一个 WM_PAINT 消息处理期间绘制新数据。一般来说,应用程序不应该在数据更改时进行绘制,而应该通过 WM_PAINT 消息路由所有绘制操作。
12.1.2.2 使更新区域失效和有效
-
应用程序通过使用 InvalidateRect 或 InvalidateRgn 函数使窗口的一部分无效并设置更新区域。这些函数将指定的矩形或区域(客户区坐标)添加到更新区域,并将矩形或区域与系统或应用程序先前添加到更新区域的任何内容相结合。
-
InvalidateRect 或 InvalidateRgn 函数不生成 WM_PAINT 消息。相反,当窗口处理其消息队列中的其他消息时,系统会累积这些函数所做的更改和它自己的更改。通过累积更改,窗口一次处理所有更改,而不是一次一步地更新一小部分。
-
ValidateRect 和 ValidateRgn 函数通过从更新区域中删除指定的矩形或区域来验证窗口的一部分。当窗口在接收 WM_PAINT 消息之前更新了屏幕的特定部分时,通常使用这些函数。
12.1.2.3 获取更新区域
-
GetUpdateRect 和 GetUpdateRgn 函数获取窗口的当前更新区域。 GetUpdateRect 获取包含整个更新区域的最小矩形(以逻辑坐标表示)。 GetUpdateRgn 获取更新区域本身。这些函数可用于计算更新区域的当前大小,以确定在何处执行绘制操作。
-
BeginPaint 还获取包含当前更新区域的最小矩形的尺寸,并将尺寸复制到 PAINTSTRUCT 结构中的 rcPaint 成员。因为 BeginPaint 验证了更新区域,所以在调用 BeginPaint 之后立即调用 GetUpdateRect 和 GetUpdateRgn 将返回一个空的更新区域。
12.1.2.4 排除更新区域
- ExcludeUpdateRgn 函数从显示设备上下文的裁剪区域中排除更新区域。这个函数在窗口中绘制时很有用,而不是在处理 WM_PAINT 消息时。它防止在将在下一个 WM_PAINT 消息期间更新的区域中绘制。
12.1.2.5 同步和异步绘制
-
在处理 WM_PAINT 消息期间执行的大多数绘制都是异步的;也就是说,在窗口的一部分失效的时间和 WM_PAINT 发送的时间之间有一个延迟。在延迟期间,应用程序通常从队列中检索消息并执行其他任务。延迟的原因是系统通常将窗口中的绘制视为低优先级操作,并且好像用户输入的消息和可能影响窗口位置或大小的消息将在 WM_PAINT 之前处理。
-
在某些情况下,应用程序需要同步绘制,也就是说,在窗口的一部分无效之后立即在窗口中绘制。典型的应用程序在创建窗口后立即绘制其主窗口,以向用户发出应用程序已成功启动的信号。系统同步绘制一些控制窗口,如按钮,因为这些窗口是用户输入的焦点。尽管任何具有简单绘制例程的窗口都可以同步绘制,但所有这些绘制都应该快速完成,并且不应该干扰应用程序响应用户输入的能力。
-
UpdateWindow 和 RedrawWindow 函数允许同步绘制。如果更新区域不是空的, UpdateWindow 直接向窗口发送 WM_PAINT 消息。 RedrawWindow 也发送一个 WM_PAINT 消息,但是给了应用程序对如何绘制窗口的更大的控制,比如是否绘制非客户端区域和窗口背景,或者是否发送消息而不管更新区域是否为空。这些函数将 WM_PAINT 消息直接发送到窗口,而不考虑应用程序消息队列中其他消息的数量。
-
任何需要耗时绘制操作的窗口都应该异步绘制,以防止在绘制窗口时阻塞挂起的消息。此外,任何经常使窗口的一小部分无效的应用程序都应该允许这些无效的部分合并到单个异步 WM_PAINT 消息中,而不是一系列同步 WM_PAINT 消息。
12.1.3 无 WM_PAINT 消息的绘制
-
虽然应用程序在处理 WM_PAINT 消息时执行大多数绘制操作,但有时应用程序直接在窗口中绘制而不依赖于 WM_PAINT 消息更有效。当用户需要即时反馈时,例如选择文本、拖动对象或调整对象大小时,这非常有用。在这种情况下,应用程序通常在处理键盘或鼠标消息时进行绘制。
-
为了在不使用 WM_PAINT 消息的情况下绘制窗口,应用程序使用 GetDC 或 GetDCEx 函数来获取窗口的显示设备上下文。使用显示设备上下文,应用程序可以在窗口内绘制,避免侵入其他窗口。当应用程序完成绘制后,它调用 ReleaseDC 函数释放显示设备上下文以供其他应用程序使用。
-
当不使用 WM_PAINT 消息进行绘制时,应用程序通常不会使窗口无效。相反,它以这样一种方式绘制,它可以很容易地恢复窗口并删除绘制。例如,当用户选择文本或对象时,应用程序通常通过反转窗口中已有的内容来绘制所选内容。应用程序只需再次反转即可删除所选内容并恢复窗口的原始内容。
-
应用程序负责仔细管理它对窗口所做的任何更改。特别是,如果一个应用程序绘制一个选择区,并且中间发生了一个 WM_PAINT 消息,应用程序必须确保在消息期间进行的任何绘制都不会破坏选择区。为了避免这种情况,许多应用程序删除选择,执行常规的绘制操作,然后在绘制完成时恢复选择。
12.1.4 窗口坐标系
-
窗口的坐标系统基于显示设备的坐标系统。测量的基本单位是设备单位(通常是像素)。屏幕上的点由 x 和 y 坐标对描述。 x 坐标向右增加; y 坐标从上到下递增。系统的原点( 0,0 )取决于所使用的坐标类型。
-
系统和应用程序以屏幕坐标指定窗口在屏幕上的位置。对于屏幕坐标,原点是屏幕的左上角。窗口的完整位置通常由一个 RECT 结构描述,该结构包含定义窗口左上角和右下角的两个点的屏幕坐标。
-
系统和应用程序通过使用客户区坐标指定窗口中点的位置。在这种情况下,原点是窗口或客户区域的左上角。客户区坐标确保应用程序在窗口中绘制时可以使用一致的坐标值,而不管窗口在屏幕上的位置如何。
-
客户区的尺寸也由包含该区域的客户坐标的 RECT 结构描述。在所有情况下,矩形的左上角坐标都包含在窗口或客户区中,而右下角坐标则不包含在窗口或客户区中。窗口或客户区中的图形操作将从封闭矩形的右边缘和下边缘排除。
-
有时,应用程序可能需要将一个窗口中的坐标映射到另一个窗口的坐标。应用程序可以通过使用 MapWindowPoints 函数来映射坐标。如果其中一个窗口是桌面窗口,则该函数有效地将屏幕坐标转换为客户区坐标,反之亦然;桌面窗口始终以屏幕坐标指定。
12.1.5 窗口区域
-
除了更新区域之外,每个窗口都有一个可见区域,该区域定义了对用户可见的窗口部分。每当窗口改变大小或每当移动另一个窗口使其遮挡或暴露窗口的一部分时,系统就改变窗口的可见区域。应用程序不能直接更改可见区域,但是系统会自动使用可见区域为窗口查询到的任何显示设备上下文创建裁剪区域。
-
裁剪区域决定了系统允许绘制的位置。当应用程序使用 BeginPaint 、 GetDC 或 GetDCEx 函数查询显示设备上下文时,系统将设备上下文的裁剪区域设置为可见区域和更新区域的交集。应用程序可以通过使用 SetWindowRgn 、 SelectClipPath 和 SelectClipRgn 等函数来改变裁剪区域,从而进一步将绘制限制在更新区域的特定部分。
-
WS_CLIPCHILDREN 和 WS_CLIPSIBLINGS 样式进一步指定了系统如何计算窗口的可见区域。如果窗口具有这些样式中的一个或两个,则可见区域将排除任何子窗口或兄弟窗口(具有相同父窗口的窗口)。因此,侵入这些窗口的绘制将始终被裁剪。
12.1.6 窗口背景
-
窗口背景是在窗口开始绘制之前用于填充客户端区域的颜色或图案。窗口背景覆盖在窗口移动到那里之前屏幕上的任何内容,擦除现有图像并防止应用程序的新输出与不相关的信息混合在一起。
-
当应用程序调用 BeginPaint 时,系统为窗口绘制背景,或者通过向窗口发送 WM_ERASEBKGND 消息给窗口提供这样做的机会。
如果应用程序不处理该消息,而是将其传递给 DefWindowProc ,则系统通过在窗口类指定的背景刷中填充图案来擦除背景。如果画刷无效或类没有背景画刷,系统将在 BeginPaint 返回的 PAINTSTRUCT 结构中设置 fErase 成员,但不执行其他操作。如果需要的话,应用程序有第二次机会绘制窗口背景。
如果它处理 WM_ERASEBKGND ,应用程序应该使用消息的 wParam 参数来绘制背景。该参数包含窗口显示设备上下文的句柄。绘制完背景后,应用程序应该返回一个非零值。这确保了当应用程序处理后续的 WM_PAINT 消息时, BeginPaint 不会错误地将 PAINTSTRUCT 结构的 fErase 成员设置为非零值(表明背景应该被擦除)。
-
当使用 RegisterClass 函数注册类时,应用程序可以通过给 WNDCLASS 结构体的 hbrBackground 成员分配画刷句柄或系统颜色值来定义类背景刷。 GetStockObject 或 CreateSolidBrush 函数可用于创建画刷句柄。系统颜色值可以是为 SetSysColors 函数定义的值之一。(该值在赋值给成员之前必须加 1 。)
-
即使定义了类背景刷,应用程序也可以处理 WM_ERASEBKGND 消息。这允许用户更改指定窗口的窗口背景颜色或图案而不影响类中的其他窗口。在这种情况下,应用程序不能将消息传递给 DefWindowProc 。
-
应用程序不需要对齐画刷,因为系统使用窗口原点作为参考点绘制画刷。考虑到这一点,用户可以移动窗口而不影响图案画刷的对齐。
12.1.7 最小化的窗口
-
当用户从窗口菜单中单击最小化或应用程序调用 ShowWindow 函数并指定一个值(如 SW_MINIMIZE )时,系统将应用程序的主窗口(重叠样式)减少为最小化窗口。最小化窗口通过减少应用程序在更新其主窗口时必须做的工作量来加快系统性能。
-
对于一个典型的应用程序,系统在最小化窗口时绘制一个称为类图标的图标,并用窗口的名称标记该图标。类图标是代表应用程序的静态图像,由应用程序在注册窗口类时指定。在调用 RegisterClass 之前,应用程序将类图标的句柄分配给 WNDCLASS 的 hIcon 成员。应用程序可以使用 LoadIcon 函数来获取图标句柄。
12.1.8 被调整大小的窗口
-
当用户选择窗口菜单命令(如 Size 和 Maximize )或应用程序调用函数(如 SetWindowPos 函数)时,系统会更改窗口的大小。当窗口改变大小时,系统假定窗口先前暴露部分的内容不受影响,不需要重新绘制。系统只使窗口的新暴露部分无效,这节省了应用程序处理最终 WM_PAINT 消息时的时间。在这种情况下,当窗口大小减小时不生成 WM_PAINT 。
-
对于某些窗口,对窗口大小的任何更改都会使其内容无效。例如,时钟应用程序调整时钟的表面以整齐地适应其窗口,必须在窗口改变大小时重新绘制时钟。当发生垂直、水平或垂直和水平变化时,要强制系统使窗口的整个客户端区域无效,应用程序必须在注册窗口类时指定 CS_VREDRAW 或 CS_HREDRAW 样式,或两者都指定。任何属于具有这些样式的窗口类的窗口在每次用户或应用程序更改窗口大小时都会无效(会被重绘)。
12.1.9 非客户区
-
每当窗口的非客户区的一部分,如标题栏、菜单栏,或窗口框架必须更新时,系统发送一个 WM_NCPAINT 消息给窗口。所述系统还可以发送其他消息以指示窗口更新其客户区的一部分;例如,当一个窗口变为激活或非激活时,它发送 WM_NCACTIVATE 消息来更新它的标题栏。一般来说,不建议为标准窗口处理这些消息,因为应用程序必须能够为窗口绘制非客户区的所有必需部分。出于这个原因,大多数应用程序将这些消息传递给 DefWindowProc 进行默认处理。
-
为其窗口创建自定义非客户区的应用程序必须处理这些消息。当这样做时,应用程序必须使用窗口设备上下文来在窗口中执行绘图。窗口设备上下文使应用程序能够绘制窗口的所有部分,包括非客户区。应用程序通过使用 GetWindowDC 或 GetDCEx 函数查询窗口设备上下文,并且在绘制完成时,必须使用 ReleaseDC 函数释放窗口设备上下文。
-
系统为非客户区维护一个更新区域。当应用程序接收到 WM_NCPAINT 消息时, wParam 参数包含一个用于定义更新区域尺寸的区域句柄。应用程序可以使用句柄将更新区域与窗口设备上下文的裁剪区域结合起来。当查询窗口设备上下文时,系统不会自动组合更新区域,除非应用程序使用 GetDCEx 并指定区域句柄和 DCX_INTERSECTRGN 标志。如果应用程序没有合并更新区域,则只会裁剪原本会扩展到窗口外的绘制操作。应用程序不负责清除更新区域,无论它是否使用该区域。
-
如果一个应用程序处理了 WM_NCACTIVATE 消息,处理后它必须返回 TRUE 来指示系统完成活动窗口的更改。如果当应用程序接收到 WM_NCACTIVATE 消息时窗口被最小化,它应该将消息传递给 DefWindowProc 。在这种情况下,默认函数将重新绘制图标的标签。
12.1.10 子窗口更新区域
-
子窗口是具有 WS_CHILD 或 WS_CHILDWINDOW 样式的窗口。像其他窗口样式一样,子窗口接收 WM_PAINT 消息来提示更新。每个子窗口都有一个更新区域,系统或应用程序可以将其设置这个区域来生成最终的 WM_PAINT 消息。
-
子窗口的更新和可见区域受到父窗口的影响;这对于其他样式的 Windows 是不成立的。系统通常在设置父窗口的更新区域时设置子窗口的更新区域,导致子窗口在父窗口接收到 WM_PAINT 消息时接收到它们。系统将子窗口可见区域的位置限制在父窗口的客户端区域内,并裁剪子窗口移动到父窗口外的任何部分。
-
当父窗口的更新区域的一部分包含子窗口的一部分时,系统将为子窗口设置更新区域。在这种情况下,系统首先向父窗口发送一个 WM_PAINT 消息,然后向子窗口发送一个消息,允许子窗口恢复父窗口可能已经绘制的窗口的任何部分。
-
设置子窗口的更新区域时,系统不会设置父窗口的更新区域。应用程序不能通过使子窗口无效来为父窗口生成 WM_PAINT 消息。类似地,应用程序不能通过使完全位于子窗口下方的父窗口的客户区的一部分无效来为子窗口生成 WM_PAINT 消息。在这种情况下,两个窗口都不会收到 WM_PAINT 消息。
-
通过在创建父窗口时指定 WS_CLIPCHILDREN 样式,应用程序可以防止在父窗口设置时设置子窗口的更新区域。设置此样式后,系统会将子窗口从父窗口的可见区域中排除,从而忽略更新区域中可能包含子窗口的任何部分。当应用程序在父窗口中绘制时,任何将覆盖子窗口的绘图都将被裁剪,从而使后续的 WM_PAINT 消息不必要地传递给子窗口。
-
子窗口的更新区域和可见区域也受到该子窗口的兄弟窗口的影响。兄弟窗口是具有公共父窗口的任何窗口。如果兄弟窗口重叠,那么为一个窗口设置更新区域会影响另一个窗口的更新区域,导致 WM_PAINT 消息被发送到两个窗口。如果父链中的一个窗口是复合的(具有 WX_EX_COMPOSITED 的窗口),则兄弟窗口以与 Z 顺序相反的顺序接收 WM_PAINT 消息。考虑到这一点,在 Z 顺序中最高的窗口(在顶部)最后接收到它的 WM_PAINT 消息,反之亦然。如果父链中的窗口没有合成,兄弟窗口将按 Z 顺序接收 WM_PAINT 消息。
-
兄弟窗口不会被自动裁剪。即使正在绘制的窗口在 Z 轴上的位置较低,一个同级窗口也可以在另一个重叠的同级窗口上绘制。应用程序可以通过在创建窗口时指定 WS_CLIPSIBLINGS 样式来防止这种情况。当设置此样式时,如果重叠的兄弟窗口在 Z 顺序中具有较高的位置,则系统将从窗口的可见区域中排除重叠的兄弟窗口的所有部分。
12.1.11 显示设备
-
在绘制之前,系统必须准备好显示设备进行绘制操作。显示设备上下文定义了一组图形对象及其相关属性,以及影响输出的图形模式。系统准备输出到窗口的每个显示设备上下文,为窗口而不是显示设备设置绘图对象、颜色和模式。当应用程序通过调用 GDI 函数提供显示设备上下文时, GDI 使用上下文中的信息在指定窗口中生成输出,而不会干扰其他窗口或屏幕的其他部分。
-
系统提供了五种显示设备上下文:
类型 含义 通用( Common ) 允许在指定窗口的客户区中绘制。 类( Common ) 允许在指定窗口的客户区中绘制。 父( Common ) 允许在窗口中的任意位置进行绘制。 尽管父设备上下文也允许在父窗口中绘制,但不应以此方式使用。 私有( Common ) 允许在指定窗口的客户区中绘制。 窗口( Common ) 允许在窗口中的任意位置进行绘制。 -
系统根据在窗口的类样式中指定的显示设备上下文的类型为窗口提供通用、类、父或私有设备上下文。系统只在应用程序显式请求时才提供窗口设备上下文,例如,通过调用 GetWindowDC 或 GetDCEx 函数。在所有情况下,应用程序都可以使用 WindowFromDC 函数来确定显示 DC 当前代表哪个窗口。
12.1.11.1 显示设备上下文缓存
-
系统维护一个显示设备上下文的缓存,它用于通用、父和窗口设备上下文。每当应用程序调用 GetDC 或 BeginPaint 函数时,系统都会从缓存中查询设备上下文;当应用程序随后调用 ReleaseDC 或 EndPaint 函数时,系统将 DC 返回到缓存中。
-
缓存可以保存的设备上下文数量没有预先确定的限制;如果没有可用的缓存,系统将为缓存创建一个新的显示设备上下文。鉴于此,应用程序可以同时从缓存中获得五个以上激活的设备上下文。但是,应用程序必须在使用后继续释放这些设备上下文。因为缓存的新显示设备上下文是在应用程序的堆空间中分配的,如果不能释放设备上下文,最终会消耗所有可用的堆空间。当系统无法为新设备上下文分配空间时,系统通过返回错误来指示此失败。其他与缓存无关的函数也可能返回错误。
12.1.11.2 显示设备上下文默认值
-
在第一次创建显示设备上下文时,系统为组成设备上下文的属性(即绘图对象、颜色和模式)分配默认值。下表显示了显示设备上下文属性的默认值。
属性 默认值 背景色 背景颜色设置来自于控制面板(通常为白色)。 后台模式 OPAQUE 位图 无 画刷 WHITE_BRUSH 画刷原点 (0,0) 裁剪区域 整个窗口或客户区,并酌情裁剪更新区域。客户区中的子窗口和弹出窗口也可能被裁剪。 调色板 DEFAULT_PALETTE 当前笔位置 (0,0) 设备原点 窗口或工作区的左上角。 绘制模式 R2_COPYPEN 字体 SYSTEM_FONT 字符间间距 0 映射模式 MM_TEXT 笔 BLACK_PEN 多边形填充模式 ALTERNATE 拉伸模式 BLACKONWHITE 文本颜色 文本颜色设置来自于控制面板(通常为黑色)。 视口范围 (1,1) 视口原点 (0,0) 窗口范围 (1,1) 窗口源原点 (0,0) -
应用程序可以通过使用选择和属性函数(如 SelectObject 、 SetMapMode 和 SetTextColor )来修改显示设备上下文属性的值。例如,应用程序可以通过使用 SetMapMode 更改映射模式来修改坐标系统中的默认度量单位。
-
对通用、父或窗口设备上下文的属性值的更改不是永久性的。当应用程序释放这些设备上下文时,当上下文返回到缓存时,当前选择(例如映射模式和裁剪区域)将丢失。对类或私有设备上下文的更改将无限期地保留。要将它们恢复到初始默认值,应用程序必须显式设置每个属性。
12.1.11.3 通用显示设备上下文
-
通用设备上下文用于在窗口的客户区中绘制。系统默认为窗口类的没有显式指定任何显示设备上下文样式的窗口对象提供通用设备上下文。通用设备上下文通常用于无需对设备上下文属性进行大量更改即可绘制的窗口。通用设备上下文很方便,因为它们不需要额外的内存或系统资源,但是如果应用程序在使用它们之前必须设置许多属性,那么它们就不方便了。
-
系统从显示设备上下文缓存中查询所有通用设备上下文。应用程序可以在创建窗口后立即查询通用设备上下文。因为通用设备上下文来自缓存,应用程序必须在绘制后尽快释放设备上下文。在通用设备上下文被释放后,它不再有效,应用程序不能尝试使用它进行绘制。要再次绘制,应用程序必须查询一个新的通用设备上下文,并在每次在窗口中绘制时继续查询和释放一个通用设备上下文。如果应用程序通过使用 GetDC 函数来查询设备上下文句柄,那么它必须使用 ReleaseDC 函数来释放句柄。类似地,对于每个 BeginPaint 函数,应用程序必须使用相应的 EndPaint 函数。
-
当应用程序查询设备上下文时,系统将调整原点,使其与客户端区域的左上角对齐。它还设置了裁剪区域,以便将设备上下文的输出裁剪到客户区。否则出现在客户区之外的任何输出都将被裁剪。如果应用程序通过使用 BeginPaint 查询通用设备上下文,系统还会在裁剪区域中包含更新区域,以进一步限制输出。
-
当应用程序释放通用设备上下文时,系统将恢复该设备上下文的属性的默认值。修改属性值的应用程序在每次查询通用设备上下文时都必须这样做。释放设备上下文会释放应用程序可能选择到其中的任何绘图对象,因此应用程序不需要在释放设备上下文之前释放这些对象。在任何情况下,应用程序都不能假设通用设备上下文在被释放后保留非默认选择。
12.1.11.4 私有显示设备上下文
-
私有设备上下文使应用程序能够避免每次在应用程序必须绘制窗口时查询和初始化显示设备上下文。私有设备上下文对于需要对设备上下文的属性值进行许多更改以准备绘制的窗口非常有用。私有设备上下文减少了准备设备上下文所需的时间,从而减少了在窗口中执行绘图所需的时间。
-
应用程序指示系统通过在窗口类中指定 CS_OWNDC 样式来为窗口创建私有设备上下文。系统每次创建一个属于类的新窗口对象时都会创建一个唯一的私有设备上下文。最初,私有设备上下文具有与通用设备上下文相同的属性默认值,但应用程序可以随时修改这些值。系统在窗口的生命周期内保留对设备上下文的更改,或者直到应用程序进行其他更改。
-
应用程序可以在创建窗口后的任何时间使用 GetDC 函数查询私有设备上下文的句柄。应用程序必须只查询句柄一次。此后,它可以保留并使用句柄任意次数。因为私有设备上下文不是显示设备上下文缓存的一部分,所以应用程序永远不需要通过使用 ReleaseDC 函数来释放私有设备上下文。
-
系统自动调整设备上下文以反映窗口的变化,例如移动或更改大小。这确保了任何重叠的窗口总是被适当地裁剪;也就是说,应用程序不需要任何操作来确保裁剪。但是,系统不会修改设备上下文以包含更新区域。因此,当处理 WM_PAINT 消息时,应用程序必须通过调用 BeginPaint 或通过查询更新区域并将其与当前裁剪区域相交来合并更新区域。如果应用程序不调用 BeginPaint ,它必须使用 ValidateRect 或 ValidateRgn 函数显式地验证更新区域。如果应用程序没有验证更新区域,窗口将接收到一系列无休止的 WM_PAINT 消息。
-
因为当窗口显示插入符号时, BeginPaint 会隐藏插入符号,所以调用 BeginPaint 的应用程序也应该调用 EndPaint 函数来恢复插入符号。 EndPaint 对私有设备上下文没有其他影响。
-
尽管私有设备上下文使用起来很方便,但就系统资源而言,它是内存密集型的,需要 800 或更多字节来存储。当性能考虑超过存储成本时,建议使用私有设备上下文。
-
当向应用程序发送 WM_ERASEBKGND 消息时,系统包含私有设备上下文。当应用程序或系统处理这些消息时,私有设备上下文的当前选择(包括映射模式)将生效。为了避免不良影响,系统在擦除背景时使用逻辑坐标;例如,它使用 GetClipBox 函数来查询要擦除的区域的逻辑坐标,并将这些坐标传递给 FillRect 函数。处理这些消息的应用程序可以使用类似的技术。
-
应用程序可以使用 GetDCEx 函数强制系统为具有私有设备上下文的窗口返回通用设备上下文。这对于在不更改私有设备上下文的属性的当前值的情况下对窗口进行快速修补非常有用。
12.1.11.5 类显示设备上下文
-
通过使用类设备上下文,应用程序可以为属于指定类的每个窗口使用单个显示设备上下文。类设备上下文通常与使用相同属性值绘制的控件窗口一起使用。与私有设备上下文一样,类设备上下文可以最大限度地减少为绘图准备设备上下文所需的时间。
-
如果一个窗口属于一个具有 CS_CLASSDC 样式的窗口类,系统将为它提供一个类设备上下文。系统在创建属于类的第一个窗口时创建设备上下文,然后在类中随后创建的所有窗口对象中使用相同的设备上下文。最初,类设备上下文具有与通用设备上下文相同的属性默认值,但应用程序可以随时修改这些值。系统保留所有更改,除了裁剪区域和设备原点,直到类中的最后一个窗口被销毁。对一个窗口所做的更改将应用于该类中的所有窗口。
-
应用程序可以在创建第一个窗口后的任何时间使用 GetDC 函数查询类设备上下文的句柄。应用程序可以保留和使用句柄而不释放它,因为类设备上下文不是显示设备上下文缓存的一部分。如果应用程序在同一个窗口类中创建另一个窗口,则应用程序必须再次查询类设备上下文。查询设备上下文将为新窗口设置正确的设备原点和裁剪区域。在应用程序为类中的新窗口查询类设备上下文之后,如果不再次为该窗口查询设备上下文,则不能再使用设备上下文在原始窗口中进行绘制。通常,每次必须在窗口中绘制时,应用程序必须显式查询窗口的类设备上下文。
-
使用类设备上下文的应用程序在处理 WM_PAINT 消息时应该始终调用 BeginPaint 。该函数为窗口设置正确的设备原点和裁剪区域,并合并更新区域。如果 BeginPaint 隐藏了插入符号,应用程序还应该调用 EndPaint 来恢复插入符号。 EndPaint 对类设备上下文没有其他影响。
-
当向应用程序发送 WM_ERASEBKGND 消息时,系统传递类设备上下文,允许当前属性值影响应用程序或系统在处理此消息时执行的任何绘图。与具有私有设备上下文的窗口一样,应用程序可以使用 GetDCEx 强制系统为具有类设备上下文的窗口返回通用设备上下文。
-
不建议使用类设备上下文。
12.1.11.6 窗口显示设备上下文
-
窗口设备上下文使应用程序能够在窗口的任何地方绘制,包括非客户区。窗口设备上下文通常由处理 WM_NCPAINT 和 WM_NCACTIVATE 消息的应用程序使用,这些消息用于具有自定义非客户区的窗口。不建议将窗口设备上下文用于任何其他目的。
-
应用程序可以通过使用指定了 DCX_WINDOW 选项的 GetWindowDC 或 GetDCEx 函数来查询窗口设备上下文。该函数从显示设备上下文缓存中查询窗口设备上下文。使用窗口设备上下文的窗口必须在绘制后尽快通过 ReleaseDC 函数释放它。窗口设备上下文总是来自缓存; CS_OWNDC 和 CS_CLASSDC 类样式不影响设备上下文。
-
当应用程序查询窗口设备上下文时,系统将设备原点设置为窗口的左上角,而不是客户区的左上角。它还将裁剪区域设置为包括整个窗口,而不仅仅是客户区。系统将窗口设备上下文的当前属性值设置为与通用设备上下文相同的默认值。应用程序可以更改属性值,但是当设备上下文被释放时,系统不会保留任何更改。
12.1.11.7 父显示设备上下文
-
父设备上下文使应用程序能够最大限度地减少为窗口设置裁剪区域所需的时间。应用程序通常使用父设备上下文来加速控件窗口的绘制,而不需要私有或类设备上下文。例如,系统为按钮和编辑控件使用父设备上下文。父设备上下文仅用于子窗口,而不能用于顶级窗口或弹出窗口。
-
应用程序可以指定 CS_PARENTDC 样式来将子窗口的剪切区域设置为父窗口的剪切区域,以便子窗口可以在父窗口中绘制。指定 CS_PARENTDC 可以提高应用程序的性能,因为系统不需要为每个子窗口重新计算可见区域。
-
父窗口设置的属性值不会为子窗口保留;例如,父窗口不能为其子窗口设置画笔。唯一保留的属性是裁剪区域。窗口必须将自己的输出裁剪到窗口的极限。因为父设备上下文的裁剪区域与父窗口相同,子窗口可以绘制整个父窗口,但父设备上下文不能以这种方式使用。
-
如果父窗口使用私有或类设备上下文,如果父窗口裁剪它的子窗口,或者如果子窗口裁剪它的子窗口或兄弟窗口,系统会忽略 CS_PARENTDC 样式。
12.1.12 窗口更新锁
-
窗口更新锁是窗口中绘制(操作)的临时挂起。当用户移动或调整窗口大小时,系统使用锁来防止其他窗口在跟踪矩形上绘图。如果应用程序对自己的窗口执行类似的移动或调整大小操作,则可以使用锁来防止绘图。
-
应用程序使用 LockWindowUpdate 函数设置或清除窗口更新锁,指定要锁定的窗口。锁应用于指定的窗口及其所有子窗口。当锁被设置后, GetDC 和 BeginPaint 函数返回一个显示设备上下文,其中有一个可见的空区域。在此情况下,应用程序可以继续在窗口中绘制,但所有输出都被裁剪。该锁一直存在,直到应用程序通过调用 LockWindowUpdate 将其清除,并为窗口指定 NULL 。虽然 LockWindowUpdate 强制窗口的可见区域为空,但该函数不会使指定的窗口不可见,也不会清除 WS_VISIBLE 样式位。
-
设置锁定后,应用程序可以使用 GetDCEx 函数(带有 DCX_LOCKWINDOWUPDATE 值)查询显示设备上下文,以便在锁定的窗口上绘制。这允许应用程序在处理键盘或鼠标消息时绘制跟踪矩形。当用户移动和调整窗口大小时,系统使用该方法。 GetDCEx 从显示设备上下文缓存中查询显示设备上下文,因此应用程序必须在绘制后尽快释放设备上下文。
-
当设置了窗口更新锁时,系统会为每个锁定的窗口创建一个累积的边界矩形。当锁被清除时,系统使用这个边界矩形来设置窗口及其子窗口的更新区域,最终强制 WM_PAINT 消息。如果累积的边界矩形为空(即如果在设置锁时没有发生绘图),则不设置更新区域。
12.1.13 累积边界矩形
-
累积边界矩形是包围受最近绘图操作影响的窗口或客户区部分的最小矩形。应用程序可以使用此矩形方便地确定绘图操作引起的更改的程度。它有时与 LockWindowUpdate 一起使用,以确定在清除更新锁后必须重新绘制客户区的哪一部分。
-
应用程序使用 SetBoundsRect 函数(指定 DCB_ENABLE )开始累积边界矩形。当应用程序使用指定的显示设备上下文时,系统随后为边界矩形累积点数。应用程序可以通过使用 GetBoundsRect 函数随时查询当前的边界矩形。应用程序通过再次调用 SetBoundsRect 并指定 DCB_DISABLE 值来停止累积。
12.2 使用 WM_PAINT 消息
- 可以使用 WM_PAINT 消息来执行显示信息所需的绘制。因为当窗口必须更新或当显式地请求更新时,系统会向应用程序发送 WM_PAINT 消息,所以可以在的应用程序的窗口程序中合并绘制代码。然后,只要应用程序必须绘制新的或现有的信息,就可以使用此代码。
12.2.1 在工作区中绘制
- 有关详细信息,请参阅在工作区中绘制。
12.2.2 重绘整个工作区
- 有关详细信息,请参阅重绘整个工作区。
12.2.3 在更新区域中重绘
- 有关详细信息,请参阅在更新区域中重绘。
12.2.4 使工作区失效
- 有关详细信息,请参阅使工作区失效。
12.2.5 绘制最小化窗口
- 有关详细信息,请参阅绘制最小化窗口。
12.2.6 绘制自定义窗口背景
- 有关详细信息,请参阅绘制自定义窗口背景。
12.3 使用 GetDC 函数
- 可以使用 GetDC 函数来执行必须立即发生的绘制,而不是在处理 WM_PAINT 消息时执行。这种绘制通常是对用户操作的响应,例如进行选择或用鼠标绘制。在这种情况下,用户应该得到即时的反馈,并且不能为了应用程序显示结果而被迫停止选择或绘制。
12.3.1 鼠标绘制
- 有关详细信息,请参阅鼠标绘制。
12.3.2 定时绘制
- 有关详细信息,请参阅定时绘制。
12.4 绘画和绘制参考
12.4.1 绘画和绘制函数
- 有关详细信息,请参阅绘画和绘制函数。
12.4.2 绘画和绘制结构体
- 有关详细信息,请参阅绘画和绘制结构体。
12.4.3 绘画和绘制消息
- 有关详细信息,请参阅绘画和绘制消息。
12.4.4 光栅操作代码
- 有关详细信息,请参阅光栅操作代码。