VC++/MFC消息映射机制(2):MFC消息路由原理

VC++/ MFC消息映射机制(2):模仿MFC的消息路由

本文要求对C++语法比较熟悉(特别是虚函数的使用),若不熟悉建议参阅《C++语法详解》一书,电子工业出版社出版。并且本文需结合上一篇文章《MFC消息映射原理》阅读。

消息路由的目的就是把当前类没有处理的消息,上传给其父类进行处理,一直传递到最顶级父类进行处理。
本小节应注意区别本文所指的消息映射和消息映射表的概念,在本小节,消息映射指的是<消息,处理函数>对,即程序中的ss数组,消息映射只保存了消息和函数的关系。而消息映射表指的是程序中的msgmp变量,消息映射表不但保存有消息映射(即ss),而且保还保存了其子类与父类的链表关系。
一、直线(消息)路由
VC++/MFC消息映射机制(2):MFC消息路由原理

1、直线路由:指的是若子类没有调用的函数的定义,则查找父类中是否有该函数的定义,一直向上直至查找到顶级父类。比如,假设A是顶级父类,D是最终的子类,继承关系为A→B→C→D,则D md; 则调用md.f();首先查找D中是否有该函数,若没有则再查找父类C,直至查找到顶级父类A为止。如右图所示。其实使用C++的继承或虚函数机制就可以直接实现直线路由,但由于处理窗口时需要映射消息与消息处理函数,而且在调用时不知道处理消息函数的名称,所以实际在MFC之中消息的直线路由经过了一系列的算法才实现的。
2、下面以示例说明MFC中消息直线路由的算法,为避免MFC的复杂性及MFC模仿时产生的大量警告信息,以下示例以C++程序(即VC++编译器应创建“控制台应用程序”)进行演示,主要为了说明MFC消息路由的原理。

示例3.10:消息路由(直线路由)(原理见后文图示及说明)
说明:为避免错误及复杂性,以下程序为C++程序(即C++控制台应用程序)

#include "stdafx.h"   //C++程序,VC++必须包含此头文件
#include<iostream>
using namespace std;
class A                   //前置声明
typedef void (A::*PF)();
//❶、抽象出两种新类型S和MP,其中S是存储消息映射的类型,MP是存储消息映射表的类型。
struct S{int msg;int msgid; PF pf;};      //抽象出的消息映射(即<消息,处理函数>对)的类型
struct MP{const MP* bMp; const S *pss;}; //抽象出的消息映射表的类型
/*❷、DE宏向类中添加三个成员:ss是存储消息映射的数组,msgmp表示消息映射表,getMp()反回当前消息映射表的函数*/
#define DE() public:static const S ss[];\
					static const MP msgmp;\
					virtual const MP* getMp();
/*❸、BEGIN宏用于定义使用DE宏添加的三个成员,初始化了消息映射表msgmp,定义了函数getMp(),并且开始初始化消息映射数组ss,ss的初始化需要配合宏ON和宏END共同完成。其中消息映射表msgmp是实现消息路由的关键,消息映射表msgmp在各类之间建立起了一个链表,消息路由时沿着该链表向上向父类进行路由。*/
#define BEGIN(tcl,bcl)	const MP tcl::msgmp={&bcl::msgmp,&tcl::ss[0]};\
						const MP* tcl::getMp(){return &tcl::msgmp;}\
						const S tcl::ss[]={
#define END() {0,0,(PF)0}};
#define ON(msg,pfn) {msg,1,(PF)(void (A::*)(int,int))(&pfn)},  
//❹、以下UN共用体中的成员都是指向类成员函数的指针,在使用他们时应注意C++的语法问题。
union UN{PF pf; void (A::*pf_0)(int,int);void (A::*pf_1)(int,int);};

int a;    //❺、本示例以全局变量a,表示由窗口产生的消息,a的值使用cin进行输入。
class A{public:   //类A为顶级父类。
	void f1(int,int){cout<<"FA"<<endl;}
	virtual int g(int i);  //❻、该成员会被类似于过程函数的函数gg调用。
	DE()    //❼、使用DE向类A中添加成员
//DE展开后的结果如下:
/*public:static const S ss[];
		static const MP msgmp;
		virtual const MP* getMp();*/
};
class B:public A{public:void f2(int,int){cout<<"FB"<<endl;}DE()}; //宏DE与A相同。
class C:public B{public:void f3(int,int){cout<<"FC"<<endl;}DE()};
class D:public C{public:void f4(int,int){cout<<"FD"<<endl;}DE()};
class E:public D{public: void f5(int,int){cout<<"FE"<<endl;}DE()};
//❽定义类A中使用宏DE添加的成员,因为A是顶级类,所以需要特殊处理。
const MP A::msgmp={0,&A::ss[0]};
const MP* A::getMp(){return &A::msgmp;}
const S A::ss[]={{0,0,(PF)0}};
//❾类B:使用BEGIN和END宏,定义在类B中由宏DE添加的成员。
BEGIN(B,A)
ON(2,f2) //添加消息映射,即添加<消息,处理函数>对<2,f2>。
END()
/*以上BEGIN、ON、END宏展开后如下:
const MP B::msgmp={&A::msgmp,&B::ss[0]};
const MP* B::getMp(){return &B::msgmp;}
const S B::ss[]={{2,1,(PF)(void (A::*)(int,int))(&pfn)}, {0,0,(PF)0}}; */

BEGIN(C,B)  //定义类C的成员。
END()
BEGIN(D,C)//定义类D的成员
ON(4,f4) //使用宏ON添加<消息,处理函数>对<4,f4>。
END()
BEGIN(E,D)//定义类E的成员。
ON(1,f1) 
ON(3,f3) 
ON(5,f5) 
END()

A *pa;   //❿、重要全局变量。
int gg(int i){   //⓫、gg类似于MFC中的过程函数
return pa->g(i);} //⓬、调用g函数应使用顶级父类的指针进行,这样虚函数getMp()才能起作用。使用顶级父类的指针调用其中的成员函数,也是实现消息路由的关键步骤,因为没有此步骤,虚函数将无用武之地。*/
int A::g(int i)  //定义过程函数gg间接调用的函数
{const MP *mp1;
   	 mp1=getMp();  //⓭、调用哪个类中的虚函数getMp,要视全局变量pa指向的类型而定。
	const S *s2=0;
	UN meff;
for(;mp1!=0;mp1=mp1->bMp){  /*⓮、外围循环就是消息路由,此循环用于遍历消息映射表msgmp,此循环会从最终子类一直向上循环到顶级父类A。*/
		const S *s1=mp1->pss;	
		cout<<"A"<<endl;   //用于测试
while(s1->msgid!=0){  /*⓯、嵌套循环用于遍历消息映射表msgmp中的成员pss(即消息映射数组ss的值)。*/
			cout<<"X"<<endl;	//用于测试
if(s1->msg==i){    /*⓰、判断输出的消息(即全局变量a的值,a通过调用函数g传递给i),是否与消息映射数组ss中的消息msg相等。*/
				s2=s1;	meff.pf=s2->pf;  //使用共用体成员
switch(s2->msgid)  /*⓱、若处理消息i的函数与消息映射数组ss中的msgid相等,则调用拥有以下函数原型的函数。*/
/*以下为简洁,使用了任意两个实参值,实际上消息处理函数的这两个实参是WPARAM和LPARAM。*/
{case 1:{(this->*meff.pf_0)(11,2);return 0;} 
					case 3:return 0;  
					case 4:return 0;}	}  //if结束
			s1++; } //while结束。
		if(s2==0){cout<<"Y"<<endl; }	//若最终没有找到处理消息i的函数,则输出Y。
	}  //for循环结束
return 1;
}  //A::g结束
void main(){	
	pa=new E();
cin>>a;  //根据输入的值(相当于是MFC中的消息)确定调用哪个函数。
	gg(a);  }

VC++/MFC消息映射机制(2):MFC消息路由原理

请按如下输入进行测试
1、输入1时,调用类A的f1,依次输出A,X,FA,因为ON(1,f1) 中1与f1对应,而f1在类A的各子类都未定义。
2、输入3原理与输入1类似。
3、输入4时(直线消息路由),调用类D中的f4,依次输出A,X,X,X,Y,A,X,FD。由此可见for循环执行了两次(输出了两次A),while循环执行了4次(输出了4次X)。分析如下:
1)、第一次for循环,检查最终子类E的消息映射表E::msgmp,然后依次对E::msgmp中的成员pss进行逐个检查以判断ON(4,f4)是否在类E之中进行了相关的映射(或者说检查类E的成员数组ss中,是否有值为{4,f4}的元素),最终的结果是映射ON(4,f4)未在类E之中,此时第一轮for循环结束,其中while循环共执行3次(输出3个X),因为在类E之中有3个ON映射(或者说类E的成员数组ss有3个元素),输出Y是因为while循环结束后未对s2赋值,此时s2=0。
2)、然后进行第二次for循环,检查其父类D的消息映射表D::msgmp(此时消息向上路由至父类D),然后对D::msgmp中的成员pss进行逐个检查以判断ON(4,f4)是否在类D之中进行了相关的映射,最后找到该映射,调用f4,输出FD,此时while循环一次(输出一个X),因为查找一次就找到了ON(4,f4)映射。
4、输入2的原理与输入4类似。
5、若输入1~5之外的其他字符,则依次输出AXXXXYAXYAYAYAY,共输出5个A(因为继承关系含有5个类),原理请读者自行分析。

二、拐弯路由
VC++/MFC消息映射机制(2):MFC消息路由原理

示例3.11:消息拐弯路由
说明:1、为避免错误及复杂性,以下程序为C++程序(即C++控制台应用程序)
2、本示例大部分代码与直线消息路由是相同的,不同之处使用注释进行标注。
3、以下程序的继承关系见上面的图示。
#include “stdafx.h”
#include
using namespace std;

class A;
typedef void (A::*PF)();
struct S{int msg;int msgid; PF pf;};     
struct MP{const MP* bMp; const S *pss;};
#define DE() public:static const S ss[];\
					static const MP msgmp;\
					virtual const MP* getMp();
#define BEGIN(tcl,bcl)	const MP tcl::msgmp={&bcl::msgmp,&tcl::ss[0]};\
						const MP* tcl::getMp(){return &tcl::msgmp;}\
						const S tcl::ss[]={
#define END() {0,0,(PF)0}};
#define ON(msg,pfn) {msg,1,(PF)(void (A::*)(int,int))(&pfn)},  
union UN{PF pf; void (A::*pf_0)(int,int);void (A::*pf_1)(int,int);};

int a;    //本示例以全局变量a,表示由窗口产生的消息,a的值使用cin进行输入。
class A{public:   //类A为顶级父类。
	void f1(int,int){cout<<"FA"<<endl;}
virtual int g1(int i){return g(i);}  /*❶、增加一个虚函数g1,用于消息拐弯路由,该虚函数需要在类B、D、F被重写。*/
	virtual int g(int i);  
	DE()  };
class B:public A{public:void f2(int,int){cout<<"FB"<<endl;}
virtual int g1(int i);   //❷、类B需要重写虚函数g1。
DE()}; 
class C:public B{public:void f3(int,int){cout<<"FC"<<endl;}DE()}; //注意末尾添加有DE宏
class D:public A{public:void f4(int,int){cout<<"FD"<<endl;}
virtual int g1(int i); //❷、类D需要重写虚函数g1。
DE()};
class E:public D{public: void f5(int,int){cout<<"FE"<<endl;}DE()};
class F:public A{public: void f6(int,int){cout<<"FF"<<endl;}
virtual int g1(int i); //❷、类F需要重写虚函数g1。
DE()};

//定义类A中使用宏DE添加的成员,因为A是顶级类,所以需要特殊处理。
const MP A::msgmp={0,&A::ss[0]};
const MP* A::getMp(){return &A::msgmp;}
const S A::ss[]={{0,0,(PF)0}};
//类B:使用BEGIN和END宏定义在类B中由宏DE添加的成员。
BEGIN(B,A)
ON(2,f2) 
END()
/*BEGIN和END展开后如下:
const MP B::msgmp={&A::msgmp,&B::ss[0]};
const MP* B::getMp(){return &B::msgmp;}
const S B::ss[]={{2,1,(PF)(void (A::*)(int,int))(&pfn)}, {0,0,(PF)0}}; */ 

//定义类C的成员。
BEGIN(C,B) 
ON(3,f3)
END()
//定义类D的成员
BEGIN(D,A)
ON(4,f4) //使用宏ON把函数f4与输入的消息4进行关联。
END()
//定义类E的成员。
BEGIN(E,D)
ON(1,f1) 
ON(5,f5) 
END()

BEGIN(F,A)
ON(6,f6) //使用宏ON把函数f6与输入的消息6进行关联。
END()

A *pa;   //重要全局变量。
int gg(int i){   //gg类似于MFC中的过程函数
	return pa->g1(i);} //❸、关键重点:此处不再直接调用g,而是调用g1,然后由g1间接调用g。

int B::g1(int i){  //函数g1用于实现消息拐弯,此处消息被分成3路。
if(g(i)) return 1;      /*❹、若消息来自类B的子类,则调用函数A::g()查找该继承子树,以处理消息,若该继承子树未能处理消息,则执行下面的步骤。*/
	pa=new E();             //❺、消息拐弯至另一继承子树结构的最终子类E。
if(pa->g(i)) return 1;  /*❻、调用函数A::g()查找该继承子树,以处理消息,若未能处理消息,则执行下面的步骤。*/
	pa=new F();            //❼、消息再次拐弯至另一继承子树结构的最终子类F。
	if(pa->g(i)) return 1; //原理同上。
	return 0;    }          //❽、若消息最终都未被处理,则反回。

int D::g1(int i){  //原理与B::g1()相同。消息被分成3路。
	if(g(i)) return 1;
	pa=new C();	if(pa->g(i)) return 1;
	pa=new F();	if(pa->g(i)) return 1;
	return 0;	}

int F::g1(int i){ //原理与B::g1()相同。消息被分成3路。
	if(g(i)) return 1;
	pa=new C();	if(pa->g(i)) return 1;
	pa=new E();	if(pa->g(i)) return 1;
	return 0;}

int A::g(int i)  //此函数的原理见直线消息路由的示例3.10,此函数可实现子继承树的直线路由。
{	const MP *mp1;
mp1=getMp(); 
	const S *s2=0;
	UN meff;
for(;mp1!=0;mp1=mp1->bMp){  
		const S *s1=mp1->pss;	
		cout<<"A"<<endl;   //用于测试
while(s1->msgid!=0){  
			cout<<"X"<<endl;	//用于测试
if(s1->msg==i){    
				s2=s1;	meff.pf=s2->pf;
switch(s2->msgid) 
/*以下为简洁,使用了任意两个实参值,实际上消息处理函数的这两个实参是WPARAM和LPARAM。*/
{case 1:{(this->*meff.pf_0)(11,2);return 1;} 
					case 3:return 1;  
					case 4:return 1;}	}  //if结束
				s1++; } //while结束。
			if(s2==0){cout<<"Y"<<endl; }	//若最终没有找到处理消息i的函数,则输出Y。
		}  //for循环结束
return 0;
}  //A::g结束

void main(){	
cout<<"XXXXXXXX消息从类C进入的情形XXXXXXXX"<<endl;
	pa=new C();   //消息从类C进入。
	while(1){ 	
cout<<"输入消息a的值,退出请输入0:";
		cin>>a;  //根据输入的值(相当于是MFC中的消息)确定调用哪个函数。
		gg(a);
if(a==0) break;  /*注意:为简洁,本示例未对输入的值作完全的正确性检测,因此在进行测试时,请输入全局变量a能接受的值(即整型值),否则有可能因输入错误的值而陷入死循环。*/
		}

	cout<<"XXXXXXXX消息从类E进入的情形XXXXXXXXXx"<<endl;
	pa=new E();      //消息从类E进入。
	while(1){	cout<<"输入消息a的值,退出请输入0:";
		cin>>a;  //根据输入的值(相当于是MFC中的消息)确定调用哪个函数。
		gg(a);
		if(a==0) break; }

	cout<<"XXXXXXXXX消息从类F进入的情形XXXXXX"<<endl;
	pa=new F();//消息从类F进入。
	while(1){
		cout<<"输入消息a的值,退出请输入0:";
		cin>>a;  //根据输入的值(相当于是MFC中的消息)确定调用哪个函数。
		gg(a);
		if(a==0) break;}}

运行结果:
请读者自行按以下规则进行测试:消息从每一个类进入时,分别输入1,2,3,4,5,6进行测试其正确性(对程序的执行分流,请阅读完以下两幅原理图之后再进行讲解)。再次注意:在输入时需要输入全局变量a能接受的值,也就是说只能输入整数类型的值,不能输入浮点值或字母等非整型值,否则程序可能因输入错误而陷入死循环。
VC++/MFC消息映射机制(2):MFC消息路由原理

VC++/MFC消息映射机制(2):MFC消息路由原理
程序执行结果的原理分析:
假设消息由类C进入,并输入值6,程序输出:AXY AXY AY AXXY AXY AY AX FF。其中输出Y表示未查找到消息,程序按如下步骤执行:
1、调用全局函数::gg(),并执行其中的语句pa->g1(i);因此时pa=new C();因此调用类B中的B::g1()函数。
2、执行B::g1()中的第一条语句if(g(i)),此时this指针指向的是new C(),因此函数A::g()首先查找类C继承子树(即C,B,A三个类),并首先从类C开始查找消息,程序进入A::g();
3、在A::g()中,因为在类C继承子树中的类C、类B、类A中都未找到匹配的消息,所以程序会输出三个Y,又由于类C的继承子树有3个类,所以会执行三次for循环,所以会输出3个A,由于类A的ON()宏中的msgid的值为0,因此在while循环内,不会输出由类A产生的X,程序最终输出2个X,因此查找完类C继承子树后的输出结果为AXY AXY AY
4、在继承子树C、B、A中未找到匹配的消息,程序返回到B::g1()中,继续执行其后的下一条语句,pa=new E(); if(pa->g(i)) return 1; 由此可知,程序转入类E继承子树(即E、D、A),并从类E开始查找是否有匹配的消息(注意,消息在此时已经进行了拐弯),此过程与类C继承子树类似,程序最后输出AXXY AXY AY,第一个AXXY是查找类E时输出的,因为类E添加了两个ON宏,所以输出两个X。
5、查找完类E继承子树后,程序返回B::g1(),并继续执行其后的语句pa=new F(); if(pa->g(i)) return 1; 此时的过程与4相同,但在类F中找到匹配的消息,程序最后输出AXFF,程序结束。
6、输入6之后,程序最后在类F中找到匹配的消息,并最终输出AXY AXY AY AXXY AXY AY AX FF,输入其他值,和消息从其他类进入时的原理与此类似,请读者自行测试并分析。

对MFC消息路由的源码分析时,还要明白钩子函数的原理。

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