MFC HOOK实现换肤
刚接触MFC基于对话框程序换肤,一开始的想法很简单,写一个对话框基类,然后该程序所有的对话框都从这个基类中派生,这样,我只要修改基类中对话框的样式进行修改就OK了,但是问题是WINDOWS通用对话框(文件选择对话框、颜色框、字体框、消息框)的样式无法改变,无奈之下想了一个办法,把这些通用对话框嵌入到我的对话框中,这样也许就OK了,倒腾了一段时间,终于把通用对话框通过VIEW嵌入到自己的对话框了,这个是解决了,但是系统消息框怎么解决了,无奈,继续百度谷歌,发现了一个关键字HOOK,原来通过HOOK可以实现换肤的功能,继续百度谷歌,但是没有关于HOOK换肤详解的文章,幸好百度谷歌到了一份源码,打开时那个惊讶啊,里面的类老多了,函数就更是不计其数,而且采用了类继承特性,绕来绕去!无奈,只好硬着头皮看和实践!下面进入该文主题:
这里我谈谈对话框窗体的换肤,先说下原理吧!
应用程序中安装WH_CALLWNDPROC HOOK(SetWindowsHookEx),去捕获每个窗体的创建,然后用自己写的窗体过程函数替换原有窗体的窗体过程函数,这样,我们就可以自己制定所有窗体的样式。
下面结合代码说说具体的流程:
1.在WH_CALLWNDPROC的处理过程中监听是否有对话框创建
TCHAR szClassName[MAX_PATH];
int Count = GetClassName(hWnd, szClassName,sizeof(szClassName) / sizeof(szClassName[0]));
CStringW strClassName = szClassName;
if(strClassName.CompareNoCase(_T("#32770")) == 0) // "#32770" 代表对话框窗体
{
....
}
2.监听到有对话框创建之后,就得替换该对话框窗体的窗口过程函数了,这里创建一个基类CSkinWnd,该类用作所有窗口的基类(按钮,文本框,对话框...),我们这里只讨论对话框,所以从这个基类中派生一个类CSkinDlg,CSkinWnd类提供了窗体过程函数的替换处理函数,以及换肤窗体的登记(CMap,key:窗体句柄,Value:实现该窗体换肤功能的类(例如CSkinDlg)),CSkinDlg这个类主要封装了自己创建的窗体过程的静态函数,以及对窗体创建过程中一些消息的处理函数。
3.下面谈谈CSkinDlg这个类中的窗体过程处理函数:
先说一下自己制定对话框哪些部分的样式:主要是针对非客户区重绘:标题栏图标,标题栏文本,标题栏按钮,标题栏背景,以及边框。既然针对的是非客户区的的重绘,那就该在非客户区的消息的中去处理,这里用到了如下消息:WM_NCPAINT、WM_NCACTIVATE、WM_NCLBUTTONDOWN、WM_NCLBUTTONUP、WM_NCMOUSEMOVE、WM_NCCALCSIZE,从单词缩写(NC意思是No Client ,呵呵,这是我自己的乱猜的)中就可以看出这些消息的意思了。
WM_NCPAINT, 在这个消息中处理非客户区的重绘,对于非客户区的最大化、最小化、关闭按钮的绘制,可能不同的对话框不一样,因为有的对话框没有最大化和最小化按钮,所以绘制按钮之前先判断该对话框是否支持最大化和最小化(至于关闭,对话框应都支持),这就需要捕捉WM_CREATE的消息,在这个消息获取窗体创建的样式了,代码如下:
case WM_CREATE:
{
DWORD dwStyle = GetWindowLongPtr(hWnd, GWL_STYLE);
if((dwStyle & WS_MAXIMIZEBOX) == WS_MAXIMIZEBOX) // 最大化按钮
pSkinDlg->m_bMaxBtn = TRUE;
... ...
提到WM_CREATE这个消息,在这个消息中还有一个处理,那就是修改窗体风格,去掉窗体再带的标题栏(至于为什么去掉自带的标题栏,我的理解是更好的控制标题栏的制定,因为XP和WIN7的标题栏效果不一样,为了稳定,还是去掉自带的标题栏,自己绘制)
dwStyle &= ~WS_CAPTION; // 去除默认的标题栏
SetWindowLong(hWnd, GWL_STYLE, dwStyle);
CRect rcWnd;
GetWindowRect(hWnd, rcWnd);
HRGN hRgn = CreateRectRgn(0, 0,rcWnd.Width(), rcWnd.Height());
SetWindowRgn(hWnd, hRgn, TRUE); // 触发 WM_NCCLACSIZE消息
分析代码,去了标题栏,为什么还要调用SetWindowRgn重新设定窗体区域了,经过调试,发现执行SetWindowRgn触发了WM_NCCALCSIZE消息,MSDN一下,发现这个消息是当需要重新计算窗体大小时触发,至于本程序中该消息的作用是重新计算客户区的RECT,以腾出位置来绘制标题栏和边框。
这个消息的参数值得推敲,这里,我针对本程序简单的说说它的参数,主要是这两个参数:
WPARAM wParam, // 当该参数为TRUE, 需要指定窗体的哪些部分是有效的(我的理解是,哪些部分是客户区)
LPARAM lParam // NCCALCSIZE_PARAMS结构体,这个结构体的RECT rgrc[3]需要处理
[ rgrc该数组,rgrc[0]表示窗体移动或改变大小之后的新坐标,rgrc[1]表示窗体移动或大小改变之前的坐标,rgrc[2]表示窗体移动或大小改变之前客户区的坐标 ]
WM_NCCALCSIZE这个消息的处理函数的返回值也有特殊意义,这里我们用的是WVR_VALIDRECTS,返回该值意味着,rgrc[1]和rgrc[2]成员分别包含有效的目标区域矩形和源区域矩形,因此,重新计算的客户区的大小放到rgrc[1]中,看一下该消息的处理代码
BOOL CSkinDlg::OnNcCalcSize(BOOL bCalcValidRects,NCCALCSIZE_PARAMS * lpncsp)
{
if (bCalcValidRects)
{
if(m_bTitle) // 调整出标题栏的位置和边框的位置
{
lpncsp->rgrc[0].top += GetTitleHeight();
lpncsp->rgrc[0].left += GetBorderWidth();
lpncsp->rgrc[0].right -= GetBorderWidth();
lpncsp->rgrc[0].bottom -= GetBorderWidth();
}
lpncsp->rgrc[1] = lpncsp->rgrc[0];
return TRUE;
}
return FALSE;
}
说到这里,我们回到WM_NCPAINT,在WM_NCCALCSIZE消息中调整出了标题栏和边框的位置,下面就可以绘制了,绘制的时候建议,创建一个Bitmap,这该位图中绘制标题栏的所有元素,然后在BitBlt到标题栏区域,这样的好处是防止非客户区刷新,图标,标题,按钮不停的闪动(XP中察觉不出,但在WIN7中就很明显),至于边框和标题栏的大小,没有使用系统的,而是自己指定的,根据实际情况自己调整。
再看看WM_NCHITTEST这个消息,这个消息主要是判断出鼠标点击了标题栏的哪个地方,它的返回值nHitTest很有用,WM_NCMOUSEMOVE、WM_NCMOUSEDOWN、WM_NCMOUSEUP这些消息可以根据nHitTest判断出鼠标点击了那个按钮,就可以对响应的按钮进行效果处理和功能实现,这里需要说明的是由于标题栏上是我们自己绘制的,重新绘制的最大化,最小化,关闭按钮的位置未必和系统默认的位置一致,所以,我们捕捉了WM_NCHITTEST消息,自己判断,代码如下:
CRect rcWnd;
GetWindowRect(hWnd, &rcWnd);
// 判断当前鼠标落在了那个按钮上
if(GetTWndRect(rcWnd, TBTNCLS).PtInRect(point)) // 关闭按钮
return HTCLOSE;
if(GetTWndRect(rcWnd, TBTNMIN).PtInRect(point) && m_bMinBtn)
return HTMINBUTTON;
if(GetTWndRect(rcWnd, TBTNMAX).PtInRect(point) && m_bMaxBtn)
return HTMAXBUTTON;
if(GetTWndRect(rcWnd, TITLE).PtInRect(point) && m_bTitle) //鼠标是否在标题栏上
return HTCAPTION;
return nHitTest;
注:GetTWndRect(CRect &rcWnd, int nWhich)是获取标题栏上控件的RECT, TBTNCLS是标识,表示关闭按钮
消息说的差不多了,哦,对了,还差一个WM_SIZE消息,还记得,我们在WM_CREATE消息中通过SetWindowRgn设定了窗体的有效区域了吗,如果我们不再WM_SIZE中做处理,那么窗体的大小变化将出现问题,那在WM_SIZE中应该怎样处理了,很简单,就是获取当前窗体区域,在SetWindowRgn一遍就OK了,因为在WM_SIZE之前WM_NCCALCSIZE已经重新计算了窗体的大小。
好了,就说到这里,程序的大致流程已经say结束了,我再传一份源码,供需要的网友参考。
我要注明一下,我参考源码是网上下载的,是贾巍云大牛编写的,根据他的源码进行分析,并自己根据理解写了一份代码,我的代码主要实现了对话框的换肤功能,大牛的源码则比较全,各个控件基本都设计到了,就是注释有些少,看的费力啊!!!!!!!!!但还是要谢谢贾巍云前辈,他的源码给了我很大帮助!