Windows
 
Windows消息机制初步探索
acmilan 2015-8-10 23:33:55
在Windows中,有两种应用程序,一种就是像普通C语言程序那样顺序执行的程序,另一种则是使用消息循环,基于消息的程序。Windows中的图形界面(GUI)程序都是基于消息的程序。

在阅读本文之前,先认识一下一个完整的Windows消息循环:
#include <windows.h>
                                
int main()
{
    MSG msg;
    while (GetMessageW(&msg, 0, 0, 0)) // 读取消息队列
    {
        TranslateMessage(&msg); // 转换键盘消息为字符消息
        DispatchMessageW(&msg); // 分发消息到窗口函数
    }
    return (int)msg.wParam; // 返回WM_QUIT返回值
}

一、线程消息

Windows进程都由若干个运行线程构成,main()函数也是一个线程,称作主线程。每个线程都有一个消息队列,Windows负责维护这个消息队列。

Windows消息有很多,下面列举几个常用的消息:
WM_CREATE 窗口创建
WM_PAINT 窗口绘图
WM_TIMER 计时器嘀嗒
WM_DESTROY 窗口关闭
WM_KEYDOWN 键盘按下
WM_CHAR 字符输入
WM_QUIT 退出消息循环

读取消息队列的方法是使用GetMessageW函数:
BOOL GetMessageW(&MSG结构, 限定窗口句柄, 限定消息下界, 限定消息上界);
该函数有四个参数,但是后三个参数都是限定参数,可以全部填0,这样就会接收全部的消息。
读取的消息被放在第一个参数指定的MSG结构中,MSG结构的定义如下:
typedef struct tagMSG {
    HWND        hwnd; // 窗口消息hwnd=窗口句柄,线程消息hwnd=0
    UINT        message; // 消息类型
    WPARAM      wParam; // 相关参数1
    LPARAM      lParam; // 相关参数2
    DWORD       time; // 消息产生时间
    POINT       pt; // 消息产生时鼠标指针位置
} MSG;

GetMessageW会等待消息,当消息队列中有消息时将消息队列中的消息读取到MSG中。GetMessageW当读取到一般消息时将返回1,而当读取到WM_QUIT消息时则返回0。WM_QUIT一般由PostQuitMessage产生,返回码保存在msg.wParam中。最简单的,基于消息的,什么也不做的程序如下:
#include <windows.h>
                               
int main()
{
    MSG msg;
    while (GetMessageW(&msg, 0, 0, 0)) // 读取消息队列
    {
        // 什么也不做
    }
    return (int)msg.wParam;
}

运行效果:
249497


这个程序就只是在等待消息的产生,然而并没有什么消息产生,因此将会无限运行下去,实际上并没有什么用。为此我们要新建另一个线程,模拟操作系统给我们的程序发送消息。

在Visual C++中提供了一个函数_beginthread用以新建线程,它在<process.h>中定义。使用方法如下:
HANDLE 线程句柄 = _beginthread(线程函数, 栈大小, 可选参数);
其中栈大小和可选参数都是可选的,一般填0就行了。重要的是线程函数。线程函数的形式是:
void thread1(void* 可选参数);

在线程中给另一个线程发送消息的方法是使用PostThreadMessageW:
BOOL PostThreadMessageW(线程ID, 消息, 相关参数1, 相关参数2);

于是我们的程序就成了这样:
#include <stdio.h>
#include <windows.h>
#include <process.h>
                               
DWORD mainthread; // 主线程ID
                               
void thread1(void*)
{
    Sleep(1000);
                               
    // 向主线程发送一个消息
    PostThreadMessageW(mainthread, WM_PAINT, 4, 5);
}
                               
int main()
{
    // 获得主线程的ID
    mainthread = GetCurrentThreadId();
                               
    // 创建另一个线程用以发送消息
    _beginthread(thread1, 0, 0);
                               
    MSG msg;
                               
    // 读取线程消息队列,后边三个参数都是限定条件,填0就可以
    while (GetMessageW(&msg, 0, 0, 0))
    {
        // 打印消息的详细信息
        printf("hWnd=%p message=%u wParam=%p lParam=%p, time=%u pt=(%d,%d)\n",
            msg.hwnd, msg.message, msg.wParam, msg.lParam,
            msg.time, msg.pt.x, msg.pt.y);
    }
    // 返回WM_QUIT的退出码
    return (int)msg.wParam;
}

运行效果:
249498


二、窗口消息

刚刚实验了一个线程消息,然而线程消息只是Windows消息机制中不常用的一部分,最常用的还是窗口消息。

窗口消息需要一个窗口。实际上,我们并不真的想要一个窗口,只是为了实验一下窗口消息而已。们要创建一个简单的不可见窗口。Windows中创建窗口的函数是CreateWindowExW,它有12个参数,功能各不相同,但是创建不可见窗口却只需要第二个参数,其它的参数全部填0即可:
HWND 窗口句柄 = CreateWindowExW(0, L"static", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);

发送一个窗口消息可以用PostMessageW,其用法和PostThreadMessageW几乎一样,只是第一个参数由线程ID换成了窗口句柄。

于是我们的程序变成了这样:
#include <stdio.h>
#include <windows.h>
#include <process.h>
                             
HWND vWindow; // 一个不可见的窗口
DWORD mainthread; // 主线程的ID
                             
// 新建的一个线程,模拟操作系统向程序发送消息
void thread1(void*)
{
    Sleep(1000);
                                     
    // 向主线程发送一个消息
    PostThreadMessageW(mainthread, WM_PAINT, 4, 5);
                           
    Sleep(1000);
                                     
    // 向窗口发送一个消息
    PostMessageW(vWindow, WM_PAINT, 2, 3);
}
                             
int main()
{
    // 获得主线程的ID
    mainthread = GetCurrentThreadId();
                             
    // 简单地创建一个不可见窗口
    vWindow = CreateWindowExW(0, L"static", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
                             
    // 创建另一个线程用以发送消息
    _beginthread(thread1, 0, 0);
                             
    MSG msg;
                             
    // 读取线程消息队列,后边三个参数都是限定条件,填0就可以
    while (GetMessageW(&msg, 0, 0, 0))
    {
        // 打印消息的详细信息
        printf("hWnd=%p message=%u wParam=%p lParam=%p, time=%u pt=(%d,%d)\n",
            msg.hwnd, msg.message, msg.wParam, msg.lParam,
            msg.time, msg.pt.x, msg.pt.y);
    }
    // 返回WM_QUIT的退出码
    return (int)msg.wParam;
}

运行效果如图,可以看见,第一个线程消息的hWnd参数为0,第二个窗口消息的hWnd参数不为0:
249493


三、分发窗口消息

每个窗口消息都有很多,不可能都在main()函数中处理,因此需要将获得的消息分发给各个窗口各自的处理函数,由它们进行处理。

不过这里有一个问题,我们新建窗口时并没有指定窗口函数。窗口函数通常由RegisterClassW函数在注册窗口类别时指定,并跟随窗口类而存在(见我的另一个帖子)。其实Windows也提供了一个机制,让我们事后也可以替换窗口函数,方法是使用SetWindowLongPtrW:
SetWindowLongPtrW(窗口句柄, GWLP_WNDPROC, (LONG_PTR)窗口函数);
窗口函数的形式如下:
LONG_PTR __stdcall WndProc(HWND hwnd, unsigned int msg, UINT_PTR wparam, LONG_PTR lparam);

分发窗口函数使用DispatchMessageW函数。此函数的用法非常简单:
DispatchMessageW(&MSG结构);
它的作用就是为我们调用对应窗口的WndProc窗口消息处理函数。

所以我们的程序变成了:
#include <stdio.h>
#include <windows.h>
#include <process.h>
                             
HWND vWindow; // 一个不可见的窗口
DWORD mainthread; // 主线程的ID
                             
// 新建的一个线程,模拟操作系统向程序发送消息
void thread1(void*)
{
    Sleep(1000);
                                     
    // 向主线程发送一个消息
    PostThreadMessageW(mainthread, WM_PAINT, 4, 5);
                           
    Sleep(1000);
                                     
    // 向窗口发送一个消息
    PostMessageW(vWindow, WM_PAINT, 2, 3);
}
                          
LONG_PTR __stdcall WndProc(HWND hwnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
    printf("hWnd=%p message=%u wParam=%p lParam=%p [WndProc]\n", hwnd, msg, wp, lp);
    return 0;
}
                          
int main()
{
    // 获得主线程的ID
    mainthread = GetCurrentThreadId();
                             
    // 简单地创建一个不可见窗口
    vWindow = CreateWindowExW(0, L"static", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
    // 替换该窗口的窗口函数
    SetWindowLongPtrW(vWindow, GWLP_WNDPROC, (LONG_PTR)WndProc);
                             
    // 创建另一个线程用以发送消息
    _beginthread(thread1, 0, 0);
                             
    MSG msg;
                             
    // 读取线程消息队列,后边三个参数都是限定条件,填0就可以
    while (GetMessageW(&msg, 0, 0, 0))
    {
        // 打印消息的详细信息
        printf("hWnd=%p message=%u wParam=%p lParam=%p, time=%u pt=(%d,%d)\n",
            msg.hwnd, msg.message, msg.wParam, msg.lParam,
            msg.time, msg.pt.x, msg.pt.y);
                          
        // 分发窗口消息到对应的WndProc
        DispatchMessageW(&msg);
    }
    // 返回WM_QUIT的退出码
    return (int)msg.wParam;
}

运行效果如下,可以看到在我们接到了发来的消息时,DispatchMessageW自动为我们调用了WndProc:
249491


四、直接将消息发送到WndProc


实际上,很多时候系统并不是为我们的线程发送了一个消息,而是直接发送到了窗口处理函数。Windows中直接将消息发送到窗口的处理函数的方法是使用SendMessageW。它的用法和PostMessageW类似,但是略有不同的是它的返回值:
LONG_PTR SendMessageW(窗口句柄, 消息, 相关参数1, 相关参数2);
它和PostMessageW的不同点还有,PostMessageW仅仅是将消息放入消息队列,并立即返回,而SendMessageW则会等待消息处理函数WndProc运行完毕再返回,并会带回WndProc的返回值。

SendMessageW的功能是:
1. 如果被调用窗口在同一个线程中,则SendMessageW会直接调用WndProc。
2. 如果在不同线程中,则SendMessageW会在目标下一次GetMessageW等待时安排调用WndProc,调用完成后函数返回。
3. 如果在不同进程中,则会在目标下一次GetMessageW等待状态时安排远程调用,并对相关参数进行进程间传递。

SendMessageW是一个很强大的函数,它可以在不同线程中『调用』窗口处理函数,甚至可以在不同进程间操作,系统会为我们做很多内部工作。使用SendMessageW函数对窗口进行各种操作,一般不用担心线程同步、进程间通信等诸多复杂问题。

我们现在在thread1中加入了SendMessageW函数:
#include <stdio.h>
#include <windows.h>
#include <process.h>
                              
HWND vWindow; // 一个不可见的窗口
DWORD mainthread; // 主线程的ID
                              
// 新建的一个线程,模拟操作系统向程序发送消息
void thread1(void*)
{
    Sleep(1000);
                                      
    // 向主线程发送一个消息
    PostThreadMessageW(mainthread, WM_PAINT, 4, 5);
                            
    Sleep(1000);
                                      
    // 向窗口发送一个消息
    PostMessageW(vWindow, WM_PAINT, 2, 3);
                         
    Sleep(1000);
                        
    // 直接『调用』窗口处理函数
    SendMessageW(vWindow, WM_PAINT, 6, 7);
}
                           
LONG_PTR __stdcall WndProc(HWND hwnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
    printf("hWnd=%p message=%u wParam=%p lParam=%p [WndProc]\n", hwnd, msg, wp, lp);
    return 0;
}
                           
int main()
{
    // 获得主线程的ID
    mainthread = GetCurrentThreadId();
                              
    // 简单地创建一个不可见窗口
    vWindow = CreateWindowExW(0, L"static", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
    // 替换该窗口的窗口函数
    SetWindowLongPtrW(vWindow, GWLP_WNDPROC, (LONG_PTR)WndProc);
                              
    // 创建另一个线程用以发送消息
    _beginthread(thread1, 0, 0);
                              
    MSG msg;
                              
    // 读取线程消息队列,后边三个参数都是限定条件,填0就可以
    while (GetMessageW(&msg, 0, 0, 0))
    {
        // 打印消息的详细信息
        printf("hWnd=%p message=%u wParam=%p lParam=%p, time=%u pt=(%d,%d)\n",
            msg.hwnd, msg.message, msg.wParam, msg.lParam,
            msg.time, msg.pt.x, msg.pt.y);
                           
        // 分发窗口消息到对应的WndProc
        DispatchMessageW(&msg);
    }
    // 返回WM_QUIT的退出码
    return (int)msg.wParam;
}

可以发现用SendMessageW发送的消息并没有被GetMessageW获取,而是直接调用了WndProc:
249490


五、转换键盘消息为字符消息

我们能在记事本中使用键盘输入字符,系统会自动将我们的按键转换为ASCII或Unicode字符。这一切不是由程序自行完成,也不是由系统自动完成的,而是由TranslateMessage这个函数完成的。TranslateMessage一般在GetMessageW之后DispatchMessageW之前调用,用以将WM_KEYDOWN等消息转换为WM_CHAR等字符消息。TranslateMessage的使用方法也很简单:
TranslateMessage(&MSG结构);

我们现在发送一个的WM_KEYDOWN(值是256)按键消息模拟键盘动作。它的相关参数1(wParam)就是虚拟键码(vkCode),是一种系统预定义的硬件无关的按键码。对于字母键来说,它的按键码就是大写字母的ASCII码,即按键A的虚拟键码就是'A'(值是0x41)。相关参数2(lParam)则是其它的参数,可以为0。比如我们可以这样发送一个键盘消息:
PostMessageW(vWindow, WM_KEYDOWN, 'A', 0);
发送后,主线程将会接收到这个消息,然后TranslateMessage,又会发送另一个消息,这个消息是WM_CHAR(值是258),相关参数1中包含有小写字母'a'的ASCII码(值是0x61)。由于我们的CapsLock键是关闭的,因此将会收到小写字母。

我们的程序如下:
#include <stdio.h>
#include <windows.h>
#include <process.h>
                               
HWND vWindow; // 一个不可见的窗口
DWORD mainthread; // 主线程的ID
                               
// 新建的一个线程,模拟操作系统向程序发送消息
void thread1(void*)
{
    Sleep(1000);
                                       
    // 向主线程发送一个消息
    PostThreadMessageW(mainthread, WM_PAINT, 4, 5);
                             
    Sleep(1000);
                                       
    // 向窗口发送一个消息
    PostMessageW(vWindow, WM_PAINT, 2, 3);
                          
    Sleep(1000);
                         
    // 直接『调用』窗口处理函数
    SendMessageW(vWindow, WM_PAINT, 6, 7);
                        
    Sleep(1000);
                         
    // 发送一个键盘消息WM_KEYDOWN(256),该消息将由TranslateMessage翻译为WM_CHAR(258)消息
    PostMessageW(vWindow, WM_KEYDOWN, 'A', 0);
}
                            
LONG_PTR __stdcall WndProc(HWND hwnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
    printf("hWnd=%p message=%u wParam=%p lParam=%p [WndProc]\n", hwnd, msg, wp, lp);
    return 0;
}
                            
int main()
{
    // 获得主线程的ID
    mainthread = GetCurrentThreadId();
                               
    // 简单地创建一个不可见窗口
    vWindow = CreateWindowExW(0, L"static", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
    // 替换该窗口的窗口函数
    SetWindowLongPtrW(vWindow, GWLP_WNDPROC, (LONG_PTR)WndProc);
                               
    // 创建另一个线程用以发送消息
    _beginthread(thread1, 0, 0);
                               
    MSG msg;
                               
    // 读取线程消息队列,后边三个参数都是限定条件,填0就可以
    while (GetMessageW(&msg, 0, 0, 0))
    {
        // 打印消息的详细信息
        printf("hWnd=%p message=%u wParam=%p lParam=%p, time=%u pt=(%d,%d)\n",
            msg.hwnd, msg.message, msg.wParam, msg.lParam,
            msg.time, msg.pt.x, msg.pt.y);
                        
        // 转换键盘消息为字符消息
        // 注释掉这句,程序将收不到WM_CHAR消息
        TranslateMessage(&msg);
                        
        // 分发窗口消息到对应的WndProc
        DispatchMessageW(&msg);
    }
    // 返回WM_QUIT的退出码
    return (int)msg.wParam;
}

当大写锁定CapsLock关闭时,运行效果如图,注意WM_CHAR(258)接收的字符为小写字母('a'=0x61):
249495


当大写锁定CapsLock打开时,运行效果,注意WM_CHAR(258)接收的字符变为大写字母('A'=0x61):
249496


可以发现当我们取到WM_KEYDOWN(256)消息时,我们在调用TranslateMessage之后,它又发送了一个WM_CHAR(258)消息,判断了大写键的状态,并正确地传递了此时应该键入的字符。

至此,一个完整的Windows消息循环就完成了。

六、发送退出消息WM_QUIT

GetMessageW当接到退出消息WM_QUIT时,会返回0,使得while循环退出,而一般的消息则返回1。所以发送WM_QUIT的结果是退出消息循环。
发送退出消息的方法有两种,一种是使用PostThreadMessageW(线程ID, WM_QUIT, 退出码, 0),这种方式可以在其它线程中使用。另一种方式是使用PostQuitMessage(退出码)。由于WM_QUIT并不是一个窗口消息,因此不要使用SendMessageW或PostMessageW来发送它。WM_QUIT的退出码放在msg.wParam中。

在其它线程中使用PostThreadMessageW发送WM_QUIT的示例:
#include <stdio.h>
#include <windows.h>
#include <process.h>
                                
HWND vWindow; // 一个不可见的窗口
DWORD mainthread; // 主线程的ID
                                
// 新建的一个线程,模拟操作系统向程序发送消息
void thread1(void*)
{
    Sleep(1000);
                                        
    // 向主线程发送一个消息
    PostThreadMessageW(mainthread, WM_PAINT, 4, 5);
                              
    Sleep(1000);
                                        
    // 向窗口发送一个消息
    PostMessageW(vWindow, WM_PAINT, 2, 3);
                           
    Sleep(1000);
                          
    // 直接『调用』窗口处理函数
    SendMessageW(vWindow, WM_PAINT, 6, 7);
                         
    Sleep(1000);
                          
    // 发送一个键盘消息WM_KEYDOWN(256),该消息将由TranslateMessage翻译为WM_CHAR(258)消息
    PostMessageW(vWindow, WM_KEYDOWN, 'A', 0);
                      
    Sleep(1000);
                                      
    // 发送WM_QUIT退出消息,返回值为2
    PostThreadMessageW(mainthread, WM_QUIT, 2, 0);
}
                             
LONG_PTR __stdcall WndProc(HWND hwnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
    printf("hWnd=%p message=%u wParam=%p lParam=%p [WndProc]\n", hwnd, msg, wp, lp);
    return 0;
}
                             
int main()
{
    // 获得主线程的ID
    mainthread = GetCurrentThreadId();
                                
    // 简单地创建一个不可见窗口
    vWindow = CreateWindowExW(0, L"static", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
    // 替换该窗口的窗口函数
    SetWindowLongPtrW(vWindow, GWLP_WNDPROC, (LONG_PTR)WndProc);
                                
    // 创建另一个线程用以发送消息
    _beginthread(thread1, 0, 0);
                                
    MSG msg;
                                
    // 读取线程消息队列,后边三个参数都是限定条件,填0就可以
    while (GetMessageW(&msg, 0, 0, 0))
    {
        // 打印消息的详细信息
        printf("hWnd=%p message=%u wParam=%p lParam=%p, time=%u pt=(%d,%d)\n",
            msg.hwnd, msg.message, msg.wParam, msg.lParam,
            msg.time, msg.pt.x, msg.pt.y);
                         
        // 转换键盘消息为字符消息
        // 注释掉这句,程序将收不到WM_CHAR消息
        TranslateMessage(&msg);
                         
        // 分发窗口消息到对应的WndProc
        DispatchMessageW(&msg);
    }
                       
    // 显示WM_QUIT的退出码(只有按Ctrl+F5才能看到)
    printf("WM_QUIT wParam=%d\n", msg.wParam);
                       
    // 返回WM_QUIT的退出码
    return (int)msg.wParam;
}

运行效果如下(按Ctrl+F5运行):
249499


使用PostQuitMessage的示例如下:
#include <stdio.h>
#include <windows.h>
#include <process.h>
                                
DWORD mainthread; // 主线程ID
                                
void thread1(void*)
{
    Sleep(1000);
                                
    // 向主线程发送一个消息
    PostThreadMessageW(mainthread, WM_PAINT, 4, 5);
}
                                
int main()
{
    // 获得主线程的ID
    mainthread = GetCurrentThreadId();
                                
    // 创建另一个线程用以发送消息
    _beginthread(thread1, 0, 0);
                                
    MSG msg;
                                
    // 读取线程消息队列,后边三个参数都是限定条件,填0就可以
    while (GetMessageW(&msg, 0, 0, 0))
    {
        // 打印消息的详细信息
        printf("hWnd=%p message=%u wParam=%p lParam=%p, time=%u pt=(%d,%d)\n",
            msg.hwnd, msg.message, msg.wParam, msg.lParam,
            msg.time, msg.pt.x, msg.pt.y);
                     
        // 如果接到了WM_PAINT消息则退出,退出码为25
        if (msg.message == WM_PAINT)
            PostQuitMessage(25);
    }
                     
    // 显示WM_QUIT的退出码(只有按Ctrl+F5才能看到)
    printf("WM_QUIT wParam=%d\n", msg.wParam);
                     
    // 返回WM_QUIT的退出码
    return (int)msg.wParam;
}

运行效果(按Ctrl+F5运行):
249500

[修改于 4 年前 - 2015-08-16 23:03:54]

2015-8-11 00:23:34
acmilan(作者)
1楼
Windows 消息机制初步探索 之 快捷键与非独占对话框

Windows中,快捷键(accelerators)将WM_KEYDOWN按键消息转换为WM_COMMAND菜单命令消息,使得用按键来模拟菜单命令变得十分方便。快捷键一般在Windows程序的资源文件(.rc)中定义。使用快捷键需要对消息循环进行一些修改。

Windows中,对话框分为独占对话框(modal dialog)和非独占对话框(modeless dialog)。对话框的模板也可在资源文件(.rc)中定义,并且Visual C++对其提供了可视化的设计器。独占对话框的消息由系统托管,而非独占对话框则需要对原有的消息循环进行修改。

为了做好下一步的实验,我们要首先要为我们的程序定义一个快捷键和对话框:
249501


建立的快捷键和对话框如图所示:
249504


其中我们需要对快捷键进行一些修改,将命令ID改为0x111,将键改为VK_RETURN(回车):
249523


然后在程序中导入resource.h中的资源符号:
// 导入资源符号
#include "resource.h"

一、快捷键的消息机制

在程序中,加载快捷键可以用以下方式:
HACCEL 快捷键句柄 = LoadAcceleratorsW(模块句柄, (wchar_t*)快捷键序号);
其中模块句柄可以为0,表示当前模块。

使用快捷键的方法是在消息循环中使用TranslateAcceleratorW。TranslateAcceleratorW读取到将WM_KEYDOWN消息,将会转换为WM_COMMAND菜单命令并分发,返回TRUE表示已处理。否则返回FALSE表示未处理。

TranslateAcceleratorW的作用是处理和分发快捷键消息,已处理的消息不需要再被分发。

TranslateAcceleratorW的用法如下:
BOOL 是否已处理 = TranslateAcceleratorW(窗口句柄, 快捷键句柄, &MSG结构);

加了快捷键以后的消息循环如下:
// 加载快捷键
hAccel = LoadAcceleratorsW(0, (wchar_t*)IDR_ACCELERATOR1);
                                   
MSG msg;
                                   
// 读取线程消息队列,后边三个参数都是限定条件,填0就可以
while (GetMessageW(&msg, 0, 0, 0))
{
    // 转换快捷键为WM_COMMAND(273)并分发,处理后的消息不需要再次分发!
    if (!TranslateAcceleratorW(vWindow, hAccel, &msg))
    {
        // 将键盘消息转换为字符消息
        TranslateMessage(&msg);
                                   
        // 将获取到的消息转发给WndProc
        DispatchMessageW(&msg);
    }
}
// 返回WM_QUIT的退出码
return (int)msg.wParam;

使用快捷键的示例如下:
#include <stdio.h>
#include <windows.h>
#include <process.h>
                                   
// 导入资源符号
#include "resource.h"
                                   
HWND vWindow; // 一个不可见的窗口
DWORD mainthread; // 主线程的ID
                                   
HACCEL hAccel; // 快捷键
HWND hDialog; // 非独占对话框
                                   
// 新建的一个线程,模拟操作系统向程序发送消息
void thread1(void*)
{
    Sleep(1000);
                                       
    // 发送一个hWnd为0的消息
    // 该消息只能由GetMessageW循环接收
    PostThreadMessageW(mainthread, WM_PAINT, 4, 5);
                                   
    Sleep(1000);
                                   
    // 发送一个hWnd不为0的消息
    // 该消息将由GetMessageW循环接收
    // 接收后可通过DispatchMessageW分发给WndProc消息处理函数
    PostMessageW(vWindow, WM_PAINT, 2, 3);
                                       
    Sleep(1000);
                                       
    // 直接调用WndProc消息处理函数
    // 该消息将不会被GetMessageW接收!
    SendMessageW(vWindow, WM_PAINT, 6, 7);
                                   
    Sleep(1000);
                                   
    // 发送键盘消息,如果调用了TranslateMessage,该窗口还将接收一个WM_CHAR(258)消息
    PostMessageW(vWindow, WM_KEYDOWN, 'A', 0);
                                   
    Sleep(1000);
                                   
    // 发送已定义快捷键的VK_RETURN键盘消息,将转换为WM_COMMAND(273),wParam=0x11111
    PostMessageW(vWindow, WM_KEYDOWN, VK_RETURN, 0);
}
                                   
LONG_PTR __stdcall WndProc(HWND hwnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
    printf("hWnd=%p message=%u wParam=%p lParam=%p [WndProc]\n",
        hwnd, msg,wp, lp);
    return 0;
}
                                   
int main()
{
    // 获得主线程的ID
    mainthread = GetCurrentThreadId();
                                   
    // 简单地创建一个不可见窗口,该窗口与主进程绑定
    vWindow = CreateWindowExW(0, L"static", 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0);
    // 替换该窗口的WndProc函数
    SetWindowLongPtrW(vWindow, GWLP_WNDPROC, (LONG_PTR)WndProc);
                                       
    // 加载快捷键
    hAccel = LoadAcceleratorsW(0, (wchar_t*)IDR_ACCELERATOR1);
                                       
                                   
    // 创建另一个线程用以发送消息
    _beginthread(thread1, 0, 0);
                                   
    MSG msg;
                                   
    // 读取线程消息队列,后边三个参数都是限定条件,填0就可以
    while (GetMessageW(&msg, 0, 0, 0))
    {
        // 打印消息的详细信息
        printf("hWnd=%p message=%u wParam=%p lParam=%p, time=%u pt=(%d,%d)\n",
            msg.hwnd, msg.message, msg.wParam, msg.lParam,
            msg.time, msg.pt.x, msg.pt.y);
                                           
        // 转换快捷键为WM_COMMAND(273)并分发,处理后的消息不需要再次分发!
        if (!TranslateAcceleratorW(vWindow, hAccel, &msg))
        {
            // 将键盘消息转换为字符消息
            TranslateMessage(&msg);
                                   
            // 将获取到的消息转发给WndProc
            DispatchMessageW(&msg);
        }
    }
                                   
    // 返回WM_QUIT的退出码
    return (int)msg.wParam;
}

运行效果如下,注意发送的WM_KEYDOWN(256)VK_RETURN(0xD)被转换为WM_COMMAND(2730001 / 0111
(wParam高四位表示命令类型,0是菜单,1是快捷键,其它的表示控件通知;低四位表示命令编号)
249524


二、非独占对话框

非独占对话框使用CreateDialogParamW函数创建,它返回一个窗口句柄:
HWND 对话框句柄 = CreateDialogParamW(模块句柄, (wchar_t*)对话框模板序号, 父窗口句柄, 对话框处理函数, 可选参数);
对话框处理函数DlgProc的形式如下,DlgProc对于已处理的消息返回TRUE,未处理的返回FALSE:
INT_PTR __stdcall DlgProc(HWND hwnd, unsigned int msg, UINT_PTR wparam, LONG_PTR lparam);
CreateDialogParamW不会立即显示对话框,需要使用ShowWindow(对话框句柄, SW_SHOW)显示。

日常生活中常见的对话框是『查找和替换』对话框,如图所示。和一般的独占式对话框不同,在打开非独占对话框时,主窗口仍然可以被访问,程序也会继续运行。
249511


在对话框中使用Tab等按键可以移动控件焦点,这是对话框的标准功能。但是传统TranslateMessage只能将键盘消息WM_KEYDOWN转换成字符消息WM_CHAR。为了将Tab等按键翻译成焦点移动动作,需使用IsDialogMessageW

IsDialogMessageW的作用是处理和分发对话框消息,已处理的消息不需要再被分发。

实际上,不只是对话框能用,其它的窗口如果想实现使用Tab等按键移动控件焦点,也可以使用这个函数来实现。

IsDialogMessageW的用法如下:
BOOL 是否已处理 = IsDialogMessageW(对话框句柄, &MSG结构);

加了IsDialogMessageW的消息循环如下:
// 加载快捷键
hAccel = LoadAcceleratorsW(0, (wchar_t*)IDR_ACCELERATOR1);
                                  
// 加载非独占对话框
hDialog = CreateDialogParamW(0, (wchar_t*)IDD_DIALOG1, vWindow, DlgProc, 0);
ShowWindow(hDialog, SW_SHOW); // 显示对话框
                                  
MSG msg;
                                  
// 读取线程消息队列,后边三个参数都是限定条件,填0就可以
while (GetMessageW(&msg, 0, 0, 0))
{
    // 如果对话框已加载,处理和分发对话框消息,处理后的消息不需要被再次分发!
    if (!hDialog || !IsDialogMessageW(hDialog, &msg))
    {
        // 转换快捷键为WM_COMMAND(273)并分发,处理过的消息不需要再次分发!
        if (!TranslateAcceleratorW(vWindow, hAccel, &msg))
        {
            // 将键盘消息转换为字符消息
            TranslateMessage(&msg);
                                  
            // 将获取到的消息转发给WndProc
            DispatchMessageW(&msg);
        }
    }
}
                                  
// 返回WM_QUIT的退出码
return (int)msg.wParam;

非独占对话框示例程序:
#include <stdio.h>
#include <windows.h>
#include <process.h>
                                  
// 导入资源符号
#include "resource.h"
                                  
HWND vWindow; // 一个不可见的窗口
DWORD mainthread; // 主线程的ID
                                  
HACCEL hAccel; // 快捷键
HWND hDialog; // 非独占对话框
                                  
// 新建的一个线程,模拟操作系统向程序发送消息
void thread1(void*)
{
    Sleep(1000);
                                      
    // 发送一个hWnd为0的消息
    // 该消息只能由GetMessageW循环接收
    PostThreadMessageW(mainthread, WM_PAINT, 4, 5);
                                  
    Sleep(1000);
                                  
    // 发送一个hWnd不为0的消息
    // 该消息将由GetMessageW循环接收
    // 接收后可通过DispatchMessageW分发给WndProc消息处理函数
    PostMessageW(vWindow, WM_PAINT, 2, 3);
                                      
    Sleep(1000);
                                      
    // 直接调用WndProc消息处理函数
    // 该消息将不会被GetMessageW接收!
    SendMessageW(vWindow, WM_PAINT, 6, 7);
                                  
    Sleep(1000);
                                  
    // 发送键盘消息,如果调用了TranslateMessage,该窗口还将接收一个WM_CHAR(258)消息
    PostMessageW(vWindow, WM_KEYDOWN, 'A', 0);
                                  
    Sleep(1000);
                                  
    // 发送已定义快捷键的VK_RETURN键盘消息,将转换为WM_COMMAND(273),wParam=0x11111
    PostMessageW(vWindow, WM_KEYDOWN, VK_RETURN, 0);
                                  
    Sleep(1000);
                                  
    // 发送Tab键到非独占对话框
    // 若在消息循环中调用了IsDialogMessageW,则会转换为一系列焦点移动消息
    // 否则只会转换为普通的字符消息
    PostMessageW(hDialog, WM_KEYDOWN, VK_TAB, 0);
}
                                  
LONG_PTR __stdcall WndProc(HWND hwnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
    printf("hWnd=%p message=%u wParam=%p lParam=%p [WndProc]\n",
        hwnd, msg,wp, lp);
    return 0;
}
                                  
INT_PTR __stdcall DlgProc(HWND hwnd, unsigned int msg, UINT_PTR wp, LONG_PTR lp)
{
    printf("hWnd=%p message=%u wParam=%p lParam=%p [DlgProc]\n",
        hwnd, msg, wp, lp);
    return FALSE;
}
                                  
int main()
{
    // 获得主线程的ID
    mainthread = GetCurrentThreadId();
                                  
    // 简单地创建一个不可见窗口,该窗口与主进程绑定
    vWindow = CreateWindowExW(0, L"static", 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0);
    // 替换该窗口的WndProc函数
    SetWindowLongPtrW(vWindow, GWLP_WNDPROC, (LONG_PTR)WndProc);
                                      
    // 加载快捷键
    hAccel = LoadAcceleratorsW(0, (wchar_t*)IDR_ACCELERATOR1);
                                      
    // 加载非独占对话框
    hDialog = CreateDialogParamW(0, (wchar_t*)IDD_DIALOG1, vWindow, DlgProc, 0);
    //ShowWindow(hDialog, SW_SHOW); // 不显示对话框,防止多余的消息产生
                                  
    // 创建另一个线程用以发送消息
    _beginthread(thread1, 0, 0);
                                  
    MSG msg;
                                  
    // 读取线程消息队列,后边三个参数都是限定条件,填0就可以
    while (GetMessageW(&msg, 0, 0, 0))
    {
        // 打印消息的详细信息
        printf("hWnd=%p message=%u wParam=%p lParam=%p, time=%u pt=(%d,%d)\n",
            msg.hwnd, msg.message, msg.wParam, msg.lParam,
            msg.time, msg.pt.x, msg.pt.y);
                                  
        // 如果对话框已加载,处理和分发对话框消息,处理后的消息不需要被再次分发!
        // 删掉这一句,则对话框不能使用Tab键移动焦点
        if (!hDialog || !IsDialogMessageW(hDialog, &msg))
        {
            // 转换快捷键为WM_COMMAND(273)并分发,处理过的消息不需要再次分发!
            if (!TranslateAcceleratorW(vWindow, hAccel, &msg))
            {
                // 将键盘消息转换为字符消息
                TranslateMessage(&msg);
                                  
                // 将获取到的消息转发给WndProc
                DispatchMessageW(&msg);
            }
        }
    }
                                  
    // 返回WM_QUIT的退出码
    return (int)msg.wParam;
}

程序运行结果如下,可以看到Tab键消息被IsDialogMessageW转换为了一系列的焦点移动消息:
249527

添加ShowWindow(hDialog, SW_SHOW)后,在显示的对话框中按Tab键,可以移动焦点:
249517


删除掉IsDialogMessageW(hDialog, &msg)调用时,运行结果如下,可以看到对话框仅接收到了WM_KEYDOWN和WM_CHAR消息:
249525

添加ShowWindow(hDialog, SW_SHOW)后,在显示的对话框中按Tab键,不能移动焦点
249516

[修改于 4 年前 - 2015-08-23 08:02:16]

acmilan(作者)
2楼
Windows 消息机制初步探索 之 总结

一、消息循环各函数

BOOL GetMessageW(&MSG结构, 0, 0, 0)
等待并获取消息到MSG结构,常规消息返回TRUE,WM_QUIT返回FALSE表示需要退出消息循环。

BOOL TranslateMessage(&MSG结构)
主要作用是将WM_KEYDOWN等键盘消息转换为WM_CHAR等字符消息并加到消息队列,允许在窗口中输入字符。

LONG_PTR DispatchMessageW(&MSG结构)
将窗口消息分发到各消息处理函数。

BOOL TranslateAcceleratorW(窗口句柄, 快捷键句柄, &MSG结构)
根据资源中的快捷键表,将WM_KEYDOWN等键盘消息翻译成对应的WM_COMMAND等菜单消息并分发。

BOOL IsDialogMessageW(对话框句柄, &MSG结构)
处理并分发对话框消息,允许使用Tab等按键移动焦点。

二、常用消息发送函数

BOOL PostThreadMessageW(线程ID, 消息, 相关参数1, 相关参数2)
发送线程消息到该线程的消息队列,该消息的hwnd=0。

BOOL PostMessageW(窗口句柄, 消息,  相关参数1, 相关参数2)
发送窗口消息到窗口所属线程的消息队列。

LONG_PTR SendMessageW(窗口句柄, 消息, 相关参数1, 相关参数2)
直接将消息发送到窗口的消息处理函数并等待返回,一般不用担心线程同步、进程间通信等问题。

PostQuitMessage(返回值)
向当前线程发送WM_QUIT消息,返回值储存在msg.wParam中。

-

[修改于 4 年前 - 2015-08-23 08:01:45]

2015-8-14 21:04:35
3楼
好,非常不错,通俗易懂。
2015-8-23 07:57:31
acmilan(作者)
4楼
万能消息循环(处理所有的快捷键、所有窗口的焦点移动)(需要C++11编译器并#include <unordered_set>)
MSG msg;
         
std::unordered_set<HACCEL> haccel_set;
haccel_set.insert(LoadAcceleratorsW(hInstance, (wchar_t*)IDR_ACCELERATOR1));
         
// 主消息循环: 
while (GetMessageW(&msg, 0, 0, 0))
{
    BOOL bOK = FALSE;
    for (HACCEL haccel : haccel_set)
    {
        bOK = bOK || TranslateAcceleratorW(msg.hwnd, haccel, &msg);
    }
    if (!bOK && !IsDialogMessageW(msg.hwnd, &msg))
    {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
}

[修改于 4 年前 - 2015-08-23 15:56:41]

想参与大家的讨论?现在就 登录 或者 注册

{{submitted?"":"投诉"}}
请选择违规类型:
{{reason.description}}
支持的图片格式:jpg, jpeg, png