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

C++中SendMessage与PostMessage的区别

2023-08-307.9k 阅读

一、函数基本概念与定义

在Windows编程环境下,SendMessagePostMessage是两个用于窗口间消息传递的重要函数,它们都是Windows API函数,在C++编程中被广泛应用于基于Windows的应用程序开发。

1. SendMessage函数

SendMessage函数的作用是将指定的消息发送到一个窗口或一组窗口。其函数原型如下:

LRESULT SendMessage(
  HWND   hWnd,
  UINT   Msg,
  WPARAM wParam,
  LPARAM lParam
);
  • hWnd:指定要接收消息的窗口句柄。如果此参数为HWND_BROADCAST,则消息将被发送到系统中的所有顶层窗口,包括无效或不可见的非自身拥有的窗口、被覆盖的窗口和弹出式窗口,但消息不被发送到子窗口。
  • Msg:指定被发送的消息。Windows系统定义了大量的消息常量,例如WM_PAINT用于窗口绘制,WM_CLOSE用于关闭窗口等。
  • wParam:指定附加的消息特定信息,其含义取决于所发送的消息。
  • lParam:指定附加的消息特定信息,其含义同样取决于所发送的消息。

SendMessage函数会一直等待,直到目标窗口处理完该消息才返回。也就是说,它是一种同步的消息发送方式。

2. PostMessage函数

PostMessage函数用于将一个消息放入(寄送)到与指定窗口创建的线程相联系消息队列里,不等待线程处理消息就返回。函数原型如下:

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

各参数含义与SendMessage函数的对应参数相同。

PostMessage函数只是将消息放入目标窗口所属线程的消息队列中,然后立即返回,不会等待消息被处理。这是一种异步的消息发送方式。

二、消息处理机制差异

  1. 同步与异步特性
    • SendMessage(同步)SendMessage是同步消息发送函数,它会阻塞当前线程,直到目标窗口的窗口过程处理完消息才返回。这就像是打电话,打电话的人(发送消息的线程)必须等待接电话的人(目标窗口的消息处理函数)回应后,才能继续做其他事情。例如,在一个图形界面应用程序中,如果主线程使用SendMessage发送一个WM_PAINT消息给某个子窗口进行重绘操作,主线程会一直等待子窗口完成重绘,在这个等待过程中,主线程无法处理其他任务,包括用户输入等。
    • PostMessage(异步)PostMessage是异步消息发送函数,它将消息放入目标窗口所属线程的消息队列后就立即返回。这类似于寄信,寄信的人(发送消息的线程)把信(消息)放入邮箱(消息队列)后,不需要等待对方收到信并阅读(消息被处理),就可以继续做其他事情。例如,同样是在图形界面应用程序中,使用PostMessage发送WM_PAINT消息给子窗口,主线程在发送消息后,马上可以继续执行后续代码,处理其他用户输入或执行其他任务,而子窗口会在其所属线程的消息循环中取出并处理该WM_PAINT消息。
  2. 消息队列的影响
    • SendMessage:由于SendMessage是同步发送,它并不通过常规的消息队列。当调用SendMessage时,系统会直接调用目标窗口的窗口过程函数来处理消息。这意味着SendMessage发送的消息不会在消息队列中排队等待,而是立即被处理。例如,假设有一个窗口A,其窗口过程函数中有一些复杂的计算任务。当另一个线程通过SendMessage向窗口A发送消息时,窗口A的窗口过程函数会立即执行,即使此时窗口A所属线程的消息队列中有其他消息,这些消息也会被暂时搁置,直到SendMessage发送的消息处理完毕。
    • PostMessagePostMessage将消息放入目标窗口所属线程的消息队列中。消息会按照放入的顺序在队列中排队等待处理。目标窗口所属线程的消息循环会不断从消息队列中取出消息并分发到相应的窗口过程函数进行处理。例如,在一个多窗口应用程序中,有窗口B和窗口C,窗口B通过PostMessage向窗口C发送多个消息,这些消息会依次进入窗口C所属线程的消息队列,窗口C的消息循环会按照队列顺序逐个处理这些消息。
  3. 消息处理的顺序
    • SendMessage:因为SendMessage是直接调用目标窗口的窗口过程,所以其发送的消息会在目标窗口的消息处理顺序中优先于通过消息队列的消息被处理。例如,假设目标窗口的消息队列中有一些WM_TIMER消息(定时器消息)等待处理,此时通过SendMessage发送了一个WM_COMMAND消息(命令消息),那么WM_COMMAND消息会先被处理,处理完后才会继续处理WM_TIMER消息。
    • PostMessagePostMessage发送的消息按照进入消息队列的顺序处理。如果在同一时间内,有多个消息通过PostMessage发送到同一个窗口的消息队列中,它们会按照发送的先后顺序依次被处理。

三、应用场景差异

  1. 需要立即响应的场景
    • SendMessage:在一些需要立即得到处理结果的场景中,SendMessage非常适用。例如,在一个文本编辑器应用程序中,当用户点击“全选”按钮时,程序需要立即选中编辑框中的所有文本。此时可以使用SendMessage向编辑框窗口发送EM_SETSEL消息,编辑框窗口会立即处理该消息并选中所有文本,发送消息的线程可以马上得到处理结果,进而可以继续执行后续操作,比如设置复制按钮为可用状态等。
    • PostMessagePostMessage由于是异步的,不适合这种需要立即响应的场景。如果使用PostMessage发送EM_SETSEL消息,发送线程无法立即得知编辑框是否已经选中了所有文本,可能会导致后续操作(如设置复制按钮状态)出现错误。
  2. 后台任务与低优先级任务
    • PostMessage:对于一些后台任务或者低优先级的任务,PostMessage是比较好的选择。例如,在一个音乐播放器应用程序中,当用户切换歌曲时,需要更新歌曲的播放进度条。可以使用PostMessage向进度条窗口发送更新消息,这样主线程在发送消息后可以继续处理其他更重要的任务,如解码音频数据等,而进度条窗口会在其所属线程的消息循环中处理更新消息,更新进度条显示。
    • SendMessage:如果使用SendMessage来处理这种情况,主线程会等待进度条窗口处理完更新消息才继续执行,这可能会影响音频解码等重要任务的实时性,导致音频播放出现卡顿等问题。
  3. 跨线程通信
    • SendMessage:在跨线程通信中,如果需要确保目标线程在处理完消息后才进行下一步操作,SendMessage可以满足需求。例如,一个主线程需要获取另一个工作线程中某个变量的值,主线程可以通过SendMessage向工作线程发送自定义消息,工作线程在收到消息后,将变量值通过消息的返回值或者lParam等参数传递给主线程。主线程等待工作线程处理完消息并返回结果,这样可以保证数据的一致性和准确性。
    • PostMessagePostMessage在跨线程通信中也很常用,特别是当不需要立即得到结果,只是通知目标线程执行某个任务时。例如,主线程需要通知工作线程开始一个长时间运行的计算任务,使用PostMessage发送启动任务的消息,主线程可以继续执行其他任务,而工作线程会在其消息循环中收到并处理该消息,开始计算任务。

四、返回值与错误处理差异

  1. SendMessage的返回值与错误处理
    • 返回值SendMessage函数的返回值是目标窗口处理消息后的返回值,其含义取决于所发送的消息。例如,对于WM_GETTEXT消息,返回值是拷贝到缓冲区中的字符数;对于WM_COMMAND消息,返回值通常表示命令的执行结果等。
    • 错误处理:如果SendMessage函数调用失败,返回值为0。可以通过调用GetLastError函数获取更详细的错误信息。例如:
LRESULT result = SendMessage(hWnd, WM_CUSTOM_MESSAGE, wParam, lParam);
if (result == 0) {
    DWORD error = GetLastError();
    // 根据error值进行相应的错误处理
}
  1. PostMessage的返回值与错误处理
    • 返回值PostMessage函数如果成功将消息放入消息队列,返回值为非零;如果失败,返回值为0。
    • 错误处理:与SendMessage类似,如果PostMessage返回0,表示消息发送失败,可以调用GetLastError获取错误信息。例如:
BOOL success = PostMessage(hWnd, WM_CUSTOM_MESSAGE, wParam, lParam);
if (!success) {
    DWORD error = GetLastError();
    // 根据error值进行相应的错误处理
}

但需要注意的是,PostMessage的错误可能由于多种原因,比如目标窗口句柄无效、消息队列已满等。

五、代码示例

  1. SendMessage示例
#include <windows.h>
#include <iostream>

// 窗口过程函数
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
    case WM_CREATE:
        std::cout << "Window created." << std::endl;
        break;
    case WM_COMMAND:
        std::cout << "WM_COMMAND message received." << std::endl;
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR szCmdLine, int iCmdShow) {
    static TCHAR szAppName[] = TEXT("SendMessageDemo");
    WNDCLASS wndClass;
    wndClass.style = CS_HREDRAW | CS_VREDRAW;
    wndClass.lpfnWndProc = WndProc;
    wndClass.cbClsExtra = 0;
    wndClass.cbWndExtra = 0;
    wndClass.hInstance = hInstance;
    wndClass.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
    wndClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndClass.lpszMenuName = nullptr;
    wndClass.lpszClassName = szAppName;

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

    HWND hWnd = CreateWindow(szAppName, TEXT("SendMessage Demonstration"),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        nullptr, nullptr, hInstance, nullptr);

    ShowWindow(hWnd, iCmdShow);
    UpdateWindow(hWnd);

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

    // 发送自定义消息
    HWND targetWnd = hWnd;
    UINT customMsg = WM_USER + 1;
    WPARAM wParam = 0;
    LPARAM lParam = 0;
    LRESULT result = SendMessage(targetWnd, customMsg, wParam, lParam);
    if (result == 0) {
        DWORD error = GetLastError();
        std::cout << "SendMessage failed with error: " << error << std::endl;
    }

    return msg.wParam;
}

在上述代码中,我们创建了一个简单的Windows窗口。在WinMain函数的末尾,通过SendMessage发送了一个自定义消息WM_USER + 1。目标窗口(即创建的主窗口)的窗口过程函数WndProc中可以添加对该自定义消息的处理逻辑。如果SendMessage调用失败,通过GetLastError获取错误信息并输出。

  1. PostMessage示例
#include <windows.h>
#include <iostream>

// 窗口过程函数
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
    case WM_CREATE:
        std::cout << "Window created." << std::endl;
        break;
    case WM_COMMAND:
        std::cout << "WM_COMMAND message received." << std::endl;
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR szCmdLine, int iCmdShow) {
    static TCHAR szAppName[] = TEXT("PostMessageDemo");
    WNDCLASS wndClass;
    wndClass.style = CS_HREDRAW | CS_VREDRAW;
    wndClass.lpfnWndProc = WndProc;
    wndClass.cbClsExtra = 0;
    wndClass.cbWndExtra = 0;
    wndClass.hInstance = hInstance;
    wndClass.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
    wndClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndClass.lpszMenuName = nullptr;
    wndClass.lpszClassName = szAppName;

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

    HWND hWnd = CreateWindow(szAppName, TEXT("PostMessage Demonstration"),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        nullptr, nullptr, hInstance, nullptr);

    ShowWindow(hWnd, iCmdShow);
    UpdateWindow(hWnd);

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

    // 发送自定义消息
    HWND targetWnd = hWnd;
    UINT customMsg = WM_USER + 1;
    WPARAM wParam = 0;
    LPARAM lParam = 0;
    BOOL success = PostMessage(targetWnd, customMsg, wParam, lParam);
    if (!success) {
        DWORD error = GetLastError();
        std::cout << "PostMessage failed with error: " << error << std::endl;
    }

    return msg.wParam;
}

此代码同样创建了一个简单的Windows窗口。在WinMain函数末尾,通过PostMessage发送了一个自定义消息WM_USER + 1。如果PostMessage调用失败,通过GetLastError获取错误信息并输出。与SendMessage示例不同的是,PostMessage发送消息后不会等待目标窗口处理消息就返回,而SendMessage会等待目标窗口处理完消息。

六、总结二者差异对程序设计的影响

  1. 性能方面
    • SendMessage:由于SendMessage是同步的,在某些情况下可能会影响程序的性能。例如,当目标窗口的消息处理函数执行时间较长时,发送消息的线程会被阻塞,导致整个程序看起来像是“卡住”了。特别是在主线程中使用SendMessage发送消息给处理时间长的窗口时,用户界面会失去响应,影响用户体验。因此,在使用SendMessage时,需要确保目标窗口的消息处理函数能够快速执行。
    • PostMessagePostMessage的异步特性使得发送消息的线程不会被阻塞,不会影响主线程的正常运行。这在处理一些后台任务或者不需要立即响应的任务时,对程序的整体性能影响较小。例如,在一个多媒体应用程序中,使用PostMessage发送一些更新界面元素(如音量图标)的消息,主线程可以继续处理音频或视频的播放,不会因为等待界面更新而出现卡顿。
  2. 程序逻辑复杂性
    • SendMessage:使用SendMessage时,由于消息的处理是同步的,程序逻辑相对简单。发送消息的线程可以依赖目标窗口处理完消息后的返回值进行下一步操作,代码的执行流程比较清晰。例如,在一个数据库管理应用程序中,主线程通过SendMessage发送消息给一个负责数据库查询的窗口,等待查询窗口返回查询结果后,主线程可以根据结果更新界面显示查询数据。
    • PostMessagePostMessage的异步特性增加了程序逻辑的复杂性。因为发送消息的线程在消息发送后就继续执行后续代码,无法立即得知消息的处理结果。这就需要在程序设计中引入其他机制来处理消息处理结果,比如使用事件、回调函数等。例如,在一个网络通信应用程序中,主线程通过PostMessage发送消息给一个负责网络连接的窗口,当网络连接成功或失败时,网络连接窗口需要通过其他方式(如发送自定义消息并携带连接结果)通知主线程,主线程再根据结果进行相应处理。
  3. 可靠性与稳定性
    • SendMessage:在一些对消息处理结果要求严格的场景下,SendMessage更可靠。因为它确保消息被处理后才返回,不会出现消息丢失的情况(除非目标窗口处理函数出现异常)。例如,在一个金融交易系统中,发送确认交易的消息必须确保目标窗口(负责交易处理的模块对应的窗口)正确处理并返回确认结果,否则可能导致交易数据不一致等严重问题。
    • PostMessagePostMessage虽然方便,但存在一定的消息丢失风险。如果目标窗口所属线程的消息队列已满,新发送的消息可能会被丢弃。此外,由于消息是异步处理的,在某些复杂的多线程环境下,可能会出现竞态条件等问题,影响程序的稳定性。例如,在一个多线程的游戏开发中,如果多个线程频繁使用PostMessage发送消息给游戏场景窗口进行渲染更新,可能会因为消息处理顺序的不确定性导致画面闪烁等问题。

通过深入理解SendMessagePostMessage的区别,开发者可以根据具体的应用场景和需求,选择合适的消息发送方式,从而开发出性能更好、逻辑更清晰、可靠性更高的Windows应用程序。无论是在简单的桌面应用程序还是复杂的大型系统中,正确运用这两个函数对于程序的质量和用户体验都有着至关重要的影响。