首页 >> 大全

Skin技术实现框架

2023-08-04 大全 29 作者:考证青年

前言

嘿嘿,估计今天写不了多少,就叫前言吧,下次再写原理

说到skin技术,大家都不会陌生,最早接触这东西,可能是吧,可以灵活的更换界面风格,非常的花哨。后来使用skin的软件就越来越多了,毕竟做一个漂亮的界面对软件还是很重要的。虽然标准界面越做也是越花哨,但总不能满足人的胃口。有一个自己特殊的华丽界面总是值得夸耀的,看看MSN ,Media , ...。实现这种定制的外观方法很多,早期的Skin技术都需要程序本身做许多处理,基本就是贴一些图片在界面上,然后通过换图片获得不同的视觉效果,象就是这样的。这种方式其实非常灵活,可以实现想要的任何效果,缺点是编码实现起来太麻烦了。

随着希望有自己特定Skin的软件越来越多,就出现了专门的Skin插件,这个比较有名的是和,我所知道和用过的就这俩,也不知道是不是最有名的

,这些产品一般都是提供一个COM组件,需要Skin支持的程序创建这个COM组件,然后调用几个方法,就可以使自己的程序外观完全改变,甚至可以在运行时动态改变外观。这样的组件包使用起来非常的方便,不需要编程者对skin技术有任何的了解。缺点么,主要是要收费的,当然我们可以用破解版,我当初用的组件就是我们公司一大拿花了一晚上弄出来的破解版

。收费只是一方面,用人家的劳动成果是应该给钱的,真正的问题在于往往还不能满足要求。为了弄出100%符合自己要求的Skin,当然就只能自己写了。

从今天起我就来讲讲怎么写这样的Skin插件。2002年的时候写了一个这样的插件,当初的目的是在PC机上模拟Mac的效果。一开始用组件,总是不能令人满意,终于说还是自己写吧,就开始写了。花了一个多月的时间吧大概,本来已经写的差不多了,后来由于商务上的原因,居然项目取消了,白干了

。当然对于技术人员没有什么白干的东西,工资没少发,技术上提高了

前两天有人问我关于消息钩子的问题,忽然想起前年写的这个东西了(前年?!怎么过的这么快,老了

)。看看当初的代码都还在,而且这东西的设计,当初颇让我自己得意的,现在看看,也确实是不错的

。与其让它躺在硬盘上腐烂,还不如拿出来晾晾,说不定对同学们有帮助,没准有兴趣的人一起弄个的项目继续写也是不错的

设计目标

前言差不多了,下面写点设计目标。这东西最重要的设计目标是使用方便,已有的程序创建一个COM对象,调一个方法就可以把界面外观全部改成Mac风格的。另外一个目标是要有扩展性,因为另外存在要在上模拟 XP界面效果的需求,以后还可以出现模拟其他系统的要求。所以,基本的设计是定义一个统一的接口,然后做不同的实现。每一种实现单独做在一个COM DLL中,调用方选择一个CLSID创建对象就行了。干脆把接口的定义先贴出来吧

:

[(" Skin hook")] ([in] long );

[(" Skin hook")] ();

};

调用安装Skin,卸掉Skin,是线程ID,这个后面会解释。

今天就到这里吧,最后贴几个图片,看看效果先

原理

上次基本上是些介绍,也就是废话,今天讲讲实现Skin的基本原理吧。要实现自己独特的界面,方法有很多啦,上次也说过,这里只讲一种,就是通过消息钩子改变已有控件的外观。这种方法的好处是可以不必修改程序已经完成的标准界面,只要把钩子函数挂上,所有的界面就都变了,使用起来非常方便。这里的基本原理就是下面这个调用:

(, , 0, );

钩子可以截获所有线程ID为的线程内的窗口消息,这样我们就有机会处理这些消息。

但是,光截获消息还不够,我们还必须知道这些消息是谁发出的,和发出的相同消息显然必须得到不同的处理。幸运的是,从消息的参数里,我们可以得到窗口句柄,而通过窗口句柄,我们可以得到窗口类。这里说的窗口类可不是C++的类,而是系统中的窗口类名。例如,按钮的窗口类是“”,组合框的窗口类是“”...这些在MSDN里面都可以找到的,另外,还有一些文档中不存在的窗口类名,比如对话框,有一个叫“#32770”的类名,而菜单,实际上也是一个窗口,其类名是“#32768”。有意思吧,有了这些信息,我们就可以区分不同窗口进行处理了。

至于处理些什么消息,显然最重要的是消息。这样我们可以重载系统默认的绘图方式,而把控件窗口画成我们想要的样子。但是只处理消息也是不够的,因为控件的样式不是一成不变的,看看的显示效果,以按钮为例,有很多种样式,普通样式、鼠标在按钮上的样式、鼠标按住按钮的样式、鼠标按住按钮又移动到按钮外的样式...... 为了实现动态的炫目的Skin效果,我们还需要截取一些其他消息,例如鼠标消息。下载的代码里有Mac按钮的一个实现,看一下就知道了。

原理就这么多了,好像不是很复杂是吧

,不过知道了原理和能写出实际工作的代码,还是有很大区别的。还有非常关键的设计和编码,这些,留等下次在说吧,今天就到这里,就到这里了

再贴个图吧

上次说了hook和窗口类的原理,有了hook,我们可以截取所有消息,有了窗口类,我们可以识别窗口类型,不同类型的窗口给予不同处理。这样,我们要在钩子函数里面识别不同的窗口和不同的消息,有大量的分派工作,更要命的是,光区分窗口类还不够,同类型的不同窗口经常需要不同的处理,例如两个窗口,大小不同,文字不同,是否有鼠标按下不同...... 这些状态有些是可以从窗口读到的,例如大小和文字,而有些则读不到,比如是否有鼠标按下,对这些读不到的状态,我们必须自己记录,例如在收到消息时记下按钮被按下了。也就是说,对于每个窗口,我们还需要记录一些与其相对应的数据,以便在收到消息时做不同处理。把所有这些逻辑写在钩子函数里显然太麻烦了,即使能写出来也没法维护,我们需要一个好的设计。

根据面向对象的思想,我们需要为每种窗口类型写一个类,并为每个窗口生成一个对应类的实例,由这些实例来处理窗口消息,并记录必要的窗口状态数据。这样,处理窗口消息的任务就交给这些对象了,那么,怎么把消息传递给这些对象呢,用钩子函数转发是一种方案,不过我们这里采用了另一种:

,关于的原理,就不多讲了,可以参看MSDN,其实就是替换一个窗口过程函数。ATL提供了现成的支持,用起来还是很方便的,替代的窗口过程函数不用全部自己写,而可以用消息映射宏生成。

现在我们用的方式可以直接把我们的对象链接到窗口的消息链中,这好像有点和钩子函数的功能重复了,因为钩子函数本来就是用来截获消息的。现在以后,窗口的消息已经可以被截获了,那还要钩子函数干什么呢。

答案是:钩子函数用来执行操作

。原因有两个,第一,我们要做的是一个skin ,我们希望使用者调用一个函数就可以改变整个界面风格,而不是为每个窗口调用函数;第二,有些窗口的创建根本不是在代码里控制的,例如菜单窗口,除了使用钩子函数,我们甚至不能获得菜单窗口的句柄。所以,我们必须使用钩子函数,但在钩子函数中,我们只处理一个消息:,在任何一个可识别窗口创建时,生成一个对于的对象实例,并用挂接这个实例到目标窗口,剩下的事情让这个对象实例去完成。

粗略的设计已经有了,总结一下:

1、为每种可识别的窗口类编写类,实现必要的消息处理和状态保存;

2、用钩子函数截取消息,并创建对应的类实例;

3、通过操作把生成的类实例挂接到目标窗口,完成消息处理和状态保存的工作;

今天有点空了,继续写。上次我们已经得出了基本的设计,由此确定了每种窗口必须有一个类来与之对应,这里所说的窗口种类是按照窗口的 class名称来区分的,class名称相同的就认为是一种窗口。这种分类方法和我们看到的窗口种类可能有一些差异,例如,普通按钮,单选按钮和复选框的类名都是“”,对于这种情况,我们仍然用一个类来对应这些窗口,而在类内部区分对待这些不同的窗口。

这样,我们要为每种需要改变外观的控件窗口编写一个类,根据面向对象的思想,我们很自然的想到提取出它们的公共基类,这就是,所有控件窗口处理类的公共基类,实际上是一个C++接口,因为它只包含一个纯虚函数,下面是它的定义:

///

/// base class for hook

class

:

void (HWND ) = 0; // in

};

这个接口中唯一的函数用来实现把对象链接到窗口的功能,也就是,这会在继承类实现,后面我们再说怎么实现它。今天要讲的实际上是控件类工厂,也就是及其继承类。下面是的完整声明和实现:

_框架开发技术简介_主流的框架技术

/// class for hooks. hook

class

:

*;

:

// the

()

(==NULL);

= this;

// Get the

* ()

;

* ( ) = 0;

};

* :: = NULL;

使用了两个设计模式,模式和 模式,实际上还包括 模式。

首先看抽象工厂模式,我们希望控件工厂根据窗口class的名字创建出不同的控件窗口消息处理类。对于模拟Mac的系统,这些控件窗口消息处理类包括, , 等;而对于模拟KDE的系统,则是, 等。这样,我们就可以定义两个的继承类,分别叫和,分别产生这两个系列的对象。::就是用来产生这些对象的方法,它是个纯虚函数,必须在继承类中实现。接受窗口class的名字为参数,返回指针,也就是所有控件类的基类。这样,每个对象工厂负责产生一系列对象,但对于一个应用程序来说,应该只有一种风格,也就是说,只能有一个工厂的实例,单件模式来了

这里使用了简化版的模式,需要声明一个继承类的实例,然后通过的静态函数得到这个唯一实例。这里没有控制不能生成第二个实例,不过这不是大问题。

现在来看的一个实现,,完整的代码如下:

class :

:

* ( )

if ((, "") == 0 )

new ;

else if ((, "#32770") == 0) //

new ;

else if ((, "") == 0)

new ;

else if ((, ) == 0)

new ;

else if ((, "#32768") == 0) //menu

new ;

else if ((, "") == 0) //

new ;

else if ((, ) == 0) //

new ;

NULL;

};

再看看消息钩子的代码,都很简单吧

::(int nCode, , )

cwps;

if( nCode == )

(&cwps, (), ());

(cwps.)

case :

CHAR [];

(cwps.hwnd, , );

* =NULL;

= ::()->();

if ()

->(cwps.hwnd);

break;

((HHOOK)::, nCode, , );

不管对于还是其他的工厂实现,以及不同的控件类系列,钩子函数的实现都是一样的。继承类的实现也非常相似,只是替换一些类名而已。

今天把工厂类和钩子都讲完了,可能不是很清楚,那也只能这样了

,其实看代码最清楚了

。下次讲怎么实现控件吧,包括如何利用ATL/WTL的基础架构,那是我最喜欢的部分了

接上篇,控件类的接口有了:,产生控件对象的工厂也有了,下面就该实现控件类了。在上篇定义控件基类的时候,我们只定义了一个抽象函数,而没有任何其他代码,那么,所有的实现代码都交给各个控件类去实现吗?不是的,这些控件类还有许多公共代码可以在基类实现,但是,我们选择不在中加入这些代码,而是再加入一个中间类:。为什么不把这两个基类合成一个类呢?其实,最初的设计是只有一个基类的,就是,而我们又希望在继承类中使用WTL对控件的包装类,这样,根据ATL/WTL的架构,就必须是一个模板类,而模板类是不能作为基类指针的,因为模板是类型不定的,而我们的抽象工厂模式要求一个基类指针,所以我们又提取出这样的纯虚的接口类。结果就是如下图所示的结构:

下面是模板类的声明:

///

/// Base class for all hook

/// :

/// T

/// class

/// TBase

/// , use WTL for

class : ,

这里用了多重继承,也是ATL里常用的,第一个父类是我们前面定义的接口,我们需要这个接口来实现抽象工厂设计模式。第二个父类是,这是ATL定义的,是所有窗口类的高层父类(虽然还不是顶层

)。

接收两个模板参数,同时也是的模板参数。第一个模板参数是继承类,这也是ATL中常用的技巧,这个技巧使得我们可以在父类中知道继承类的类型,于是,把this指针强制转换成继承类的类型,就可以调用继承类的方法,这种方式实现了类似于虚函数的多态,却不需要付出虚函数的性能代价。这也算把C++模板运用到及至了吧,好像只有在ATL中有这种用法,STL中有很多其他的看起来近似古怪的技巧,这些其实正是C++的魅力所在。后来的语言,如java,C#虽然也在各方面都有新的进步,但比起C++,真正值得人兴奋的地方还真很难找出来。

扯远了,继续说我们的第二个模板参数

。追踪ATL的代码,可以看到其实TBase参数最终是作为基类的,通过模板参数改变基类,这也是模板之于OO不同的地方。TBase有一个默认值,可以在这里传入其他类,但必须是的继承类,最有价值的参数当然是WTL窗口包装类,这样,在实现控件消息处理类的时候,就可以使用WTL包装类提供的函数,而不需要只依赖 API了,确实可以带来不少帮助。

才讲完的声明,真够罗嗦的

。下面来看的定义和实现,首先看几个函数声明:

void () {}; //

void () {}; //

void () {}; //class

void () {}; //class

上面四个函数都包含了空的实现,在继承类中可以选择的重载它们。重载非虚函数,利用上面讲到的模板技巧实现类似虚函数的多态,这就是模板给我们带来的新概念

。在每个实例生成时被调用,在实例销毁前被调用;静态函数在类的第一个实例生成时调用,在类的最后一个实例销毁时调用。

为什么要有两个静态函数呢,因为一个类代表同一种窗口,这些窗口会使用同样的资源,例如,需要几张不同状态的图片,而这些图片对于每个来说都是一样的,如果为每个实例保存这样一份资源,就有点浪费内存了。对于一个Skin插件来说,效率还是很重要的,所以我们选择用静态变量保持这些图片,并且在第一个生成的时候载入这些资源,后续生成的其他可以重用这些资源,然后在最后一个消失的时候释放这些资源,这样,内存的使用量被优化到最小。

这两个静态函数的调用在构造函数和析构函数中,另外还有一个实例计数值配合,这里就不多说了。下面还剩下和函数没讲。

比较简单,调用并删除自己,由于是继承类,函数会在窗口销毁的时候被自动调用,这样,就保证了实例会自己释放,不会造成内存泄漏。另外值得一提的是:在调用等上面提到的四个函数时,都是先把this指针转换成继承类T的指针pT,然后在pT指针上调用。这样才能实现类似虚函数的多态,这种方式据说比使用虚函数的方式效率高,不过我这样写倒不是因为要刻意提高效率,只不过,That's the ATL way

最后来看函数,这是在中定义的纯虚函数,在模板类中,这个函数得以实现。函数的主要功能是调用,从而获得对窗口消息的控制。另外,还实现了控制消息反射和初始化实例的操作。下面是它的代码:

void (HWND )

(::());

();

//if it is a child , a for its

//so that the will back to me

if ( (::(, ) & ) == )

HWND = ::();

(::());

//the get the , if

* =

(*)::(, , 0, 0);

if (!)

new ();

T* pT = (this);

pT->();

中间的一大块代码都是关于消息反射的,留到下次在讲,今天写的够长的了,最后讲一下吧。其实这里可以看到一个设计模式:模板方法。函数是模板方法,它调用的方法则要在继承类中重载。那么,方法是实例的初始化函数,为什么不放在构造函数里呢?因为每个实例的初始化可能不太一样,要根据被挂钩的窗口的状态决定,所以,必须等到被调用之后,才调用方法,在继承类的实现中,可以通过直接获得窗口句柄,调用API或者WTL包装类方法检查窗口状态,并执行必要的实例初始化代码。

好了,今天差不多了,剩点尾巴下次讲

有过去一个周末了,昨天去看跳水比赛,现场的气氛还是不错的。可惜田亮没有来,否则,光看看观众席的fans也是一种享受啊

废话结束,进入正题,今天讲点以前没说清楚的内容。上次提到了消息反射,但没有深入,这个概念是这样的,许多窗口控件会向父窗口发送一些消息,比如消息和消息,通知父窗口一些事件。因为是发给父窗口的,所以控件窗口的过程函数不能捕捉到这些消息。但是,经常我们希望在控件窗口对象中处理这些消息,这样使控件类更加独立。为了实现这个目的,MFC和ATL都提供了消息反射的机制,就是让父窗口在收到这类消息的时候,把它们再发还给控件窗口,这就是消息反射。我们要实现Skin插件,也需要在控件窗口类中收到这些消息,但是,我们不能依赖ATL或者MFC的反射,因为我们希望Skin插件可以被不同的宿主程序使用,而不是局限于ATL和MFC。其他程序可能没有消息反射机制,或者使用了不同的消息反射机制。所以,我们实现了自己的消息反射机制。

类就是用来完成消息反射的,其构造函数接受父窗口的句柄作为参数,然后调用把对象实例链接到窗口上去,这和控件的实现类似。是个虚函数,它的定义可以追述到ATL窗口类的最底层,::实现反射功能,把收到的需要反射的消息发送回控件窗口。但也不是简单的原样返回,而是包装成另一个消息,以免和其他消息冲突。当反射消息发会给控件窗口时,控件窗口利用下面三个宏解开,并得到原来的消息:

这几个宏的含义不多说了,熟悉WTL的同学很容易找到答案。另外值得一提的时消息,这个自定义消息可以用来向父窗口查询于其相关联的实例,以避免重复安装反射钩子,因为::为这个消息返回了this指针。::使用了消息。

值得讲的就这么多了吧,最后说一下怎么写继承类吧(即控件类),这也是直接影响最终效果的。WTL定义了许多通用控件包装类,把这些类作为的第二个模板参数可以是后续工作大大简化,当然,如果没有对应的包装类,也可以接收默认参数。

因为借助了ATL/WTL的基础架构,控件类的编写和写一个ATL窗口类非常相似,可以使用ATL/WTL消息映射宏,当然,这些宏需要手工输入,而不象MFC一样提供了。另外,控件类还可以选择的重载定义的4个初始化和清理函数,参考Skin技术实现框架(五)。附带的例子中提供了一个Mac按钮类的实现,可以参考,这个按钮例子算是比较复杂的,因为窗口类名为“”的窗口实际上包括普通按钮、单选钮和复选框,其他的许多控件实现起来比容易,当然也有一些比较麻烦的。

关于这个skin框架,基本上应该都讲清楚了,不过肯定比较乱,也许以后有时间整理吧

。这里使用了很多WTL的东西,可能熟悉的人并不多,而且最终没有得到微软官方支持,所以要说有多少价值,也说不上,只是喜欢的朋友可以玩一下。我现在也不怎么关注C++的东西了,没办法,新技术发展太快,不得不跟上啊。随便写一点以前的积累,和同学们共勉。

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了