关于如何换肤、子类化的解决方案
作者:peterbing@sohu.com
对于应用程序的换肤及子类化。下面是我尝试过一些方法,以在CAboutDlg中子类化其中的Button为例:
第一种:直接用现成的类
1、自己写一个类class CButtonXP : public CButton{/*...*/}
用MessageMap处理感兴趣的消息。
2、用CButtonXP代替CButton来声明变量m_btn;
3、在void CAboutDlg:DoDataExchange(CDataExchange* pDX)中加上一句:
DDX_Control(pDX, IDB_BUTTON1, m_edit);
或者在 InitDialog() 中加上
m_btn.SubclassDlgItem(IDB_BUTTON1, this);
这两种效果差不多的。
第二种:在 Hook 中使用现成的类
1、自己写一个类 class CButtonXP : public CButton{/*...*/}
用 MessageMap 处理感兴趣的消息。
2、使用 SetWindowsHookEx 安装一个钩子:
g_hWndProcHook = ::SetWindowsHookEx(WH_CALLWNDPROC,WndProcHook,NULL,::GetCurrentThreadId());
3、在 WndProcHook 中处理窗口创建和销毁的消息:
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中使用窗口过程
1、自己写一个按钮的窗口过程
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 中枚举它的所有子窗体,例如用下面两句来实现:
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,放到和应用程序对应的目录下,或者把它作为资源类型为24的资源编译进应用程序中。这样程序在XP下就自动拥有了XP的风格。
第六种:使用第三方的库Skin++(www.uipower.com)实现换肤
第七种:用第三方应用程序给整个windows换肤(windowblinds)
以上七种方式各有优缺点。我在使用过程中也遇到不少问题,现在一一道来,希望和大家共同解决问题。先排除几种不准备深入探讨的方式:
第五种,manifest 方式最快速和简洁,但是功能有限,存在严重的平台限制,不过好处在于应用程序可以和windows共一种风格。
第六种,使用第三方的库 Skin++(www.uipower.com) 实现换肤方式使用起来很简单,定制性也不错,可供选择的皮肤种类非常的多,支持的语言非常广泛,可以称得上是换肤功能的终结者,对于共享软件开发者和注重界面的企业来说是个不错的解决方案,他的换肤理念很新,有些地方做得很独特,比如可以对 BCG 换肤等,有些技术点,很多同类产品都没有做到,比如 ComboBox 的滚动条,系统对话框(open or close Dialog)的菜单等等。
第七种,属于自娱性质的,也就不多说了。
第一种,直接使用现成的类,属于很常见的一种用法,一般来说使用上不会出什么问题,缺点就不说了,如果这种方式让我满意,我就不必发这篇帖子了。
下面看看第二三四种:
第二种是用 HOOK+ 窗口类,实现起来比较方便,和做一个自绘控件的工作量其实是一样的。
第三种是用HOOK+窗口过程,实现起来比较麻烦,需要自己处理一堆switch case, 自己转换消息参数,自己找地方维护一堆状态变量,工作量很大。
第四种不用 HOOK 的方式,有个缺点:对被换肤的程序的源代码的修改比较多。当然,直接到进程中去找窗口句柄,然后子类化那么就不用源代码了,不过这样的话还不如用HOOK呢。
实际上,HOOK机制和枚举窗体虽然过程不同,不过最终目的是一样的,都是为了子类化窗口。所以在此不去探讨孰优孰劣了。现在切入正题,谈谈在子类化过程中遇到的问题:
一个是重复 subclass 的问题,上面提到,子类化的两种方式:用窗口类或者用窗口过程。使用窗口类是从CWnd派生一个类,调用CWnd 的 protected 函数 SubclassWindow。可是如果正常使用一个窗口类(声明成员变量,加入DDX_Control),实际上在 DDX_Control 中也是是用了 SubclassWindow 的。假如为一个控件声明变量,而在 Hook 中又进行了子类化,结果会怎么样呢?答案是:程序崩溃或弹出消息框"不支持的操作"。因为 SubclassWindow 函数调用前是要先 Attach 到一个HWND上去的。重复的 Attach 看来是不允许。要避免程序崩溃也有办法:
1、只为控件声明一个指针变量,动态的去获取CWnd类的实例,但是这样就达不到换肤的目的了。
2、还有一种方法,经过我试验,如果两个SubclassWindow的调用位于不同的模块,例如一个位于exe,一个位于dll(我是通过exe中调用dll中的函数显示该dll中的对话框来测试的),那么就不会出现问题。在还没有找到更好的方法之前,这也姑且算是一种解决方法吧。
但是如果使用窗口过程来子类化,就不存在重复subclass的问题了,只要小心处理,子类化无数次都没问题,但是对于复杂的自绘事件,在一个窗口过程中来写switch语句,好像很麻烦。
我尝试过自己写一个新的SubclassWindow函数来尝试借用CWnd的窗口过程,这样就可以按照MFC的方式来写消息响应函数了。只可惜,最终还是无功而返,因为SubclassWindow不是虚函数,而CWnd的窗口过程是作为一个protected成员存在的。所以没法在外部借用MFC的消息机制。所以,自己写代码处理 wParam 和 lParam 看来在所难免。
零一个是子类化系统对话框的问题,系统的对话框和自己的对话框表现的总不一样。目前我还没有对所有的系统对话框进行测试。在 MessageBox 弹出的对话框中遇到的问题可以见我这一片帖子:
http://community.csdn.net/Expert/To....asp?id=3103399
在文件对话框中我遇到一个问题,子类化过的 CStatic 的背景好像没有重绘一样,照理说应该由CStatic的父窗体负责背景的。
我在我的 CStaticNew 类中只重载了 OnPaint,里面只处理文字和图标的绘制,背景的绘制留给父窗体完成。这样的处理在 MessageBox 和自己的 AboutDlg 中都没有问题,Static 控件的背景就是父窗口的背景,可是在 CFileDlg 中背景就没有重绘了:
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 }
类名的识别问题,到现在为止,我所使用的子类化方法都是基于GetClassName这个函数获得窗口类名,再根据用spy++所得到的知识,如"#32770"表示对话框,"ToolbarWindow32"是工具栏,等等。但是窗口类名是可以在创建时任意指定的呀,而像CMainFrame的类名根本就不能够确定,例如记事本主窗体的类名是"Notepad",写字板主窗体的类名是"WordPadClass"。这样的话,子类化如何去进行呢。真想知道windows是怎么做的,skinmagic又是怎么做的。目前主要就是这三个问题了。希望大家能展开讨论,给出一个换肤的完善的解决方案。
我写了一个简化的CWnd类来解决重复子类化问题和简化窗口过程,不过它不支持对自己的重复子类化(即只能用于没有被子类化的或者被CWnd子类化的HWND)。
因为不想弄得和MessageMap那样复杂,所以功能也有限:须手工转化WPARAM和LPARAM、消息处理无法继承、不支持多线程。使用很简单:
CWndNew* pWnd = new CWndNew; pWnd->SubclassWindow(hWnd);
用完了,记得释放处理:
pWnd->UnsubclassWindow(); delete pWnd;
如果要进行功能扩充(继承),就改写那几个虚函数:
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 m_map; static LRESULT CALLBACK StaticWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); }; ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// map CWndNew::m_map; CWndNew::CWndNew() { m_hWnd = NULL; } CWndNew::~CWndNew() { ASSERT(m_hWnd == NULL); } bool CWndNew::SubclassWindow(HWND hWnd) { m_map[hWnd] = this; ASSERT(m_hWnd == NULL); m_hWnd = hWnd; //允许派生类在子类化之前做一些初始化. PresubclassWindow(); m_oldProc = (WNDPROC) GetWindowLong(hWnd, GWL_WNDPROC); ASSERT(m_oldProc != 0); SetWindowLong(hWnd, GWL_WNDPROC, (LONG) StaticWindowProc); return true; } void CWndNew::UnsubclassWindow() { SetWindowLong(m_hWnd, GWL_WNDPROC, (LONG)m_oldProc); PostunsubclassWindow(); m_map.erase(m_hWnd); m_hWnd = NULL; } LRESULT CALLBACK CWndNew::StaticWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWndNew* pWnd = m_map[hWnd]; ASSERT(pWnd != NULL); return pWnd->WindowProc(uMsg, wParam, lParam); } LRESULT CWndNew::PrevWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { return CallWindowProc(m_oldProc, m_hWnd, uMsg, wParam, lParam); } LRESULT CWndNew::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { return PrevWindowProc(uMsg, wParam, lParam); }
关于子类化及其撤销的顺序问题,当用自己的类或者过程子类化窗口时,需要处理好与MFC类子类化的顺序冲突。假设我们自己的类叫CWndNew,那么不管CWnd和CWndNew谁先子类化一个窗口,最终两者协同工作的结果应该是该窗口的窗口过程还原到未子类化之前的状态。首先,不要在HOOK过程中处理WM_NCDESTROY消息。理由:如果CWndNew比CWnd先子类化,由于HOOK的原因,你仍然会先处理WM_NCDESTROY,这时候如果你撤销子类化,那么CWnd类就得不到机会清理。而如果你不撤销子类化,CWnd没有能力把被子类化的窗口还原到最初状态。在HOOK过程中,不能通过调用SendMessage函数让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是否发生过变动,如果没有的话是最好的,赶紧撤销子类化,再把消息传递给之前窗口过程,然后功成身退,不问世事了。
如果发生过变动,那么也就是说有别的类在CWndNew子类化以后又进行了子类化,而现在又把WM_NCDESTROY传给了CWndNew。这好办,如法炮制,把消息继续往前传,如果WNDPROC又发生了改变,说明之前的某个窗口过程已经作了处理,就不需要再进行撤销子类化的操作了。这点MFC的CWnd类也是这样做的。
另外还有一个问题不解,就是Edit,ListBox,ListCtrl等等控件的内嵌的滚动条是怎么换肤的?网上一般介绍的方法是隐藏原来的,然后换上自己重新实现的。这种在Spy++中一看就能现出原形,可是Skin++ 换肤后的滚动条就不知道是怎么实现的了?我看过coolsb这个文章,他能实现给滚动条换肤的功能,但是对Combobox支持不好。