VC++/MFC消息映射机制(1):MFC消息映射原理

VC++/MFC消息映射机制(1):模仿MFC的消息映射原理

本文要求对C++语法比较熟悉(特别是虚函数的使用),若不熟悉建议参阅《C++语法详解》一书,电子工业出版社出版
1、消息映射:就是把指定的消息交给指定的函数进行处理的方法,这样就形成了一个<消息,处理函数>对。
2、本文有时会使用<M,F>表示<消息,处理函数>对。
一、共用体(union)的使用
1、共用体可以实现以下两个功能(详见示例说明)。
1)、调用函数时不需要知道函数的名称(通过函数指针调用),以及确定调用该函数时需要传递什么类型的形参(即可以确定函数原型)。
2)、在类继承体系中实现类似虚函数的功能。
2、语法问题(指向类成员的指针):使用指向类成员的函数指针时,必须通过类的对象或指向类的指针间接使用,比如class A{public:void f(){}}; A m; A *pa=&m; void (A::*pf)()=&A::f; 则应这样通过pf调用f,即(ma.*pf)()或(pa->*pf)();是正确的,但不能直接使用pf调用成员函数f,即(*pf)();或(A::*pf)();错误。
示例3.5:共用体的使用(以下示例为C++程序)

#include "stdafx.h"  //对于C++程序,VC++编译器必须包含此头文件。
#include<iostream>
using namespace std;
class B;
typedef void (B::*PF)();  
class B{public:void f2(int,int){cout<<"FB"<<endl;}  };
class C:public B{public:void f3(int){cout<<"FC"<<endl;} };
class D:public C{public:void f4(int,int){cout<<"FD"<<endl;}
static PF pf;	static PF pf1; };
PF D::pf=(PF)&f3;  
PF D::pf1=(PF)&f4;/*定义静态成员,此时pf指向C::f3的地址,pf1指向D::f4的地址。*/
union UN{PF uf;    //uf用于存储类D中pf和pf1的地址。
void (B::*pf_0)(int,int);  /*通过共用体的成员pf_0调用相关的函数时,该函数必须具有void 
(B*::)(int,int)形式的原型。*/
void (B::*pf_1)(int);};   /*因为UN是共用体,因此其成员uf,pf_0,pf_1拥有相同的值,即他们都是指向同一地址的指针,但是三个函数指针的类型不一样(即指向的函数的形参和返回类型不一样)。*/
void main()
{	B *pb;	C mc;	D md;
	pb=&md;		UN meff;
//以下步骤可通过传递给meff.uf的函数的形参的不同,而选择调用meff中的不同成员函数。
meff.uf=D::pf1;        /*meff.uf=D::pf1=(PF)&f4;可见,此时uf指向的是D::f4函数的地址*/
(pb->*meff.pf_0)(1,2);  /*输出FD,因为meff是共用体对象,因此meff.uf,meff.pf_0,meff.pf_1是指向的相同地址的指针,即都指向的是uf所指向的D::f4函数的地址。 但是meff.pf_0的类型为void (B*::)(int,int),而meff.pf_0的类型为void (B*::)(int),因此(pb->*meff.pf_0)(1,2)该语句是在调用地址为D::f4处的函数,也就是调用类D中的函数f4,这就实现了通过父类的指针,间接的调用子类D中的成员函数f4,从而间接实现了虚函数的功能。此处应注意使用指向类成员函数的指针调用成员函数的语法。*/
//(pb->*meff.pf_0)(1,2);  //错误,实参太少,因为meff.pf_0的类型为void (B*::pf_0)(int,int)
	meff.uf=D::pf;        //此时uf指向C::f3,其原理同上
	(pb->*meff.pf_1)(1);}  //输出FC,其原理同上。

二、处理单个<消息,处理函数>对的消息映射原理
示例3.6:简单的消息映射原理(本示例只能处理单个的<消息,处理函数>对,以下程序为MFC程序)

本示例需要明白C++语法原理:指针与类型的关系
#include <afxwin.h>   
class A:public CWinApp{public:   BOOL InitInstance(); }; 
class B:public CFrameWnd{public:  B(){Create(NULL,_T("HYONG"),WS_OVERLAPPEDWINDOW);}};  

//❷、使用结构体类型建立<消息,处理函数>对。
typedef void (*PF)();
struct S{UINT msg;UINT msgid; PF pf;};
LRESULT f1(WPARAM w, LPARAM l){::MessageBox(0,"C","D",0);return 0;} //用于处理消息的函数
LRESULT f(WPARAM w, LPARAM l){ ::MessageBox(0,"A","B",0);return 0;} //用于处理消息的函数
//❸、使用结构体类型的数组关联不同的<消息,处理函数>对。以下代码可使用宏进行封装(包装)
/*以下每个一个数组元素都指定了一个<M,F>对,比如ss[0]代表一对<M,F>,ss[1]又表示一对<M,F>,程序员只需把需要处理的<消息,处理函数>对添加到以下数组中即可实现消息映射原理(即把指定的消息使用指定的函数进行处理),也就是说数组ss中的数据,是由程序员指定的。*/
const S ss[]={{WM_LBUTTONDOWN,1,PF(f1)},{WM_RBUTTONDOWN,2,PF(f)}, {0,0,(PF)0}}; //重点数组

/*❹、使用共用体间接调用消息处理函数。以下共用体用于讲解目的,只列出了一部分消息处理函数可能出现的原型。MFC源代码的内容是很长的。*/
union UN{PF pf; LRESULT (*pf_0)(WPARAM,LPARAM);LRESULT (*pf_1)(WPARAM,LPARAM);};
UN meff;  

LRESULT CALLBACK g(HWND h1,int msg, WPARAM w, LPARAM l){  //自定义的过程函数。
	switch (msg) {
		case WM_LBUTTONDOWN:
{meff.pf=ss[0].pf;  //初始化共用体变量meff,此时ss[0].pf指向的函数是f1。
meff.pf_1(w,l); /*❹、使用共用体间接调用消息处理函数f1。程序员可能认为可以在此处直接调用消息处理函数f1不就行了吗?何必这么麻烦?但是在MFC源码中,这部分内容是对程序员隐藏的,源码并不知道程序员向数组ss中添加的“<消息,处理函数>对”中的处理函数的名称是什么,因此不可能直接对“<消息,处理函数>对”中的处理函数进行调用,而只能使用共用体的形式进行间接调用。*/
break;}
		case WM_RBUTTONDOWN:
			{meff.pf=ss[1].pf; //使共用体变量meff.pf指向ss[1].pf指向的函数是f。
				meff.pf_0(w,l); //调用f函数,处理鼠标右键消息
				break;}
		case WM_DESTROY: {	::PostQuitMessage(0);	break; }
		default:return ::DefWindowProc(h1, msg, w, l);	}
	return 0;	}
BOOL A::InitInstance(){   m_pMainWnd=new B();  
m_pMainWnd->ShowWindow(m_nCmdShow);	m_pMainWnd->UpdateWindow();
		//❶、重新设置MFC的过程函数为自定义的函数。
		SetWindowLongPtr(m_pMainWnd->m_hWnd,GWLP_WNDPROC,(LONG)g);  //重置过程函数为函数g。
		return TRUE;}
A ma;    

按下鼠标左键后弹出的消息框如下图(省略主窗口):
在这里插入图片描述
程序算法步骤详解:
1、使用SetWindowLongPtr函数重新设置MFC程序的过程函数。
2、把需要处理的<消息,处理函数>对(即<M,F>),抽像为一个类型,假设使用结构体类型S进行表示,那么每个结构体类型变量都会保存有一个相对应的<M,F>。比如:
typedef void (*PF)();
struct S{UINT msg;UINT msgid; PF pf;};
1)、msg表示需要处理的消息。
2)、msgid用于标示该结构体变量的一个id符号。该成员在本例无用处,但在后面会有用。
3)、pf表示用于处理消息msg的处理函数。
4)、为什么pf的类型是PF:因为消息处理函数的原型并不是全部一致的,在进行消息映射时应使用相同的函数原型形式(即PF的形式)以便对消息处理函数进行统一管理,因此在使用消息处理函数初始化该成员时需要把消息处理函数强制转换为PF类型。
3、创建一个结构体类型S的数组用于保存不同的<M,F>对,该数组就是程序员把消息指定给自定义函数进行处理的地方。比如:
LRESULT f1(WPARAM w, LPARAM l){return 0;} //处理消息的函数f1
LRESULT f(WPARAM w, LPARAM l){return 0;} //处理消息的函数f
const S ss[]={{WM_LBUTTONDOWN,1,PF(f1)},{WM_RBUTTONDOWN,2,PF(f)}, {0,0,(PF)0}};
1)、数组ss保存有两个<M,F>对,即处理鼠标左键按下消息的<WM_LBUTTONDOWN,f1>和处理鼠标右键按下消息的<WM_RBUTTONDOWN,f>。
2)、若程序员需要把其他消息使用另外的函数进行处理,则只需把相应的<消息,处理函数>对,添加到数组ss中即可,这样就实现了消息的映射。
3)、完成以上步骤之后,则在过程函数中接收到需要处理的消息时,只需调用“<M,F>”中的处理函数F处理该消息即可。问题的关键是怎样调用“处理函数F”。
4、使用共用体间接调用消息处理函数:
怎样调用相关联的消息处理函数:因为MFC的源码实现的消息映射是向程序员隐藏了的,那么在调用消息处理函数时,MFC源码肯定是不知道程序员自定义的“消息处理函数”的名称的,这就意味着,不能在源码中直接调用消息处理函数,而只能间接的调用类似以上数组ss中的结构体S中的成员pf,即只能这样调用消息处理函数ss[1].pf();但因为pf的原型与消息处理函数的原型并不相同(本例pf与f原型就不一致),这就可能会产生错误,为了解决函数原型的问题,可以使用共用体类型的成员保存消息处理函数的原型,然后使用共用体成员间接调用消息处理函数。比如:

union UN{PF pf; LRESULT (*pf_0)(WPARAM,LPARAM);LRESULT (*pf_1)(WPARAM,LPARAM);};
   UN meff;  
meff.pf=ss[0].pf;  //初始化共用体变量meff,其中ss[0].pf指向的函数是f1。
meff.pf_0(w,l);   //通过共用体成员pf_0间接调用消息处理函数f1。

1)、以上共用体是用于讲解目的,只列出了一部分消息处理函数可能出现的原型。MFC源代码的内容是很长的(因为包括了所有可能的消息处理函数的原型)。
2)、注意共用体的特点,成员pf与pf_0和pf_1是共用的同一内存段,因此访问共用体变量中的任一成员(比如pf、pf_0、pf_1),他们的值都是一样的。但除pf之外的其他成员保存了“消息处理函数”可能出现的各种原型,这就解决了消息处理函数原型不一致的问题。
3)、共用体中的第一个成员pf主要是用于进入(或关联)结构体S而使用的,或者说用于初始化共用体成员变量的,否则会因为类型不相同而无法初始化。比如UN meff; meff.pf=ss[0].pf; 此时便可在UN中寻找与ss[0].pf相对应的消息处理函数原型相同的成员调用消息处理函数,在本例中ss[0].pf指向的消息处理函数是f1,在UN中与f1原型相同的成员是pf_0和pf_1,因此可使用其中任意一个间接的调用消息处理函数f1,即可以这样调用meff.pf_0(w,l); 其中w和l是假设的两个正确的实参。

三、实现处理多个<消息,处理函数>对的消息映射原理
程序算法如下:
在这里插入图片描述
示例3.7:处理多个消息的消息映射原理
注:以下示例需要结合上一示例进行阅读

#include <afxwin.h>   //编写MFC程序,必须包含此头文件
class A:public CWinApp{public:   BOOL InitInstance(); }; 
class B:public CFrameWnd{public:  B(){Create(NULL,_T("HYONG"),WS_OVERLAPPEDWINDOW);}};  
//以下代码见示例3.6
LRESULT f1(WPARAM w, LPARAM l){::MessageBox(0,"C","D",0);return 0;} 
LRESULT f(WPARAM w, LPARAM l){ ::MessageBox(0,"A","B",0);return 0;} 
typedef void (*PF)();
struct S{UINT msg;UINT msgid; PF pf;};
const S ss[]={{WM_LBUTTONDOWN,1,PF(f1)},{WM_RBUTTONDOWN,2,PF(f)}, {0,0,(PF)0}};
union UN{PF pf; LRESULT (*pf_0)(WPARAM,LPARAM);LRESULT (*pf_1)(WPARAM,LPARAM);};

LRESULT CALLBACK g(HWND h1,int msg, WPARAM w, LPARAM l){ 
const S *s1=&ss[0];		const S *s2=0;		UN meff;
	//❶重点循环体:循环处理数组ss中的<消息,处理函数>对。
while(s1->msgid!=0){  //❷若末达到数组ss的末尾则循环。
if(s1->msg==msg)  /*❸判断捕获到的消息是否与程序员添加到数组ss中“<消息,处理函数>对”中的消息相等,若相等,则说明该消息需要调用数组ss中的处理函数进行处理。*/
			{s2=s1;//❹s1用于循环处理数组ss,不用于调用消息处理函数,s2用来调用消息处理函数。
			meff.pf=s2->pf;   //通过共用体调用与s1->msg相对应的消息处理函数(原理见例3.6)。
switch(s2->msgid)  /*❺根据结构体S中的成员msgid的值,判断使用共用体UN中的哪一个成员调用消息处理函数,在MFC源码中,msgid是使用一个比较庞大的枚举来设置其与共用体UN相关联的值的,本例为简化原理,使用一个任意值。*/
{case 1:{(*meff.pf_0)(w,l);return 0;} /*此处的原理见示例3.6,此处必须使用retun跳出函数。*/
			case 2:{(*meff.pf_1)(w,l);return 0;}
case 3:return 0;   /*❻调用其他的消息处理函数处理消息的代码,在MFC源码中,该swtich结构是很庞大的,因为他包含了所有可能的消息处理函数的原型的调用,本示例是一个简化示例*/
			case 4:return 0;}	}  //if结束
		s1++;  //❼接着处理数组ss中的下一个<消息,处理函数>对。
			} //while循环结束。
if(s2==0);/*❽此处表示,若s2为空(即表示ss中没有相关联的<消息,处理函数>对,或ss已达到末尾),则什么也不做。*/
/*以下为处理其他消息的情形,在MFC源码中是使用CWnd类的成员函数DefWindowProc(注意,并非全局的DefWindowProc)进行处理的。*/
switch (msg) {
		case WM_DESTROY: {	::PostQuitMessage(0);	break; }
		default:return ::DefWindowProc(h1, msg, w, l);	}	return 0;}  //函数g结束
BOOL A::InitInstance(){		m_pMainWnd=new B();  
		m_pMainWnd->ShowWindow(m_nCmdShow);	m_pMainWnd->UpdateWindow();
		HWND hh1=FindWindow(0,"HYONG"); 		SetWindowLong(hh1,GWLP_WNDPROC,(LONG)g); 
		return TRUE;}
A ma;  

程序运行结果如下:
在这里插入图片描述

四、使用宏封装(包装)之后的消息映射原理

示例3.8:使用宏封装之后的消息映射原理
注:以下示例需要结合上面两个示例进行阅读

#include <afxwin.h>   //编写MFC程序,必须包含此头文件
//❶、使用四个宏DE,BEGIN,END,ON封装消息映射的代码。这些宏在源代码中是被封装于另一个文件中的
typedef void (*PF)();
struct S{UINT msg;UINT msgid; PF pf;};
#define DE() const S ss[]  //本示例使用DE代替源代码中的DECLARE_MESSAGE_MAP宏。
#define BEGIN() ={         
#define END() {0,0,(PF)0}};
#define ON(msg,pfn) {msg,1,PF(pfn)},  /*源代码中S的成员msgid的值对于每一个ON_XXX都有一个特定的值。本示例为说明原理,简化为值1。*/
union UN{PF pf; LRESULT (*pf_0)(WPARAM,LPARAM);LRESULT (*pf_1)(WPARAM,LPARAM);};

class A:public CWinApp{public:   BOOL InitInstance(); }; 
class B:public CFrameWnd{public:  B(){Create(NULL,_T("HYONG"),WS_OVERLAPPEDWINDOW);}};  

LRESULT f1(WPARAM w, LPARAM l){::MessageBox(0,"C","D",0);return 0;} 
LRESULT f(WPARAM w, LPARAM l){ ::MessageBox(0,"A","B",0);return 0;} 
//❷、在程序中使用宏
DE()   //源代码该语句是位于类之中的,即把数组ss声明为类的成员,本示例暂不考虑类成员的情形。
BEGIN()
ON(WM_LBUTTONDOWN,f)  //添加<消息,处理函数>对,实现消息映射。
ON(WM_RBUTTONDOWN,f1)
END()
//以上宏展开后的源码如下:
//const S ss[]={ {WM_LBUTTONDOWN,1,PF(f)},{WM_RBUTTONDOWN,1,PF(f1)},{0,0,(PF)0}};
LRESULT CALLBACK g(HWND h1,int msg, WPARAM w, LPARAM l){ 
	const S *s1=&ss[0];		const S *s2=0;
	UN meff;
	//循环处理数组ss中的<消息,处理函数>对,见示例3.7的分析。
	while(s1->msgid!=0){  
		if(s1->msg==msg) {s2=s1;	  	meff.pf=s2->pf;	
switch(s2->msgid) 
				{case 1:{(*meff.pf_0)(w,l);return 0;} 
				case 2:{(*meff.pf_1)(w,l);return 0;}
				case 3:return 0; 
case 4:return 0;} }  //if结束
s1++;}//while循环结束。
	if(s2==0) ;
switch (msg) {
	case WM_DESTROY: {	PostQuitMessage(0);	break; }
	default:return ::DefWindowProc(h1, msg, w, l);	}	return 0;} //函数g结束
BOOL A::InitInstance(){   	m_pMainWnd=new B();  
		m_pMainWnd->ShowWindow(m_nCmdShow);	m_pMainWnd->UpdateWindow();
		HWND hh1=FindWindow(0,"HYONG"); 	SetWindowLong(hh1,GWLP_WNDPROC,(LONG)g); 
return TRUE;}
A ma;    

运行结果与示例3.7类似。

五、完整的模仿MFC源码的消息映射原理(请结合以上三个示例理解)
1、源代码的消息映射原理,是把保存“<消息,处理函数>对”的数组声明为类的成员的,本示例就是按这种方式来模仿消息映射的。
2、注意:本模仿示例程序编译器会产生大量的警告消息,本示例主要是为了讲解其原理,因此对程序尽量简化,其中的警告消息不影响原理的执行,读者也可把其原理转换为C++程序进行验证(下一小节的消息路由就是转换为C++语言的示例)。
示例3.9:完整的模仿MFC源码的消息映射原理

#include <afxwin.h>   
/*❶、使用四个宏DE,BEGIN,END,ON封装消息映射的代码,本示例使用了继承和类的机制。以下宏在源代码中是被封装于另一个文件中的*/
typedef void (CCmdTarget::*PF)();
typedef LRESULT (CWnd::*PF1)(WPARAM, LPARAM);
struct S{UINT msg;UINT msgid; PF pf;};/*源代码中S的成员msgid的值对于每一个ON_XXX都有一个特定的值。本示例为说明原理,简化为值1。*/
#define DE() static const S ss[];  //本示例使用DE代替源代码中的DECLARE_MESSAGE_MAP宏。
#define BEGIN(cl) const S cl::ss[]={         
#define END() {0,0,(PF)0}};
#define ON(msg,pfn) {msg,1,(PF)(LRESULT (CWnd::*)(WPARAM, LPARAM))(&pfn)},  
//❷、以下UN共用体中的成员都是指向类成员函数的指针,在使用他们时应注意C++的语法问题。
union UN{PF pf; LRESULT (CWnd::*pf_0)(WPARAM,LPARAM);LRESULT (CWnd::*pf_1)(WPARAM,LPARAM);};

class A:public CWinApp{public:   BOOL InitInstance(); }; 
class B:public CFrameWnd{public:  B(){Create(NULL,_T("HYONG"),WS_OVERLAPPEDWINDOW);}
LRESULT f1(WPARAM w, LPARAM l){::MessageBox(0,"C","D",0);return 0;} 
LRESULT f(WPARAM w, LPARAM l){ ::MessageBox(0,"A","B",0);return 0;} 
LRESULT CALLBACK g(HWND h1,int msg, WPARAM w, LPARAM l);
DE() //❸、在类之中使用宏。展开后的代码为:static const S ss[];
};  
//❹、在类之外使用宏。
BEGIN(B)
ON(WM_LBUTTONDOWN,f)  //添加<消息,处理函数>对,实现消息映射。
ON(WM_RBUTTONDOWN,f1) 
END()
//以上宏展开后的代码如下:由代码可见,就是给数组成员ss赋值。
/*const S B::ss[]={ {WM_LBUTTONDOWN,1,(PF)(LRESULT (CWnd::*)(WPARAM, LPARAM))(&f)}, {WM_RBUTTONDOWN,1,(PF)(LRESULT (CWnd::*)(WPARAM, LPARAM))(&f1)}, {0,0,(PF)0}};*/

LRESULT CALLBACK gg(HWND h1,int msg, WPARAM w, LPARAM l) //过程函数
{B mb; return mb.g(h1,msg,w,l);}  /*❺、关键重点:把过程函数的具体处理交给类B的成员函数g进行处理,MFC源码使用的是类似的迂回的处理消息的机制。*/
LRESULT CALLBACK B::g(HWND h1,int msg, WPARAM w, LPARAM l){ 
	const S *s1=&B::ss[0];
	const S *s2=0;
	UN meff;
	//循环处理数组ss中的<消息,处理函数>对,具体原理详见示例3.6、3.7、3.8。
	while(s1->msgid!=0){  
		if(s1->msg==msg){s2=s1;	meff.pf=s2->pf;
switch(s2->msgid) /*根据结构体S中的成员msgid的值,判断使用共用体UN中的哪一个成员调用消息处理函数,在MFC源码中,msgid是使用一个比较庞大的枚举来设置其与共用体UN相关联的值的,本例为简化原理,使用一个任意值。*/
{case 1:{(this->*meff.pf_0)(w,l); /*❻、this指针不能省略,否则是错误的,因为使用指向类成员的指针,必须通过类的对象或指针进行调用。此处pf_0是指向类成员函数的指针。*/
return 0;}
case 3:return 0;   /*调用其他的消息处理函数处理消息的代码,略,在MFC源码中,该swtich结构是很庞大的,因为他包含了所有可能的消息处理函数的原型的调用,本示例是一个简化示例。*/
			case 4:return 0;}	}  
		s1++;  }//while循环结束。
	if(s2==0) ;	

	switch (msg) {
	case WM_DESTROY: {	PostQuitMessage(0);	break; }
	default:return ::DefWindowProc(h1, msg, w, l);	}
	return 0;}
BOOL A::InitInstance(){   	m_pMainWnd=new B();  
		m_pMainWnd->ShowWindow(m_nCmdShow);	m_pMainWnd->UpdateWindow();
		HWND hh1=FindWindow(0,"HYONG"); 	SetWindowLong(hh1,GWLP_WNDPROC,(LONG)gg); 
		return TRUE;}
A ma;   

运行结果如下:
在这里插入图片描述

本文作者:黄邦勇帅(原名:黄勇)

猜你喜欢

转载自blog.csdn.net/hyongilfmmm/article/details/83030813
今日推荐