MFC訊息對映機制(2):MFC訊息路由原理
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訊息路由的原始碼分析時,還要明白鉤子函式的原理。
本文作者:黃邦勇帥(原名:黃勇)