关于换肤、子类化,征求解决方案

(转我自己在论坛上发的老帖2004-06-25 14:52:52 在 VC/MFC / 界面 提问)

对于应用程序的换肤及子类化。下面是我尝试过一些方法,
以在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,用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的风格。

第六种:使用第三方的库skinmagic等实现换肤。
第七种:用第三方应用程序给整个windows换肤。

以上七种方式各有优缺点。我在使用过程中也遇到不少问题,现在一一道来,希望和大家共同解决问题。
先排除几种不准备深入探讨的方式:
第五种manifest方式 最快速和简洁,但是功能有限,存在严重的平台限制,
不过好处在于应用程序可以和windows共一种风格。
第六种skinmagic方式 使用起来很简单,定制性也不错,如果他肯开源,并且能在贴图之外提供超强的自绘方式,
那么可以称得上是换肤功能的终结者,否则,对开发者来说,意义并不大
第七种, 属于自娱性质的,也就不多说了。
第一种, 直接使用现成的类,属于很常见的一种用法,一般来说使用上不会出什么问题,缺点就不说了,
如果这种方式让我满意,我就不必发这篇帖子了。

下面看看第二三四种:
第二种是用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/TopicView1.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又是怎么做的。

目前主要就是这三个问题了。
希望大家能展开讨论,给出一个换肤的完善的解决方案

你可能感兴趣的:(windows,XP,Microsoft,asp.net,mfc)