(1)WM_NOTIFY消息
还有一种通知消息WM_NOTIFY,在Win32中用来传递信息复杂的通知消息。WM_NOTIFY消息怎么来传递复杂的信息呢?WM_NOTIFY的消息参数wParam包含了发送通知消息的控制窗口ID,另一个参数lParam包含了一个指针。该指针指向一个NMHDR结构,或者更大的结构,只要它的第一个结构成员是NMHDR结构。
NMHDR结构:
typedef struct tagNMHDR {
HWND hwndFrom;
UINT idFrom;
UINT code;
} NMHDR;
上述结构有三个成员,分别是发送通知消息的控制窗口的句柄、ID和通知消息代码。
举一个更大、更复杂的结构例子:列表控制窗发送LVN_KEYDOWN控制通知消息,则lParam包含了一个指向LV_KEYDOWN结构的指针。其结构如下:
typedef struct tagLV_KEYDOWN {
NMHDR hdr;
WORD wVKey;
UINT flags;
}LV_KEYDOWN;
它的第一个结构成员hdr就是NMHDR类型。其他成员包含了更多的信息:哪个键被按下,哪些辅助键(SHIFT、CTRL、ALT等)被按下。
(2)WM_NOTIFY消息的处理
在分析CWnd::OnWndMsg函数时,曾指出当消息是WM_NOTIFY时,它把消息传递给OnNotify虚拟函数处理。这是一个虚拟函数,类似于OnCommand,CWnd和派生类都可以覆盖该函数。OnNotify的函数原型如下:
BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)
参数1是发送通知消息的控制窗口ID,没有被使用;参数2是一个指针;参数3指向一个long类型的数据,用来返回处理结果。
WM_NOTIFY消息的处理过程如下:
-
- 反射消息给控制子窗口处理。
- 如果子窗口不处理反射消息,则交给OnCmdMsg处理。给OnCmdMsg的四个参数分别如下:第一个是命令消息ID,第四个为空;第二个高阶word是WM_NOTIFY,低阶word是通知消息;第三个参数是指向AFX_NOTIFY结构的指针。第二、三个参数有别于OnCommand送给OnCmdMsg的参数。
AFX_NOTIFY结构:
struct AFX_NOTIFY
{
LRESULT* pResult;
NMHDR* pNMHDR;
};
pNMHDR的值来源于参数2 lParam,该结构的域pResult用来保存处理结果,域pNMHDR用来传递信息。
OnCmdMsg后续的处理和WM_COMMAND通知消息基本相同,只是在派发消息给消息处理函数时,DispatchMsdMsg的第五个参数pExtra指向OnCmdMsg传递给它的AFX_NOTIFY类型的参数,而不是空指针。这样,处理函数就得到了复杂的通知消息信息。
(1)消息反射的概念
前面讨论控制通知消息时,曾经多次提到了消息反射。MFC提供了两种消息反射机制,一种用于OLE控件,一种用于Windows控制窗口。这里只讨论后一种消息反射。
Windows控制常常发送通知消息给它们的父窗口,通常控制消息由父窗口处理。但是在MFC里头,父窗口在收到这些消息后,或者自己处理,或者反射这些消息给控制窗口自己处理,或者两者都进行处理。如果程序员在父窗口类覆盖了通知消息的处理(假定不调用基类的实现),消息将不会反射给控制子窗口。这种反射机制是MFC实现的,便于程序员创建可重用的控制窗口类。
MFC的CWnd类处理以下控制通知消息时,必要或者可能的话,把它们反射给子窗口处理:
WM_CTLCOLOR,
WM_VSCROLL,WM_HSCROLL,
WM_DRAWITEM,WM_MEASUREITEM,
WM_COMPAREITEM,WM_DELETEITEM,
WM_CHARTOITEM,WM_VKEYTOITEM,
WM_COMMAND、WM_NOTIFY。
例如,对WM_VSCROLL、WM_HSCROLL消息的处理,其消息处理函数如下:
void CWnd::OnHScroll(UINT, UINT, CScrollBar* pScrollBar)
{
//如果是一个滚动条控制,首先反射消息给它处理
if (pScrollBar != NULL && pScrollBar->SendChildNotifyLastMsg())
return; //控制窗口成功处理了该消息
Default();
}
又如:在讨论OnCommand和OnNofity函数处理通知消息时,都曾经指出,它们首先调用ReflectLastMsg把消息反射给控制窗口处理。
为了利用消息反射的功能,首先需要从适当的MFC窗口派生出一个控制窗口类,然后使用ClassWizard给它添加消息映射条目,指定它处理感兴趣的反射消息。下面,讨论反射消息映射宏。
上述消息的反射消息映射宏的命名遵循以下格式:“ON”前缀+消息名+“REFLECT”后缀,例如:消息WM_VSCROLL的反射消息映射宏是ON_WM_VSCROLL_REFECT。但是通知消息WM_COMMAND和WM_NOTIFY是例外,分别为ON_CONTROL_REFLECT和ON_NOFITY_REFLECT。状态更新通知消息的反射消息映射宏是ON_UPDATE_COMMAND_UI_REFLECT。
消息处理函数的名字和去掉“WM_”前缀的消息名相同,例如WM_HSCROLL反射消息处理函数是Hscroll。
消息处理函数的原型这里不一一列举了。
这些消息映射宏和消息处理函数的原型可以借助于ClassWizard自动地添加到程序中。ClassWizard添加消息处理函数时,可以处理的反射消息前面有一个等号,例如处理WM_HSCROLL的反射消息,选择映射消息“=EN_HSC ROLL”。ClassWizard自动的添加消息映射宏和处理函数到框架文件。
(2)消息反射的处理过程
如果不考虑有OLE控件的情况,消息反射的处理流程如下图所示:
首先,调用CWnd的成员函数SendChildNotifyLastMsg,它从线程状态得到本线程最近一次获取的消息(关于线程状态,后面第9章会详细介绍)和消息参数,并且把这些参数传递给函数OnChildNotify。注意,当前的CWnd对象就是MFC控制子窗口对象。
OnChlidNofify是CWnd定义的虚拟函数,不考虑OLE控制的话,它仅仅只调用ReflectChildNotify。OnChlidNofify可以被覆盖,所以如果程序员希望处理某个控制的通知消息,除了采用消息映射的方法处理通知反射消息以外,还可以覆盖OnChlidNotify虚拟函数,如果成功地处理了通知消息,则返回TRUE。
ReflectChildNotify是CWnd的成员函数,完成反射消息的派发。对于WM_COMMAND,它直接调用CWnd::OnCmdMsg派发反射消息WM_REFLECT_BASE+WM_COMMAND;对于WM_NOTIFY,它直接调用CWnd::OnCmdMsg派发反射消息WM_REFLECT_BASE+WM_NOFITY;对于其他消息,则直接调用CWnd::OnWndMsg(即CmdTarge::OnWndMsg)派发相应的反射消息,例如WM_REFLECT_BASE+WM_HSCROLL。
注意:ReflectChildNotify直接调用了CWnd的OnCmdMsg或OnWndMsg,这样反射消息被直接派发给控制子窗口,省却了消息发送的过程。
接着,控制子窗口如果处理了当前的反射消息,则返回反射消息被成员处理的信息。
(3)一个示例
如果要创建一个编辑框控制,要求它背景使用黄色,其他特性不变,则可以从CEdit派生一个类CYellowEdit,处理通知消息WM_CTLCOLOR的反射消息。CYellowEdit有三个属性,定义如下:
CYellowEdit::CYellowEdit()
{
m_clrText = RGB( 0, 0, 0 );
m_clrBkgnd = RGB( 255, 255, 0 );
m_brBkgnd.CreateSolidBrush( m_clrBkgnd );
}
使用ClassWizard添加反射消息处理函数:
函数原型:
afx_msg void HScroll();
消息映射宏:
ON_WM_CTLCOLOR_REFLECT()
函数的框架
HBRUSH CYellowEdit::CtlColor(CDC* pDC, UINT nCtlColor)
{
// TODO:添加代码改变设备描述表的属性
// TODO: 如果不再调用父窗口的处理,则返回一个非空的刷子句柄
return NULL;
}
添加一些处理到函数CtlColor中,如下:
pDC->SetTextColor( m_clrText );//设置文本颜色
pDC->SetBkColor( m_clrBkgnd );//设置背景颜色
return m_brBkgnd; //返回背景刷
这样,如果某个地方需要使用黄色背景的编辑框,则可以使用CYellowEdit控制。
-
- 对更新命令的接收和处理
用户接口对象如菜单、工具条有多种状态,例如:禁止,可用,选中,未选中,等等。这些状态随着运行条件的变化,由程序来进行更新。虽然程序员可以自己来完成更新,但是MFC框架为自动更新用户接口对象提供了一个方便的接口,使用它对程序员来说可能是一个好的选择。
-
- 实现方法
每一个用户接口对象,如菜单、工具条、控制窗口的子窗口,都由唯一的ID号标识,用户和它们交互时,产生相应ID号的命令消息。在MFC里,一个用户接口对象还可以响应CN_UPDATE_COMMAND_UI通知消息。因此,对每个标号ID的接口对象,可以有两个处理函数:一个消息处理函数用来处理该对象产生的命令消息ID,另一个状态更新函数用来处理给该对象的CN_UPDATE_COMMAND_UID的通知消息。
使用ClassWizard可把状态更新函数加入到某个消息处理类,其结果是:
在类的定义中声明一个状态函数;
在消息映射中使用ON_UPDATE_COMMAND_UI宏添加一个映射条目;
在类的实现文件中实现状态更新函数的定义。
ON_UPDATE_COMMAND_UI给指定ID的用户对象指定状态更新函数,例如:
ON_UPDATE_COMMAND_UI(ID_EDIT_COPY, OnUpdateEditCopy)
映射标识号ID为ID_EDIT_COPY菜单的通知消息CN_UPDATE_COMMAND_UI到函数OnUpdateEditCopy。用于给EDIT(编辑菜单)的菜单项ID_EDIT_COPY(复制)添加一个状态处理函数OnUpdateEditCopy,通过处理通知消息CN_UPDATE_COMMAND_UI实现该菜单项的状态更新。
状态处理函数的原型如下:
afxmsg void ClassName::OnUpdateEditPaste(CCmdUI* pCmdUI)
CCmdUI对象由MFC自动地构造。在完善函数的实现时,使用pCmdUI对象和CmdUI的成员函数实现菜单项ID_EDIT_COPY的状态更新,让它变灰或者变亮,也就是禁止或者允许用户使用该菜单项。
- 状态更新命令消息
要讨论MFC的状态更新处理,先得了解一条特殊的消息。MFC的消息映射机制除了处理各种Windows消息、控制通知消息、命令消息、反射消息外,还处理一种特别的“通知命令消息”,并通过它来更新菜单、工具栏(包括对话框工具栏)等命令目标的状态。
这种“通知命令消息”是MFC内部定义的,消息ID是WM_COMMAND,通知代码是CN_UPDATE_COMMAND_UI(0XFFFFFFFF)。
它不是一个真正意义上的通知消息,因为没有控制窗口产生这样的通知消息,而是MFC自己主动产生,用于送给工具条窗口或者主边框窗口,通知它们更新用户接口对象的状态。
它和标准WM_COMMAND命令消息也不相同,因为它有特定的通知代码,而命令消息通知代码是0。
但是,从消息的处理角度,可以把它看作是一条通知消息。如果是工具条窗口接收该消息,则在发送机制上它和WM_COMMAND控制通知消息是相同的,相当于让工具条窗口处理一条通知消息。如果是边框窗口接收该消息,则在消息的发送机制上它和WM_COMMAND命令消息是相同的,可以让任意命令目标处理该消息,也就是说边框窗口可以把该条通知消息发送给任意命令目标处理。
从程序员的角度,可以把它看作一条“状态更新命令消息”,像处理命令消息那样处理该消息。每条命令消息都可以对应有一条“状态更新命令消息”。ClassWizard也支持让任意消息目标处理“状态更新命令消息”(包括非窗口命令目标),实现用户接口状态的更新。
在这条消息发送时,通过OnCmdMsg的第三个参数pExtra传递一些信息,表示要更新的用户接口对象。pExtra指向一个CCmdUI对象。这些信息将传递给状态更新命令消息的处理函数。
下面讨论用于更新用户接口对象状态的类CCmdUI。
- 类CCmdUI
-
CCmdUI不是从CObject派生,没有基类。
-
- 成员变量
m_nID 用户接口对象的ID
m_nIndex 用户接口对象的index
m_pMenu 指向CCmdUI对象表示的菜单
m_pSubMenu 指向CCmdUI对象表示的子菜单
m_pOther 指向其他发送通知消息的窗口对象
m_pParentMenu 指向CCmdUI对象表示的子菜单
- 成员函数
Enable(BOOL bOn = TRUE ) 禁止用户接口对象或者使之可用
SetCheck( int nCheck = 1) 标记用户接口对象选中或未选中
SetRadio(BOOL bOn = TRUE)
SetText(LPCTSTR lpszText)
ContinueRouting()
还有一个MFC内部使用的成员函数:
DoUpdate(CCmdTarget* pTarget, BOOL bDisableIfNoHndler)
其中,参数1指向处理接收更新通知的命令目标,一般是边框窗口;参数2指示如果没有提供处理函数(例如某个菜单没有对应的命令处理函数),是否禁止用户对象。
DoUpdate作以下事情:
首先,发送状态更新命令消息给参数1表示的命令目标:调用pTarget->OnCmdMsg(m_nID, CN_UPDATE_COMMAND_UI, this, NULL)发送m_nID对象的通知消息CN_UPDATE_COMMAND_UI。OnCmdMsg的参数3取值this,包含了当前要更新的用户接口对象的信息。
然后,如果参数2为TRUE,调用pTarget->OnCmdMsg(m_nID, CN_COMMAND, this, &info)测试命令消息m_nID是否被处理。这时,OnCmdMsg的第四个参数非空,表示仅仅是测试,不是真的要派发消息。如果没有提供命令消息m_nID的处理函数,则禁止用户对象m_nID,否则使之可用。
从上面的讨论可以知道:通过其结构,一个CCmdUI对象标识它表示了哪一个用户接口对象,如果是菜单接口对象,pMenu表示了要更新的菜单对象;如果是工具条,pOther表示了要更新的工具条窗口对象,nID表示了工具条按钮ID。
所以,由参数上状态更新消息的消息处理函数就知道要更新什么接口对象的状态。例如,第1节的函数OnUpdateEditPaste,函数参数pCmdUI表示一个菜单对象,需要更新该菜单对象的状态。
通过其成员函数,一个CCmdUI可以更新、改变用户接口对象的状态。例如,CCmdUI可以管理菜单和对话框控制的状态,调用Enable禁止或者允许菜单或者控制子窗口,等等。
所以,函数OnUpdateEditPaste可以直接调用参数的成员函数(如pCmdUI->Enable)实现菜单对象的状态更新。
由于接口对象的多样性,其他接口对象将从CCmdUI派生出管理自己的类来,覆盖基类的有关成员函数如Enable等,提供对自身状态更新的功能。例如管理状态条和工具栏更新的CStatusCmdUI类和CToolCmdUI类。
MFC提供了分别用于更新菜单和工具条的两种途径。
-
- 更新菜单状态
当用户对菜单如File单击鼠标时,就产生一条WM_INITMENUPOPUP消息,边框窗口在菜单下拉之前响应该消息,从而更新该菜单所有项的状态。
在应用程序开始运行时,边框也会收到WM_INITMENUPOPUP消息。
- 更新工具条等状态
当应用程序进入空闲处理状态时,将发送WM_IDLEUPDATECMDUI消息,导致所有的工具条用户对象的状态处理函数被调用,从而改变其状态。WM_IDLEUPDATECMDUI是MFC自己定义和使用的消息。
在窗口初始化时,工具条也会收到WM_IDLEUPDATECMDUI消息。
- 菜单状态更新的实现
MFC让边框窗口来响应WM_INITMENUPOPUP消息,消息处理函数是OnInitMenuPopup,其原型如下:
afx_msg void CFrameWnd::OnInitMenuPopup( CMenu* pPopupMenu,
UINT nIndex, BOOL bSysMenu );
第一个参数指向一个CMenu对象,是当前按击的菜单;第二个参数是菜单索引;第三个参数表示子菜单是否是系统控制菜单。
函数的处理:
如果是系统控制菜单,不作处理;否则,创建CCmdUI对象state,给它的各个成员如m_pMenu,m_pParentMenu,m_pOther等赋值。
对该菜单的各个菜单项,调函数state.DoUpdate,用CCmdUI的DoUpdate来更新状态。DoUpdate的第一个参数是this,表示命令目标是边框窗口;在CFrameWnd的成员变量m_bAutoMenuEnable为TRUE时(表示如果菜单m_nID没有对应的消息处理函数或状态更新函数,则禁止它),把DoUpdate的第二个参数bDisableIfNoHndler置为TRUE。
顺便指出,m_bAutoMenuEnable缺省时为TRUE,所以,应用程序启动时菜单经过初始化处理,没有提供消息处理函数或状态更新函数的菜单项被禁止。
- 工具条等状态更新的实现
图4-5表示了消息空闲时MFC更新用户对象状态的流程:
MFC提供的缺省空闲处理向顶层窗口(框架窗口)的所有子窗口发送消息WM_IDLEUPDATECMDUI;MFC的控制窗口(工具条、状态栏等)实现了对该消息的处理,导致用户对象状态处理函数的调用。
虽然两种途径调用了同一状态处理函数,但是传递的 CCmdUI参数从内部构成上是不一样的:第一种传递的CCmdUI对象表示了一菜单对象,(pMenu域被赋值);第二种传递了一个窗口对象(pOther域被赋值)。同样的状态改变动作,如禁止、允许状态的改变,前者调用了CMenu的成员函数EnableMenuItem,后者使用了CWnd的成员函数EnabelWindow。但是,这些不同由CCmdUI对象内部区分、处理,对用户是透明的:不论菜单还是对应的工具条,用户都用同一个状态处理函数使用同样的形式来处理。
这一节分析了用户界面更新的原理和机制。在后面第13章讨论工具条和状态栏时,将详细的分析这种机制的具体实现。
-
- 消息的预处理
到现在为止,详细的讨论了MFC的消息映射机制。但是,为了提高效率和简化处理,MFC提供了一种消息预处理机制,如果一条消息在预处理时被过滤掉了(被处理),则不会被派发给目的窗口的窗口过程,更不会进入消息循环了。
显然,能够进行预处理的消息只可能是队列消息,而且必须在消息派发之前进行预处理。因此,MFC在实现消息循环时,对于得到的每一条消息,首先送给目的窗口、其父窗口、其祖父窗口乃至最顶层父窗口,依次进行预处理,如果没有被处理,则进行消息转换和消息派发,如果某个窗口实现了预处理,则终止。有关实现见后面关于CWinThread线程类的章节,CWinThread的Run函数和PreTranslateMessage函数以及CWnd的函数WalkPreTranslateTree实现了上述要求和功能。这里要讨论的是MFC窗口类如何进行消息预处理。
CWnd提供了虚拟函数PreTranslateMessage来进行消息预处理。CWnd的派生类可以覆盖该函数,实现自己的预处理。下面,讨论几个典型的预处理。
首先,是CWnd的预处理:
预处理函数的原型为:
BOOL CWnd::PreTranslateMessage(MSG* pMsg)
CWnd类主要是处理和过滤Tooltips消息。关于该函数的实现和Tooltips消息,见后面第13章关于工具栏的讨论。
然后,是CFrameWnd的预处理:
CFrameWnd除了调用基类CWnd的实现过滤Tooltips消息之外,还要判断当前消息是否是键盘快捷键被按下,如果是,则调用函数::TranslateAccelerator(m_hWnd, hAccel, pMsg)处理快捷键。
接着,是CMDIChildWnd的预处理:
CMDIChildWnd的预处理过程和CFrameWnd的一样,但是不能依靠基类CFrameWnd的实现,必须覆盖它。因为MDI子窗口没有菜单,所以它必须在MDI边框窗口的上下文中来处理快捷键,它调用了函数::TranslateAccelerator(GetMDIFrame()->m_hWnd, hAccel, pMsg)。
讨论了MDI子窗口的预处理后,还要讨论MDI边框窗口:
CMDIFrameWnd的实现除了CFrameWnd的实现的功能外,它还要处理MDI快捷键(标准MDI界面统一使用的系统快捷键)。
在后面,还会讨论CDialog、CFormView、CToolBar等的消息预处理及其实现。
至于CWnd::WalkPreTranslateTree函数,它从接受消息的窗口开始,逐级向父窗回溯,逐一对各层窗口调用PreTranslateMessage函数,直到消息被处理或者到最顶层窗口为止。
- MFC消息映射的回顾
从处理命令消息的过程可以看出,Windows消息和控制消息的处理要比命令消息的处理简单,因为查找消息处理函数时,后者只要搜索当前窗口对象(this所指)的类或其基类的消息映射入口表。但是,命令消息就要复杂多了,它沿一定的顺序链查找链上的各个命令目标,每一个被查找的命令目标都要搜索它的类或基类的消息映射入口表。
MFC通过消息映射的手段,以一种类似C++虚拟函数的概念向程序员提供了一种处理消息的方式。但是,若使用C++虚拟函数实现众多的消息,将导致虚拟函数表极其庞大;而使用消息映射,则仅仅感兴趣的消息才加入映射表,这样就要节省资源、提高效率。这套消息映射机制的基础包括以下几个方面:
-
- 消息映射入口表的实现:采用了C++静态成员和虚拟函数的方法来表示和得到一个消息映射类(CCmdTarget或派生类)的映射表。
- 消息查找的实现:从低层到高层搜索消息映射入口表,直至根类CCmdTarget。
- 消息发送的实现:主要以几个虚拟函数为基础来实现标准MFC消息发送路径:OnComamnd、OnNotify、OnWndMsg和OnCmdMsg。、
OnWndMsg是CWnd类或其派生类的成员函数,由窗口过程调用。它处理标准的Windows消息。
OnCommand是CWnd类或其派生类的成员函数,由OnWndMsg调用来处理WM_COMMAND消息,实现命令消息或者控制通知消息的发送。如果派生类覆盖该函数,则必须调用基类的实现,否则将不能自动的处理命令消息映射,而且必须使用该函数接受的参数(不是程序员给定值)调用基类的OnCommand。
OnNotify是CWnd类或其派生类的成员函数,由OnWndMsg调用来处理WM_NOTIFY消息,实现控制通知消息的发送。
OnCmdMsg是CCmdTarget类或其派生类的成员函数。被OnCommand调用,用来实现命令消息发送和派发命令消息到命令消息处理函数。
自动更新用户对象状态是通过MFC的命令消息发送机制实现的。
控制消息可以反射给控制窗口处理。
队列消息在发送给窗口过程之前可以进行消息预处理,如果消息被MFC窗口对象预处理了,则不会进入消息发送过程。