对于应用程序的换肤及子类化。下面是我尝试过一些方法,以在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();
关于子类化及其撤销的顺序问题,当用自己的类或者过程子类化窗口时,需要处理好与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支持不好。