關于如何換膚、子類化的解決方案 對于應用程序的換膚及子類化。下面是我嘗試過一些方法,以在CAboutDlg中子類化其中的Button為例: DDX_Control(pDX, IDB_BUTTON1, m_edit); 或者在 InitDialog() 中加上 m_btn.SubclassDlgItem(IDB_BUTTON1, this); 這兩種效果差不多的。 g_hWndProcHook = ::SetWindowsHookEx(WH_CALLWNDPROC,WndProcHook,NULL,::GetCurrentThreadId()); 3、在 WndProcHook 中處理窗口創(chuàng)建和銷毀的消息: LRESULT CALLBACK WndProcHook(int code, WPARAM wParam, LPARAM lParam) { if (code == HC_ACTION) { switch (((CWPSTRUCT*) lParam)->message) { case WM_CREATE: BeginSubclassing(((CWPSTRUCT*) lParam)->hwnd); break; case WM_NCDESTROY: // TODO: clear subclass info. EndSubclassing(((CWPSTRUCT*) lParam)->hwnd); break; default: break; } } return CallNextHookEx(g_hWndProcHook, code, wParam, lParam); }4、在 BeginSubclassing 中用 GetClassName 得到類名,例如 "Button",然后用 CButtonXP 類進行子類化。 CButtonXP pButton = new CButtonXP; VERIFY(pButton ->SubclassWindow(hWnd)); 第三種 在Hook中使用窗口過程 WNDPROC oldProc; LRESULT CALLBACK ProcButton(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { ASSERT(oldProc != 0); if (oldProc == 0) return TRUE; switch (uMsg) { case WM_ERASEBKGND: break; //...... default: break; } return CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); }2、同第二種 3、同第二種 4、在 BeginSubclassing 中得到類名后,用 SetWindowLong 的方式子類化: oldProc = (WNDPROC) GetWindowLong(hWnd, GWL_WNDPROC); SetWindowLong(hWnd, GWL_WNDPROC, (LONG) ProcButton);第四種:不用 Hook 在一個對話框的 OnInitDialog 中枚舉它的所有子窗體,例如用下面兩句來實現(xiàn): hWnd=GetWindow(hDlg,GW_CHILD); hWnd=GetWindow(hWnd,GW_HWNDNEXT); 對每個子窗體進行子類化處理,處理過程同第二種與第三種。 第五種:如果是在XP下運行,可以使用manifest,也就是如下的一個XML文件<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assemblyIdentity name="Microsoft.Windows.XXXX" processorArchitecture="x86" version="5.1.0.0" type="win32"/> <description>Windows Shell</description> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="x86" publicKeyToken="6595b64144ccf1df" language="*"/> </dependentAssembly> </dependency> </assembly>把它存為應用程序名 .manifest,放到和應用程序?qū)哪夸浵?,或者把它作為資源類型為24的資源編譯進應用程序中。這樣程序在XP下就自動擁有了XP的風格。 第六種:使用第三方的庫Skin++(www.uipower.com)實現(xiàn)換膚 第七種:用第三方應用程序給整個windows換膚(windowblinds) 以上七種方式各有優(yōu)缺點。我在使用過程中也遇到不少問題,現(xiàn)在一一道來,希望和大家共同解決問題。先排除幾種不準備深入探討的方式: 第五種,manifest 方式最快速和簡潔,但是功能有限,存在嚴重的平臺限制,不過好處在于應用程序可以和windows共一種風格。 第六種,使用第三方的庫 Skin++(www.uipower.com) 實現(xiàn)換膚方式使用起來很簡單,定制性也不錯,可供選擇的皮膚種類非常的多,支持的語言非常廣泛,可以稱得上是換膚功能的終結者,對于共享軟件開發(fā)者和注重界面的企業(yè)來說是個不錯的解決方案,他的換膚理念很新,有些地方做得很獨特,比如可以對 BCG 換膚等,有些技術點,很多同類產(chǎn)品都沒有做到,比如 ComboBox 的滾動條,系統(tǒng)對話框(open or close Dialog)的菜單等等。 第七種,屬于自娛性質(zhì)的,也就不多說了。 第一種,直接使用現(xiàn)成的類,屬于很常見的一種用法,一般來說使用上不會出什么問題,缺點就不說了,如果這種方式讓我滿意,我就不必發(fā)這篇帖子了。 下面看看第二三四種: 第二種是用 HOOK+ 窗口類,實現(xiàn)起來比較方便,和做一個自繪控件的工作量其實是一樣的。 第三種是用HOOK+窗口過程,實現(xiàn)起來比較麻煩,需要自己處理一堆switch case, 自己轉(zhuǎn)換消息參數(shù),自己找地方維護一堆狀態(tài)變量,工作量很大。 第四種不用 HOOK 的方式,有個缺點:對被換膚的程序的源代碼的修改比較多。當然,直接到進程中去找窗口句柄,然后子類化那么就不用源代碼了,不過這樣的話還不如用HOOK呢。 實際上,HOOK機制和枚舉窗體雖然過程不同,不過最終目的是一樣的,都是為了子類化窗口。所以在此不去探討孰優(yōu)孰劣了?,F(xiàn)在切入正題,談談在子類化過程中遇到的問題: 一個是重復 subclass 的問題,上面提到,子類化的兩種方式:用窗口類或者用窗口過程。使用窗口類是從CWnd派生一個類,調(diào)用CWnd 的 protected 函數(shù) SubclassWindow。可是如果正常使用一個窗口類(聲明成員變量,加入DDX_Control),實際上在 DDX_Control 中也是是用了 SubclassWindow 的。假如為一個控件聲明變量,而在 Hook 中又進行了子類化,結果會怎么樣呢?答案是:程序崩潰或彈出消息框"不支持的操作"。因為 SubclassWindow 函數(shù)調(diào)用前是要先 Attach 到一個HWND上去的。重復的 Attach 看來是不允許。要避免程序崩潰也有辦法: 1、只為控件聲明一個指針變量,動態(tài)的去獲取CWnd類的實例,但是這樣就達不到換膚的目的了。 2、還有一種方法,經(jīng)過我試驗,如果兩個SubclassWindow的調(diào)用位于不同的模塊,例如一個位于exe,一個位于dll(我是通過exe中調(diào)用dll中的函數(shù)顯示該dll中的對話框來測試的),那么就不會出現(xiàn)問題。在還沒有找到更好的方法之前,這也姑且算是一種解決方法吧。 但是如果使用窗口過程來子類化,就不存在重復subclass的問題了,只要小心處理,子類化無數(shù)次都沒問題,但是對于復雜的自繪事件,在一個窗口過程中來寫switch語句,好像很麻煩。 我嘗試過自己寫一個新的SubclassWindow函數(shù)來嘗試借用CWnd的窗口過程,這樣就可以按照MFC的方式來寫消息響應函數(shù)了。只可惜,最終還是無功而返,因為SubclassWindow不是虛函數(shù),而CWnd的窗口過程是作為一個protected成員存在的。所以沒法在外部借用MFC的消息機制。所以,自己寫代碼處理 wParam 和 lParam 看來在所難免。 零一個是子類化系統(tǒng)對話框的問題,系統(tǒng)的對話框和自己的對話框表現(xiàn)的總不一樣。目前我還沒有對所有的系統(tǒng)對話框進行測試。在 MessageBox 彈出的對話框中遇到的問題可以見我這一片帖子: http://community.csdn.net/Expert/To....asp?id=3103399 在文件對話框中我遇到一個問題,子類化過的 CStatic 的背景好像沒有重繪一樣,照理說應該由CStatic的父窗體負責背景的。 void CStaticNew::OnPaint() { CPaintDC dc(this); // device context for painting // TODO: Add your message handler code here CRect rt; GetWindowRect(rt); // 繪制背景 dc.SetBkMode(TRANSPARENT); // 繪制文字 CFont *pfont, * pOldFont; pfont = GetFont(); if (pfont) pOldFont = dc.SelectObject(pfont); CString szTitle; GetWindowText(szTitle); dc.DrawText(szTitle, CRect(0, 0, rt.Width(), rt.Height()), DT_LEFT | DT_WORDBREAK ); if (pfont) dc.SelectObject(pOldFont); // 繪制圖標 if ((GetStyle() & SS_ICON) != 0) { dc.DrawIcon(0, 0, GetIcon()); } // Do not call CStatic::OnPaint() for painting messages }類名的識別問題,到現(xiàn)在為止,我所使用的子類化方法都是基于GetClassName這個函數(shù)獲得窗口類名,再根據(jù)用spy++所得到的知識,如"#32770"表示對話框,"ToolbarWindow32"是工具欄,等等。但是窗口類名是可以在創(chuàng)建時任意指定的呀,而像CMainFrame的類名根本就不能夠確定,例如記事本主窗體的類名是"Notepad",寫字板主窗體的類名是"WordPadClass"。這樣的話,子類化如何去進行呢。真想知道windows是怎么做的,skinmagic又是怎么做的。目前主要就是這三個問題了。希望大家能展開討論,給出一個換膚的完善的解決方案。 我寫了一個簡化的CWnd類來解決重復子類化問題和簡化窗口過程,不過它不支持對自己的重復子類化(即只能用于沒有被子類化的或者被CWnd子類化的HWND)。 因為不想弄得和MessageMap那樣復雜,所以功能也有限:須手工轉(zhuǎn)化WPARAM和LPARAM、消息處理無法繼承、不支持多線程。使用很簡單: CWndNew* pWnd = new CWndNew; pWnd->SubclassWindow(hWnd); 用完了,記得釋放處理: pWnd->UnsubclassWindow(); delete pWnd; 如果要進行功能擴充(繼承),就改寫那幾個虛函數(shù): class CWndNew { public: CWndNew(); virtual ~CWndNew(); bool SubclassWindow(HWND hWnd); void UnsubclassWindow(); protected: // virtual virtual LRESULT WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam); virtual void PresubclassWindow(){}; virtual void PostunsubclassWindow(){}; protected: LRESULT PrevWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam); HWND m_hWnd; private: WNDPROC m_oldProc; static map關于子類化及其撤銷的順序問題,當用自己的類或者過程子類化窗口時,需要處理好與MFC類子類化的順序沖突。假設我們自己的類叫CWndNew,那么不管CWnd和CWndNew誰先子類化一個窗口,最終兩者協(xié)同工作的結果應該是該窗口的窗口過程還原到未子類化之前的狀態(tài)。首先,不要在HOOK過程中處理WM_NCDESTROY消息。理由:如果CWndNew比CWnd先子類化,由于HOOK的原因,你仍然會先處理WM_NCDESTROY,這時候如果你撤銷子類化,那么CWnd類就得不到機會清理。而如果你不撤銷子類化,CWnd沒有能力把被子類化的窗口還原到最初狀態(tài)。在HOOK過程中,不能通過調(diào)用SendMessage函數(shù)讓CWnd先行處理,然后你自己再處理,因為SendMessage后,消息又會被HOOK攔截。 由于上述原因,在CWndNew的消息過程中處理WM_NCDESTROY是很不錯的選擇,MFC也是這樣做的。參照如下的代碼進行解釋: case WM_NCDESTROY: { LRESULT lret; WNDPROC wndproc; wndproc = (WNDPROC)GetWindowLong(m_hWnd, GWL_WNDPROC); if (wndproc == CWndNew::StaticWindowProc) { HWND hWnd = m_hWnd; UnsubclassWindow(); lret = CallWindowProc(m_oldProc, hWnd, uMsg, wParam, lParam); } else { lret = CallWindowProc(m_oldProc, m_hWnd, uMsg, wParam, lParam); if(wndproc == (WNDPROC)GetWindowLong(m_hWnd, GWL_WNDPROC)) UnsubclassWindow(); } delete this; return lret; }首先判斷該窗口的WNDPROC是否發(fā)生過變動,如果沒有的話是最好的,趕緊撤銷子類化,再把消息傳遞給之前窗口過程,然后功成身退,不問世事了。 如果發(fā)生過變動,那么也就是說有別的類在CWndNew子類化以后又進行了子類化,而現(xiàn)在又把WM_NCDESTROY傳給了CWndNew。這好辦,如法炮制,把消息繼續(xù)往前傳,如果WNDPROC又發(fā)生了改變,說明之前的某個窗口過程已經(jīng)作了處理,就不需要再進行撤銷子類化的操作了。這點MFC的CWnd類也是這樣做的。 另外還有一個問題不解,就是Edit,ListBox,ListCtrl等等控件的內(nèi)嵌的滾動條是怎么換膚的?網(wǎng)上一般介紹的方法是隱藏原來的,然后換上自己重新實現(xiàn)的。這種在Spy++中一看就能現(xiàn)出原形,可是Skin++ 換膚后的滾動條就不知道是怎么實現(xiàn)的了?我看過coolsb這個文章,他能實現(xiàn)給滾動條換膚的功能,但是對Combobox支持不好。 (#) |
|