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

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

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

相关推荐

“版本末期”了?下周平衡补丁!国服最强5套牌!上分首选

明天,酒馆战棋就将迎来大更新,也聊了很多天战棋相关的内容了,趁此机会,给兄弟们穿插一篇构筑模式的卡组推荐!老规矩,我们先来看10职业胜率。目前10职业胜率排名与一周前基本类似,没有太多的变化。平衡补丁...

VS2017 C++ 程序报错“error C2065:“M_PI”: 未声明的标识符&quot;

首先,程序中头文件的选择,要选择头文件,在文件中是没有对M_PI的定义的。选择:项目——>”XXX属性"——>配置属性——>C/C++——>预处理器——>预处理器定义,...

东营交警实名曝光一批酒驾人员名单 88人受处罚

齐鲁网·闪电新闻5月24日讯酒后驾驶是对自己和他人生命安全极不负责的行为,为守护大家的平安出行路,东营交警一直将酒驾作为重点打击对象。5月23日,东营交警公布最新一批饮酒、醉酒名单。对以下驾驶人醉酒...

Qt界面——搭配QCustomPlot(qt platform)

这是我第一个使用QCustomPlot控件的上位机,通过串口精确的5ms发送一次数据,再将读取的数据绘制到图表中。界面方面,尝试卡片式设计,外加QSS简单的配了个色。QCustomPlot官网:Qt...

大话西游2分享赢取种族坐骑手办!PK趣闻录由你书写

老友相聚,仗剑江湖!《大话西游2》2021全民PK季4月激燃打响,各PK玩法鏖战齐开,零门槛参与热情高涨。PK季期间,不仅各种玩法奖励丰厚,参与PK趣闻录活动,投稿自己在PK季遇到的趣事,还有机会带走...

测试谷歌VS Code AI 编程插件 Gemini Code Assist

用ClaudeSonnet3.7的天气测试编码,让谷歌VSCodeAI编程插件GeminiCodeAssist自动编程。生成的文件在浏览器中的效果如下:(附源代码)VSCode...

顾爷想知道第4.5期 国服便利性到底需优化啥?

前段时间DNF国服推出了名为“阿拉德B计划”的系列改版计划,截至目前我们已经看到了两项实装。不过关于便利性上,国服似乎还有很多路要走。自从顾爷回归DNF以来,几乎每天都在跟我抱怨关于DNF里面各种各样...

掌握Visual Studio项目配置【基础篇】

1.前言VisualStudio是Windows上最常用的C++集成开发环境之一,简称VS。VS功能十分强大,对应的,其配置系统较为复杂。不管是对于初学者还是有一定开发经验的开发者来说,捋清楚VS...

还嫌LED驱动设计套路深?那就来看看这篇文章吧

随着LED在各个领域的不同应用需求,LED驱动电路也在不断进步和发展。本文从LED的特性入手,推导出适合LED的电源驱动类型,再进一步介绍各类LED驱动设计。设计必读:LED四个关键特性特性一:非线...

Visual Studio Community 2022(VS2022)安装图文方法

直接上步骤:1,首先可以下载安装一个VisualStudio安装器,叫做VisualStudioinstaller。这个安装文件很小,很快就安装完成了。2,打开VisualStudioins...

Qt添加MSVC构建套件的方法(qt添加c++11)

前言有些时候,在Windows下因为某些需求需要使用MSVC编译器对程序进行编译,假设我们安装Qt的时候又只是安装了MingW构建套件,那么此时我们该如何给现有的Qt添加一个MSVC构建套件呢?本文以...

Qt为什么站稳c++GUI的top1(qt c)

为什么现在QT越来越成为c++界面编程的第一选择,从事QT编程多年,在这之前做C++界面都是基于MFC。当时为什么会从MFC转到QT?主要原因是MFC开发界面想做得好看一些十分困难,引用第三方基于MF...

qt开发IDE应该选择VS还是qt creator

如果一个公司选择了qt来开发自己的产品,在面临IDE的选择时会出现vs或者qtcreator,选择qt的IDE需要结合产品需求、部署平台、项目定位、程序猿本身和公司战略,因为大的软件产品需要明确IDE...

Qt 5.14.2超详细安装教程,不会来打我

Qt简介Qt(官方发音[kju:t],音同cute)是一个跨平台的C++开库,主要用来开发图形用户界面(GraphicalUserInterface,GUI)程序。Qt是纯C++开...

Cygwin配置与使用(四)——VI字体和颜色的配置

简介:VI的操作模式,基本上VI可以分为三种状态,分别是命令模式(commandmode)、插入模式(Insertmode)和底行模式(lastlinemode),各模式的功能区分如下:1)...

取消回复欢迎 发表评论: