百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

VC|窗口函数的模块化,底层阐述消息映射如何串接消息与处理函数

liebian365 2024-10-24 14:35 21 浏览 0 评论

对于GUI程序,主要包括UI和代码(包括库)部分:

GUI程序以消息为基础,以事件驱动之(message based, event driven),窗口操作产生事件,事件封装为消息,区分不同消息执行不同的代码块(如switch/case结构),或按消息调用不同的消息处理函数。

每一个Windows 程序都应该有一个回路如下:

MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

接受并处理消息的主角就是窗口。每一个窗口都应该有一个函数负责处理消息,程序员必须负责设计这个所谓的「窗口函数」(window procedure,或称为window function)。

如果窗口获得一个消息,这个窗口函数必须判断消息的类别,决定处理的方式。

我们知道,面向过程其实通过一定的语法结构也是可以较为笨拙地实现面向对象的封装思想的。核心在是在结构体中包含函数指针,再建立结构体数组,便可将上述的可能很大一块的switch/case结构模块化,这就是消息映射(Message Map)的雏形。

1 定义一个MSGMAP_ENTRY 结构和一个dim 宏

struct MSGMAP_ENTRY {
 UINT nMessage;
 LONG (*pfn)(HWND, UINT, WPARAM, LPARAM);
};
#define dim(x) (sizeof(x) / sizeof(x[0])) //计算数组元素个数

请注意MSGMAP_ENTRY 的第二元素pfn 是一个函数指针,以此指针所指之函数处理nMessage 消息。这正是对象导向观念中把「数据」和「处理数据的方法」封装起来的一种具体实现,只不过我们用的不是C++ 语言。

2 建立结构体数组,把程序中欲处理的消息以及消息处理例程的关联性建立起来

定义结构体数组_messageEntries[ ] 把程序中欲处理的消息以及消息处理函数的关联性建立起来。对WM_COMMAND消息,另定义一个结构体数组_commandEntries[ ],将WM_COMMAND 命令项和命令处理函数的关联性建立起来。

2.1 消息与处理函数之对照的结构体数组

struct MSGMAP_ENTRY _messageEntries[] =
{
 WM_CREATE, OnCreate,
 WM_PAINT, OnPaint,
 WM_SIZE, OnSize,
 WM_COMMAND, OnCommand,
 WM_SETFOCUS, OnSetFocus,
 WM_CLOSE, OnClose,
 WM_DESTROY, OnDestroy,
} ; //消息 vs 消息处理函数

2.1 Command-ID与处理函数

各种消息之中,来自菜单或工具栏(包括快捷键)的消息,都以WM_COMMAND 表示,所以这一类消息我们又称之为命令消息(Command Message),其wParam 记录着此一消息来自哪一个菜单项目。(SDK程序主要靠消息的wParam 辨识之,MFC 程序则主要靠菜单项目的识别码(menu ID)辨识之-- 两者其实是相同的。)

除了命令消息,还有一种消息也比较特殊,出现在对话框函数中,是控件(controls)传送给父窗口(即对话框)的消息。虽然它们也是以WM_COMMAND 为外衣,但特别归类为「notification 消息」,例如当你在ListBox 上选择其中一个项目,ListBox 就会产生BN_SELCHANGE 传送给父窗口。如ON_NOTIFY(TCN_SELCHANGE, IDC_TAB, OnSelchangeTab)。所以消息的分类有3种:窗口消息、命令消息和控件通知消息。为什么要特别区隔出命令消息WM_COMMAND 和通告消息WM_NOTIFY 两类呢?因为它们的上溯路径不同。

struct MSGMAP_ENTRY _commandEntries =
{
 IDM_ABOUT, OnAbout,
 IDM_FILEOPEN, OnFileOpen,
 IDM_SAVEAS, OnSaveAs,
} ; //WM_COMMAND 命令项 vs 命令处理函数

3 窗口回调函数

在窗口回调函数中通过一个for循环(而不是switch/case结构)来匹配消息与消息处理函数:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
 int i;
 for(i=0; i < dim(_messageEntries); i++) { // 对照消息调用对应的处理函数
 if (message == _messageEntries[i].nMessage)
 return((*_messageEntries[i].pfn)(hWnd, message, wParam, lParam));
 }
 return(DefWindowProc(hWnd, message, wParam, lParam));
}
// OnCommand --专门处理WM_COMMAND
LONG OnCommand(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
 int i;
 for(i=0; i < dim(_commandEntries); i++) { // 对照命令调用对应的命令处理函数
 if (LOWORD(wParam) == _commandEntries[i].nMessage)
 return((*_commandEntries[i].pfn)(hWnd, message, wParam, lParam));
}
return(DefWindowProc(hWnd, message, wParam, lParam));
}

上面的窗口回调函数已没有了那一大块的switch/case结构,而实现了模块化,用一个简单的for循环代替,而具体的函数代码则由其中的函数指针的指向去实现。

4 编写消息和命令处理函数

具体的消息对应的消息处理函数可以分开来写了:

LONG OnCreate(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{
 ...
}
LONG OnAbout(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{
 ...
}
...

这么一来,WndProc 和OnCommand 永远不必改变,每有新要处理的消息,只要在_messageEntries[ ] 和_commandEntries[ ] 两个数组中加上新元素,并针对新消息撰写新的处理函数即可。

这种观念以及作法就是MFC 的Message Map 的雏形。MFC 把其中的动作包装得更好更精致(当然因此也就更复杂得多,使用了类和对象的语法机制),成为一张庞大的消息地图;程序一旦获得消息,就可以按图上溯,直到被处理为止。

C++的继承与多态性质,使衍生类别与基础类别的成员函数之间有着特殊的关联。但这当中并没有牵扯到Windows消息。的确,C++语言完全没有考虑Windows消息这一回事(那当然)。如何让Windows消息也能够在对象导向以及继承性质中扮演一个角色?既然语言没有支持,只好自求多福了。消息映射机制的三个相关宏就是MFC自求多福的结果。

「消息映射」是MFC内建的一个消息分派机制,只要利用数个宏以及固定形式的写法,类似填表格(结构体数组),就可以让Framework知道,一旦消息发生,该循哪一条路递送。每一个类别只能拥有一个消息映射表格(实质是实现一个结构体数组,结构成员有消息和消息处理函数指针),但也可以没有。下面是ScribbleDocument建立消息映射表的动作:

首先你必须在类别实现文件(.H)声明拥有消息映射表格:

class CScribbleDoc : public CDocument

{

...

DECLARE_MESSAGE_MAP()

};

然后在类别实作档(.CPP)实现此一表格:

BEGIN_MESSAGE_MAP(CScribbleDoc, CDocument)

//{{AFX_MSG_MAP(CScribbleDoc)

ON_COMMAND(ID_EDIT_CLEAR_ALL, OnEditClearAll)

ON_COMMAND(ID_PEN_THICK_OR_THIN, OnPenThickOrThin)

...

//}}AFX_MSG_MAP

END_MESSAGE_MAP()

消息映射的本质其实是一个巨大的数据结构,用来为诸如WM_PAINT这样的标准消息决定流动路线,使它得以流到父类别去;也用来为WM_COMMAND这个特殊消息决定流动路线,使它能够七拐八弯地流到类别阶层结构的旁支去。观察机密的最好方法就是挖掘源代码:

// in AFXWIN.H
#define DECLARE_MESSAGE_MAP() \
private: \
static const AFX_MSGMAP_ENTRY _messageEntries[]; \
protected: \
static AFX_DATA const AFX_MSGMAP messageMap; \
virtual const AFX_MSGMAP* GetMessageMap() const; \
// static 修饰词限制了类成员的配置,使得每个「类别」仅有一份资料,而不是每一个「对象」各有一份资料。

AFX_MSGMAP_ENTRY 是一个struct:

struct AFX_MSGMAP_ENTRY
{
UINT nMessage; // windows message
UINT nCode; // control code or WM_NOTIFY code
UINT nID; // control ID (or 0 for windows messages)
UINT nLastID; // used for entries specifying a range of control id's
UINT nSig; // signature type (action) or pointer to message #
AFX_PMSG pfn; // routine to call (or special value)
};

很明显你可以看出它的最主要作用,就是让消息nMessage 对应于函数pfn。其中pfn 的数据类型AFX_PMSG 被定义为一个函数指针:

typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);

AFX_MSGMAP也是一个struct:

// in AFXWIN.H
struct AFX_MSGMAP
{
const AFX_MSGMAP* pBaseMap;
const AFX_MSGMAP_ENTRY* lpEntries;
};

其中pBaseMap是一个指向「基础类别之消息映射表」的指针,它提供了一个走访整个继承串链的方法,有效地实作出消息映射的继承性。衍生类别将自动地「继承」其基础类别中所处理的消息,意思是,如果基础类别处理过A消息,其衍生类别即使未设计A消息之消息映射表项目,也具有对A消息的处理能力。当然啦,衍生类别也可以针对A消息设计自己的消息映射表项。真像虚拟函数!但MessageMap没有虚拟函数所带来的巨大的overhead(额外负担)。

另一组宏:

BEGIN_MESSAGE_MAP(CMyView, CView)
ON_WM_PAINT()
ON_WM_CREATE()
...
END_MESSAGE_MAP()

奥秘还是在源代码中:

// 以下源代码在AFXWIN.H

#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
const AFX_MSGMAP* theClass::GetMessageMap() const \
{ return &theClass::messageMap; } \
AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \
{ &baseClass::messageMap, &theClass::_messageEntries[0] }; \
const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \
{ \
#define END_MESSAGE_MAP() \
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
}; \

// 以下源代码在AFXMSG_.H

#define ON_COMMAND(id, memberFxn) \
{ WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)memberFxn },
#define ON_WM_CREATE() \
{ WM_CREATE, 0, 0, 0, AfxSig_is, \
(AFX_PMSG)(AFX_PMSGW)(int (AFX_MSG_CALL CWnd::*)(LPCREATESTRUCT))OnCreate },
#define ON_WM_DESTROY() \
{ WM_DESTROY, 0, 0, 0, AfxSig_vv, \
(AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))OnDestroy },
#define ON_WM_MOVE() \
{ WM_MOVE, 0, 0, 0, AfxSig_vvii, \
(AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(int, int))OnMove },
#define ON_WM_SIZE() \
{ WM_SIZE, 0, 0, 0, AfxSig_vwii, \
(AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(UINT, int, int))OnSize },
#define ON_WM_ACTIVATE() \
{ WM_ACTIVATE, 0, 0, 0, AfxSig_vwWb, \
(AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(UINT, CWnd*,
BOOL))OnActivate },
#define ON_WM_SETFOCUS() \
{ WM_SETFOCUS, 0, 0, 0, AfxSig_vW, \
(AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(CWnd*))OnSetFocus },
#define ON_WM_PAINT() \
{ WM_PAINT, 0, 0, 0, AfxSig_vv, \
(AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))OnPaint },
#define ON_WM_CLOSE() \
{ WM_CLOSE, 0, 0, 0, AfxSig_vv, \
(AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))OnClose },
...

于是,这样的宏:

BEGIN_MESSAGE_MAP(CMyView, CView)
ON_WM_CREATE()
ON_WM_PAINT()
END_MESSAGE_MAP()

便被展开成为这样的码:

const AFX_MSGMAP* CMyView::GetMessageMap() const
{ return &CMyView::messageMap; }
AFX_DATADEF const AFX_MSGMAP CMyView::messageMap = 
{ &CView::messageMap, &CMyView::_messageEntries[0] }; //结构体变量赋值
const AFX_MSGMAP_ENTRY CMyView::_messageEntries[] =
{ //结构体变量数组赋值
{ WM_CREATE, 0, 0, 0, AfxSig_is, \
(AFX_PMSG)(AFX_PMSGW)(int (AFX_MSG_CALL CWnd::*)(LPCREATESTRUCT))OnCreate },
{ WM_PAINT, 0, 0, 0, AfxSig_vv, \
(AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))OnPaint },
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
};

其中AFX_DATADEF 和AFX_MSG_CALL 又是两个看起来很奇怪的常数。你可以在两个文件中找到它们的定义:

// in \DEVSTUDIO\VC\MFC\INCLUDE\AFXVER_.H
#define AFX_DATA
#define AFX_DATADEF
// in \DEVSTUDIO\VC\MFC\INCLUDE\AFXWIN.H
#define AFX_MSG_CALL

显然它们就像afx_msg 一样,都只是个"intentional placeholder"(刻意保留的空间),可能在将来会用到,目前则为「无物」。

#define afx_msg // intentional placeholder

我们终于了解,MessageMap既可说是一套宏,也可以说是宏展开后所代表的一套数据结构;甚至也可以说MessageMap是一种动作,这个动作,就是在刚刚所提的数据结构中寻找与消息相吻合的项目,从而获得消息的处理数据的函数指针。

虽然,C++程序员看到多态(Polymorphism),直觉的反应就是虚拟函数,但请注意,各个MessageMap中的各个同名函数虽有多态的味道,却不是虚拟函数。乍想之下使用虚拟函数是合理的:你产生一个与窗口有关的C++类别,然后为此窗口所可能接收的任何消息都提供一个对应的虚拟函数。这的确散发着C++的味道和对象导向的精神,但现实与理想之间总是有些距离。

要知道,虚拟函数必须经由一个虚拟函数表(virtual function table,vtable)实现出来,每一个子类别必须有它自己的虚拟函数表,其内至少有父类别之虚拟函数表的内容复本。好哇,虚拟函数表中的每一个项目都是一个函数指针,价值4字节,如果基础类别的虚拟函数表有100个项目,经过10层继承,开枝散叶,总共需耗费多少内存在其中?最终,系统会被巨大的额外负担(overhead)拖垮!这就是为什么MFC采用独特的消息映射机制而不采用虚拟函数的原因。

窗口产生与消息映射:

Message Routing:

reference: 侯俊杰 《深入浅出MFC》

-End-

相关推荐

C#夯实基础-Lambda在List中的使用

在C#中基本类型比如List,Dictionary,数组等都有委托来实现相关的操作。此时Lambda表达式就可以使用了.实例1,查找字符串List的包含a的元素...

在C#中,如何实现对集合中元素的自定义排序?

在C#中,可以通过多种方式实现对集合中元素的自定义排序,主要包括:...

C++11 新特性面试题_c++ 11 面试题

1、C++11中引入了哪些新的智能指针类型?请描述它们的用法和区别。C++11中引入了三种新的智能指针类型:std::unique_ptr,std::shared_ptr,和std::weak_...

为什么要使用lambda表达式?原来如此,涨知识了

为什么要使用Lambda表达式先看几段Java8以前经常会遇到的代码:创建线程并启动...

[编程基础] Python lambda函数总结

Pythonlambda函数教程展示了如何在Python中创建匿名函数。Python中的匿名函数是使用lambda关键字创建的。...

硬核!Java 程序员必须掌握的 10 个 简化代码的 Lambda 表达式!

大家好,我是一位在架构师道路上狂奔的码农,今天给大家介绍一下程序员必须掌握的10个Lambda表达式,这些表达式几乎涵盖了在实际编程中经常用到的常见场景。相信通过这10个Lambda表...

一文读懂lambda表达式_lambda表达式由来

作者:youngyan,腾讯PCG数据工程工程师...

Java基础知识 - lambda 表达式_javalambda表达式用法

1、表达式语法1)lambda的命名采用的是数学符号λ;...

Python学习笔记 | 匿名函数lambda、映射函数map和过滤函数filter

什么是匿名函数?定义:没有函数名的自定义函数场景:函数体非常简单,使用次数很少,没有必要声明函数,通常搭配高阶函数使用。...

Java Lambda表达式详解(非常全面)

JavaLambda表达式是JDK8引入的,是一个比较重要的特性。@mikechenLambda表达式简介...

Python函数—lambda表达式_python中lambda函数的用法讲解

目录...

了解 Lambda:Python 中的单个表达式函数

Python中的lambda关键字提供了声明小型匿名函数的快捷方式。Lambda函数的行为与使用...

在C#中使用Lambda编写一个排序算法,比较其与传统排序算法的优劣

使用Lambda表达式编写排序算法在C#中,Lambda表达式可以用来简化排序逻辑的编写,尤其是在需要自定义排序规则时非常方便。以下示例展示了如何用Lambda表达式实现排序,并与传统排...

一日一技:python中的匿名函数 lambda用法

匿名函数lambda,语法如下:lambdaarguments:expression...

《回炉重造》——Lambda表达式_回炉重造是贬义词吗

前言Lambda表达式(LambdaExpression),相信大家对Lambda肯定是很熟悉的,毕竟我们数学上经常用到它,即λ。不过,感觉数学中的Lambda和编程语言中的Lamb...

取消回复欢迎 发表评论: