显示窗口 ShowWindow function (winuser.h)
在前文中介绍了创建窗口的过程,注册窗口类,创建窗口并返回一个窗口句柄 hWnd
,这个窗口句柄 hWnd
可以在内存中找到一块保存着窗口各种信息的内存(大小、位置等信息),创建完窗口后并不能看见窗口,必须还要在屏幕上进行绘制,这时候需要调用 show(hWnd, SW_SHOW);
函数。第一个参数即窗口的句柄,第二个参数显示窗口的方式,它可以是由 WinMain()
窗口主函数传进来的参数 nCmdShow
的值,也可以是自己指定的值:
SW_SHOW
:按照创建窗口时的参数原样显示。SW_SHOWNORMAL
:显示窗口并激活它,窗口的大小和位置由操作系统确定。SW_SHOWMAXIMIZED
:显示窗口并将其最大化。SW_SHOWMINIMIZED
:显示窗口并将其最小化。- … …
在显示窗口之后最好更新一下窗口 UpdateWindow(hWnd);
。
Sets the specified window’s show state.
1 | BOOL ShowWindow( |
消息循环
1 | //消息循环 |
MSG 是一个结构体。
GetMessage() 到消息队列中抓取消息。
TranslateMessage() 翻译消息。
DispatchMessage() 派发消息给窗口的处理函数。
消息基础
消息的组成 (在 Windows 系统上):
- 窗口句柄 hWnd:有了窗口句柄就可以知道该消息属于哪个窗口。
- 消息ID:消息的ID用来唯一标识一个消息。
- 消息的两个参数(两个附带信息):
lParam
,wParam
附带信息,不同类型的消息附带信息的含义有所不同。 - 消息产生的时间
- 消息产生时的鼠标位置
消息的作用:当系统通知窗口工作时,就采用消息的方式派发给窗口(窗口的消息处理函数 WndProc()
, 每个窗口都应有一个窗口消息处理函数,默认处理函数 DefWindowProc()
参见前文)。
Windows系统上,消息是一个结构体,用来保存消息的各种信息:
MSG 结构体的定义 MSG structure (winuser.h)
Contains message information from a thread’s message queue.
1 | typedef struct tagMSG { |
POINT structure 结构体 (windef.h)
在 MSG 结构体中封装了 POINT 结构体成员变量 pt,它用来保存鼠标位置坐标(x, y)。
The POINT structure defines the x- and y-coordinates of a point.
1 | typedef struct tagPOINT { |
The POINT structure is identical完全相同的
to the POINTL structure.
DispatchMessage() 如何将消息派发给窗口的处理函数
在 MSG 结构体中有一个成员变量 hWnd
窗口句柄,通过窗口句柄可以找到一块内存,内存中保存着各种数据(这些数据是窗口类和CreateWindow函数提供的),从这块窗口信息的内存中找到该窗口的消息处理函数 WndProc()
,并调用该函数进行消息处理。WndProc()
函数是我们自己写的,所以也就回到了我们自己定义的消息处理代码中。窗口处理函数也可以指定为默认的 DefWindowProc()
,它由微软提供,只是对于特定消息,它不能做出我们想要的处理效果。
派发消息伪代码:
调用 WndProc()
时传入了参数 msg.hWnd
窗口句柄,msg.message
消息ID(微软的消息ID总共有几千个,其本质是一个数字,用宏定义#define定义了别名,用来唯一标识一个消息),并传入了两个附带信息。
窗口处理函数
自定义的消息处理函数必须按照以下格式来:(函数名可以改,参数名可以改)
1 | // 窗口过程函数 |
对于不同的消息,可以用 switch() 语句来做不同的处理:
1 | switch (uMsg) |
GetMessage() function (winuser.h)
GetMessage()
从消息队列中获取消息。
1 | BOOL GetMessaeg( |
while ( GetMessage(&msg, NULL, 0, 0))
GetMessage() 函数的返回值作为 while 循环是否继续执行下去的条件,如果窗口关闭了但进程仍未结束意味着进程并没有退出 while 循环。注意 GetMessage() 函数只抓取本进程的消息,不会抓取其他进程的消息。
GetMessage() 函数的返回值:
- 当抓到的消息不是
WM_QUIT
的时候,返回值为非零。 - 当抓到的消息是
WM_QUIT
的时候,返回值为零。 - 出错的时候返回值是 -1。
尽量不要使用 while (GetMessage())... ...
这种形式,用下面的形式:
1 | BOOL bRet; |
或者:
1 | //message bump |
之前的文章中提到,当窗口关闭时,进程仍然没有结束,需要添加以下代码:
1 | LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam) |
当窗口关闭的消息产生时,调用 PostQuitMessage(0);
,它会往消息队列中发送一个 WM_QUIT
消息,当 GetMessage()
函数抓到 WM_QUIT
消息时返回值为0,从而结束 while
循环。
TranslateMessage() function
1 | BOOL TranslateMessage( CONST MSG *lpMsg ); |
翻译消息,将按键消息,翻译成字符消息。
- 检查消息是否为按键消息,如果不是按键消息,不做任何处理,继续执行。
TranslateMessage() 函数只翻译可见字符消息,像 a~z 这是可见字符消息,像 上下左右 箭头按键是不可见字符消息。因为字母有大小写之分,所以要 TranslateMessage() 翻译消息。游戏角色的 WASD
移动不需要翻译,打字输入文本需要区分大小写,需要翻译。
GetMessage() 函数抓取的消息非常多,包括鼠标消息等等,只有是键盘消息的情况下才会进行翻译。
常见消息
所有的消息都是使用 #define 进行定义的:
每个消息都有一个数值,0x
表示十六进制数(消息的数值ID)。
WM_DESTROY
产生时间,窗口被销毁时的消息(不等于窗口被关闭),一般用法(不是必须用法)常用于窗口被销毁前,做相应的善后处理,例如资源、内存等。
WM_CLOSE
窗口关闭按钮被点击时产生。
WM_SYSCOMMAND
产生时间:当点击窗口的最大化、最小化、关闭等时。
附带信息:
- wParam:具体点击的位置,例如关闭SC_CLOSE等
- lParam:鼠标光标的位置。lParam 是一个占 4个字节 的长整型数据,低16位保存 x 坐标,高16位保存 y 坐标。
- LOWORD(lParam); //水平位置
- HIWORD(lParam); //垂直位置
一般用法:常用在窗口关闭时,提示用户处理。
1 | LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam) |
1 | case WM_CLOSE: |
WM_CREATE
在窗口创建成功还未显示时,产生的消息。
附带信息:
- wParam :为 0;
- lParam :为 CREATESTRUCT 类型的指针,通过这个指针可以获取 CreateWindowEx 函数的全部 12 个参数的信息。
一本用法:常用于初始化窗口的参数、资源等等,包括创建子窗口等。
在创建窗口的时候,最后参数加一个附加消息:
1 | char extraMsg[20] = "Extra Message"; |
消息处理函数中加入对 WM_CREATE
消息的处理:
1 | case WM_CREATE: |
OnCreate 函数的内容如下:
1 | void OnCreate(HWND hWnd, LPARAM lParam) |
运行后,在窗口还没有显示之前,弹出了一个提示框,并将创建窗口时最后一个参数的附带信息显示了出来。
WM_SIZE
在窗口的大小发生变化的时候产生。
附加信息:
- wParam :窗口大小发生变化的原因。
- lParam :窗口变化后的大小。
- LOWORD(lParam) //低16位表示变化后的宽度
- HIWORD(lParam) //高16位表示变化后的高度
一般用法:常用于窗口的大小变化后,调整窗口各个部分的布局。
添加DOS窗口:
图形界面的窗口并不能让我们知道程序在运行过程中发生了什么,为了方便调试,可以为窗口添加一个DOS窗口,通过往DOS窗口中打印信息进行反馈从而知道程序运行过程中发生了什么。
1 | //添加全局变量 |
测试 WM_SIZE 消息:
1 | //switch语句中添加: |
每次窗口发生变化的时候在DOS窗口中打印信息:
WM_QUIT
产生时间:程序员发送。
附带信息:
- wParam :PostQuitMessage() 函数传递的参数。
- lParam :0。
一般用法:用于结束消息循环,当 GetMessage() 抓到 WM_QUIT 消息后,返回 False , 结束 while 循环。
消息循环的原理
消息循环的阻塞 : GetMessage()函数 VS PeekMessage() 函数
- GetMessage() 从系统获取消息,将消息从系统中移除,阻塞函数。当系统无消息的时候会等候消息。当消息队列中没有消息时,GetMessage() 函数会阻塞,也就意味着程序停在 GetMessage() 这里。只有有消息时才会继续执行下去。程序阻塞时不再参与 CPU 的调度,进入睡觉状态。如果消息队列中经常没有消息,意味着程序经常进入阻塞状态,这样的程序效率并不是很高。对于我们人的感觉来说,消息是每时每刻都有的,但对于高速运转的 CPU 来说,消息并不常有,意味着程序经常阻塞(对CPU来说)。
- PeekMessage() 以查看的方式从系统中获取消息,可以不将消息从系统中移除,非阻塞函数。当系统无消息时,返回 False,继续执行后面的代码。
- 函数定义:
1
2
3
4
5
6
7
8BOOL PeekMessage(
LPMSG lpMsg, //MSG结构体指针
HWND hWnd, //窗口句柄 handle to window
UINT wMsgFilterMin, //消息ID的下限
UINT wMsgFilterMax, //消息ID的上限
UINT wRemoveMsg, //移除标识,是否从消息队列中移除消息
// PM_REMOVE / PM_NOREMOVE
);
- 函数定义:
更加高效地抓取消息
1 | ... WinMain(...) { |
从DOS窗口打印的结果来看,进程绝大多数时间都处于没有消息的状态:
发送消息 : SendMessage()函数 VS PostMessage() 函数
消息如何产生的?
消息的产生:
- 操作系统进程发送
- 程序员编写的程序的进程发送
在Windows操作系统上,几乎所有的消息都是以下这两个函数发送的:
- SendMessage() :发送消息,会等候消息的处理结果。
- PostMessage() :投递消息,消息发出后立即返回,不等候消息执行结果。
1 | BOOL SendMessage / PostMessage( |
包括程序退出发出 WM_QUIT
消息时调用的 PostQuitMessage();
函数内部,也是调用了上面的 PostMessage() 函数。
1 | case WM_CLOSE: //窗口关闭 |
这两个函数都是 Windows API 提供的函数,用于在应用程序之间或者在同一个应用程序的不同窗口之间发送消息。
SendMessage() 函数会同步发送消息,即函数调用会一直等待接收方处理完消息后才返回。这使得消息的发送和接收在时间上是连续的,适用于需要确保消息被及时处理的场景。
PostMessage() 函数则是异步发送消息,即函数调用后立即返回,不等待接收方处理完消息。这使得消息的发送和接收在时间上是分离的,适用于不需要立即响应的场景。
使用 SendMessage()
函数发送 WM_QUIT
消息,程序并没有成功退出,那么该函数把 WM_QUIT
消息发送到哪了呢?参见后文。
消息分类
- 系统消息:微软官方定制好的消息,在操作系统内部,可以直接使用。ID范围:
0
~0x03FF
。 - 用户自定义消息:如果系统消息中没有一个消息能够满足程序员自己的需求,那么我们可以自己定义消息。ID范围:
0x400
~0x7FFF(31743)
。自定义消息宏:WM_USER
。自己定义的消息需要自己发送并处理。
1 | //自定义消息,添加宏定义 |
n 取值 0 ~ 31743。
消息队列
- 消息队列是用于存放消息的队列(数据结构)。
- 消息在队列中先进先出(FIFO:First-In-First-Out)。
- 所有的窗口程序都有消息队列。
- 程序可以从队列中获取消息。
消息队列分类
- 系统消息队列:由操作系统进行维护,存放系统产生的消息。系统的消息队列要保存所有进程产生的消息,所以它非常庞大。进程产生的消息首先要进系统的消息队列。
- 程序消息队列:属于每一个应用程序(主线程)的消息队列。由应用程序(线程)维护。
在操作系统上可能同时运行着很多进程,各个进程的 GetMessage() 函数是到本进程的消息队列中抓取消息。绝大多数消息产生后先进系统消息队列,再由操作系统每隔一段时间转发到各个进程的消息队列中。
操作系统可以根据消息的 窗口句柄 hWnd 找到保存窗口数据的内存,在这块内存中有一个当前程序实例句柄 Instance,而当前程序的实例句柄 Instance 可以找到当前进程所占的一块内存,从而将每一个消息正确地转发给各个进程的消息队列。
消息和消息队列的关系
- 消息和消息队列的关系
- 当鼠标、键盘等产生消息时,会将消息存放到系统消息队列。
- 系统会根据存放的消息,找到对应程序的消息队列。
- 将消息投递到程序的消息队列中。
- 根据消息和消息队列之间的使用关系,将消息分类为:
- 队列消息:消息的发送和获取,都是通过消息队列完成的。
- 非队列消息:消息的发送和获取,是直接调用消息的窗口处理函数完成的。
PostMessage() 把消息发送到操作系统的消息队列中,由操作系统派发到进程的消息队列中。
SendMessage() 把消息直接发送给 WndProc() 进行处理并等待返回。
PostMessage() 发送的消息为 队列消息。常见队列消息:WM_PAINT、WM_QUIT(必须进队列)、键盘、鼠标、定时器等。
SendMessage() 发送的消息为 非队列消息。常见消息:WM_CREATE(必须不能进队列)、WM_SIZE(窗口第一次创建到显示时产生不进队列,后面可以进队列)等。
消息本身没有队列或非队列的属性。
深入 GetMessage() 函数
到本进程的消息队列中抓去本进程的消息。GetMessage() 函数做的事情如下:
- 在程序(线程)消息队列中查找消息,如果队列有消息,检查消息是否满足指定条件(HWND, ID范围),不满足条件就不会取出消息,否则从消息队列中取出消息并返回。
- 如果程序(线程)队列中没有消息,向操作系统消息队列要本程序的消息。如果系统队列中有消息属于本程序,系统会将消息转发到程序消息队列中。
- 如果系统的消息队列中也没有属于本程序的消息,则检查当前进程的所有窗口的需要重新绘制的区域,如果发现有需要绘制的区域,产生
WM_PAINT
消息,取得消息返回处理。 - 如果没有重新绘制的区域,检查定时器如果有到时的定时器,产生
WM_TIMER
,返回处理执行。 - 如果没有到时的定时器,整理程序的资源、内存等。
- 如果以上条件都不满足,GetMessage() 才会进入阻塞状态。PeekMessage()会做以上同样的事情,但是不会阻塞等待消息,而是直接返回 False。