【MFC编程(三)】消息映射机制分析
文章目录
- 什么是消息
- Windows消息分类
- 消息映射表
- 常用消息
- WM_CREATE
- WM_SIZE
- WM_PAINT
- WM_DESTROY
- WM_COMMAND
- WM_NOTIFY
- 用户自定义的消息
- WindowProc
Windows应用程序是消息驱动的。在MFC软件开发中,界面操作或者线程之间通信都会经常用到消息,通过对消息的处理实现相应的操作。比较典型的过程是,用户操作窗口,然后有消息产生,送给窗口的消息处理函数处理,对用户的操作做出响应。
什么是消息
消息是系统定义的一个32位的值UINT message
,它唯一地定义了一个事件,向Windows发出一个通知,告诉应用程序某个事情发生了。例如,双击鼠标、改变窗口位置、按下键盘上的一个键都会使Windows发送一个消息给应用程序。
消息本身是作为一系列值传递给应用程序的,其中包括消息的类型以及其他信息。例如,对于按下键盘上的一个键所产生的消息来说,这一系列值中包括了按下键的值。此一系列值的类型叫做MSG,MSG含有 Windows应用程序消息队列的消息信息,它在 Windows中声明如下:
/* Message structure */
typedef struct tagMSG {
HWND hwnd; //接受该消息的窗口句柄
UINT message; //消息常量标识符,也就是通常所说的消息号
WPARAM wParam; //32位消息的特定附加信息,确切含义依赖于消息值
LPARAM lParam; //32位消息的特定附加信息,确切含义依赖于消息值
DWORD time; //消息创建时的时间
POINT pt; //消息创建时的光标在屏幕坐标系中的位置
} MSG, *PMSG
hwnd
表示消息所属的窗口。
MFC程序都是窗口应用程序,一个消息一般都是与某个窗口相关联的。例如, 在某个活动窗口中单击鼠标右键, 产生的按键消息就是发给该窗口的。在MFC程序中,用 HWND类型的变量来标识窗口。message
指定了消息的标识符。
在Windows中, 消息是由一个数值来表示的,不同的消息对应不同的数值。 但是由于数值不便于记忆,所以 Windows将消息对应的数值定义为WM_XXX
宏(WM是Window Message的缩写)的形式,XXX对应某种消息的英文拼写的大写形式。例如,单击鼠标右键消息是 WM_RBUTTON_DOWN,键盘按下消息是WM_KEYDOWN等。在程序中,通常是以WM_XXX宏的形式来使用消息的。wParam
和IParam
用于指定消息的附加信息。
例如, 当收到一个字符消息时,message成员变量的值就是WM_CHAR, 但用户到底输入的是什么字符,那么就由wParam和lParam来说明。wParam、 lParam表示的信息随 message的值不同而不同。从定义来看这两种类型实际上就是unsigned int和 long。time
表示消息投递到消息队列中的时间。pt
表示消息投递到消息队列中光标的当前位置。
Windows消息分类
Windows消息分为系统消息和用户自定义消息。
- 系统保留的消息标识符值的范围是
0x0000
到0x03FF
(WM_USER
- 1)。应用不能使用这些值作为私有消息 - 在
0x0400
(WM_USER
)到0x7FFFF
范围内的值用于私有窗口类的消息标识符。
Windows系统消息有三种:
- 标准消息。除
WM_COMMAND
外以WM_
开头的消息是标准消息。例如,WM_CREATE、WM_CLOSE。 - 命令消息。消息名为
WM_COMMAND
,消息中附带了标识符ID来区分是来自哪个菜单、工具栏按钮或加速键的消息。 - 通知消息。通知消息一般由列表框等子窗口发送给父窗口,消息名也是
WM_COMMAND
,其中附带了控件通知码来区分控件。
CWnd的派生类都可以接收到标准消息、通知消息和命令消息。命令消息还可以由文档类等接收。
用户自定义消息是实际上就是用户定义一个宏作为消息,此宏的值应该大于等于WM_USER,然后此宏就可以跟系统消息一样使用,窗口类中可以定义它的处理函数。
根据Windows将消息派发给消息处理函数的方式,可以将消息分为队列(Queued)消息、非队列(Nonqueued)消息
队列消息
Windows维护着一个系统消息队列,以及分别为每个GUI线程维护一个各自的线程消息队列。为了避免非GUI线程的创建线程消息队列的开销,所有线程创建初始化时,均不创建消息队列。只有当线程第一次调用GDI函数时,系统才会为线程创建消息队列。所以那些非GUI线程是没有消息队列的。
每当用户移动鼠标,点击按钮或键盘时,鼠标或键盘的设备驱动程序会将输入转换成消息,并将消息放在系统消息队列里。删windows会检查自己的消息队列,如果消息队列不为空,则每次取出并删除一个消息,然后确定消息的目标窗口,然后把消息放到创建这个窗口的线程的线程消息队列里。线程的消息队列接收由线程创建的窗口的所有的鼠标和键盘消息。然后线程会从队列中删除信息,并告诉系统把它们派发到对应的窗口消息处理函数。
除了WM_PAINT, WM_TIMER和WM_QUIT消息以外,系统总是派发放在在消息队列的末尾的消息。这将保证让一个窗口以first-in, first-out的顺序接收消息。WM_PAINT,WM_TIMER,和WM_QUIT消息,会一直被保存在队列中,只有在队列中没有其他消息时才会被派发到窗口消息处理函数。此外,同一个窗口的多个WM_PAINT消息被合并成一个WM_PAINT消息,客户区的所有无效部分也会被合并。这样是为了减少窗口重绘客户区的次数。
系统通过填充一个 MSG 结构来将消息投递到线程的消息队列,随后将其拷贝到消息队列中。通过使用 PostMessage (异步的)和 PostThreadMessage 函数,线程可以将一个消息投递到自己的消息队列或其他线程的消息队列。应用可以使用 GetMessage 来删除队列中的消息。要在不删除消息的情况下检查队列消息,应用可以使用 PeekMessage 函数,该函数会使用消息填充 MSG 。
在从队列删除消息后,应用可以使用 DispatchMessage 函数来指示系统把消息发送给窗口过程进行处理。DispatchMessage 接收一个 MSG 结构的指针,该结构已经使用 GetMessage 或 PeekMessage 填充过。DispatchMessage 将窗口句柄,消息标识符,和两个消息参数传递给窗口过程,但它不会传递时间和鼠标光标位置。应用在处理消息时可以通过 GetMessageTime 和 GetMessagePos 函数检索时间和位置信息。
非队列消息
非队列消息被立即送往目的地的窗口消息处理函数,绕过了系统的消息队列和线程消息队列。系统通常会发送非队列消息,来通知那些会影响窗口的事件。例如,当用户激活一个新的应用程序窗口时,系统会发送一些列消息到窗口,包括WM_ACTIVATE,WM_SETFOCUS,WM_SETCURSOR。这些消息通知窗口被激活,键盘输入被定向到窗口,并且鼠标光标也移到窗口的边界内。
非队列消息也有可能来源于应用程序调用系统函数。例如,系统调用SetWindowPos函数移动一个窗口后会发送WM_WINDOWPOSCHANGED消息。 一些函数也发送非队列消息, 有BroadcastSystemMessage,BroadcastSystemMessageEx,SendMessage(同步),SendMessageTimeout,和SendNotifyMessage。
消息映射表
MFC使用一种消息映射机制来处理消息,在应用程序框架中的表现就是一个消息与消息处理函数一一对应的消息映射表,以及消息处理函数的声明和实现等代码。当窗口接收到消息时,会到消息映射表中查找该消息对应的消息处理函数,然后由消息处理函数进行相应的处理 。SDK编程时需要在窗口过程中Switch判断消息值进行相应的处理,相比之下MFC的消息映射机制更加方便好用。
DECLARE_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(MainFrame, CFrameWnd)
ON_WM_CREATE()
ON_WM_DESTROY()
ON_WM_TIMER()
ON_WM_CLOSE()
ON_WM_SETTINGCHANGE()
ON_COMMAND(ID_IMPORT_FILE, &MainFrame::OnImportFile)
ON_UPDATE_COMMAND_UI(ID_IMPORT_FILE, &MainFrame::OnUpdateImportFile)
ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_XP, ID_VIEW_APPLOOK_VS_2012_LIGHT, &MainFrame::OnApplicationLook)
ON_MESSAGE(WM_ACTION_EXCEPTION, &MainFrame::OnException)
END_MESSAGE_MAP()
DECLARE_MESSAGE_MAP
声明将在类中使用消息映射来将消息映射到函数(在类声明中使用)。BEGIN_MESSAGE_MAP
开始消息映射的定义(在类实现中使用)。END_MESSAGE_MAP
结束消息映射的定义(在类实现中使用)。
常用消息
WM_CREATE
当应用通过调用 CreateWindow 或 CreateWindow 要求创建一个窗口时,会发送这个消息(在函数返回前消息就被发送)。新窗口的窗口过程在窗口创建后会接收到这个消息,但是是在窗口可见之前。
这是窗口过程接收到的第一个消息。接收该消息时,窗口过程的 wParam 参数值不被使用,它的 lParam 是一个指向 CREATESTRUCT 结构的指针。这个结构包含了窗口初始化的参数。
处理该消息后,窗口过程应该返回 0 以继续窗口的创建。如果窗口过程返回 -1,窗口会被销毁,CreateWindow 或 CreateWindowEx 会返回空句柄。
WM_SIZE
在窗口尺寸改变后向窗口发送该消息。
wParam 是尺寸改变的类型,它可以是下列值中的一个;
- SIZE_MAXHIDE,当其他窗口最大化时,该消息会发送到所有弹出(pop-up)窗口
- SIZE_MAXIMIZED,窗口已经最大化了
- SIZE_MAXSHOW,当其他窗口恢复到之前尺寸时,该消息会发送到所有弹出窗口
- SIZE_MINIMIZED,窗口已经最小化了
- SIZE_RESTORED,窗口的尺寸改变了,但不是最大化和最小化
lParam
的低位是客户区的新宽度,高位是客户区的新高度。虽然窗口的宽度和高度是 32 位值,lParam 只包含宽高值的低 16 位。
窗口过程处理该消息后应该返回 0。
WM_PAINT
当系统或其他应用要求对应用窗口的部分进行绘制时,会发送该消息。调用 UpdateWindow 或 RedrawWindow 函数时,或在应用通过使用 GetMessage 或 PeekMessage 获得 WM_PAINT 并调用 DispatchMessage 函数后,消息会被发送到窗口过程。
lParam 和 wParam 都不被使用。
Invalidate
在消息队列中加入一条WM_PAINT消息,其无效区为整个客户区。
UpdateWindow
直接发送一个WM_PAINT消息,其无效区范围就是消息队列中WM_PAINT消息(最多只有一条)的无效区。
效果很明显,调用Invalidate之后,屏幕不一定马上更新,因为WM_PAINT消息不一定在队列头部,而调用UpdateWindow会使WM_PAINT消息马上执行的,绕过了消息队列。如果你调用Invalidate之后想马上更新屏幕,那就加上UpdateWindow()这条语句。
WM_DESTROY
当窗口将被销毁时发送该消息。在窗口被从屏幕删除后,它会被发送到删除的窗口的窗口过程。
该消息首先被发送到被销毁的窗口,之后发送到子窗口(如果有的话)。在消息的主窗口处理过程中,可以假设所有子窗口还是存在的。
wParam 和 lParam 不被使用。如果处理了该消息,窗口过程应返回 0。
WM_COMMAND
发送命令消息的情况:
- 当用户从菜单选中一个命令时会发送
- 当控件向它的父窗口发送提醒消息时会发送
- 当快捷键被翻译时会发送
如果应用处理了该消息,它应该返回 0。
- 消息来源是菜单时,
wParam
的高位是 0,低位是菜单标识符(IDM_*
),lParam
是 0。 - 消息来源是快捷键时,
wParam
的高位是 1,低位是快捷键标识符(IDM_*
),lParam
是 0。 - 消息来源是控件时,
wParam
的高位是控件特定的通知码,低位是控件标识符,lParam
是控件窗口句柄。
消息映射宏
命令消息的消息映射入口项形式如:
ON_COMMAND(ID_IMPORT_FILE, &MainFrame::OnImportFile)
ON_UPDATE_COMMAND_UI(ID_IMPORT_FILE, &MainFrame::OnUpdateImportFile)
ON_COMMAND
消息ID为ID_IMPORT_FILE
,消息处理函数为OnImportFile
。
ON_UPDATE_COMMAND_UI
UPDATE_COMMAND_UI
消息用来维护菜单项的各项状态,包括激活、禁用、变灰、选中、未选中等。在下拉菜单每次打开的时候,所有菜单项的此消息都会被发送出去。如果所属类中为菜单项的该消息添加了处理函数,则执行相应函数更新菜单状态,如果菜单项没有此消息处理函数,也没有COMMAND消息的处理函数,那么它就会变灰。
对应消息处理函数
afx_msg void OnImportFile();
afx_msg void OnUpdateImportFile(CCmdUI* pCmdUI);
void MainFrame::OnImportFile()
{
//do something
}
void MainFrame::OnUpdateImportFile( CCmdUI* pCmdUI )
{
pCmdUI->Enable(TRUE);
pCmdUI->SetCheck(TRUE);
}
消息映射范围宏
ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_XP, ID_VIEW_APPLOOK_VS_2012_LIGHT, &MainFrame::OnApplicationLook)
ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_APPLOOK_WIN_XP, ID_VIEW_APPLOOK_VS_2012_LIGHT, &MainFrame::OnUpdateApplicationLook)
指示消息处理函数处理在宏的前两个参数中指定的命令 ID 的范围。
这样值在ID_VIEW_APPLOOK_WIN_XP
到ID_VIEW_APPLOOK_VS_2012_LIGHT
之间的菜单项等的命令消息都由对应消息处理函数处理。
对应消息处理函数
afx_msg void OnApplicationLook(UINT id); //参数id为用户操作的菜单项的ID
afx_msg void OnUpdateApplicationLook(CCmdUI* pCmdUI);
void MainFrame::OnApplicationLook(UINT id)
{
theApp.m_nAppLook = id;
//do something
}
void MainFrame::OnUpdateApplicationLook( CCmdUI* pCmdUI )
{
pCmdUI->Enable(isChecked == 1);
pCmdUI->SetCheck(pCmdUI->m_nID == theApp.m_nAppLook);
}
WM_NOTIFY
ON_NOTIFY
是控件向其父窗口发送消息处理的宏,扩展了ON_COMMAND
的功能,使用了相应的NMHDR结构.
ON_NOTIFY( wNotifyCode, id, memberFxn )
afx_msg void memberFxn( NMHDR * pNotifyStruct, LRESULT * result );
在操作列表框等控件时往往会给父窗口发送WM_NOTIFY通知消息。WM_NOTIFY消息的wParam参数为发送通知消息的控件的ID,lParam参数指向一个结构体,可能是NMHDR结构体,也可能是第一个元素为NMHDR结构体变量的其他结构体。NMHDR结构体的定义如下(仅作了解):
Typedef sturct tagNMHDR{
HWND hwndFrom;
UINT idFrom;
UINT code;
} NMHDR;
- hwndFrom为发送通知消息控件的句柄
- idFrom为控件ID
- code为要处理的通知消息的通知码,例如NM_CLICK。
通知消息的消息映射入口项形式如:
ON_NOTIFY( wNotifyCode, id, memberFxn )
ON_NOTIFY
是控件向其父窗口发送消息处理的宏,扩展了ON_COMMAND
的功能,使用了相应的NMHDR结构。
wNotifyCode
:要被处理的通告消息代码,如 LVN_KEYDOWN。id
:发送通告消息的控件ID。memberFxn
:通告消息发送后被调用的成员函数。
通知消息的处理函数的原型为:
afx_msg void memberFxn( NMHDR * pNotifyStruct, LRESULT * result);
用户自定义的消息
ON_MESSAGE
用来响应自定义消息,能够处理所有的消息响应,在程序中需要自己设定相应的消息响应函数。
ON_MESSAGE(message, memberFxn)
message
:消息的ID。memberFxn
:映射message的消息函数,该函数的类型必须是以下类型的
afx_msg LRESULT (CWnd::*)(WPARAM, LPARAM)。
如果需要使用用户自定义消息,,如:#define WM_UPDATE_WND (WM_USER+1),ON_MESSAGE(WM_UPDATE_WND, &CMainFrame::OnUpdateWnd),afx_msg LRESULT OnUpdateWnd(WPARAM wParam, LPARAM lParam);,最后在MainFrm.cpp中实现此函数。
- 首先要定义消息宏:在头文件定义 消息id
#define WM_MYMESSAGE (WM_USER + 100)
- 再到消息映射表中添加消息映射入口项:
BEGIN_MESSAGE_MAP(MainFrame, CFrameWnd) ON_MESSAGE(WM_UPDATE_WND, &CMainFrame::OnUpdateWnd) END_MESSAGE_MAP()
- 然后在MainFrm.h中添加消息处理函数的函数声明
afx_msg LRESULT OnUpdateWnd(WPARAM wParam, LPARAM lParam);
- 最后在MainFrm.cpp中实现此函数。
需要调用postMessage 或者 SendMessage 实现关联。
WindowProc
WindowProc
回调函数,处理发送给窗口的消息 手段更丰富了。
是一个虚函数,可以通过类向导添加。
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
hwnd
:指向窗口的句柄。uMsg
:指定消息类型。uMsg可以是WM_COMMAND、 WM_NOTIFY 从而可以实现ON_NOTIFY ON_COMMAND的处理。wParam
:指定其余的、消息特定的信息。该参数的内容与UMsg参数值有关。IParam
:指定其余的、消息特定的信息。该参数的内容与uMsg参数值有关
CWnd::WindowProc
调用 OnWndMsg
用来分辨并处理消息;
如果是命令消息,交给 OnCommand
处理;
如果是通知消息,交给 OnNotify
处理。
而一般的 Windows 消息,就直接在消息映射表中上溯,寻找其归宿(消息处理程序)