当前位置: 首页 > news >正文

2501wtl,皮肤技术

下载地址

设计目标

最重要的是使用方便,已有程序创建一个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.lryc.cn/news/519331.html

相关文章:

  • 【面试题】技术场景 6、Java 生产环境 bug 排查
  • word论文排版常见问题汇总
  • 传奇3仿韩服单机版安装教程+GM管理面板
  • 第26章 汇编语言--- 内核态与用户态
  • Spring bean的生命周期和扩展
  • 计算机网络 (33)传输控制协议TCP概述
  • Python3 JSON
  • Leetcode 698 Partition to K Equal Sum Subsets
  • 可靠的人形探测,未完待续(III)
  • Git文件夹提交错了,怎么撤销?
  • 小程序textarea组件键盘弹起会遮挡住输入框
  • Android车机DIY开发之学习篇(二)编译Kernel以正点原子为例
  • qt 窗口(window/widget)绘制/渲染顺序 QPainter QPaintDevice Qpainter渲染 失效 无效
  • Ubuntu下载时不显示无线网图标并显示Cable unplugged
  • 微信小程序实现人脸识别登录
  • atoi函数的概念和使用案例
  • Mysql--运维篇--日志管理(连接层,SQL层,存储引擎层,文件存储层)
  • poi处理多选框进行勾选操作下载word以及多word文件压缩
  • QT 键值对集合QMap
  • NetMQ里Push-Pull模式,消息隔一收一问题小记
  • 见微知著:Tripo 开创 3D 生成新时代
  • 消息队列与中间件:Java的秘密传输带
  • Bytebase 3.1.0 - 通过 Google / GitHub SSO 功能开放给专业版
  • EdgeOne安全专项实践:上传文件漏洞攻击详解与防范措施
  • k8s部署rocketmq踩坑笔记
  • Docker 通过创建Dockerfile 部署Jar包
  • shell脚本练习
  • 【计算机网络】lab4 Ipv4(IPV4的研究)
  • Python Json格式数据处理
  • 【声音场景分类--论文阅读】