通过上面的学习你以及了解了 如何创建一个窗口,假若你想在窗口中显示一些东西,那么你就需要掌握如何在窗口中执行绘制操作。在 Windows 开发术语中,该操作被称为窗口绘制(painting the window),隐喻为在一个空白的画布上进行涂抹填充的过程。

有些时候你程序本身逻辑会主动更新窗口的外观,而另一些时候,操作系统会被动通知你必须重新绘制窗口的一部分。这种情况下,操作系统会发送 WM_PAINT 消息到消息队列,其中被重新绘制的区域叫做更新区域(update region)。

窗口第一次显示的时候,客户区必须被绘制。因此当应用程序被显示的时候,你至少会收到一次 WM_PAINT 消息。

更新区域

你的程序负责绘制客户区域。图片中四周围起来的部分,包括标题默认情况下会由操作系统自动绘制。当完成客户区的绘制工作,清除更新区域,这会告诉操作系统在发生某些变化之前不需要再次发送 WM_PAINT 消息了。

现在假设用户移动窗口遮挡了你程序的一部分。当遮挡部分再次可见的时候,这部分区域会加入到更新区域,并通过 WM_PAINT 消息通知你的程序。

遮挡更新区域

除了上述情况,用户在伸缩窗口的时候也会触发窗口重绘。如下图所示,用户向右拉伸窗口,这个右侧新的扩展区域也会加入到更新区域中。

拉伸更新区域

在案例中的代码逻辑非常简单,它只是使用纯色填充整个更新客户区域,但是用来说明问题足够了。

switch(uMsg)
{
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc=BeginPaint(hwnd,&ps);
        // All painting occurs here, between BeginPaint and EndPaint.
        FillRect(hdc,&ps.rcPaint,(HBRUSH)(COLOR_WINDOW+1));
        EndPaint(hwnd,&ps);
    }
    return 0;
}

调用 BeginPaint 函数开始绘制操作。这个函数会将重绘信息填充到 PAINTSTRUCT 结构体中,结构体中的 rcPaint 成员就是当前需要重绘的区域。这个更新区域是相对于客户区来定义的:

更新坐标

在应用程序的重绘代码中,有两个常见的策略:

  • 一种策略是绘制整个客户区,不管操作系统传过来的更新区域的大小。任何在更新区域之外的内容都会被裁剪掉,也就是说操作系统会忽略它们。

  • 另一种是只绘更新区域的内容。

如果选择第一种策略,代码会很简单,反之选择第二种会让程序的效率更高,对于复杂的绘制逻辑优化效果会非常明显。

下面的代码会使用单一的颜色填充整个更新区域,使用的颜色是系统默认的窗口背景颜色(COLOR_WINDOW)。实际的颜色依赖于当前用户的配色方案。

FillRect(hdc,&ps.rcPaint,(HBRUSH)(COLOR_WINDOW+1));

FillRect 函数的内部细节对于例子来说不是很重要,但是该函数的第二个参数就是要填充的矩形坐标。在代码中,我们传入了整个更新区域。在窗口第一次收到 WM_PAINT 消息的时候,整个客户区都需要被重绘,所以 rcPaint 将包含整个客户区,而随后的 WM_PAINT 消息,rcPaint 参数内部包含的区域可能会小一些。

FillRect 函数是图形设备接口(GDI)的一部分,这套接口已经非常古老,在 Windows 7 以后的系统,微软推出了一个新的 2D 图形引擎,名字叫做 Direct2D。该引擎支持硬件加速等高性能的图形操作。

在绘制结束后,需要调用 EndPaint 函数。该函数会清除更新区域,并向 Windows 发送信号,通知它程序已经完成了窗口的绘制,在下次发送变换之前无需再次发送 WM_PAINT 消息。