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

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;   

執行結果如下:
在這裡插入圖片描述

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