MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SendMessage与PostMessage在Windows编程中的差异与应用

2023-10-314.0k 阅读

一、Windows 消息机制简介

在 Windows 编程中,消息机制是其核心架构之一。Windows 操作系统通过消息来通知应用程序发生的各种事件,这些事件可以是用户操作(如鼠标点击、键盘按键),也可以是系统事件(如窗口创建、销毁)。每个消息都有一个标识符(message identifier),用于指明消息的类型,同时可能携带一些额外的参数,以提供关于该事件的详细信息。

应用程序通过消息队列来接收这些消息。当一个事件发生时,Windows 会将对应的消息放入相关应用程序的消息队列中。应用程序的消息循环(通常是一个 while 循环)会不断从消息队列中取出消息,并将其发送到相应的窗口过程(window procedure)进行处理。窗口过程是一个回调函数,由应用程序定义,根据接收到的消息类型执行不同的操作。

例如,下面是一个简单的 Windows 程序消息循环的基本结构:

#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return msg.wParam;
}

在上述代码中,GetMessage 函数从消息队列中取出一条消息,TranslateMessage 函数将虚拟键消息转换为字符消息(例如将键盘按键消息转换为对应的字符),DispatchMessage 函数将消息发送到相应窗口的窗口过程进行处理。

二、SendMessage 函数详解

2.1 函数原型

SendMessage 函数的原型如下:

LRESULT SendMessage(
    HWND hWnd,
    UINT Msg,
    WPARAM wParam,
    LPARAM lParam
);
  • hWnd:指定接收消息的窗口句柄。如果此参数为 HWND_BROADCAST,则消息将被发送到系统中的所有顶层窗口,包括无效或不可见的非自身拥有的窗口、被覆盖的窗口和弹出式窗口,但消息不被发送到子窗口。
  • Msg:指定要发送的消息。这是一个预定义的消息标识符,例如 WM_PAINT(窗口重绘消息)、WM_LBUTTONDOWN(鼠标左键按下消息)等。
  • wParamlParam:包含附加的消息特定信息。具体含义取决于 Msg 参数所指定的消息。

2.2 工作原理

SendMessage 函数会直接调用指定窗口的窗口过程来处理消息,它不会将消息放入消息队列,而是立即执行窗口过程。这意味着调用 SendMessage 的线程会被阻塞,直到窗口过程处理完该消息并返回结果。只有在窗口过程返回后,SendMessage 才会返回,调用线程才能继续执行后续代码。

这种机制确保了消息处理的及时性和同步性。例如,当你需要立即获取某个窗口的状态信息时,使用 SendMessage 是非常合适的,因为你可以在函数返回后直接使用窗口过程返回的结果。

2.3 示例代码

下面是一个使用 SendMessage 获取编辑框文本的示例:

#include <windows.h>
#include <iostream>

// 窗口过程函数
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT("SendMessageDemo");
    HWND hwnd;
    MSG msg;
    WNDCLASS wndclass;

    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szAppName;

    if (!RegisterClass(&wndclass))
    {
        MessageBox(NULL, TEXT("This program requires Windows NT!"),
                   szAppName, MB_ICONERROR);
        return 0;
    }

    hwnd = CreateWindow(szAppName, TEXT("SendMessage Example"),
                        WS_OVERLAPPEDWINDOW,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        NULL, NULL, hInstance, NULL);

    ShowWindow(hwnd, iCmdShow);
    UpdateWindow(hwnd);

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

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HWND hEdit;
    switch (message)
    {
    case WM_CREATE:
        hEdit = CreateWindow(TEXT("EDIT"), NULL,
                             WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT,
                             10, 10, 200, 20, hwnd, (HMENU)0, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
        break;
    case WM_COMMAND:
        if (LOWORD(wParam) == 1) // 假设按钮 ID 为 1
        {
            TCHAR szText[256];
            SendMessage(hEdit, WM_GETTEXT, sizeof(szText) / sizeof(TCHAR), (LPARAM)szText);
            szText[SendMessage(hEdit, WM_GETTEXTLENGTH, 0, 0) + 1] = TEXT('\0');
            MessageBox(hwnd, szText, TEXT("Edit Box Text"), MB_OK);
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
    return 0;
}

在上述代码中,当用户点击按钮(假设按钮 ID 为 1)时,通过 SendMessage 向编辑框发送 WM_GETTEXT 消息来获取编辑框中的文本内容,并通过 MessageBox 显示出来。

三、PostMessage 函数详解

3.1 函数原型

PostMessage 函数的原型如下:

BOOL PostMessage(
    HWND hWnd,
    UINT Msg,
    WPARAM wParam,
    LPARAM lParam
);

其参数含义与 SendMessage 基本相同。hWnd 是接收消息的窗口句柄,Msg 是要发送的消息标识符,wParamlParam 是消息的附加参数。

3.2 工作原理

PostMessage 函数将消息放入指定窗口的消息队列中,然后立即返回,不会等待消息被处理。这意味着调用 PostMessage 的线程不会被阻塞,它可以继续执行后续代码。消息会在应用程序的消息循环从消息队列中取出并处理时,才会调用相应窗口的窗口过程。

由于 PostMessage 不会等待消息处理完成,它适用于一些不需要立即得到结果的操作,例如发送一个通知消息,告知窗口某个事件发生了,而调用方不关心窗口如何处理该事件。

3.3 示例代码

以下是一个使用 PostMessage 发送自定义消息的示例:

#include <windows.h>
#include <iostream>

#define WM_MYUSERMESSAGE (WM_USER + 1)

// 窗口过程函数
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT("PostMessageDemo");
    HWND hwnd;
    MSG msg;
    WNDCLASS wndclass;

    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szAppName;

    if (!RegisterClass(&wndclass))
    {
        MessageBox(NULL, TEXT("This program requires Windows NT!"),
                   szAppName, MB_ICONERROR);
        return 0;
    }

    hwnd = CreateWindow(szAppName, TEXT("PostMessage Example"),
                        WS_OVERLAPPEDWINDOW,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        NULL, NULL, hInstance, NULL);

    ShowWindow(hwnd, iCmdShow);
    UpdateWindow(hwnd);

    // 发送自定义消息
    PostMessage(hwnd, WM_MYUSERMESSAGE, 0, 0);

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

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_MYUSERMESSAGE:
        MessageBox(hwnd, TEXT("Custom message received"), TEXT("Info"), MB_OK);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
    return 0;
}

在上述代码中,我们定义了一个自定义消息 WM_MYUSERMESSAGE(基于 WM_USER 自定义)。在 WinMain 函数中,通过 PostMessage 发送这个自定义消息,窗口过程在接收到该消息时,弹出一个消息框显示提示信息。

四、SendMessage 与 PostMessage 的差异

4.1 同步与异步

  • SendMessage:是同步操作。调用线程会被阻塞,直到目标窗口的窗口过程处理完消息并返回结果。这保证了消息处理的即时性,但如果窗口过程处理消息的时间较长,调用线程将被长时间阻塞,可能导致界面卡顿。
  • PostMessage:是异步操作。调用函数后立即返回,消息被放入消息队列,由消息循环在适当的时候取出并处理。调用线程不会被阻塞,可以继续执行其他任务,但无法立即获取消息处理的结果。

4.2 消息队列

  • SendMessage:不经过消息队列,直接调用窗口过程。这意味着即使消息队列已满或出现其他与消息队列相关的问题,SendMessage 仍然可以正常工作。
  • PostMessage:将消息放入消息队列。如果消息队列已满,PostMessage 可能会失败并返回 FALSE。在一些情况下,例如系统资源紧张时,消息队列可能会出现问题,影响 PostMessage 的正常使用。

4.3 适用场景

  • SendMessage:适用于需要立即获取操作结果的场景,比如获取窗口的属性、状态等信息。例如,当你需要获取一个编辑框中的文本内容,并且在获取后立即进行后续处理时,使用 SendMessage 发送 WM_GETTEXT 消息是合适的选择。
  • PostMessage:适用于不需要立即得到结果,或者希望在后台处理消息的场景。例如,当你希望通知某个窗口进行一些耗时操作(如文件加载、数据处理),而不希望调用线程等待操作完成时,使用 PostMessage 发送相应的通知消息是比较好的方式。

4.4 返回值

  • SendMessage:返回值是窗口过程处理消息后的返回结果。这个返回值根据不同的消息类型有不同的含义,调用者可以根据返回值进行相应的处理。
  • PostMessage:返回值表示消息是否成功放入消息队列。如果返回 TRUE,表示消息成功放入队列;如果返回 FALSE,表示消息放入队列失败,可能是因为消息队列已满或窗口句柄无效等原因。

五、实际应用中的注意事项

5.1 跨线程通信

在多线程编程中,如果需要在不同线程之间进行消息传递,使用 PostMessage 通常更为安全。因为 SendMessage 的同步特性可能导致线程死锁。例如,如果线程 A 调用 SendMessage 向线程 B 拥有的窗口发送消息,而线程 B 此时正在等待线程 A 的某个资源,就可能发生死锁。而 PostMessage 的异步特性可以避免这种情况,因为它不会阻塞调用线程。

然而,在跨线程使用 PostMessage 时,需要注意窗口句柄的有效性。如果在一个线程中创建了窗口,在另一个线程中使用 PostMessage 向该窗口发送消息,要确保窗口句柄在发送消息时仍然有效。可以通过一些机制,如将窗口句柄存储在共享数据结构中,并进行适当的同步操作来保证其有效性。

5.2 消息优先级

Windows 消息队列中的消息是有优先级的。一些系统消息(如 WM_PAINT)具有较高的优先级,会优先被处理。PostMessage 发送的消息会按照其优先级顺序在消息队列中排队。而 SendMessage 由于不经过消息队列,不受消息队列优先级的影响。

在实际应用中,如果需要发送一些紧急的消息,希望其能尽快被处理,可以考虑将消息的优先级设置得较高(通过自定义消息并设置相关参数),然后使用 PostMessage 发送。但要注意,如果过度提高某些消息的优先级,可能会导致其他低优先级消息长时间得不到处理,影响程序的整体性能。

5.3 性能考虑

在性能方面,SendMessage 由于阻塞调用线程,可能会影响程序的响应速度,特别是当窗口过程处理消息时间较长时。而 PostMessage 虽然不会阻塞调用线程,但如果频繁发送大量消息,可能会导致消息队列堆积,影响消息处理的效率。

因此,在选择使用 SendMessage 还是 PostMessage 时,需要根据具体的应用场景和性能需求进行权衡。如果某个操作对及时性要求不高,且希望避免阻塞调用线程,可以优先考虑 PostMessage;如果需要立即得到操作结果,并且窗口过程处理消息的时间较短,SendMessage 可能是更好的选择。

5.4 兼容性

在不同版本的 Windows 操作系统中,SendMessagePostMessage 的行为基本保持一致,但在一些特殊情况下可能会有细微差别。例如,在处理某些系统消息时,不同版本的 Windows 可能对消息的处理方式略有不同。因此,在编写跨版本兼容的程序时,需要对这些差异进行充分的测试和处理。

同时,对于一些老旧的 Windows 操作系统,可能存在消息队列容量有限等问题,这在使用 PostMessage 时需要特别注意,以确保消息能够成功放入队列并被处理。

六、总结二者差异及应用场景选择总结

通过前面的详细介绍,我们清楚地了解了 SendMessagePostMessage 在 Windows 编程中的差异及应用场景。SendMessage 的同步特性使其适用于需要即时获取结果的操作,而 PostMessage 的异步特性则使其在不需要立即得到结果或希望在后台处理消息的场景中表现出色。

在实际应用中,我们需要根据具体的需求来选择合适的函数。对于一些简单的通知类消息,或者希望在后台进行处理的任务,使用 PostMessage 可以提高程序的响应性和整体性能;而对于需要立即获取窗口状态或执行一些依赖返回结果的操作,SendMessage 则是更好的选择。

同时,在多线程编程、消息优先级管理、性能优化以及兼容性处理等方面,我们都需要充分考虑这两个函数的特性,以编写高效、稳定且兼容的 Windows 应用程序。希望通过本文的介绍,读者能够更加熟练地运用 SendMessagePostMessage,在 Windows 编程中实现更加丰富和强大的功能。