MFC(微軟)

本页使用了标题或全文手工转换,现处于香港繁体模式
求聞百科,共筆求聞

微軟基礎類別館(英語:Microsoft Foundation Classes,簡稱MFC)是一個微軟公司提供的類別館(class libraries),以C++類的形式封裝了Windows API,並且包含一個(也是微軟產品的唯一一個)應用程式框架,以減少應用程式開發人員的工作量。其中包含的類包含大量Windows控制代碼封裝類和很多Windows的內建控制項和組件的封裝類。

特性

Visual C++包含MFC應用程式精靈,可用於相容MFC的應用程式[1]。在ATL程式中也可以手動添加MFC支援[2]。在精靈中有各種選項以客製化生成的程式的功能,例如介面風格、語種、數據庫開發支援、列印支援、自動化支援、ActiveX支援、網絡支援、基於HTML的說明文件支援等等。

COM開發方面,相對於ATL來說,MFC的組件比較大,代碼不夠短小精悍,但是支援的功能也比較多,例如有對ActiveX Document的封裝類[3]

在介面開發方面,MFC提供對訊息迴圈的封裝,使用訊息對映來避免虛擬函式的開銷。MFC也提供常用Windows通用控制項的封裝類。

MFC擴充DLL的介面使得MFC程式可以直接呼叫MFC擴充DLL中的MFC類。MFC也支援在標準DLL中被使用。

發展

MFC是在1992年隨微軟的Microsoft C/C++ 7.0編譯器發佈的,用於面向16位元Windows的軟件開發。起初,MFC是作為一個應用程式框架開發的,所以定名為Application Framework eXtensions(AFX)。[4]

隨着Visual BasicVisual Studio .NET的發佈,曾經一度被微軟重點推薦的MFC被Visual Basic .NETC#Windows Forms搶走了不少市場份額,但是MFC繼續在非寄存軟件開發中佔據重要地位。在寄存開發方面,MFC中也包括對Windows Forms和寄存/非寄存互操作的封裝。微軟在Windows VistaWindows 7發佈之後在MFC中增加了對新的Windows API支援[5][6]

MFC的優點

MFC的主要優點是可以用物件導向的方法來呼叫Windows API,以及能夠更加便捷地開發應用程式。MFC將很多應用程式開發中常用的功能自動化,並且提供了文件框架視圖結構和活動文件這樣的便於自訂的應用程式框架。同時,在Visual C++內部也內建了很多對MFC的例如類精靈這樣的支援以減少軟件開發的時間,使用類精靈可以快速生成Hello World程式。

MFC的缺點

雖然MFC的原始碼對用戶是完全開放的,但是MFC的一些封裝過程過於複雜,以致於新用戶很難迅速掌握MFC的應用程式框架,以及在除錯中定位問題的位置。同時,很多MFC物件不是線程安全的,致使在跨線程存取MFC物件時需要編寫額外的代碼。另外,MFC的很多類依賴於應用程式精靈生成的代碼,使得在使用Visual C++中其他類型的應用程式精靈生成的工程中添加MFC支援的難度大大增加。

第三方支援

很多商用類別館在MFC的基礎上進一步實現了佈景主題、漸變風格、多頂層窗口程式、屬性列表等較受歡迎的功能;同時,在C++線上社區中,很大一部分開放的原始碼也是基於MFC的。

版本

產品版本 MFC版本
Microsoft C/C++ 7.0 MFC 1.0 (1992年)
Visual C++ 1.0 MFC 2.0
Visual C++ 1.5 MFC 2.5
Visual C++ 1.51 MFC 2.51
Visual C++ 1.52c MFC 2.5 (用於Windows 3.x平台的最後一版)
Visual C++ 2.0 MFC 3.0
Visual C++ 2.1 MFC 3.1
Visual C++ 2.2 MFC 3.2
Visual C++ 4.0 MFC 4.0(mfc40.dll包含於Windows 95
Visual C++ 4.1 MFC 4.1
Visual C++ 4.2 MFC 4.2(mfc42.dll包含於Windows 98第一版)
eMbedded Visual C++ 3.0(Pocket PC) MFC 4.2(mfc42.dll)
Visual C++ 5.0 MFC 4.21(mfc42.dll)
Visual C++ 6.0 MFC 6.0(mfc42.dll)1998年
eMbedded Visual C++ 4.0 MFC 6.0(mfcce400.dll)
Visual C++ .NET 2002 (Visual C++ 7.0) MFC 7.0(mfc70.dll) .Net 1.0
Visual C++ .NET 2003 (Visual C++ 7.1) MFC 7.1(mfc71.dll).Net 1.1
Visual C++ 20051 MFC 8.0.50727.42(mfc80.dll) .Net 2.0
Visual C++ 20081 MFC 9.0.21022(mfc90.dll)
Visual C++ 2008 (Visual C++ 9.0)(包含Feature Pack)2 MFC 9.0.30411(mfc90.dll) .Net 3.5
Visual C++ 20101 MFC 10.0.30319.1(mfc100.dll) .Net 4.0
Visual C++ 20121 MFC 11.0.50727.1(mfc110.dll) .Net 4.5
Visual C++ 20131 MFC 12.0.21005.1(mfc120.dll) .Net 4.5.1
Visual C++ 20151 MFC 14.0.23026.0(mfc140.dll).Net 4.6
  • 1 Visual Studio速成版(Express)不包含MFC庫。
  • 2 Feature Pack只用於英文版本的Visual Studio 2008。非英文版本的支援將包含於Visual Studio 2008 Service Pack 1。

MFC的結構

作為一個應用程式的開發框架,必須滿足各方面的功能需求。

應用程式啟動

基於MFC開發的應用程式在啟動時,Windows作業系統:

  1. 首先呼叫WinMain函數(位於appmodul.cpp中,封裝到mfc80.dll(VS2005版)),WinMain函數內呼叫了AfxWinMain函數。
  2. AfxWinMain函數(位於WinMain.cpp中)呼叫了
    1. 該應用程式自訂的App類(這個類衍生於CWinApp的,CWinApp又是衍生於CWinThread,因此代表了應用程式的主線程)的InitInstance函數,該函數註冊並建立窗口(通過AppUI2.cpp中的ProcessShellCommmand函數),然後ShowWindow、UpdateWindow;
    2. CWinThread的InitInstance函數;
    3. CWinThread的Run函數(位於thrdcore.cpp中)。該函數內部是Windows的訊息迴圈。 當應用程式收到WM_QUIT訊息後,CWinThread::Run函數返回,緊接着CWinThread::ExitInstance被呼叫,該函數可被覆蓋。程式至此登出執行。 訊息迴圈是一個for(;;)的無窮迴圈,該無窮迴圈內部包含了一個do...while的迴圈結構。while迴圈條件是呼叫PeekMessage函數的返回值,如果當前UI線程訊息佇列為空就返回到外層的無窮迴圈;while迴圈體內做兩件事:
      1. PumpMessage()。實際呼叫AfxInternalPumpMessage函數實現其功能:GetMessage()、AfxPreTranslateMessage()、TranslateMessage()、DispatchMessage().即:從UI線程訊息佇列移除一條訊息、遍歷該訊息的CWnd類直到該窗口的各級別父窗口的CWnd類以提供預處理該訊息的機會、如果該訊息是按鍵訊息則翻譯為WM_CHAR訊息、把該訊息給相應的窗口函數。
      2. IsIdleMessage():實際呼叫了AfxInternalIsIdleMessage函數,對於WM_PAINT、WM_SYSTIMER、以及游標位置沒有變化的WM_MOUSEMOVE或WM_NCMOUSEMOVE,為Idle Message。
    4. 各個窗口函數(WndProc)內部首先取得對應當前窗口控制代碼的CWnd類的指標,然後呼叫AfxCallWndProc函數。

應用程式結束

  • 如果是點擊了IDOK按鈕,預設是呼叫OnOK(),然後是OnDestory(),最後是PostNcDestroy()
  • 如果點擊IDCANCEL按鈕,預設呼叫OnCancel(),然後是OnDestory(),最後是PostNcDestroy()
  • 如果點擊右上角的關閉按鈕:先OnClose(),然後是OnCancel(),再然後是OnDestory() ,最後是PostNcDestroy()

訊息迴圈與訊息對映

MFC Class hierarchy for Windows message process flow

訊息分類

分類 訊息 對映宏 訊息處理常式原型 註釋
系統訊息 標準Windows訊息 MFC使用專用的相關的宏。如ON_WM_CREATE() MFC使用專用的訊息處理成員函數 一般地由窗口物件來處理這類訊息
命令訊息 WM_COMMAND ON_COMMAND(id, memberFxn) void OnXXX () 通過識別碼ID來區分來自哪個選單項、工具列按鈕或者加速鍵等
ON_COMMAND_RANGE(id, idLast, memberFxn) void OnXXX (UINT id) 批次處理一定範圍內的標示符ID
ON_UPDATE_COMMAND_UI(id, idLast, memberFxn) void OxXXX(CCmdUI* pCmdUI) 程式空閒時發的介面更新訊息的宏
ON_UPDATE_COMMAND_UI_RANGE(id, idLast, memberFxn) void OxXXX(CCmdUI* pCmdUI)
ON_CONTROL(id, idLast, memberFxn) void OnXXX () 父窗口響應控制項傳送的訊息
ON_CONTROL_RANGE(id, idLast, memberFxn) void OnXXX (UINT id)
ON_CONTROL_REFLECT(id, idLast, memberFxn) void OnXXX () 子控制項響應父窗口反射回來的通知訊息
通知訊息WM_NOTIFY ON_NOTIFY(wNotifyCode,id,memberFxn) afx_msg void memberFxn(NMHDR * pNotifyStruct, LRESULT* result) wParam為控制項ID,lParam指向NMHDR結構體,結構體的code域值為控制項通知碼用來表示控制項上的動作,如NM_CLICK.一般地由父窗口物件來處理這類訊息。
ON_NOTIFY_RANGE( wNotifyCode, id, idLast, memberFxn ) 可手工添加(class wizard不支援),處理控制項id連續的一批控制項的同一通知碼訊息。
ON_NOTIFY_EX 允許通知訊息在多處被處理
ON_NOTIFY_EX_RANGE
ON_NOTIFY_REFLECT afx_msg void memberFxn(NMHDR * pNotifyStruct, LRESULT* result) 複雜子控制項響應父窗口反射回來的通知訊息
自訂訊息 窗口類內部自訂訊息WM_USER到WM_APP-1 ON_MESSAGE(訊息名,memberFxn) afx_msg LRESULT OnMyMessageXXX(WPARAM wParam, LPARAM lParam) 範例
程式內部自訂訊息WM_APP到0xBFFF ON_THREAD_MESSAGE(WM_THREADMSG,OnThreadMessage) afx_msg void OnThreadMessage(WPARAM wParam,LPARAM lParam); 範例
ON_REGISTERED_THREAD_MESSAGE afx_msg void OnMyRegisterdThreadMsg(WPARAM, LPARAM); RegisterWindowMessage()使用一個字串來登記一個自訂的訊息ID
應用程式之間自訂訊息0xC000到0xFFFF ON_REGISTERED_MESSAGE LRESULT OnMyMessageXXX(WPARAM wParam, LPARAM lParam) RegisterWindowMessage()使用一個字串來登記一個自訂的訊息ID,便於跨行程應用

控制項的通知訊息與反射訊息

表單上的控制項,應當向父表單通報控制項發生的各種事件,如被點擊、繪製、內容改變等等,稱為通知訊息(notification message )。在 Windows 3.x的16位元程式設計時代,控制項向父表單傳送WM_COMMAND訊息,由父表單的代碼負責實現這些事件。其中wParam的低16位元是 control ID,高16位元是notification code (例如BN_CLICKED);lParam是控制項控制代碼。因此,再無可能傳遞其它資訊給父表單。為此,為傳遞具有特別內容的控制項事件,Windows 3.x定義了一批特殊的通知訊息(notification messages):

  • WM_PARENTNOTIFY:子窗口的某些重大事件發生時通知父窗口,包括建立、銷毀、滑鼠各鍵按下等事件
  • 子窗口的捲動情況的通知訊息
    • WM_VSCROLL
    • WM_HSCROLL
  • 子窗口的繪製通知訊息
    • WM_DRAWITEM,
    • WM_MEASUREITEM,
    • WM_COMPAREITEM,
    • WM_DELETEITEM,
    • WM_CHARTOITEM,
    • WM_VKEYTOITEM
    • WM_CTLCOLOR:設置按鈕、編輯框、ListBox、Static、捲軸控制項與MessageBox、DialogBox的前景色、背景色、背景模式、字型,並返回一把Brush,用於控制項背景繪製。

早於4.0版本的MFC,在控制項類提供了虛擬函式處理這些通知訊息,這一辦法已經被下述的「訊息反射」取代(但仍然向下相容繼續支援)。

隨着Windows 95開始了32位元程式時代,伴之而來的是Win32 API 與 MFC 4.0。 Win32增加了很多複雜的控制項,需要使用更多的通知訊息傳遞很多複雜的數據給父表單。Win32 API僅僅增加了一個訊息WM_NOTIFY,就實現了這些功能。lParam參數開頭是NMHDR數據結構,其後是與該通知類型相關的特定數據結構。

typedef struct tagNMHDR {
    HWND hwndFrom;
    UINT idFrom;
    UINT code;
} NMHDR;

CWnd::OnNotify函數處理通知訊息。它的預設實現是檢查訊息對映表(message map)尋找通知的處理器函數並呼叫。一般說來,不必覆蓋OnNotify;而應該寫一個處理器函數並增加為該窗口類的訊息對映表條目。

 ON_NOTIFY(wNotifyCode, id, memberFxn)

成員函數應該寫為:

 afx_msg void memberFxn(NMHDR* pNotifyStruct, LRESULT* result);

MFC 4.0提供了一種特性「訊息反射」(message reflection),[7]允許控制項通知訊息既可以在父窗口中,也可以在控制項中被處理。可以對控制項建立一個衍生的控制項類,實現對從父窗口反射回來的指定類型訊息的處理。訊息反射是MFC而不是Win32的特性,因此父窗口的類必須是從CWnd衍生,從而父窗口在CWnd::OnNotify函數中處理控制項的WM_NOTIFY時,首先呼叫CWnd::ReflectLastMsg把訊息反射回控制項的CWnd::SendChildNotifyLastMsg函數去處理;ReflectLastMsg返回值就是在控制項的訊息對映中使用ON_NOTIFY_REFLECT_EX()聲明的反射訊息處理常式的返回值,可以通知父窗口該訊息是否已經被控制項處理。控制項的CWnd::SendChildNotifyLastMsg函數,首先獲得線程的最後一條message,然後呼叫傳送窗口的虛擬函式OnChildNotify函數。在子窗口處理反射回來的控制項訊息,第一種方法是多載控制項窗口的OnChildNotify虛擬函式;第二種辦法是由CWnd::OnChildNotify預設處理去呼叫CWnd::ReflectChildNotify函數,進入控制項子窗口的MFC訊息對映的標準處理(子窗口處理的訊息被譯成WM_REFLECT_BASE+WM_NOTIFY訊息)。對於WM_NOTIFY,僅當在控制項的訊息對映(message map)中,控制項沒有通過ON_NOTIFY_REFLECT()聲明的反射訊息處理常式,父窗口的相應的通知訊息處理常式才會被呼叫(在父窗口訊息對映中使用宏ON_NOTIFY聲明)。控制項中通過ON_NOTIFY_REFLECT_EX()聲明的反射訊息處理常式可以返回真或假,以決定父窗口是否繼續處理該通知訊息。WM_NOTIFY以外的其它通知訊息,父窗口在第一時間有機會處理它,控制項對它的處理排在第二位。反射訊息處理常式通常使用特定的名字,對應的訊息反射宏的名字是在訊息名字加上字首ON_,字尾_REFLECT。如WM_CTLCOLOR對應ON_WM_CTLCOLOR_REFLECT。但以下三種情況,反射訊息處理常式可以隨意自行起名,對應的訊息反射宏的名字分別為:

  • WM_COMMAND使用ON_CONTROL_REFLECT
  • WM_NOTIFY使用ON_NOTIFY_REFLECT
  • ON_UPDATE_COMMAND_UI使用ON_UPDATE_COMMAND_UI_REFLECT

訊息傳遞處理機制

MFC類體系中,Windows訊息傳遞處理機制是基於CCmdTarget類及其衍生類別的靜態成員函數GetThisMessageMap()內部定義的靜態數據成員:

  • 成員類型為AFX_MSGMAP_ENTRY的陣列_messageEntries。在類的實現檔案中,在BEGIN_MESSAGE_MAP與END_MESSAGE_MAP之間的內容來初始化訊息對映入口項陣列。
  • 資料類型為AFX_MSGMAP的變數messageMap。該結構包含兩項,分別是直接基礎類別GetThisMessageMap函數指標與本類的_messageEntries陣列首元素地址。

標頭檔的類別定義中使用宏DECLARE_MESSAGE_MAP()來聲明靜態成員函數GetThisMessageMap與虛擬函式GetMessageMap

用戶所寫的類的Windows訊息處理常式(例如OnCommand)必須轉換為CCmdTarget::*的成員函數指標類型AFX_PMSG,儲存在該類的_messageEntries陣列中。

struct AFX_MSGMAP_ENTRY
{
	UINT nMessage;   // windows消息代号
	UINT nCode;      // WM_NOTIFY的控制代码
	UINT nID;        // WM_COMMAND下面的ID号,如果为其他的消息,则这个数字为0
	UINT nLastID;    //和前面的ID一起组成一个范围,用于发送一次消息,处理执行多次
	UINT nSig;       // 标志消息处理函数的类型
	AFX_PMSG pfn;    // 函数调用指针
};
typedef void (CCmdTarget::*AFX_PMSG)(void);

呼叫用戶類中該訊息處理常式時,根據該函數儲存在_messageEntries中的signature(一個無符號整型表示的函數的形參類型列表與返回值類型),把類型為void (CCmdTarget::*AFX_PMSG)(void)的成員函數指標強制轉為其它類型的CCmdTarget成員函數指標(例如void (AFX_MSG_CALL CWnd::*pfn_v_i_i)(int, int),目前在union MessageMapFunctions中列出了近百種CCmdTarget成員函數指標),然後呼叫轉換後的成員函數指標。這是基於Visual C++編譯器把單繼承的成員函數指標編譯為只儲存了函數的記憶體起始地址,因此可以在同一個單繼承類中把一種類型的成員函數指標強制轉換為另一種成員函數指標,或者把單繼承衍生類別的成員函數指標強制轉換為基礎類別成員函數指標。這是打破了C++標準的違例辦法。例如,對於CWnd::OnCommand函數,轉換過程是:

BOOL (CWnd::*)(WPARAM, LPARAM lParam) => void (CWnd::*)() => void (CCmdTarget::*)()

CString

CString是MFC中最常見的類之一,用於封裝字串數據結構。它只有一個數據成員m_pszData,其值為字串的首地址,其資料類型為wchar_t*或char*。在CString的m_pszData的前面實際還分配了CSringData數據塊,包含了管理數據:

IAtlStringMgr* pStringMgr; 
int nDataLength; 
int nAllocLength; 
int nRefs;

由下而上,CAtlStringMgr提供記憶體管理,CStringData提供共用管理,CString提供字串操作。

CAtlStringMgr的一個成員是IAtlMemMgr介面,這是策略模式,可以參照某個記憶體管理類。CAtlStringMgr的另一個成員是CNilStringData。

因此,每次為CString動態分配地址空間,實際分配長度為:(nChars+1)*nCharSize+sizeof(CStringData)。通過 Attach 操作,將這個 CStringData* 與 CSimpleStringT::m_pszData 執行了關聯。當執行CString的預設建構函式生成一個空字串時,實際上都是構造一個CnilStringData物件。CNilStringData 衍生自 CStringData,額外擁有一個 achNil 的陣列成員,這個陣列初始化為空字串。通過這個 achNil,保證了一個經過呼叫預設建構函式初始化的 CString,其指向的真正的字串是一個空字串。

部分編譯器對std::string放棄了寫時複製(Copy On Write)機制。但是,CString一直採取這一機制。CSimpleStringT::Fork 函數就提供了這樣一個操作,具體分為下面幾步:

  1. 根據傳入的一個長度分配一段新的空間;—— Allocate(nLength, ...)
  2. 把舊數據拷貝到新的空間裏面;—— CopyChars(...)
  3. 舊數據塊的參照技術減1; —— pOldData->Release()
  4. 把 m_pszData 和新的數據塊關聯起來。—— Attach(pNewData)

GetString方法返回的是唯讀的字串地址;而GetBuffer方法返回的是可寫的字串地址(如果數據區是共用的,則寫時複製),如果修改了字串內容,這時需要呼叫ReleaseBuffer方法把新的字串長度修改到元數據中(並在尾部增加2個0位元組)。[8]

CString物件用作可變參數函數(如printf)的實參時,由於無法通過形參類型確定呼叫哪個CString的類型轉換運算子函數,因此有必要顯式指明要轉換的類型。如果需要在函數的參數傳遞CString,由於CString使用了參照計數,因此函數參數傳遞一個CString物件是可行的;不需要修改其內容時,推薦使用const CString&。

支援MFC的DLL開發

使用Visual C++可以開發3種DLL:

  • 不使用MFC的DLL;
  • 使用MFC的規則的DLL:輸出的函數不涉及MFC,因此可以被支援/不支援MFC的應用程式呼叫該DLL
    • 動態連結到MFC(Regular DLLs dynamically linked to MFC)。
    • 靜態連結到MFC(Regular DLLs statically linked to MFC)
  • 使用MFC的擴充DLL(Extension DLLs),只能動態連結到MFC:輸出的函數涉及MFC,也可以輸出基於MFC的衍生類別。

由於DLL與呼叫它的應用程式都可以有自己的MFC全域數據與控制代碼對映(handle mapping),如果控制代碼值相同,則預設使用應用程式的對映到的資源。為了不互相干擾,允許DLL內部使用自己的資源,必須在DLL函數的入口處把資源模組控制代碼從預設的應用程式切換為該DLL。辦法是:

  1. 在該DLL的每個輸出的函數的最開始之處呼叫AFX_MANAGE_STATE(AfxGetStaticModuleState( ))[9]。函數AfxGetStaticModuleState的功能是在執行棧上建立一個AFX_MODULE_STATE類別的實例,對其進行設置,函數返回值為AFX_MODULE_STATE的指標。AFX_MODULE_STATE類利用其建構函式和解構函式進行模組狀態現場儲存及恢復。
  2. 使用AfxGetResourceHandle();取得當前資源模組控制代碼。使用AfxSetResourceHandle(HINSTANCE xxx); 設置程式要使用的資源模組控制代碼。

常用標頭檔與庫檔案

  • STDAFX.H 載入其他MFC標頭檔。
  • AFXWIN.H 它和它載入的檔案聲明了所有的MFC類。其內包含AFX.H,後者包含了AFXVER_.H,後者又載入了AFXV_W32.H,後者又載入WINDOWS.H。
  • AFXEXT.H 使用工具列、狀態列的程式必須載入這個檔案
  • AFXDLGS.H 通用對話方塊(Common dialog)的MFC程式需要載入此檔案。它內部包含COMMDLG.H
  • FXCMN.H 通用控制項(common control)的MFC程式需要載入此檔案。
  • AFXCOLL.H 使用MFC提供的容器都需要載入此檔案。
  • AFXDLLX.H 凡使用MFC extension DLLs需要載入此檔案。
  • AFXRES.H MFC程式的RC檔案必須載入此檔案。此檔案中對於標準資源的ID都有預設值。它們定義於此檔案中。

參考和參照