|
走出MFC窗口子類化的迷宮
MFC向?qū)傻膶?duì)話框?yàn)槟B(tài)對(duì)話框,當(dāng)我們?cè)谫Y源編輯器中向?qū)υ捒蛲献б粋€(gè)按鈕IDC_BTN時(shí),其布局信息將同步反映在DlgDemo.rc資源腳本文件中。 // DlgDemo.rc IDD_MY_DIALOG DIALOGEX 0, 0, 320, 201 STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_APPWINDOW CAPTION "DlgDemo" FONT 9, "宋體" BEGIN DEFPUSHBUTTON "確定",IDOK,260,7,50,14 PUSHBUTTON "取消",IDCANCEL,260,23,50,14 PUSHBUTTON "MyBtn",IDC_BTN,141,79,50,14 END CDialog的構(gòu)造函數(shù)的參數(shù)一nIDTemplate指定了對(duì)話框模板的ID,即DlgDemo.rc中的IDD_MY_DIALOG。 CDialog::CDialog(UINT nIDTemplate, CWnd* pParentWnd) { // …… m_pParentWnd = pParentWnd; m_lpszTemplateName = MAKEINTRESOURCE(nIDTemplate); // …… } 模態(tài)對(duì)話框調(diào)用CDialog::DoModal()創(chuàng)建并顯示對(duì)話框,CDialog::DoModal()根據(jù)對(duì)話框模板名稱m_lpszTemplateName進(jìn)行FindResource、LoadResource加載模板資源。 CDialog::DoModal()調(diào)用CDialog::CreateDlgIndirect,最終調(diào)用::CreateDialogIndirectParam完成非模態(tài)對(duì)話框的創(chuàng)建。::CreateDialogIndirectParam參數(shù)二LPCDLGTEMPLATE lpTemplate即DlgDemo.rc中的IDD_MY_DIALOG模板資源,該API將根據(jù)腳本描述創(chuàng)建對(duì)話框及其上的子控件(底層調(diào)用CreateWindowEx,傳入風(fēng)格、標(biāo)題和布局大小等參數(shù))。 對(duì)于外部而言,可見的只是一些子控件的ID,而沒有具體的子類(例如按鈕IDC_BTNàCButton)。實(shí)際上,對(duì)話框內(nèi)部維護(hù)了一個(gè)“控件IDà控件HWND”的映射,這樣我們就可以通過(guò)::GetDlgItem(hDlg, nIDDlgItem)獲取子控件的窗口句柄,進(jìn)行相關(guān)Get/Set操作。 下面在點(diǎn)擊按鈕IDC_BTN時(shí),修改其標(biāo)題。 ON_BN_CLICKED(IDC_BTN, OnBtn) void CMyDlg::OnBtn() { // TODO: Add your control notification handler code here
GetDlgItem(IDC_BTN)->SetWindowText("FXM"); // change button caption } CWnd* GetDlgItem(int nID)調(diào)用CWnd::FromHandle(::GetDlgItem(m_hWnd, nID)),FromHandle創(chuàng)建一個(gè)臨時(shí)的CWnd(子類)對(duì)象,并把Windows對(duì)象(HWND)映射到臨時(shí)的MFC對(duì)象上,然后返回臨時(shí)MFC對(duì)象。MFC框架在線程的Idle處理中刪除臨時(shí)對(duì)象。 利用向?qū)?/span>按鈕IDC_BTN添加CButton類型的控件變量,內(nèi)部調(diào)用了Attach函數(shù)建立了控件變量(CButton)與窗口(HWND)之間的永久映射(SetPermanent)。在整個(gè)對(duì)話框的生存周期中,可以通過(guò)這個(gè)控件變量實(shí)現(xiàn)對(duì)窗口的訪問。至此,我們對(duì)按鈕IDC_BTN的操作依然局限在相關(guān)屬性的Get/Set訪問上,而其后續(xù)狀態(tài)行為依然故我地輪回著CButton的DefWindowProc。 怎樣實(shí)現(xiàn)XP風(fēng)格按鈕、釘子按鈕甚至任意形狀按鈕呢?這里涉及到一個(gè)重要的概念——窗口子類化。 所謂窗口子類化,實(shí)際上就是改變窗口內(nèi)存塊中的有關(guān)參數(shù)。由于這種修改只涉及到一個(gè)窗口的內(nèi)存塊,因此它不會(huì)影響到屬于同一窗口類的其它窗口的功能和表現(xiàn)(IDàHWNDàCWnd)。窗口子類化中最常見的是修改窗口內(nèi)存塊中的窗口函數(shù)地址(lpfnWndProc),使其指向一個(gè)新的窗口函數(shù),從而改變?cè)翱诤瘮?shù)的處理方法,做出特定功能適應(yīng)。 在實(shí)際開發(fā)中,有些情況標(biāo)準(zhǔn)控件的標(biāo)準(zhǔn)過(guò)程是無(wú)能為力的。比如:在我們的應(yīng)用中要求一個(gè)EDIT控件接收老師對(duì)學(xué)生的評(píng)價(jià),評(píng)價(jià)分三個(gè)等級(jí)A、B、C(不要對(duì)我說(shuō)你想用ComboBox實(shí)現(xiàn)),這就要求在EDIT中禁止對(duì)其它字母、數(shù)字的輸入操作,怎么辦?EDIT控件本身沒有提供這種機(jī)制,采用子類化可以很好的解決這類問題。 我們知道,每一個(gè)Windows窗口(這里指EDIT)都有一個(gè)窗口處理函數(shù)負(fù)責(zé)對(duì)消息的處理,子類化通常就是用我們自己的消息處理函數(shù)來(lái)替代窗口原有的、標(biāo)準(zhǔn)的處理函數(shù)。當(dāng)然我們自己的窗口處理過(guò)程只是關(guān)心那些特定的消息(在這里是WM_CHAR),而其它消息將發(fā)給原來(lái)的窗口函數(shù)做默認(rèn)處理。在SDK中的實(shí)現(xiàn)方法是調(diào)用函數(shù)SetWindowLong,其原型如下: WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)AfxGetAfxWndProc()); 其中AfxGetAfxWndProc()是我們自己的窗口處理函數(shù),在其中處理我們感興趣的消息后,然后調(diào)用原窗口函數(shù)oldWndProc來(lái)對(duì)其它消息做標(biāo)準(zhǔn)處理。 我們先來(lái)梳理一下一個(gè)窗口創(chuàng)建過(guò)程中的附加和子類化過(guò)程。 CWnd::CreateàCWnd::CreateExàAfxHookWindowCreate(this)à_AfxCbtFilterHook。在鉤子函數(shù)_AfxCbtFilterHook中,將已創(chuàng)建的窗口(HWND)附加到當(dāng)前正在初始化的CWnd(CEdit)對(duì)象(_AFX_THREAD_STATE:: m_hWndInit)上。然后再調(diào)用::SetWindowLong改變窗口的過(guò)程為AfxWndProc。窗口函數(shù)AfxWndProc從AFX_MODULE_THREAD_STATE::m_pmapHWND中查詢hWnd對(duì)應(yīng)的CWnd對(duì)象,AfxCallWndProc將消息委托給具體窗口對(duì)象的WindowProc函數(shù)處理。 利用MFC實(shí)現(xiàn)上面提到的EDIT控件過(guò)濾輸入要求,只能輸入A、B、C中的一個(gè)字母。 從CEdit派生一個(gè)自己的類CSuperEdit,在其中處理WM_CHAR。 void CSuperEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default TCHAR ch[20]; GetWindowText(ch, 20);
if (strlen(ch)==1 && (nChar<='C' && nChar>='A')) return;
if ((nChar!='A') && (nChar!='B') && (nChar!='C')) return;
CEdit::OnChar(nChar, nRepCnt, nFlags); } 然后再給我們CProg1Dlg類中加入一個(gè)數(shù)據(jù)成員CSuperEdit m_edit,在CProg1Dlg::OnInitDialog()中加入以下兩行代碼: m_edit.SubclassDlgItem(IDC_EDIT1, this); m_edit.SetWindowText("<請(qǐng)輸入A、B、C>"); 處理EDIT向DIALOG發(fā)送的通知消息EN_SETFOCUS: ON_EN_SETFOCUS(IDC_EDIT1, OnSetfocusEdit1) void CProg1Dlg::OnSetfocusEdit1() { // TODO: Add your control notification handler code here m_edit.SetWindowText(""); m_edit.SetFocus(); } OK,一切搞定!和SDK的子類化方法比起來(lái),這是多么的容易! 我們看看MFC背著我們到底做了什么!這里主要解決兩個(gè)容易讓初學(xué)者比較疑惑的問題: 1、m_edit只是我們定義的一個(gè)C++類對(duì)象,為什么通過(guò)它調(diào)用其成員函數(shù)SetWindowText便可以控制我們程序中資源編號(hào)為IDC_EDIT1的控件? 2、CSuperEdit類為什么可以處理WM_CHAR消息? 大家都知道,控制Windows窗口、控件、資源……都是通過(guò)它們的句柄來(lái)實(shí)現(xiàn),如HANDLE、HWND、HDC都是句柄,它表現(xiàn)為一個(gè)32位長(zhǎng)整形數(shù)據(jù),存放于Windows中的特定區(qū)域,可以把它理解為指向我們想控制的窗口、控件、資源的索引,有了它,我們就可以控制想要控制的對(duì)象。 這里你應(yīng)該聯(lián)想到為什么大多數(shù)窗口API函數(shù)都有一個(gè)參數(shù)HWND hwnd了吧! // WINUSER.H BOOL SetWindowTextW( HWND hWnd, // handle to window or control LPCWSTR lpString // title or text ); 變量m_edit要想控制IDC_EDIT1,必通過(guò)EDIT控件窗口的句柄,但這又是如何實(shí)現(xiàn)的呢?您可能注意到了m_edit.SubclassDlgItem(IDC_EDIT1, this);一句,對(duì)了,這就是關(guān)鍵所在! 在此處F9設(shè)置斷點(diǎn),F5之后,程序到達(dá)此處,F11跟入SubclassDlgItem函數(shù): // WINCORE.CPP BOOL CWnd::SubclassDlgItem(UINT nID, CWnd* pParent) { ASSERT(pParent != NULL); ASSERT(::IsWindow(pParent->m_hWnd));
// check for normal dialog control first HWND hWndControl = ::GetDlgItem(pParent->m_hWnd, nID); if (hWndControl != NULL) return SubclassWindow(hWndControl);
#ifndef _AFX_NO_OCC_SUPPORT if (pParent->m_pCtrlCont != NULL) { // normal dialog control not found COleControlSite* pSite = pParent->m_pCtrlCont->FindItem(nID); if (pSite != NULL) { ASSERT(pSite->m_hWnd != NULL); VERIFY(SubclassWindow(pSite->m_hWnd));
#ifndef _AFX_NO_OCC_SUPPORT // If the control has reparented itself (e.g., invisible control), // make sure that the CWnd gets properly wired to its control site. if (pParent->m_hWnd != ::GetParent(pSite->m_hWnd)) AttachControlSite(pParent); #endif //!_AFX_NO_OCC_SUPPORT
return TRUE; } } #endif
return FALSE; // control not found } 代碼開始時(shí)對(duì)傳入的父窗口做些檢查,然后就是 HWND hWndControl = ::GetDlgItem(pParent->m_hWnd, nID); if (hWndControl != NULL) return SubclassWindow(hWndControl); 這是關(guān)鍵的代碼,先用hWndControl得到我們IDC_EDIT1控件的句柄,然后調(diào)用SubclassWindow函數(shù),這個(gè)函數(shù)是實(shí)現(xiàn)的關(guān)鍵,我們來(lái)看一下它做了什么: // WINCORE.CPP BOOL CWnd::SubclassWindow(HWND hWnd) { if (!Attach(hWnd)) return FALSE;
// allow any other subclassing to occur PreSubclassWindow();
// now hook into the AFX WndProc WNDPROC* lplpfn = GetSuperWndProcAddr(); WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)AfxGetAfxWndProc()); ASSERT(oldWndProc != (WNDPROC)AfxGetAfxWndProc());
if (*lplpfn == NULL) *lplpfn = oldWndProc; // the first control of that type created #ifdef _DEBUG else if (*lplpfn != oldWndProc) { TRACE0("Error: Trying to use SubclassWindow with incorrect CWnd\n"); TRACE0("\tderived class.\n"); TRACE3("\thWnd = $%04X (nIDC=$%04X) is not a %hs.\n", (UINT)hWnd, _AfxGetDlgCtrlID(hWnd), GetRuntimeClass()->m_lpszClassName); ASSERT(FALSE); // undo the subclassing if continuing after assert ::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)oldWndProc); } #endif
return TRUE; } 函數(shù)Attach建立窗口對(duì)象CWnd與窗口句柄HWND的關(guān)聯(lián)(映射),其內(nèi)部實(shí)現(xiàn)如下: // WINCORE.CPP BOOL CWnd::Attach(HWND hWndNew) { ASSERT(m_hWnd == NULL); // only attach once, detach on destroy ASSERT(FromHandlePermanent(hWndNew) == NULL); // must not already be in permanent map
if (hWndNew == NULL) return FALSE;
CHandleMap* pMap = afxMapHWND(TRUE); // create map if not exist ASSERT(pMap != NULL);
pMap->SetPermanent(m_hWnd = hWndNew, this);
#ifndef _AFX_NO_OCC_SUPPORT AttachControlSite(pMap); #endif
return TRUE; } 這里要說(shuō)明的是pMap->SetPermanent(m_hWnd = hWndNew, this);一句,它把IDC_EDIT1的句柄賦值給類CSuperEdit的數(shù)據(jù)成員m_hWnd(別忘了我們的CSuperEdit類是派生于CEdit的),即建立hWndNew(IDC_EDIT1)與m_edit(this)對(duì)象之間的關(guān)聯(lián)。大家可能現(xiàn)在已經(jīng)隱約的明白了些什么,不錯(cuò),在m_edit.SetWindowText("<請(qǐng)輸入A、B、C>");中正是通過(guò)這個(gè)數(shù)據(jù)成員m_hWnd實(shí)現(xiàn)對(duì)IDC_EDIT1控制的: // WINOCC.CPP void CWnd::SetWindowText(LPCTSTR lpszString) { ASSERT(::IsWindow(m_hWnd));
if (m_pCtrlSite == NULL) ::SetWindowText(m_hWnd, lpszString); else m_pCtrlSite->SetWindowText(lpszString); } 其它CEdit類的函數(shù)也都是圍繞“API函數(shù)+HWND參數(shù)(m_hWnd)”進(jìn)行包裝的。常用的DDX_Control方法說(shuō)到底也是調(diào)用SubclassWindow: OnInitDialogàUpdateDataàDoDataExchangeàDDX_ControlàSubclassWindow。 故一般在派生了CSuperEdit類后,可利用向?qū)?/span>CProg1Dlg添加CSuperEdit類型控件變量,向?qū)⒃?/span>void CProg1Dlg::DoDataExchange(CDataExchange* pDX)中自動(dòng)添加DDX_Control(pDX, IDC_EDIT1, m_edit); 在進(jìn)行子類化時(shí),SubclassDlgItem和DDX_Control兩種方式擇其一。 怎么樣?第一個(gè)問題的來(lái)龍去脈搞明白了吧? 現(xiàn)在看看第二個(gè)問題:CSuperEdit類為什么可以處理WM_CHAR消息? 可能有的朋友現(xiàn)在疑惑,雖然通過(guò)句柄實(shí)現(xiàn)了m_edit對(duì)IDC_EDIT的控制,但發(fā)送給它的消息照樣跑到EDIT的標(biāo)準(zhǔn)處理函數(shù)中,對(duì)WM_CHAR的處理是如何實(shí)現(xiàn)的呢? 如果消息照樣跑到EDIT的標(biāo)準(zhǔn)處理函數(shù)中,那當(dāng)然是不能處理了!不知您有沒有看到在上面的SubclassWindow函數(shù)中有這么一小段我加了重點(diǎn)標(biāo)示: // now hook into the AFX WndProc WNDPROC* lplpfn = GetSuperWndProcAddr(); WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)AfxGetAfxWndProc()); ASSERT(oldWndProc != (WNDPROC)AfxGetAfxWndProc());
if (*lplpfn == NULL) *lplpfn = oldWndProc; // the first control of that type created 再和我們開始講到的SDK中子類化機(jī)制聯(lián)系起來(lái),明白了吧?MFC在這里神不知鬼不覺的搞起偷天換日的勾當(dāng)! 這個(gè)AfxGetAfxWndProc()函數(shù)是這樣的: // WINCORE.CPP // always indirectly accessed via AfxGetAfxWndProc WNDPROC AFXAPI AfxGetAfxWndProc() { #ifdef _AFXDLL return AfxGetModuleState()->m_pfnAfxWndProc; #else return &AfxWndProc; #endif } 讀過(guò)侯捷先生《深入淺出MFC》的朋友不知還是否記得MFC的命令路由機(jī)制正是以這個(gè)函數(shù)為起點(diǎn)的! 我們可以對(duì)對(duì)話框CProg1Dlg進(jìn)行WM_CREATE的消息響應(yīng),但在CProg1Dlg::OnCreate函數(shù)中對(duì)子控件所作的任何操作都會(huì)導(dǎo)致內(nèi)存非法訪問。OnCreate函數(shù)成功返回后,創(chuàng)建主對(duì)話框的CreateWindowEx接著返回,這時(shí)::CreateDialogIndirectParam過(guò)程中才開始創(chuàng)建對(duì)話框子控件窗口。等所有子控件創(chuàng)建完畢后,::CreateDialogIndirectParam發(fā)出WM_INITDIALOG消息,調(diào)用對(duì)話框的OnInitDialog的函數(shù)。因此,在OnInitDialog之后子類化,只能處理一些創(chuàng)建之后的狀態(tài)行為。通過(guò)子類化可對(duì)既有窗口特定消息進(jìn)行行為和狀態(tài)的自繪制處理。 當(dāng)程序收到發(fā)給Edit的WM_CHAR消息時(shí),本應(yīng)調(diào)用EDIT標(biāo)準(zhǔn)窗口處理函數(shù),現(xiàn)在被改為調(diào)用LRESULT CALLBACK AfxWndProc(HWND, UINT, WPARAM, LPARAM);了,然后WM_CHAR消息進(jìn)行一系列的流竄,最終成功到達(dá)我們的處理函數(shù)void CSuperEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);關(guān)于消息的分流派發(fā)請(qǐng)參考《深入淺出MFC》第9章《消息映射與命令繞行》。 終于,我們走出了FMC子類化的迷宮。
原文: 參考: 《SubClassWindow與SubClassDlgItem》 《Visual C++中窗口子類化技術(shù)的實(shí)現(xiàn)及其應(yīng)用》 發(fā)表于 @ 2010年05月24日 21:52:00 |
|
|