【转】MFC六大关键技术之(五)(六)——消息映射与命令传递解决方法
【转】MFC六大关键技术之(五)(六)——消息映射与命令传递
MFC六大关键技术之(五)(六)——消息映射与命令传递收藏
MFC六大关键技术之(五)(六)——消息映射与命令传递
题外话:刚开始学视窗程序设计的时候,我就打印了一本Windows消息详解,里面列举了各种已定义消息的意义和作用,共10多页,在编程的时候翻翻,有时觉得很受用。我发觉很多编程的朋友,虽然每天都面对消息,却很少关注它。C++程序员有一个通病,很想写“自己”的程序,即每一行代码都想自己写出来。如果用了一些库,总希望能完全理解库里的类或函数是怎么一回事,否则就“不踏实”。对于消息,许多朋友只关心常用的几个,对其余的漠不关心。其实,Windows中有很多不常用的消息却很有用,程序员可能通过响应这些消息实现更简捷的编程。
说到消息,在MFC中,“最熟悉的神秘”可算是消息映射,那是我们刚开始接触MFC时就要面对的东西。有过SDK编程经验的朋友转到MFC编程的时候,一下子觉得什么都变了样。特别是窗口消息及对消息的处理跟以前相比,更是风马牛不相及的。如文档不是窗口,是怎样响应命令消息的呢?
初次用MFC编程,我们只会用MFC ClassWizard为我们做大量的东西,最主要的是添加消息响应。记忆中,如果是自已添加消息响应,我们应何等的小心翼翼,对BEGIN_MESSAGE_MAP()……END_MESSAGE_MAP()更要奉若神灵。它就是一个魔盒子,把我们的咒语放入恰当的地方,就会发生神奇的力量,放错了,自己的程序就连“命”都没有。
据说,知道得太多未必是好事。我也曾经打算不去理解这神秘的区域,觉得编程的时候知道自己想做什么就行了。MFC外表上给我们提供了东西,直观地说,不但给了我个一个程序的外壳,更给我们许多方便。微软的出发点可能是希望达到“傻瓜编程”的结果,试想,谁不会用ClassWizard?大家知道,Windows是基于消息的,有了ClassWizard,你又会添加类,又会添加消息,那么你所学的东西似乎学到头了。于是许多程序员认为“我们没有必要走SDK的老路,直接用MFC编程,新的东西通常是简单、直观、易学……”
到你真正想用MFC编程的时候,你会发觉光会ClassWizard的你是多么的愚蠢。MFC不是一个普通的类库,普通的类库我们完全可以不理解里面的细节,只要知道这些类库能干什么,接口参数如何就万事大吉。如string类,操作顺序是定义一个string对象,然后修改属性,调用方法。
但对于MFC,你并不是在你的程序中写上一句“#include MFC.h”,然后就在你的程序中用MFC类库。
MFC是一块包着糖衣的牛骨头。你很轻松地写出一个单文档窗口,在窗口中间打印一句“I love MFC!”,然后,恶梦开始了……想逃避,打算永远不去理解MFC内幕?门都没有!在MFC这个黑暗神秘的洞中,即使你打算摸着石头前行,也注定找不到出口。对着MFC这块牛骨头,微软温和、民主地告诉你“你当然可以选择不啃掉它,咳咳……但你必然会因此而饿死!”
消息映射与命令传递体现了MFC与SDK的不同。在SDK编程中,没有消息映射的概念,它有明确的回调函数中,通过一个switch语句去判断收到了何种消息,然后对这个消息进行处理。所以,在SDK编程中,会发送消息和在回调函数中处理消息就差不多可以写SDK程序了。
在MFC中,看上去发送消息和处理消息比SDK更简单、直接,但可惜不直观。举个简单的例子,如果我们想自定义一个消息,SDK是非常简单直观的,用一条语句:SendMessage(hwnd,message/*一个大于或等于WM_USER的数字*/,wparam,lparam),之后就可以在回调函数中处理了。但MFC就不同了,因为你通常不直接去改写窗口的回调函数,所以只能亦步亦趋对照原来的MFC代码,把消息放到恰当的地方。这确实是一样很痛苦的劳动。
要了解MFC消息映射原理并不是一件轻松的事情。我们可以逆向思维,想象一下消息映射为我们做了什么工作。MFC在自动化给我们提供了很大的方便,比如,所有的MFC窗口都使用同一窗口过程,即所有的MFC窗口都有一个默认的窗口过程。不象在SDK编程中,要为每个窗口类写一个窗口过程。
对于消息映射,最直截了当地猜想是:消息映射就是用一个数据结构把“消息”与“响应消息函数名”串联起来。这样,当窗口感知消息发生时,就对结构查找,找到相应的消息响应函数执行。其实这个想法也不能简单地实现:我们每个不同的MFC窗口类,对同一种消息,有不同的响应方式。即是说,对同一种消息,不同的MFC窗口会有不同的消息响应函数。
这时,大家又想了一个可行的方法。我们设计窗口基类(CWnd)时,我们让它对每种不同的消息都来一个消息响应,并把这个消息响应函数定义为空虚函数。这样,从CWnd派生的窗口类对所有消息都有了一个空响应,我们要响应一个特定的消息就重载这个消息响应函数就可以了。但这样做的结果,一个几乎什么也不做的CWnd类要有几百个“多余”的函数,那怕这些消息响应函数都为纯虚函数,每个CWnd对象也要背负着一个巨大的虚拟表,这也是得不偿失的。
许多朋友在学习消息映射时苦无突破,其原因是一开始就认为MFC的消息映射的目的是为了替代SDK窗口过程的编写——这本来没有理解错。但他们还有多一层的理解,认为既然是替代“旧”的东西,那么MFC消息映身应该是更高层次的抽象、更简单、更容易认识。但结果是,如果我们不通过ClassWizard工具,手动添加消息是相当迷茫的一件事。
所以,我们在学习MFC消息映射时,首先要弄清楚:消息映射的目的,不是为是更加快捷地向窗口过程添加代码,而是一种机制的改变。如果不想改变窗口过程函数,那么应该在哪里进行消息响应呢?许多朋友一知半解地认为:我们可以用HOOK技术,抢在消息队列前把消息抓取,把消息响应提到窗口过程的外面。再者,不同的窗口,会有不同的感兴趣的消息,所以每个MFC窗口都应该有一个表把感兴趣的消息和相应消息响应函数连系起来。然后得出——消息映射机制执行步骤是:当消息发生,我们用HOOK技术把本发送到窗口过程的消息抓获,然后对照一下MFC窗口的消息映射表,如果是表里面有的消息,就执行其对应的函数。
当然,用HOOK技术,我们理论上可以在不改变窗口过程函数的情况下,可以完成消息响应。MFC确实是这样做,但实际操作起来可能跟你的想象差别很大。
现在我们来编写消息映射表,我们先定义一个结构,这个结构至少有两个项:一是消息ID,二是响应该消息的函数。如下:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; //感兴趣的消息
AFX_PMSG pfn; //响应以上消息的函数指针
}
当然,只有两个成员的结构连接起来的消息映射表是不成熟的。Windows消息分为标准消息、控件消息和命令消息,每类型的消息包含数百不同ID、不同意义、不同参数的消息。我们要准确地判别发生了何种消息,必须再增加几个成员。还有,对于AFX_PMSG pfn,实际上等于作以下声明:
void (CCmdTarget::*pfn)();
(提示:AFX_PMSG为类型标识,具体声明是:typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);)
pfn是不一不带参数和返回值的CCmdTarget类型函数指针,只能指向CCmdTarget类中不带参数和返回值的成员函数,这样pfn更为通用,但我们响应消息的函数许多需要传入参数的。为了解决这个矛盾,我们还要增加一个表示参数类型的成员。当然,还有其它……
最后,MFC我们消息映射表成员结构如下定义:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; //Windows 消息ID
UINT nCode; // 控制消息的通知码
UINT nID; //命令消息ID范围的起始值
UINT nLastID; //命令消息ID范围的终点
UINT nSig; // 消息的动作标识
AFX_PMSG pfn;
};
有了以上消息映射表成员结构,我们就可以定义一个AFX_MSGMAP_ENTRY类型的数组,用来容纳消息映射项。定义如下:
AFX_MSGMAP_ENTRY _messageEntries[];
但这样还不够,每个AFX_MSGMAP_ENTRY数组,只能保存着当前类感兴趣的消息,而这仅仅是我们想处理的消息中的一部分。对于一个MFC程序,一般有多个窗口类,里面都应该有一个AFX_MSGMAP_ENTRY数组。我们知道,MFC还有一个消息传递机制,可以把自己不处理的消息传送给别的类进行处理。为了能查找各下MFC对象的消息映射表,我们还要增加一个结构,把所有的AFX_MSGMAP_ENTRY数组串联起来。
MFC六大关键技术之(五)(六)——消息映射与命令传递收藏
MFC六大关键技术之(五)(六)——消息映射与命令传递
题外话:刚开始学视窗程序设计的时候,我就打印了一本Windows消息详解,里面列举了各种已定义消息的意义和作用,共10多页,在编程的时候翻翻,有时觉得很受用。我发觉很多编程的朋友,虽然每天都面对消息,却很少关注它。C++程序员有一个通病,很想写“自己”的程序,即每一行代码都想自己写出来。如果用了一些库,总希望能完全理解库里的类或函数是怎么一回事,否则就“不踏实”。对于消息,许多朋友只关心常用的几个,对其余的漠不关心。其实,Windows中有很多不常用的消息却很有用,程序员可能通过响应这些消息实现更简捷的编程。
说到消息,在MFC中,“最熟悉的神秘”可算是消息映射,那是我们刚开始接触MFC时就要面对的东西。有过SDK编程经验的朋友转到MFC编程的时候,一下子觉得什么都变了样。特别是窗口消息及对消息的处理跟以前相比,更是风马牛不相及的。如文档不是窗口,是怎样响应命令消息的呢?
初次用MFC编程,我们只会用MFC ClassWizard为我们做大量的东西,最主要的是添加消息响应。记忆中,如果是自已添加消息响应,我们应何等的小心翼翼,对BEGIN_MESSAGE_MAP()……END_MESSAGE_MAP()更要奉若神灵。它就是一个魔盒子,把我们的咒语放入恰当的地方,就会发生神奇的力量,放错了,自己的程序就连“命”都没有。
据说,知道得太多未必是好事。我也曾经打算不去理解这神秘的区域,觉得编程的时候知道自己想做什么就行了。MFC外表上给我们提供了东西,直观地说,不但给了我个一个程序的外壳,更给我们许多方便。微软的出发点可能是希望达到“傻瓜编程”的结果,试想,谁不会用ClassWizard?大家知道,Windows是基于消息的,有了ClassWizard,你又会添加类,又会添加消息,那么你所学的东西似乎学到头了。于是许多程序员认为“我们没有必要走SDK的老路,直接用MFC编程,新的东西通常是简单、直观、易学……”
到你真正想用MFC编程的时候,你会发觉光会ClassWizard的你是多么的愚蠢。MFC不是一个普通的类库,普通的类库我们完全可以不理解里面的细节,只要知道这些类库能干什么,接口参数如何就万事大吉。如string类,操作顺序是定义一个string对象,然后修改属性,调用方法。
但对于MFC,你并不是在你的程序中写上一句“#include MFC.h”,然后就在你的程序中用MFC类库。
MFC是一块包着糖衣的牛骨头。你很轻松地写出一个单文档窗口,在窗口中间打印一句“I love MFC!”,然后,恶梦开始了……想逃避,打算永远不去理解MFC内幕?门都没有!在MFC这个黑暗神秘的洞中,即使你打算摸着石头前行,也注定找不到出口。对着MFC这块牛骨头,微软温和、民主地告诉你“你当然可以选择不啃掉它,咳咳……但你必然会因此而饿死!”
消息映射与命令传递体现了MFC与SDK的不同。在SDK编程中,没有消息映射的概念,它有明确的回调函数中,通过一个switch语句去判断收到了何种消息,然后对这个消息进行处理。所以,在SDK编程中,会发送消息和在回调函数中处理消息就差不多可以写SDK程序了。
在MFC中,看上去发送消息和处理消息比SDK更简单、直接,但可惜不直观。举个简单的例子,如果我们想自定义一个消息,SDK是非常简单直观的,用一条语句:SendMessage(hwnd,message/*一个大于或等于WM_USER的数字*/,wparam,lparam),之后就可以在回调函数中处理了。但MFC就不同了,因为你通常不直接去改写窗口的回调函数,所以只能亦步亦趋对照原来的MFC代码,把消息放到恰当的地方。这确实是一样很痛苦的劳动。
要了解MFC消息映射原理并不是一件轻松的事情。我们可以逆向思维,想象一下消息映射为我们做了什么工作。MFC在自动化给我们提供了很大的方便,比如,所有的MFC窗口都使用同一窗口过程,即所有的MFC窗口都有一个默认的窗口过程。不象在SDK编程中,要为每个窗口类写一个窗口过程。
对于消息映射,最直截了当地猜想是:消息映射就是用一个数据结构把“消息”与“响应消息函数名”串联起来。这样,当窗口感知消息发生时,就对结构查找,找到相应的消息响应函数执行。其实这个想法也不能简单地实现:我们每个不同的MFC窗口类,对同一种消息,有不同的响应方式。即是说,对同一种消息,不同的MFC窗口会有不同的消息响应函数。
这时,大家又想了一个可行的方法。我们设计窗口基类(CWnd)时,我们让它对每种不同的消息都来一个消息响应,并把这个消息响应函数定义为空虚函数。这样,从CWnd派生的窗口类对所有消息都有了一个空响应,我们要响应一个特定的消息就重载这个消息响应函数就可以了。但这样做的结果,一个几乎什么也不做的CWnd类要有几百个“多余”的函数,那怕这些消息响应函数都为纯虚函数,每个CWnd对象也要背负着一个巨大的虚拟表,这也是得不偿失的。
许多朋友在学习消息映射时苦无突破,其原因是一开始就认为MFC的消息映射的目的是为了替代SDK窗口过程的编写——这本来没有理解错。但他们还有多一层的理解,认为既然是替代“旧”的东西,那么MFC消息映身应该是更高层次的抽象、更简单、更容易认识。但结果是,如果我们不通过ClassWizard工具,手动添加消息是相当迷茫的一件事。
所以,我们在学习MFC消息映射时,首先要弄清楚:消息映射的目的,不是为是更加快捷地向窗口过程添加代码,而是一种机制的改变。如果不想改变窗口过程函数,那么应该在哪里进行消息响应呢?许多朋友一知半解地认为:我们可以用HOOK技术,抢在消息队列前把消息抓取,把消息响应提到窗口过程的外面。再者,不同的窗口,会有不同的感兴趣的消息,所以每个MFC窗口都应该有一个表把感兴趣的消息和相应消息响应函数连系起来。然后得出——消息映射机制执行步骤是:当消息发生,我们用HOOK技术把本发送到窗口过程的消息抓获,然后对照一下MFC窗口的消息映射表,如果是表里面有的消息,就执行其对应的函数。
当然,用HOOK技术,我们理论上可以在不改变窗口过程函数的情况下,可以完成消息响应。MFC确实是这样做,但实际操作起来可能跟你的想象差别很大。
现在我们来编写消息映射表,我们先定义一个结构,这个结构至少有两个项:一是消息ID,二是响应该消息的函数。如下:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; //感兴趣的消息
AFX_PMSG pfn; //响应以上消息的函数指针
}
当然,只有两个成员的结构连接起来的消息映射表是不成熟的。Windows消息分为标准消息、控件消息和命令消息,每类型的消息包含数百不同ID、不同意义、不同参数的消息。我们要准确地判别发生了何种消息,必须再增加几个成员。还有,对于AFX_PMSG pfn,实际上等于作以下声明:
void (CCmdTarget::*pfn)();
(提示:AFX_PMSG为类型标识,具体声明是:typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);)
pfn是不一不带参数和返回值的CCmdTarget类型函数指针,只能指向CCmdTarget类中不带参数和返回值的成员函数,这样pfn更为通用,但我们响应消息的函数许多需要传入参数的。为了解决这个矛盾,我们还要增加一个表示参数类型的成员。当然,还有其它……
最后,MFC我们消息映射表成员结构如下定义:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; //Windows 消息ID
UINT nCode; // 控制消息的通知码
UINT nID; //命令消息ID范围的起始值
UINT nLastID; //命令消息ID范围的终点
UINT nSig; // 消息的动作标识
AFX_PMSG pfn;
};
有了以上消息映射表成员结构,我们就可以定义一个AFX_MSGMAP_ENTRY类型的数组,用来容纳消息映射项。定义如下:
AFX_MSGMAP_ENTRY _messageEntries[];
但这样还不够,每个AFX_MSGMAP_ENTRY数组,只能保存着当前类感兴趣的消息,而这仅仅是我们想处理的消息中的一部分。对于一个MFC程序,一般有多个窗口类,里面都应该有一个AFX_MSGMAP_ENTRY数组。我们知道,MFC还有一个消息传递机制,可以把自己不处理的消息传送给别的类进行处理。为了能查找各下MFC对象的消息映射表,我们还要增加一个结构,把所有的AFX_MSGMAP_ENTRY数组串联起来。