按惯例,这一篇文章主要还是作者读《深入浅出MFC》整理的一些笔记。
事件–消息–窗口模型
之所以要在讲消息映射之前专门讲一讲这个模型,是为了避免大家看消息映射的时候没有心理准备,忽略了一些教材没有讲,老师觉得想当然,但需要给你强调一下的内容。
我们需要在开展MFC之前达成以下共识:
1.Windows编程从WinMain进入,目前我们只讨论简单程序的前提下(不讨论使用指令让编译器或连接器做出多程序接口的情况),只有一个WinMain函数。
2.窗口需要多方面的变量共同作用,具体来说,一个窗口至少包含以下方面的内容:(PS:你可以使用记事本打开.rc文件,变量名都在其中)
2.a.窗口的各种选项在RC文件中定义,各类可能存在的组件,在RC中会有一个名称,比如可以在RC文件中定义OK选项叫做IDOK,那么IDOK被按下时,message会记录下RC中这个button对应的ID,传给窗口回调函数之后,窗口回调函数也会根据这个IDOK变量(message中的IDOK变量)确定message是在窗口的哪个地方发生的,从而执行对应的程序。
(总的来说就是:画窗口,只需要RC文件配合窗口注册表就行了)
2.b.窗口的RC文件定义好后,窗口的message种类也就自然而然地定义好了。这里你可以理解为RC文件就是单纯地画画,画好了一个窗口,而message就是告诉你,事件发生在这个窗口的哪个地方,什么时候发生的。比如你在窗口上画了10个不同的组件,那么message的WPARAM参数可能就要有10个定义来确定具体的RC文件是如何对应到message的。
(总的来说就是:message结构体内部各个变量可能取什么值,只跟你的RC文件与窗口注册表有关,包括但不限于message.message的值所对应的集合,WPARAM的值所对应的集合以及LPARAM的值所对应的集合;当你的RC文件、窗口文件都定义好了之后,理论上你的事件种类已经全部确定,响应的,分门别类描述你事件种类的message自变量定义域也确定了)
(总总的来说就是:画好窗口之后,message结构体也就知道了)
2.c.窗口的message种类确定之后,对应的回调函数就可以根据不同的message种类执行不同的操作了。
(总的来说就是:message结构体知道之后,回调函数也就可以分门别类的写了)
3.标准的回调函数看着有四个参数输入,但本质,可以看作只有一个的参数——message结构体。只不过回调函数本身也存在于父类之中,所以对其格式不能乱写,因而不能真写message结构体罢了。
4.回调函数是在注册的时候录入窗口的,所以回调函数的输入只要满足父类的格式要求,内容可以乱写。
5.我们对程序窗口的普遍要求是,点一个不同的地方,会有一个不同的响应(这源自于用户使用Windows的习惯)。因而回调函数的本质是描述在这一窗口下各种可能的操作对应的响应是什么。
6.单个窗口的三部件集齐后,由此窗口的响应产生出来的新窗口又会有新的三部件,同样使用三部件结构表达用户实际使用中的各类操作,因而窗口与窗口之间的各种消息可以构成三维图(单个窗口是二维图)。这就是message_map产生的前提——消息映射。
好了,如果你准备好了以上共识,让我们正式开始message_map的描述。
Message_Map
一个窗口的所有可能事件,使用一个MSG结构体可以完备的描述。因此,一个窗口我们可以定义一个如下的结构体:
struct MSGMAP_ENTRY {
UINT nMessage;
LONG (*pfn)(HWND, UINT, WPARAM, LPARAM);
};
#define dim(x) (sizeof(x) / sizeof(x[0]))
这样的结构体描述了不同的message对应的不同的响应函数。具体的来说,我们可以调用这个结构内的message变量来判断窗口发生的是哪一个事件,然后针对这一事件写一个专门的响应函数。
比如,我们可以做如下声明:
UINT me = 5;
LONG me_pf(HWND, UINT, WPARAM, LPARAM)
struct MSGMAP_ENTRY me_p = {me,me_pf};
LONG me_pf(HWND, UINT, WPARAM, LPARAM)
{
...
...
}
这样就可以直接声明函数了。这里需要注意的是:1.在声明结构体变量时,用于结构体变量内部各变量初始化的变量名需要用前向的方式提前声明;2.函数指针的调用方式和函数指示符的调用方式完全等价,所以再后面定义的时候可以把函数指示符赋值给函数指针;3.结构体数组在赋值的时候,既可以一个结构体一个结构体的赋值,也可以进一步细分为一个结构体中的变量一个结构体中的变量这种方式赋值。
结构体本身就是面向对象观念中把[数据]和[处理数据的方法]封装起来的一种具体实现。
// 消息与处理例程之对照表
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,
};这是消息 这是消息处理例程
// Command-ID 与处理例程之对照表格
struct MSGMAP_ENTRY _commandEntries =
{
IDM_ABOUT, OnAbout,
IDM_FILEOPEN, OnFileOpen,
IDM_SAVEAS, OnSaveAs,
};这是WM_COMMAND命令项 这是命令处理例程
// 窗口函数
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));
}
//----------------------------------------------------------------------
LONG OnCreate(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{
...
}
//----------------------------------------------------------------------
LONG OnAbout(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{
...
}
这之中,结构体内变量WM_CREATE、WM_PAINT、WM_SIZE等是UINT nMessage变量,而OnCreate、OnPaint、OnSize等是LONG (*pfn)(HWND, UINT, WPARAM, LPARAM)变量。UINT nMessage变量的种类早已由RC文件所决定,所以在此我们只需具体的写出各类消息对应的处理函数即可。
这么一来,WndProc和OnCommand永远不必改变,每有新要处理的消息,只要在_messageEntries[ ] 和 _commandEntries[ ] 两个数组中加上新元素,并针对新消息撰写新的处理例程。
这种观念以及作法就是MFC的Message Map的雏形。