[转帖]Skin技术实现框架
Skin技术实现框架(一)
本来想把代码和E文的文章提交到codeproject再写,不知道为什么,这两天codeproject提交向导一直有问题,也罢,先开始写原理吧,反正恐怕也要写几天的
前言
嘿嘿,估计今天写不了多少,就叫前言吧,下次再写原理
说到skin技术,大家都不会陌生,最早接触这东西,可能是winamp吧,可以灵活的更换界面风格,非常的花哨。后来使用skin的软件就越来越多了,毕竟做一个漂亮的界面对软件还是很重要的。虽然Windows标准界面越做也是越花哨,但总不能满足人的胃口。有一个自己特殊的华丽界面总是值得夸耀的,看看MSN Explorer,Media Player, RealOne...。实现这种定制的外观方法很多,早期的Skin技术都需要程序本身做许多处理,基本就是贴一些图片在界面上,然后通过换图片获得不同的视觉效果,象winamp就是这样的。这种方式其实非常灵活,可以实现想要的任何效果,缺点是编码实现起来太麻烦了。
随着希望有自己特定Skin的软件越来越多,就出现了专门的Skin插件,这个比较有名的是WindowBlinds和ActiveSkin,我所知道和用过的就这俩,也不知道是不是最有名的,这些产品一般都是提供一个COM组件,需要Skin支持的程序创建这个COM组件,然后调用几个方法,就可以使自己的程序外观完全改变,甚至可以在运行时动态改变外观。这样的组件包使用起来非常的方便,不需要编程者对skin技术有任何的了解。缺点么,主要是要收费的,当然我们可以用破解版,我当初用的WindowBlinds组件就是我们公司一大拿花了一晚上弄出来的破解版。收费只是一方面,用人家的劳动成果是应该给钱的,真正的问题在于往往还不能满足要求。为了弄出100%符合自己要求的Skin,当然就只能自己写了。
从今天起我就来讲讲怎么写这样的Skin插件。2002年的时候写了一个这样的插件,当初的目的是在PC机上模拟Mac的效果。一开始用windowblinds组件,总是不能令人满意,终于说还是自己写吧,就开始写了。花了一个多月的时间吧大概,本来已经写的差不多了,后来由于商务上的原因,居然项目取消了,白干了。当然对于技术人员没有什么白干的东西,工资没少发,技术上提高了
前两天有人问我关于消息钩子的问题,忽然想起前年写的这个东西了(前年?!怎么过的这么快,老了)。看看当初的代码都还在,而且这东西的设计,当初颇让我自己得意的,现在看看,也确实是不错的。与其让它躺在硬盘上腐烂,还不如拿出来晾晾,说不定对同学们有帮助,没准有兴趣的人一起弄个OpenSource的项目继续写也是不错的
设计目标
前言差不多了,下面写点设计目标。这东西最重要的设计目标是使用方便,已有的程序创建一个COM对象,调一个方法就可以把界面外观全部改成Mac风格的。另外一个目标是要有扩展性,因为另外存在要在Windows98上模拟Windows XP界面效果的需求,以后还可以出现模拟其他系统的要求。所以,基本的设计是定义一个统一的接口,然后做不同的实现。每一种实现单独做在一个COM DLL中,调用方选择一个CLSID创建对象就行了。干脆把接口的定义先贴出来吧
interface ISkinX : IUnknown
{
[helpstring("Install Skin hook")] HRESULT InstallSkin([in] long lThreadID);
[helpstring("Uninstall Skin hook")] HRESULT UninstallSkin();
};
调用InstallSkin安装Skin,UninstallSkin卸掉Skin,lThreadID是线程ID,这个后面会解释。
今天就到这里吧,最后贴几个图片,看看效果先
Skin技术实现框架(二)
原理
上次基本上是些介绍,也就是废话,今天讲讲实现Skin的基本原理吧。要实现自己独特的界面,方法有很多啦,上次也说过,这里只讲一种,就是通过消息钩子改变已有控件的外观。这种方法的好处是可以不必修改程序已经完成的标准界面,只要把钩子函数挂上,所有的界面就都变了,使用起来非常方便。这里的基本原理就是下面这个调用:
SetWindowsHookEx(WH_CALLWNDPROC, HookProc, 0, lThreadID);
WH_CALLWNDPROC钩子可以截获所有线程ID为lThreadID的线程内的窗口消息,这样我们就有机会处理这些消息。
但是,光截获消息还不够,我们还必须知道这些消息是谁发出的,Button和EditBox发出的相同消息显然必须得到不同的处理。幸运的是,从消息的参数里,我们可以得到窗口句柄,而通过窗口句柄,我们可以得到窗口类。这里说的窗口类可不是C++的类,而是Windows系统中的窗口类名。例如,按钮的窗口类是“Button”,组合框的窗口类是“ComboBox”...这些在MSDN里面都可以找到的,另外,还有一些文档中不存在的窗口类名,比如对话框,有一个叫“#32770”的类名,而菜单,实际上也是一个窗口,其类名是“#32768”。有意思吧,有了这些信息,我们就可以区分不同窗口进行处理了。
至于处理些什么消息,显然最重要的是WM_PAINT消息。这样我们可以重载系统默认的绘图方式,而把控件窗口画成我们想要的样子。但是只处理WM_PAINT消息也是不够的,因为控件的样式不是一成不变的,看看WindowsXP的显示效果,以按钮为例,有很多种样式,普通样式、鼠标在按钮上的样式、鼠标按住按钮的样式、鼠标按住按钮又移动到按钮外的样式...... 为了实现动态的炫目的Skin效果,我们还需要截取一些其他消息,例如鼠标消息。下载的代码里有Mac按钮的一个实现,看一下就知道了。
原理就这么多了,好像不是很复杂是吧,不过知道了原理和能写出实际工作的代码,还是有很大区别的。还有非常关键的设计和编码,这些,留等下次在说吧,今天就到这里,就到这里了
再贴个图吧
[转贴]Skin技术实现框架(三)
上次说了hook和窗口类的原理,有了hook,我们可以截取所有消息,有了窗口类,我们可以识别窗口类型,不同类型的窗口给予不同处理。这样,我们要在钩子函数里面识别不同的窗口和不同的消息,有大量的分派工作,更要命的是,光区分窗口类还不够,同类型的不同窗口经常需要不同的处理,例如两个button窗口,大小不同,文字不同,是否有鼠标按下不同...... 这些状态有些是可以从button窗口读到的,例如大小和文字,而有些则读不到,比如是否有鼠标按下,对这些读不到的状态,我们必须自己记录,例如在收到WM_LBUTTONDOWN消息时记下按钮被按下了。也就是说,对于每个窗口,我们还需要记录一些与其相对应的数据,以便在收到WM_PAINT消息时做不同处理。把所有这些逻辑写在钩子函数里显然太麻烦了,即使能写出来也没法维护,我们需要一个好的设计。
根据面向对象的思想,我们需要为每种窗口类型写一个类,并为每个窗口生成一个对应类的实例,由这些实例来处理窗口消息,并记录必要的窗口状态数据。这样,处理窗口消息的任务就交给这些对象了,那么,怎么把消息传递给这些对象呢,用钩子函数转发是一种方案,不过我们这里采用了另一种:SubclassWindow,关于SubclassWindow的原理,就不多讲了,可以参看MSDN,其实就是替换一个窗口过程函数。ATL提供了现成的支持,用起来还是很方便的,替代的窗口过程函数不用全部自己写,而可以用消息映射宏生成。
现在我们用SubclassWindow的方式可以直接把我们的对象链接到窗口的消息链中,这好像有点和钩子函数的功能重复了,因为钩子函数本来就是用来截获消息的。现在SubclassWindow以后,窗口的消息已经可以被截获了,那还要钩子函数干什么呢。
答案是:钩子函数用来执行SubclassWindow操作。原因有两个,第一,我们要做的是一个skin plugin,我们希望使用者调用一个函数就可以改变整个界面风格,而不是为每个窗口调用SubclassWindow函数;第二,有些窗口的创建根本不是在代码里控制的,例如菜单窗口,除了使用钩子函数,我们甚至不能获得菜单窗口的句柄。所以,我们必须使用钩子函数,但在钩子函数中,我们只处理一个消息:WM_CREATE,在任何一个可识别窗口创建时,生成一个对于的对象实例,并用SubclassWindow挂接这个实例到目标窗口,剩下的事情让这个对象实例去完成。
粗略的设计已经有了,总结一下:
1、为每种可识别的窗口类编写类,实现必要的消息处理和状态保存;
2、用钩子函数截取WM_CREATE消息,并创建对应的类实例;
3、通过SubclassWindow操作把生成的类实例挂接到目标窗口,完成消息处理和状态保存的工作;
[窗口重绘技术--虚拟窗口实现法]
Windows程序是图形窗口,各窗口之间可以互相切换。然而,就在这窗口的切换之中,涉及到一个窗口重绘的问题:当A窗口被B窗口覆盖或者部分覆盖之后,移去B窗口时,A窗口中的内容会被B窗口擦去……
如下图:
2---------------------------
当B窗口移去的时候,如何实现A窗口的重绘呢?
这里有三种方法:
1)当窗口的内容是用某种计算方法创建的时候,可以的WM_PAINT消息处理之中再次计算重新绘出窗口。这种方法适用于计算量很小的情况,否则,计算时间太长,重绘效果仍然不理想~
2)预先保存窗口显示事件的记录,当窗口重绘的时候,再使这些事件发生。
3) 建立一个与显示窗口(屏幕上的应用程序窗口)对应的虚拟窗口(相当于它的镜子),每次向显示窗口中写内容时,同时也向虚拟窗口中写入同样的内容,两者始终保持同步。
当B窗口移去时,会产生WM_PAINT消息,要求程序重绘窗口。这时,就可以直接将虚拟窗口中的内容复制到显示窗口中去,从而实现窗口的重绘!!这也是大多数Windows应用程序重绘窗口最常用的技术。
如下图:
3--------------------------
实现
使用Windows API来实现该技术。分为以下3个过程:
1)创建虚拟窗口。(在WM_CREATE实现)
2)向虚拟窗口同步输出(在绘图的过程中实现)
3)重绘时,虚拟窗口拷贝到显示窗口(在WM_PAINT)中实现
附实现的源代码(如果将绿色的部分(有[color]标记的部分)注释掉,就会出现首贴中的那种不重绘的情况,因为DefWindowProc过程不会处理窗口重绘的,这写都需要程序员的劳动,所以程序员也不是吃干饭的,必须考虑周到,用最好的方法实现最好的功能-----------大家不妨试一试!)
代码
#include <windows.h>
#include "resource.h"
//Globals
//Proc
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
//*******************************************************************
// WinMain
//*******************************************************************
HINSTANCE pInstance;
int WINAPI WinMain (HINSTANCE hInstance,HINSTANCE hPrevInstance,
PSTR szCmdLine,int iCmdShow)
{
static char szAppName[]="AppName";
HWND hwnd;
MSG msg;
WNDCLASSEX wndclass;
wndclass.cbSize =sizeof(wndclass);
wndclass.style =CS_HREDRAW|CS_VREDRAW;
wndclass.lpfnWndProc =WndProc;
wndclass.cbClsExtra =0;
wndclass.cbWndExtra =0;
wndclass.hInstance=hInstance;
wndclass.hIcon =LoadIcon(NULL,IDI_APPLICATION);
wndclass.hCursor =LoadCursor(NULL,IDC_ARROW);
wndclass.hbrBackground =(HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName=MAKEINTRESOURCE(IDR_MENU1);
wndclass.lpszClassName =szAppName;
wndclass.hIconSm=LoadIcon(NULL,IDI_APPLICATION);
RegisterClassEx(&wndclass);
hwnd=CreateWindow(szAppName,
"窗口标题",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hwnd,iCmdShow);
UpdateWindow(hwnd);
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
//*******************************************************************
// 窗口过程
//*******************************************************************
LRESULT CALLBACK WndProc (HWND hwnd,UINT iMsg,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static HDC memDC;
static HBITMAP hBitmap;
HBRUSH hBrush;
static int maxX,maxY;
int response;
switch(iMsg)
{
case WM_CREATE:{
maxX=GetSystemMetrics(SM_CXSCREEN);
maxY=GetSystemMetrics(SM_CYSCREEN);
hdc = GetDC (hwnd); //得到当前的设备描述表
memDC=CreateCompatibleDC(hdc);//得到兼容的设备描述表
hBitmap=CreateCompatibleBitmap(hdc,maxX,maxY);//创建兼容位图
SelectObject(memDC,hBitmap);//将位图选入内存设备描述表
hBrush=(HBRUSH)GetStockObject(WHITE_BRUSH);//得到白色画刷
SelectObject(memDC,hBrush);//将画刷选入内存设备描述表
PatBlt(memDC,0,0,maxX,maxY,PATCOPY);//用当前画刷填充
ReleaseDC(hwnd,hdc);//释放当前设备描述表 break;
}
case WM_PAINT:
hdc=BeginPaint(hwnd,&ps);
BitBlt(hdc,ps.rcPaint.left,ps.rcPaint.top,ps.rcPaint.right-ps.rcPaint.left,ps.rcPaint.bottom-ps.rcPaint.top,
memDC,ps.rcPaint.left,ps.rcPaint.top,SRCCOPY);//对需要重画的区域进行重画
//将memDC总得内容复制到hdc中去 */ EndPaint(hwnd,&ps);
return 0;
case WM_COMMAND:
switch(LOWORD (wParam))
{
case IDM_DRAW:
hdc=GetDC(hwnd);
int x,y,width,height;
int red,green,blue;
width=GetSystemMetrics(SM_CXFULLSCREEN);
height=GetSystemMetrics(SM_CYFULLSCREEN);//得到客户区的高和宽
for(x=0;x<width;x++) //画出晚霞~
for(y=0;y<height;y++)
{
red=x*255/width;
green=y*255/height;
blue=(x*255/width+(height-y)*255/height)/2;
SetPixel(hdc,x,y,RGB(red,green,blue));//输出到物理窗口
SetPixel(memDC,x,y,RGB(red,green,blue));//输出到虚拟窗口 }
ReleaseDC(hwnd,hdc);
break;
case IDM_CLEAR:
hdc=GetDC(hwnd);
PatBlt(hdc,0,0,maxX,maxY,PATCOPY);//清除物理屏幕
PatBlt(memDC,0,0,maxX,maxY,PATCOPY);//清除虚拟屏幕
ReleaseDC(hwnd,hdc);
break;
case ID_EXIT:
response=MessageBox(hwnd,"真的要退出吗?","退出",MB_YESNO);
if(response==IDYES) PostQuitMessage(0);
break;
}
return 0;
break;
case WM_DESTROY:
DeleteDC(memDC);//释放内存设备描述表
DeleteObject(hBitmap); PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd,iMsg,wParam,lParam);
}
WM_PIANT消息与窗口重画
Windows给窗口发送消息,这意味著Windows调用窗口消息处理程序。但是,Windows程序也有一个消息循环,它调用GetMessage从消息队列中取出消息,并且调用DispatchMessage将消息发送给窗口消息处理程序。
那么,Windows程序是依次等待消息(类似于普通程序中相同的键盘输入),然后将消息送到某地方去的吗?或者,它是直接从程序外面接收消息的吗?实际上,两种情况都存在。
消息能够被分为「队列化的」和「非队列化的」。队列化的消息是由Windows放入程序消息队列中的。在程序的消息循环中,重新传回并分配给窗口消息处理程序。非队列化的消息在Windows调用窗口时直接送给窗口消息处理程序。也就是说,队列化的消息被「发送」给消息队列,而非队列化的消息则「发送」给窗口消息处理程序。任何情况下,窗口消息处理程序都将获得窗口所有的消息--包括队列化的和非队列化的。窗口消息处理程序是窗口的「消息中心」。
队列化消息基本上是使用者输入的结果,以击键(如WM_KEYDOWN和WM_KEYUP消息)、击键产生的字(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)和鼠标按钮(WM_LBUTTONDOWN)的形式给出。队列化消息还包含时钟消息(WM_TIMER)、更新消息(WM_PAINT)和退出消息(WM_QUIT)。
非队列化消息则是其他消息。在许多情况下,非队列化消息来自调用特定的Windows函数。例如,当WinMain调用CreateWindow时,Windows将建立窗口并在处理中给窗口消息处理程序发送一个WM_CREATE消息。当WinMain调用ShowWindow时,Windows将给窗口消息处理程序发送WM_SIZE和WM_SHOWWINDOW消息。当WinMain调用UpdateWindow时,Windows将给窗口消息处理程序发送WM_PAINT消息。键盘或鼠标输入时发出的队列化消息信号,也能在非队列化消息中出现。例如,用键盘或鼠标选择了一个菜单项时,键盘或鼠标消息就是队列化的,而说明菜单项已选中的WM_COMMAND消息则可能就是非队列化的。
向WINDOWS发送WM_PAINT消息请求重画的几个方法
1 用PostMessage(),SendMessage()函数发送WM_PAINT消息
使用以上两函数发送WM_PAINT消息,能将WM_PAINT消息发送到WINDOWS程序消息队列中,当WINDOWS将WM_PAINT消息发送给具体的消息处理函数时,如果窗口的无效区域为空则WINDOWS将不理睬该消息。若存在无效区域,则调用窗口处理函数处理。
2 Invalidate(),Invalidaterect(), InvalidateRgn();
以上函数将窗口的特定区域标定为无效,当WINDOWS检测到窗口中存在无效区域时将向消息队列发送WM_PAINT 消息。
3 UpdateWindow()
该函数调用后WINDOWS将向窗口发送一个非队列化的WM_PAINT消息,它不经过消息循环而直接发送给了窗口消息处理函数。如果窗口无效区域不存在,WINDOWS将不理睬该消
评论