6.4 消息映射
6.4.1 消息映射表
直接在窗口函数WndProc中处理消息很烦琐。我们希望能够直接使用类的成员函数响应感兴趣的消息。比如现在有一个CMyWnd类,当处理某个消息的时候,只要在这个类中添加一个对应的成员函数就行了,如图6.4所示。
在CMyWnd类中,OnCreate函数响应的是WM_CREATE消息,即当窗口接收到WM_CREATE消息以后,想让框架程序自动调用OnCreate函数。函数的名称是任意的,这里命名为OnCreate只是为了有意义。OnPaint和OnDestroy函数分别响应WM_PAINT和WM_DESTROY消息。要是对其他消息感兴趣的话,你还可以随意在CMyWnd类中增加消息处理函数的数量。
图6.4中的消息和处理消息的成员是一一对应的,这就是所谓的消息映射。每一对消息和处理消息的成员组成一个映射项,类中所有的映射项连在一起形成消息映射表。窗口函数只有知道类的消息映射表,才能在消息到来的时候,主动调用表中此消息对应的处理函数。所以,我们必须想办法把映射表中的数据记录下来。
每个消息映射项最基本的内容应该包括消息的值和处理该消息的成员函数,下面用一个名称为AFX_MSGMAP_ENTRY的结构来描述它。
// 处理消息映射的代码 // 注意,这些代码在_AFXWIN.H文件的开头,要按顺序写下
class CCmdTarget;
typedef void (CCmdTarget::*AFX_PMSG)(void);
// 一个映射表项
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; // 窗口消息
UINT nCode; // 控制代码或WM_NOTIFY通知码
UINT nID; // 控件ID,如果为窗口消息其值为0
UINT nLastID; // 一定范围的命令的最后一个命令或控件ID,用于支持组消息映射
UINT nSig; // 指定了消息处理函数的类型
AFX_PMSG pfn; // 消息处理函数
};
现在只注意nMessage和pfn两个成员就行了,其他的成员都是对nMessage消息的更具体的描述,以后再谈。你可能会对AFX_PMSG宏的定义感到奇怪,为什么要把消息处理函数都定义成CCmdTarget类的成员函数呢?事实上,我们对用户还有一个要求,就是所有有消息处理能力的类都要从CCmdTarget继承。既然无法预知用户定义的消息处理函数的具体类型,只好先统一转化成AFX_PMSG宏指定的类型了。
现在只要在CMyWnd类中添加一个AFX_MSGMAP_ENTRY类型的静态数组就即可将该类的消息和消息处理函数关联起来,如下代码所示。
class CMyWnd // 这是示例代码,不属于类库的一部分
{
public:
void OnCreate() { /* 响应WM_CREAT消息的代码*/ }
void OnPaint() { /* 响应WM_PAINT 消息的代码*/ }
void OnDestory() { /* 响应WM_DESTROY 消息的代码*/}
private:
// 此数组记录了消息映射表中的数据
static const AFX_MSGMAP_ENTRY _messageEntries[];
};
const AFX_MSGMAP_ENTRY CMyWnd::_messageEntries[] =
{
{WM_CREATE, 0, 0, 0, 0, (AFX_PMSG)CMyWnd::OnCreate},
{WM_PAINT, 0, 0, 0, 0, (AFX_PMSG)CMyWnd::OnPaint},
{WM_DESTROY, 0, 0, 0, 0, (AFX_PMSG)CMyWnd::OnDestory}
};
静态成员_messageEntries指向的数组记录了CMyWnd类要处理的消息和对应的消息处理函数。窗口函数接收到消息后,可以遍历此数组找到响应该消息的成员函数,然后把消息的处理权交给这个函数。
在类的继承结构中,对于CMyWnd类没有处理的消息,应该由CMyWnd类的基类来做默认处理。如果负责做默认处理的类是CCmdTarget的话,CMyWnd类就应该从CCmdTarget类继承,还应该记录下其基类中消息映射表的地址(这样消息才能向上传递)。所以我们最终用AFX_MSGMAP结构来描述类的消息映射表。
struct AFX_MSGMAP // 继续在AFX_MSGMAP_ENTRY结构之后定义此结构,_AFXWIN.H文件中
{
const AFX_MSGMAP* pBaseMap; // 其基类的消息映射表的地址
const AFX_MSGMAP_ENTRY* pEntries; // 消息映射项的指针
};
下面是具有消息处理能力以后CMyWnd类的完整代码。
class CMyWnd : public CCmdTarget // 示例代码
{
public:
void OnCreate() { /* 响应WM_CREAT消息的代码*/ }
void OnPaint() { /* 响应WM_PAINT 消息的代码*/}
void OnDestory() { /* 响应WM_DESTROY 消息的代码*/}
// 定义消息映射的代码
private:
static const AFX_MSGMAP_ENTRY _messageEntries[];
protected:
static const AFX_MSGMAP messageMap;
virtual const AFX_MSGMAP* GetMessageMap() const;
};
// 实现消息映射的代码
const AFX_MSGMAP* CMyWnd::GetMessageMap() const
{ return &CMyWnd::messageMap; }
const AFX_MSGMAP CMyWnd::messageMap =
{ NULL/*&CCmdTarget::messageMap*/, &CMyWnd::_messageEntries[0] };
const AFX_MSGMAP_ENTRY CMyWnd::_messageEntries[] =
{
{WM_CREATE, 0, 0, 0, 0, (AFX_PMSG)CMyWnd::OnCreate},
{WM_PAINT, 0, 0, 0, 0, (AFX_PMSG)CMyWnd::OnPaint},
{WM_DESTROY, 0, 0, 0, 0, (AFX_PMSG)CMyWnd::OnDestory}
// 在这里添加你要处理的消息
};
GetMessageMap是一个虚函数,用来取得当前类中消息映射表的地址。现在的CCmdTarget类中还没有消息映射表,所以在初始化messageMap变量的时候,为其成员pBaseMap传递的是NULL,否则就应该传递&CCmdTarget::messageMap了。
6.4.2 DECLARE_MESSAGE_MAP等宏的定义
框架程序就是采取上一小节所述的机制处理Windows消息的。CCmdTarget类位于消息映射的最顶层,这个类的消息映射表中pBaseMap成员的值是NULL。类要想具有消息处理能力的话,必须从CCmdTarget类继承,而且还必须有自己的消息映射表。与为CObject派生类添加运行时类信息类似,我们也可以通过一组宏简化添加消息映射表的过程。
CMyWnd类中定义消息映射的代码并不与具体的类相关,只用一个不带参数的宏就可以将它们代替。将此宏命名为DECLARE_MESSAGE_MAP,即“声明消息映射”的意思,下面是定义它的代码。
// 在消息映射表AFX_MSGMAP结构的定义之后定义下面的宏,_AFXWIN.H文件中
#define DECLARE_MESSAGE_MAP() \
private: \
static const AFX_MSGMAP_ENTRY _messageEntries[]; \
protected: \
static const AFX_MSGMAP messageMap; \
virtual const AFX_MSGMAP* GetMessageMap() const; \
消息映射表项中记录的数据是由用户填写的,所以再添加BEGIN_MESSAGE_MAP和END_MESSAGE_MAP两个宏代替实现消息映射的代码,在这两个宏中间用户填写初始化映射表项的代码。下面是这两个宏的定义。
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
const AFX_MSGMAP* theClass::GetMessageMap() const \
{ return &theClass::messageMap; } \
const AFX_MSGMAP theClass::messageMap = \
{ &baseClass::messageMap, &theClass::_messageEntries[0] }; \
const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \
{ \
#define END_MESSAGE_MAP() \
{0, 0, 0, 0, 0, (AFX_PMSG)0} \
}; \
实现消息映射的代码中出现了当前类和基类的名字,所以替代它们的宏应该有这两个参数。“begin messge map”和“end message map”这两个名字比较能说明问题,一个是开始消息映射,一个是结束消息映射,它们中间的代码是真正的消息映射。
END_MESSAGE_MAP宏向映射表中添加的映射表项“{0, 0, 0, 0, 0, (AFX_PMSG)0}”是数组的结束标记。遍历类的映射表时,遇到此项就意味着映射表中已经没有数据了。
这两个宏必须配对使用,下面是利用它们初始化CMyWnd类的消息映射表的例子。
// 注意,必须当CCmdTarget中添加类消息映射表之后,这些代码才能提供编译 // 示例代码
BEGIN_MESSAGE_MAP(CMyWnd, CCmdTarget)
{WM_CREATE, 0, 0, 0, 0, (AFX_PMSG)CMyWnd::OnCreate},
{WM_PAINT, 0, 0, 0, 0, (AFX_PMSG)CMyWnd::OnPaint},
{WM_DESTROY, 0, 0, 0, 0, (AFX_PMSG)CMyWnd::OnDestory},
END_MESSAGE_MAP()
现在该给类库中的类添加消息映射表了。首先是CCmdTarget类,在这个类的定义代码中加入DECLARE_MESSAGE_MAP宏。
class CCmdTarget : public CObject
{
//...... // 其他成员
DECLARE_MESSAGE_MAP()
};
因为CCmdTarget类位于消息映射的最顶层,在消息处理这一体系中,它是没有父类的,所以就不能直接用BEGIN_MESSAGE_MAP和END_MESSAGE_MAP这一对宏,而只能手工添加实现消息映射的代码。下面是CMDTARG.CPP文件中相应的代码。
// 实现消息映射的代码
const AFX_MSGMAP* CCmdTarget::GetMessageMap () const
{
return &CCmdTarget::messageMap ;
}
const AFX_MSGMAP CCmdTarget::messageMap =
{ NULL, &CCmdTarget::_messageEntries[0] };
const AFX_MSGMAP_ENTRY CCmdTarget::_messageEntries[] =
{
// 一个消息也不处理
{ 0, 0, 0, 0, 0, (AFX_PMSG)0 }
};
现在按F7键编译连接做实验的工程,应该是不会出错的。
处理消息的核心是CWnd类,因为消息由此类的非静态成员函数WindowProc传入到类的对象中。由于CWnd类从CCmdTarget类继承,所以直接使用本小节设计的宏即可。
class CWnd : public CCmdTarget // _AFXWIN.H文件
{
... // 其他成员
DECLARE_MESSAGE_MAP()
};
在WINCORE.CPP文件中添加如下代码。
BEGIN_MESSAGE_MAP(CWnd, CCmdTarget)
// 在此处添加CWnd类要处理的消息
END_MESSAGE_MAP()
消息映射表有了,下一节介绍如何向表中添加消息映射项。