2501wtl,皮肤技术

ops/2025/1/12 21:43:01/

下载地址

设计目标

最重要的是使用方便,已有程序创建一个COM对象,调一个方法就可把界面外观全部改成Mac风格的.
另外一个目标要有扩展性.

所以,基本设计定义一个统一的接口,然后用不同实现.每一个实现单独放在一个COMDLL中,调用者选择一个类标创建对象就行了.
接口的定义:

 interface ISkinX : IUnknown{[helpstring("Install Skin hook")] HRESULT InstallSkin([in] long lThreadID);[helpstring("Uninstall Skin hook")] HRESULT UninstallSkin();};

调用InstallSkin安装皮肤,UninstallSkin卸掉皮肤,lThreadID是线程ID.

原理

就是通过消息勾挂改变已有控件外观.

好处是可不必修改程序完成的标准界面,只要勾挂上勾挂函数,所有的界面都变了,使用起来非常方便.

原理就是下面的调用:

SetWindowsHookEx(WH_CALLWNDPROC, HookProc, 0, lThreadID);

WH_CALLWNDPROC勾挂,可拦截所有线程IDlThreadID线程内的窗口消息,这样就可处理这些消息.

但是,光拦截消息还不够,还必须知道是谁发出的这些消息.
幸好,从消息的参数里,可得到窗口句柄,而通过窗口句柄,可得到窗口类.这里说的窗口类窗口系统中的窗口类名.

如,按钮的窗口类"按钮",组合框的窗口类"ComboBox"...这些在MSDN里面都可找到的,另外,还有一些文档中没有的窗口类名,比如对话框,有一个叫"#32770"的类名,而菜单,实际上也是一个窗口,其类名"#32768".

有了这些信息,就可区分不同窗口处理了.
处理消息,显然最重要的是WM_PAINT消息.这样可重载系统默认的绘画方式,把控件窗口画成想要的样子.

但是只处理WM_PAINT消息也是不够的,因为控件的风格不是一成不变的,看看窗口xp的显示效果,以按钮为例,有很多种风格,普通风格,鼠标在按钮上的风格,鼠标按住按钮的风格,鼠标按住按钮又移动到按钮外的风格

为了实现动态炫目皮肤效果,还需要截取一些其他消息,如鼠标消息.下载代码里有Mac按钮的一个实现,看一下就知道了.

有了勾挂,就可截取所有消息,有了窗口类,就可识别窗口类型,对不同类型的窗口给予不同处理.

这样,要在勾挂函数里面识别不同窗口和不同消息,有大量的分发工作,更重要的是,光区分窗口类还不够,同类型不同窗口经常需要不同处理,如两个按钮窗口,大小不同,文字不同,是否有鼠标按下不同.

这些状态有些是可从按钮窗口读到的,如大小和文字,而有些则读不到,比如是否按下鼠标,对这些读不到的状态,必须自己记录,如在收到WM_LBUTTONDOWN消息时记下按下了按钮.

也即,对每个窗口,还需要记录一些与其相应的数据,这样在收到WM_PAINT消息时做不同处理.把所有这些逻辑写在勾挂函数显然太麻烦了,即使写出来也没法维护,需要一个好的设计.

根据面向对象的思想,需要为每种窗口类型写一个类,并为每个窗口生成一个对应类的实例,由这些实例处理窗口消息,并记录必要的窗口状态数据.

这样,就由这些对象处理窗口消息,怎么给这些对象传递消息呢,可以转发勾挂函数,不过这里用了另一个:SubclassWindow,SubclassWindow的原理,就不多讲了,可参看MSDN,其实就是替换一个窗口过程函数.

ATL提供了现成的支持,用起来还是很方便的,替代的窗口过程函数不用全部自己写,而可用消息映射宏生成.

现在用SubclassWindow的方式,可直接把对象链接到窗口的消息链中,这好像有点和勾挂函数的功能重复了,因为勾挂函数本来就是用来拦截消息的.

现在SubclassWindow以后,已可拦截窗口消息,那还要勾挂函数干什么呢?

答案是:用勾挂函数来执行SubclassWindow操作.原因有两个,第一,要做的是一个皮肤插件,想用户调用一个函数就可改变整个界面风格,而不是为每个窗口调用SubclassWindow函数;

第二,创建有些窗口,根本不是在代码控制的,如菜单窗口,除了使用勾挂函数,甚至不能取得菜单窗口的句柄.

所以,必须使用勾挂函数,但在勾挂函数中,只处理一个消息:WM_CREATE,在创建一个可识别窗口时,生成一个对象实例,并用SubclassWindow勾挂该实例目标窗口,让该对象实例去完成剩下的事情.

总结一下:
1,为每种可识别的窗口类编写类,实现必要的消息处理及保存状态;
2,用勾挂函数拦截WM_CREATE消息,并创建对应的类实例;

3,通过SubclassWindow操作把生成的类实例勾挂到目标窗口,处理消息及保存状态;

PushButton绘画的问题,我冒昧对你的代码做了点修改.思路就是在子类化按钮后,将按钮风格改成自绘画,这样就不会和按钮自身的绘画干扰了,而且我估计只要处理WM_DRAWITEMWM_MEASUREITEM消息就可以了,不过我没有这么试过.

但是复选框RadioButton没有BS_OWNERDRAW风格,所以没法这么做.绘画干扰问题还没有解决.下面贴出我对CMacButton修改过的地方:
1,增加long m_iCtrlType;成员变量
2,

 void Initialize(){m_iCtrlType=-1;//..m_nState = STATE_NORMAL;m_bTracking = false;if(BASETYPE_BUTTON==GetBaseType())//..{::SetWindowLong(m_hWnd,GWL_STYLE,::GetWindowLong(m_hWnd,GWL_STYLE)|BS_OWNERDRAW);}}
3,long GetBaseType(){if(m_iCtrlType!=-1)return m_iCtrlType;//..long lStyle = GetWindowLong(GWL_STYLE);if ( (lStyle & BS_OWNERDRAW) == BS_OWNERDRAW )//`物主画`return m_iCtrlType=BASETYPE_OWNERDRAW;//..if ((lStyle & BS_GROUPBOX)==BS_GROUPBOX)//组框return m_iCtrlType=BASETYPE_GROUPBOX;else if ((lStyle & BS_CHECKBOX)==BS_CHECKBOX|| (lStyle & BS_AUTOCHECKBOX) == BS_AUTOCHECKBOX  )//复选框return m_iCtrlType=BASETYPE_CHECKBOX;else if ((lStyle & BS_AUTORADIOBUTTON)==BS_AUTORADIOBUTTON|| (lStyle & BS_RADIOBUTTON)==BS_RADIOBUTTON)//收音机return m_iCtrlType=BASETYPE_RADIOBUTTON;//普通按钮return m_iCtrlType=BASETYPE_BUTTON;}

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

组件勾挂的抽象基类
class CWidgetHookBase
{
public:virtual void Install(HWND hWidget) = 0; //在`CWidgetHook`中实现
};

该接口中唯一的安装函数,用来实现把对象链接到窗口的功能,也就是SubclassWindow,这会在继承类实现.

这里要讲的实际上是控件类工厂,也就是CWidgetFactory其继承类.下面是CWidgetFactory完整声明和实现:

///用来勾挂`组件`的抽象工厂类.创建`勾挂`实例
class CWidgetFactory
{
protected:static CWidgetFactory* m_pInstance;
public://初化实例CWidgetFactory(){ATLASSERT(m_pInstance==NULL);m_pInstance = this;}//取单例实例static CWidgetFactory* Instance(){return m_pInstance;}virtual CWidgetHookBase*    CreateWidget(LPCTSTR szClass) = 0;
};
CWidgetFactory* CWidgetFactory::m_pInstance = NULL;

CWidgetFactory使用了两个设计模式,单件模式和抽象工厂模式,实际上还包括抽象方法模式.

首先看抽象工厂模式,想让控件工厂根据窗口的名字创建不同控件窗口消息处理类.对仿真Mac的系统,这些控件窗口消息处理类包括CMacButton,CMacComboBox,CMacTrackBar等;

而对仿真KDE的系统,则是CKDEButton,CKDEComboBox等.这样,就可定义两个CWidgetFactory的继承类,分别叫CMacFactoryCKDEFactory,分别产生这两个族的对象.

CWidgetFactory::CreateWidget就是用来产生这些对象的方法,它是个必须在继承类中实现纯虚函数.CreateWidget接受窗口的名字为参数,返回所有控件类的基类CWidgetHookBase指针.

这样,每个对象工厂负责产生一族对象,但对一个应用来说,应该只有一个风格,也即,只有一个工厂的实例,单件模式来了.

这里使用简化版的单件模式,需要声明一个继承类的实例,然后通过CWidgetFactory实例静态函数得到该唯一实例.

这里没有控制不能生成第二个实例,不过问题不大.
现在来看工厂的一个实现,CMacFactory,完整的代码如下:

class CMacFactory : public CWidgetFactory
{
public:virtual CWidgetHookBase*    CreateWidget(LPCTSTR szClass){if (lstrcmpi(szClass, "Button") == 0 )return new CMacButton;else if (lstrcmpi(szClass, "#32770") == 0) //对话return new CMacDialog;else if (lstrcmpi(szClass, "ListBox") == 0)return new CMacListBox;else if (lstrcmpi(szClass, WC_TABCONTROL) == 0)return new CMacTabCtrl;else if (lstrcmpi(szClass, "#32768") == 0) //菜单return new CMacMenu;else if (lstrcmpi(szClass, "ComboBox") == 0) //组合框return new CMacCombo;else if (lstrcmpi(szClass, TRACKBAR_CLASS) == 0) //跟踪杆return new CMacTrackBar;return NULL;}
};

再看看消息勾挂的代码:

LRESULT CALLBACK CMacSkin::HookProc(int nCode, WPARAM wParam, LPARAM lParam)
{CWPSTRUCT cwps;if( nCode == HC_ACTION ){CopyMemory(&cwps, (LPVOID)lParam, sizeof(CWPSTRUCT));switch(cwps.message){case WM_CREATE:{CHAR szClass[MAX_PATH];GetClassName(cwps.hwnd, szClass, MAX_PATH);CWidgetHookBase*   pWidget=NULL;pWidget = CWidgetFactory::Instance()->CreateWidget(szClass);if (pWidget)pWidget->Install(cwps.hwnd);}break;}}return CallNextHookEx((HHOOK)CMacSkin::m_hHook, nCode, wParam, lParam);
}

不管对CMacFactory还是其他的工厂实现,及不同控件类族,勾挂函数的实现都是一样的.CWidgetFactory继承类的实现也类似,只是替换一些类名而已.

现在,有了控件类的接口:CWidgetHookBase,产生控件对象的工厂也有了,下面就该实现控件类了.在定义控件基类时,只定义了一个抽象安装函数,而没有其他代码,则,所有实现代码都交给各个控件类去实现吗?

不是,这些控件类还有许多公共代码可在基类实现,但是,选择不在CWidgetHookBase加入这些代码,而是再加入一个中间类:CWidgetHook.

不可把这两个基类合为一个类呢?其实,最初的设计是只有一个基类的,就是CWidgetHook,而又想在继承类中使用WTL包装窗口控件的类,这样,根据ATL/WTL的架构,CWidgetHook就必须是一个模板类,而模板类是不能作为基类指针的,因为模板是类型不定的,而抽象工厂模式要求一个基类指针,所以又取出CWidgetHookBase纯虚的接口类.

下面是CWidgetHook模板类的声明:

//`//`所有`组件`勾挂的基类/参数:`/T/`继承类`/TBase/Widget`窗口包装器,为了方便,使用`WTL`包装器template <class T, class TBase = CWindow>
class CWidgetHook : public CWidgetHookBase, public CWindowImpl<T, TBase>
//CRTP

这里用了多继承,它也是ATL里常用的,第一个父类前面定义的CWidgetHookBase接口,需要该接口实现抽象工厂设计模式.

第二个父类CWindowImp,这是ATL定义的,是所有窗口类的(虽然不是顶层)高层父类.

CWindowImp接收两个模板参数,同时也是CWidgetHook模板参数.第一个模板参数继承类,这也是ATL中常用的技巧,该技巧使得可在父类中知道继承类的类型,于是,把指针转换成继承类的类型,就可调用继承类的方法,这样实现了类似虚函数的多态,却不需要付出虚函数的性能代价.

追踪ATL的代码,可见其实TBase参数最终是作为基类的,通过模板参数改变基类,这也是模板OO不同的地方.

TBase有一个默认CWindow值,可在此传入其他类,但必须是CWindow继承类,最有价值的参数当然是WTL窗口包装类,这样,在实现控件消息处理类时,就可用WTL包装类提供的功能,而不需要只依赖窗口接口了,确实可带来不少帮助.

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

 void Initialize() {}; //初化实例void Finalize() {};    //实例`终止器`static void InitializeClass() {}; //初化类static void FinalizeClass() {};  //类的`终止器`

上面四个函数包含了空的实现,在继承类可选择重载它们.
生成每个实例时,调用初化,在析构实例前,调用终止器;在生成类的第一个实例时调用InitializeClass静态函数,在析构类的最后实例时调用FinalizeClass.

为什么要有两个静态函数呢,因为一个类代表同一个窗口,这些窗口会使用同样的资源,如复选框,需要几张不同状态的图片,而这些图片对每个复选框来说都是一样的,如果为每个实例保存这样一份资源,就有点浪费内存了.

对一个皮肤插件来说,效率还是很重要的,所以选择用静态变量保存这些图片,并在第一个生成复选框时,加载这些资源,后续生成的其他复选框重用这些资源,然后在最后复选框消失时释放这些资源,这样,最小化了内存的使用量.

构造器和析构器调用这两个静态函数,另外还有个m_lRef实例计数值,下面还剩下OnFinalMessage安装函数没讲.

OnFinalMessage比较简单,调用Finalize删除自己,因为是CWindowImp继承类,OnFinalMessage函数会在析构窗口时自动调用它,这样,就保证了会自动释放实例,不会造成泄漏内存.

最后看安装函数,这是在CWidgetHookBase中定义的纯虚函数,在CWidgetHook模板类中实现它.

安装函数的主要功能是调用SubclassWindow,从而控制窗口消息.另外,还控制反射消息和初化实例.
下面是它的代码:

 virtual void Install(HWND hWidget){ATLASSERT(::IsWindow(hWidget));SubclassWindow(hWidget);//如果它是子窗口,请为其父窗口安装一个`反射器`,这样`父窗口`会反射回消息给我if ( (::GetWindowLong(hWidget, GWL_STYLE) & WS_CHILD) == WS_CHILD){HWND hWndParent = ::GetParent(hWidget);ATLASSERT(::IsWindow(hWndParent));// `WM_GETREFLECTOR`取已安装的反射器(如果有)CReflectHook* pReflector =(CReflectHook*)::SendMessage(hWndParent, WM_GETREFLECTOR, 0, 0);if (!pReflector)new CReflectHook(hWndParent);}T* pT = static_cast<T*>(this);pT->Initialize();//继承类要实现.在此针对性的初化.}

安装函数是模板方法,它调用的初化方法则要在继承类中重载.则,初化方法是实例的初化函数,不可放在构造器里呢?

因为每个实例初化可能不太一样,要根据被勾挂的窗口的状态决定,所以,必须等到调用SubclassWindow后,才调用初化方法,在继承类初化实现中,可通过m_hWnd直接取得窗口句柄,调用APIWTL包装类方法检查窗口状态,并执行必要的实例初化代码.

MFCATL都提供了反射消息的机制,就是让父窗口收到这类消息时,把它们再返回给控件窗口,这就是反射消息.

要实现皮肤插件,也需要在控件窗口类收到这些消息,但是,不能依赖ATLMFC的反射,因为想可让不同主机程序使用皮肤插件,而不是局限于ATLMFC.

其他程序可能没有反射消息机制,或使用了不同反射消息机制.所以,实现了自己的反射消息机制.

CReflectHook类就是用来完成反射消息的,其构造器按参数接受父窗口的句柄,然后调用SubclassWindow对象实例链接到窗口上去,这和控件的实现类似.

ProcessWindowMessage是个虚函数,它的定义可追述到ATL窗口类的最底层,CReflectHook::ProcessWindowMessage实现反射功能,把收到需要反射的消息返回给控件窗口.

但也不是简单的原样返回,而是包装成另一个WM_REFLECT消息,以免和其他消息冲突.当反射消息发回给控件窗口时,控件窗口利用下面三个宏展开WM_REFLECT,并得到原消息:

LF_REFLECTED_NOTIFY_CODE_HANDLER
LF_REFLECTED_COMMAND_CODE_HANDLER
LF_REFLECTED_MESSAGE_HANDLER

另外注意WM_GETREFLECTOR消息,可用该自定义消息来向父窗口查询与其关联的CReflectHook实例,以避免重复安装反射勾挂,因为CReflectHook::ProcessWindowMessage该消息返回了指针.
CWidgetHook::Install使用了WM_GETREFLECTOR消息.

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

因为借助了ATL/WTL基本架构,编写控件类和写一个ATL窗口类类似,可用ATL/WTL消息映射宏,当然,不象MFC一样提供了向导,需要手动输入这些宏.

另外,控件类还可选择的重载CWidgetHook定义的4个初化和清理函数.附带示例提供了一个Mac按钮类的实现,可参考,该按钮示例算是比较复杂的,因为窗口窗口类名为"按钮"的窗口,实际上包括普通按钮,单选钮和复选框,其他的许多控件实现起来比按钮容易,当然也有一些比较麻烦的.


http://www.ppmy.cn/ops/149558.html

相关文章

鸿蒙中自定义slider实现字体大小变化

ui&#xff1a; import { display, mediaquery, router } from kit.ArkUI import CommonConstants from ./CommonConstants; import PreferencesUtil from ./PreferencesUtil; import StyleConstants from ./StyleConstants;// 字体大小 Entry Component struct FontSize {Sta…

c语言提供后端,提供页面显示跳转服务

后端代码: #define SERVER_IP_ADDR "0.0.0.0" // 服务器IP地址 #define SERVER_PORT 8080 // 服务器端口号 #define BACKLOG 10 #define BUF_SIZE 8192 #define OK 1 #define ERROR 0#include <stdio.h> #include <stdlib.h> #include <st…

Chapter 4.6:Coding the GPT model

4 Implementing a GPT model from Scratch To Generate Text 4.6 Coding the GPT model 本章从宏观视角介绍了 DummyGPTModel&#xff0c;使用占位符表示其构建模块&#xff0c;随后用真实的 TransformerBlock 和 LayerNorm 类替换占位符&#xff0c;组装出完整的 1.24 亿参数…

UML系列之Rational Rose笔记三:活动图(泳道图)

一、新建活动图&#xff08;泳道图&#xff09; 依旧在用例视图里面&#xff0c;新建一个activity diagram&#xff1b;新建好之后&#xff0c;就可以绘制活动图了&#xff1a; 正常每个活动需要一个开始&#xff0c;点击黑点&#xff0c;然后在图中某个位置安放&#xff0c;接…

网络原理(二)—— https

https 简介 https 也是一个应用层协议&#xff0c;他是由 http 和 SSL 组成的&#xff08;在 http 的基础上进行加密&#xff0c;把原本http 的明文传输变为了密文传输&#xff09;&#xff0c;简称为 https。 加密的方式大体分为两大类&#xff0c;分别是对称加密和非对称加…

将光源视角的深度贴图应用于摄像机视角的渲染

将光源视角的深度贴图应用于摄像机视角的渲染是阴影映射&#xff08;Shadow Mapping&#xff09;技术的核心步骤之一。这个过程涉及到将摄像机视角下的片段坐标转换到光源视角下&#xff0c;并使用深度贴图来判断这些片段是否处于阴影中。 1. 生成光源视角的深度贴图 首先&…

机器学习之决策树的分类树模型及决策树绘制

决策树分类模型 目录 决策树分类模型决策树概念组成部分&#xff1a;决策树的构建过程&#xff1a;优缺点决策树的优点&#xff1a;决策树的缺点&#xff1a; 熵概念算法数据理解 决策树的三种分法ID3&#xff08;Iterative Dichotomiser 3&#xff09;概念算法步骤 C4.5概念信…

【k8s】监控metrics-server

metrics-server介绍 Metrics Server是一个集群范围的资源使用情况的数据聚合器。作为一个应用部署在集群中。Metric server从每个节点上KubeletAPI收集指标&#xff0c;通过Kubernetes聚合器注册在Master APIServer中。为集群提供Node、Pods资源利用率指标。 就像Linux 系统一样…