在做“HOOK檔案開啟/儲存對話方塊”的過程中,我首先研究了介面庫的相關知識。介面庫一般都是由C/C++這種中低級語言編碼,這是因為在Windows下的介面庫實現技術大都以直接操作控制Windows的訊息和調用Windows的API為主,這就是這種中低級語言的優勢了。無論何種介面庫,最為根本的原理就是獲得或者截獲視窗的某些訊息,按照自己的需要處理這些訊息,畫出自己需要的介面。
按照Windows下的介面庫的使用方法來分類,可以分為兩種:
1、 通過派生、繼承介面庫中的類來使用庫。這類介面庫現在是占絕大多數。這類介面庫通常可以對同種類型的控制項、視窗自己控制顯示風格。這種類型的介面庫典型的代表就是GuiToolkit、ProfUIS。
2、 通過Link標頭檔,使用DLL來使用的介面庫。這類介面庫一般都是商業化的介面庫。這類介面庫一般對於同種類型的控制項、視窗都是顯示統一的風格。這種類型的介面庫的典型代表是Skin++、AppFace。
上面的分類,其實同時也代表著兩種介面庫實現技術,也就是擷取用於自繪視窗的訊息的兩種來源:
1、 通過子類化、超類化改變視窗風格。其實就是調用Windows的API SetWindowLong或者通過類的派生和繼承來改變Windows視窗的預設的訊息處理函數。
2、 使用HOOK技術改變Windows的預設訊息處理。
一、SetWindowLong
SetWindowLong(HWND hWnd,//需要改變UI的視窗的視窗控制代碼
Int nIndex,//替換視窗的預設訊息處理函數時為GWL_WNDPROC
Long dwNewLong)//新的預設的視窗訊息處理函數
調用這個API函數可以替換一個視窗的預設的訊息處理函數,這樣就可以在新的視窗訊息處理函數中截獲到目標視窗的相關訊息,然後根據需要處理這些訊息。這個API用於擷取當前視窗的訊息,它不能擷取視窗中子視窗的訊息。
這種類型的介面庫,一般是開源的或者是提供了標頭檔和Lib檔案的介面庫。
這個Windows API大家很可能很少直接調用,但是它的封裝――――SubclassWindow這個成員函數,我想大家都使用過。先讓我們看看各種類型的子類化過程中的SetWindowLong都藏在什麼地方,它們是如何工作的。
1、 MFC下的實現
在MFC程式中,可以先在工程中添加一個用於子類化的視窗類別,然後就可以通過ClassWizard這個工具來完成剩下的子類化的工作了。我們以一個自訂的Button類CMyButton類來舉例。在開啟ClassWizard為我們的基於Dialog的工程中的一個Button資源添加一個對應的變數的時候我們就可以看到可以直接定義了CMyButton類,如:
我們為ID為IDC_BUTTON1的一個按鈕資源定義一個類型為CMyButton的成員變數m_MyBtn。這時候ClassWizard就會在重載的虛函數DoDataExchange中為我們添加上一條語句,如下:
void CMFCSampleDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CMFCSampleDlg)
DDX_Control(pDX, IDC_BUTTON1, m_MyBtn);//這就是ClassWizard為我們添加的
//}}AFX_DATA_MAP
}
讓我們來看看DDX_Control這個函數為我們做了什麼,在MFC的原始碼DLGDATA.CPP檔案中我們能找到這個函數的原始碼:
void AFXAPI DDX_Control(CDataExchange* pDX, int nIDC, CWnd& rControl)
{
if (rControl.m_hWnd == NULL) // not subclassed yet
{
……
…
//注意看看,噢,原來SubclassWindow在這裡
if (!rControl.SubclassWindow(hWndCtrl))
{
ASSERT(FALSE); // possibly trying to subclass twice?
AfxThrowNotSupportedException();
}
……
…
}
}
讓我們再看看MFC下的SubclassWindow這個成員函數的實現,在MFC的原始碼wincore.cpp中可以看到所有MFC下視窗類別的基類CWnd中的SubclassWindow的實現:
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();
//注意看看,原來它也是使用SetWindowLong啊
WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC,
(DWORD)AfxGetAfxWndProc());
……
…
return TRUE;
}
上面的原始碼跟蹤說明,ClassWizard為我們自動完成的子類化工作中,其實也是調用的SetWindowLong這個Windows API。當然在MFC下你也可以自己調用SubclassWindow這個成員函數去手動的完成子類化。
2、 ATL/WTL下的實現
在ATL的原始碼中,檔案ATLWIN.H檔案中,我們可以找到所有ATL視窗類別的基類CwindowImplBaseT中的SubClassWindow的實現:
template <class TBase, class TWinTraits>
BOOL CWindowImplBaseT< TBase, TWinTraits >::SubclassWindow(HWND hWnd)
{
ATLASSERT(m_hWnd == NULL);
ATLASSERT(::IsWindow(hWnd));
m_thunk.Init(GetWindowProc(), this);
WNDPROC pProc = (WNDPROC)&(m_thunk.thunk);
//注意看看,它也是調用SetWindowLong的!
WNDPROC pfnWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc);
if(pfnWndProc == NULL)
return FALSE;
m_pfnSuperWindowProc = pfnWndProc;
m_hWnd = hWnd;
return TRUE;
}
在ATL/WTL沒有MFC下的那種自動子類化的機制,如果需要子類化一般都是直接調用SubclassWindow這個成員函數。
二、SetWindowsHookEx
SetWindowsHookEx( int idHook,//HOOK的類型
HOOKPROC lpfn,//HOOK的回呼函數
HINSTANCE hMod,//應用程式的執行個體控制代碼
DWORD dwThreadId)//線程的ID
SetWindowsHookEx這個API可以設定很多種類型的HOOK,它們分別在不同的時間截獲線程ID為dwThreadId的線程的不同訊息。在編寫介面庫的時候一般設定類型為WH_CALLWNDPROC的HOOK,用以在視窗處理訊息之前獲得訊息並進行處理。在HOOK的CALLBACK函數中可以獲得視窗控制代碼,進而獲得視窗的類型和視窗的風格,這樣就可以知道需要處理的訊息類型。一般都是從截獲WM_CREATE訊息開始,然後處理WM_PAINT、WM_NCPAINT等等和UI有關的訊息以達到自繪視窗UI的目的。
使用HOOK技術的介面庫中當然也可以使用SetWindowLong技術,在C++寫的介面庫中一般是在獲得了視窗控制代碼以後,即使用SetWindowLong技術將視窗控制代碼關聯到一種特定的視窗類別上。並且可以根據視窗的風格讓同一個類做出不同風格的顯示效果,可惜的是現在市面上的介面庫都很少進行這種比較繁瑣的區分工作。
這種類型的介面庫一般都是以DLL檔案格式出現,現在最為流行的是將介面庫封裝為一個COM DLL,在應用程式中建立這個COM組件,獲得相關的介面,調用相關的介面函數來為應用程式安裝HOOK。在做“Picasso風格的檔案開啟/儲存對話方塊”過程中,我們一直以Yahoo Messenger的Save對話方塊作為參考,其實Yahoo Messenger就是使用的這種類型的介面庫的。Yahoo messenger的介面庫截獲了進程中所有線程的訊息,並做了相關的處理,所以它可以截獲一些系統的視窗的建立訊息和UI相關的訊息,以達到改變Windows系統視窗的顯示風格的目的。
最後介紹一下幾個比較好的開源的介面庫或者例子。
1、 ClassXP(http://www.yonsm.net/read.php?26)
一個個人的開源介面庫,C語言寫的,不完善,使用HOOK技術。Yahoo messenger的介面庫就類似於這個介面庫,只不過Yahoo messenger做的更完善而已。
2、 ProfUIS(http://www.codeproject.com/docking/prod_profuis.asp)
一個部分開源的介面庫,有完善功能的商業版。比較完善,MFC寫的,使用的是SetWindowLong技術。
3、 GuiToolkit(http://www.codeproject.com/library/guitoolkit.asp)
一個開源的介面庫,比較完善,MFC寫的,使用的是SetWindowLong技術。
4、 MSDN中的ControlSpy例子。
MSDN中的一個例子,用於瞭解各種控制項的訊息。