Win32:窗口消息

在 GUI(图形用户界面)应用程序中,事件驱动编程是核心概念。应用程序需要响应用户和操作系统的事件,以提供交互性和动态行为。

事件驱动编程

在 GUI 应用程序中,程序的行为是由事件驱动的。事件可以分为两类:

用户事件

  • 用户与应用程序交互时触发的事件。例如:鼠标点击、键盘输入、触摸屏手势等。

系统事件

  • 操作系统或其他外部因素触发的事件。例如:硬件插入、电源状态变化、窗口大小调整等。

这些事件可以在程序运行过程中的任何时间以几乎任何顺序发生。

如何构建无法提前预测其执行流的程序?

为了解决此问题,Windows 使用消息传递模型。 操作系统通过向应用程序窗口传递消息来与其通信。 消息只是指定特定事件的数值代码。 例如,如果用户按下鼠标左键,窗口将收到一条消息,消息代码如下。

#define WM_LBUTTONDOWN    0x0201

某些消息具有与其关联的数据。 例如,WM_LBUTTONDOWN 消息包括鼠标光标的 x 坐标和 y 坐标。若要向窗口传递消息,操作系统将调用为该窗口注册的窗口过程。若要向窗口传递消息,操作系统将调用为该窗口注册的窗口过程。

消息传递模型的工作原理

在 Windows 中,消息传递模型的工作流程如下:

事件发生:用户或系统触发事件(如鼠标点击、键盘输入、窗口大小调整等)。

生成消息:操作系统将事件转换为消息(如 WM_LBUTTONDOWNWM_KEYDOWN 等)。

发送消息:操作系统将消息放入应用程序的消息队列中。

处理消息:应用程序从消息队列中获取消息,并将其分发给相应的窗口过程函数。

响应消息:窗口过程函数根据消息类型执行相应的操作。

消息循环

应用程序在运行时将收到数千条消息。 (考虑到每次击键和单击鼠标按钮都会生成一条消息。)此外,应用程序可以有多个窗口,每个窗口都有其自己的窗口过程。 程序如何接收所有这些消息并将其传递到正确的窗口过程? 应用程序需要一个循环来检索消息并将其调度到正确的窗口。

对于创建窗口的每个线程,操作系统都会为窗口消息创建队列。 此队列保存在该线程上创建的所有窗口的消息。 队列本身已隐藏在程序中。 不能直接操作队列。 但是,可以通过调用 GetMessage 函数从队列拉取消息。

MSG msg;
GetMessage(&msg, NULL, 0, 0);

GetMessage 是消息循环的核心函数,用于从消息队列中获取消息。它的原型如下:

BOOL GetMessage(
    LPMSG lpMsg,      // 指向 MSG 结构的指针
    HWND  hWnd,       // 窗口句柄(通常为 NULL,表示获取所有窗口的消息)
    UINT  wMsgFilterMin,  // 过滤消息的最小值
    UINT  wMsgFilterMax   // 过滤消息的最大值
);

此函数从队列的头中删除第一条消息。

GetMessage 的阻塞行为

如果消息队列为空,GetMessage 会阻塞,直到有新消息进入队列,这种阻塞行为不会导致程序无响应,因为程序在没有消息时不需要执行任何操作。如果程序需要执行后台处理(如文件下载、计算等),可以创建额外的线程,主线程继续运行消息循环,而其他线程在后台执行任务。

GetMessage 的第一个参数是 MSG 结构的地址。 如果函数成功,它将使用有关消息的信息填充 MSG 结构。 这包括目标窗口和消息代码。 通过其他三个参数,可以过滤从队列中获取的消息。 在几乎所有情况下,可以将这些参数设置为零。MSG 结构包含了消息的详细信息,但通常我们不会直接检查或操作这个结构。相反,我们会将 MSG 结构传递给以下两个函数来处理消息:

TranslateMessage(&msg); 
DispatchMessage(&msg);

TranslateMessage:该函数与键盘输入相关, 它将击键(按下按键,松开按键)转换为字符。 我们不必了解此函数的工作原理;只需记得在 DispatchMessage 之前调用它即可。

DispatchMessage:该函数告诉操作系统调用消息目标窗口的窗口过程。 换句话说,操作系统会在其窗口表中查找窗口句柄,找到与窗口关联的函数指针,并调用该函数。

例如,假设用户按下鼠标左键。 这会引发一连串事件:

  1. 操作系统在消息队列上放置 WM_LBUTTONDOWN 消息。

  2. 您的程序调用 GetMessage 函数。

  3. GetMessage 从队列中提取 WM_LBUTTONDOWN 消息,并填写 MSG 结构。

  4. 程序调用 TranslateMessageDispatchMessage 函数。

  5. DispatchMessag 中,操作系统调用您的窗口过程。

  6. 窗口过程可以响应消息或忽略它。

当窗口过程返回时,它将返回到 DispatchMessage。 这会返回到下一条消息的消息循环。 只要程序正在运行,消息就会继续到达队列。 因此,必须有一个循环,不断从队列中拉取消息并将其发送出去。 可以将循环视为执行以下操作:

while (GetMessage(&msg, NULL, 0,  0);)      
{
    TranslateMessage(&msg); 
    DispatchMessage(&msg);
}

当然,正如字面意思,此循环永远不会结束。 这就是 GetMessage 函数的返回值传入的位置。 通常,GetMessage 会返回非零值。 如果要退出应用程序并中断消息循环,请调用 PostQuitMessage 函数。

PostQuitMessage(0);

PostQuitMessage 函数在消息队列上放置 WM_QUIT 消息。 WM_QUIT 是一条特殊消息:它会导致 GetMessage 返回零,从而向消息循环的末尾发出信号。 下面是修订的消息循环。

MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

只要 GetMessage 返回非零值,则 while 循环中的表达式的计算结果为 true。 调用 PostQuitMessage 后,表达式变为 false,程序会中断循环。 下一个明显的问题是何时调用 PostQuitMessage。 我们将在《Win32:关闭窗口》中描述。

最后代码
#include <windows.h>
#include <stdio.h>
​
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) ;
​
int WINAPI wWinMain(
    HINSTANCE hInstance, 
    HINSTANCE hPrevInstance, 
    PWSTR pCmdLine, 
    int nCmdShow) 
{
    //注册窗口类
    const wchar_t CLASS_NAME[] = L"WolvenChan's Window Class";
    WNDCLASS wc = { 0 };
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;
    RegisterClass(&wc); 
​
    //创建窗口
    HWND hwnd = CreateWindowEx(
        0,
        CLASS_NAME,
        TEXT("Hello Wolven"),
        WS_OVERLAPPEDWINDOW,
        //坐标、窗口长、高
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL,
        NULL,
        hInstance,
        NULL
        );
​
    if (hwnd == NULL) {
        MessageBox(NULL, L"Failed to create window!", L"Error", MB_ICONERROR);
        return 0;
    }   
​
    ShowWindow(hwnd, nCmdShow);
    
    //消息循环
    MSG msg = { 0 };
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);  // 翻译消息
        DispatchMessage(&msg);   // 分发消息
    }
​
    return (int)msg.wParam;
​
    return 0;
 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值