本节内容主要介绍 Windows 窗口创建 的第三步,如何编写窗口过程函数。窗口过程函数是一个 Windows 程序的重中之重,因为 Windows 窗口交互时基于消息的,而与 Windows 窗口交互的大部分工作都在窗口过程函数中完成。具体来说当 Windows 消息循环 中执行 DispathMessage 函数时,函数内部会调用咱们写好的窗口过程函数处理当前的 Windows 消息。

窗口过程函数的声明如下:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

函数有四个参数:

  • hwnd 该参数代表窗口句柄。

  • uMsg 是消息类型,例如 WM_SIZE 消息表示窗口大小发生变化。

  • wParam 和 lParam 这两个参数存储着消息的附加参数。具体内容和消息的类型息息相关。

LRESULT 是一个整形变量,应用程序在执行完窗口过程函数后通过该值将结果返回给 Windows。这个值包含了应用程序对具体消息的处理结果,不同的消息该值可能不同。

CALLBACK 是函数调用约定。窗口过程函数本质上是一个回调函数,调用者是操作系统。一个典型的窗口过程函数内部是一个巨大的选择/分支语句,根据不同的消息类型执行不同的代码逻辑。

switch (uMsg)
{
case WM_SIZE: // Handle window resizing

// etc

}

消息附加的数据存储在 lParam 和 wParam 中,二者都是一个整形,占据一个指针大小(32位或者64位)。因为根据消息的不同,两个参数的含义可能有所变化,如果你需要了解具体的信息,最好按照消息类型到 MSDN 查找相关资料。通常情况下该参数是一个数值或者是一个结构体指针,值得注意的是不是所有消息都包含附加数据。

举个例子,对于 WM_SIZE 消息:

  • wParam 是一个标记,代表着窗口的状态。包括最小化、最大化和调整大小三种类型。

  • lParam 包含窗口新的宽高,宽高都是16位的数值,通过位运算合并存储到一起。根据平台的不同你需要执行一些位移操作来获取它们。幸运的是,你不需要专门研究不同平台下具体的移动位数,因为在 WinDef.h 包含了一些宏可以帮助你完成这个操作。

窗口存储过程一般都包含很多消息的处理逻辑,随着程序功能的丰富函数会不断增长。解决的这个问题一种方法是你将每个消息的处理逻辑都单独用一个函数封装,窗口过程只是根据类型的不同调用它们,这种做法可以保持你窗口过程函数干净整洁。

例如对于 WM_SIZE 消息处理可以这样:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_SIZE:
        {
            int width = LOWORD(lParam);  // Macro to get the low-order word.
            int height = HIWORD(lParam); // Macro to get the high-order word.

            // Respond to the message:
            OnSize(hwnd, (UINT)wParam, width, height);
        }
        break;

    }
}

void OnSize(HWND hwnd, UINT flag, int width, int height)
{
    // Handle resizing
}

上面代码中的 LOWORD 和 HIWORD 两个宏可以从 lParam 参数中获取两个16位数,它们分别代表着窗口的宽和高。然后再将二者传递到 OnSize 函数执行具体的逻辑。

在窗口过程函数中,对于你不感兴趣的消息,你可以将消息参数传递给 DefWindowProc 函数进行处理。这个函数会执行这些消息的默认行为,具体代码如下:

return DefWindowProc(hwnd, uMsg, wParam, lParam);

当窗口过程被执行的时候,它会堵塞同一个线程创建的任何其它 Windows 消息。因此,你应该避免在窗口过程中执行过于复杂或者耗时较长的逻辑。例如,你的程序想使用 TCP 协议连接一个服务器,如果你把它放到窗口过程中,如果连接过程中耗时过长,会让你的程序 UI 陷入假死状态,在假死状态下鼠标、键盘、窗口重绘甚至关闭按钮都会无法响应。

为了避免类似事件的发生,你可以将耗时操作放到其他线程来执行,也可以使用异步执行接口。

目前常见的策略有以下几种:

  • 创建一个新的线程。

  • 使用一个线程池。

  • 使用异步I/O。

  • 使用异步执行方式。