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

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

liebian365 2024-10-24 14:35 8 浏览 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-

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: