1. 程式人生 > >VC++/MFC訊息對映機制(2):MFC訊息路由原理

VC++/MFC訊息對映機制(2):MFC訊息路由原理

VC++/ MFC訊息對映機制(2):模仿MFC的訊息路由

本文要求對C++語法比較熟悉(特別是虛擬函式的使用),若不熟悉建議參閱《C++語法詳解》一書,電子工業出版社出版。並且本文需結合上一篇文章《MFC訊息對映原理》閱讀。

訊息路由的目的就是把當前類沒有處理的訊息,上傳給其父類進行處理,一直傳遞到最頂級父類進行處理。
本小節應注意區別本文所指的訊息對映和訊息對映表的概念,在本小節,訊息對映指的是<訊息,處理函式>對,即程式中的ss陣列,訊息對映只儲存了訊息和函式的關係。而訊息對映表指的是程式中的msgmp變數,訊息對映表不但儲存有訊息對映(即ss),而且保還儲存了其子類與父類的連結串列關係。
一、直線(訊息)路由
在這裡插入圖片描述

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);  }

在這裡插入圖片描述

請按如下輸入進行測試
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個類),原理請讀者自行分析。

二、拐彎路由
在這裡插入圖片描述

示例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能接受的值,也就是說只能輸入整數型別的值,不能輸入浮點值或字母等非整型值,否則程式可能因輸入錯誤而陷入死迴圈。
在這裡插入圖片描述

在這裡插入圖片描述
程式執行結果的原理分析:
假設訊息由類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訊息路由的原始碼分析時,還要明白鉤子函式的原理。

本文作者:黃邦勇帥(原名:黃勇)