Skin技术实现框架

前言

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

随着希望有自己特定Skin的软件越来越多,就出现了专门的Skin插件,这个比较有名的是WindowBlindsActiveSkin,我所知道和用过的就这俩,也不知道是不是最有名的,这些产品一般都是提供一个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的基本原理吧。要实现自己独特的界面,方法有很多啦,上次也说过,这里只讲一种,就是通过消息钩子改变已有控件的外观。这种方法的好处是可以不必修改程序已经完成的标准界面,只要把钩子函数挂上,所有的界面就都变了,使用起来非常方便。这里的基本原理就是下面这个调用:
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按钮的一个实现,看一下就知道了。
原理就这么多了,好像不是很复杂是吧,不过知道了原理和能写出实际工作的代码,还是有很大区别的。还有非常关键的设计和编码,这些,留等下次在说吧,今天就到这里,就到这里了
再贴个图吧

上次说了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 class名称来区分的,class名称相同的就认为是一种窗口。这种分类方法和我们看到的窗口种类可能有一些差异,例如,普通按钮,单选按钮和复选框的类名都是“Button”,对于这种情况,我们仍然用一个类来对应这些窗口,而在类内部区分对待这些不同的窗口。
这样,我们要为每种需要改变外观的控件窗口编写一个类,根据面向对象的思想,我们很自然的想到提取出它们的公共基类,这就是CWidgetHookBase,所有控件窗口处理类的公共基类,实际上是一个C++接口,因为它只包含一个纯虚函数,下面是它的定义:

///

/// Abstract base class for widget hook

class CWidgetHookBase

{

public:

     virtual void Install(HWND hWidget) = 0; //implemented in CWidgetHook

    

};

 


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

/

/// Abstract factory class for widget hooks. create hook instances

class CWidgetFactory

{

protected:

     static CWidgetFactory* m_pInstance;

public:

     // initialize the instance

     CWidgetFactory()

     {

         ATLASSERT(m_pInstance==NULL);

         m_pInstance = this;

     }

     // Get the singleton instance

     static CWidgetFactory* Instance()

     {

         return m_pInstance;

     }

     virtual CWidgetHookBase*    CreateWidget(LPCTSTR szClass) = 0;

};

 

CWidgetFactory* CWidgetFactory::m_pInstance = NULL;


CWidgetFactory使用了两个设计模式,Singleton模式和Abstract Factory模式,实际上还包括Factory Method模式。
首先看抽象工厂模式,我们希望控件工厂根据窗口class的名字创建出不同的控件窗口消息处理类。对于模拟Mac的系统,这些控件窗口消息处理类包括CMacButton, CMacComboBox, CMacTrackBar等;而对于模拟KDE的系统,则是CKDEButton, CKDEComboBox等。这样,我们就可以定义两个CWidgetFactory的继承类,分别叫CMacFactory和CKDEFactory,分别产生这两个系列的对象。CWidgetFactory::CreateWidget就是用来产生这些对象的方法,它是个纯虚函数,必须在继承类中实现。CreateWidget接受窗口class的名字为参数,返回CWidgetHookBase指针,也就是所有控件类的基类。这样,每个对象工厂负责产生一系列对象,但对于一个应用程序来说,应该只有一种风格,也就是说,只能有一个工厂的实例,单件模式来了
这里使用了简化版的Singleton模式,需要声明一个继承类的实例,然后通过CWidgetFactory的静态函数Instance得到这个唯一实例。这里没有控制不能生成第二个实例,不过这不是大问题。
现在来看Factory的一个实现,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) //dialog

              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) //menu

              return new CMacMenu;

         else if (lstrcmpi(szClass, "ComboBox") == 0) //combobox

              return new CMacCombo;

         else if (lstrcmpi(szClass, TRACKBAR_CLASS) == 0) //trackbar

              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继承类的实现也非常相似,只是替换一些类名而已。
今天把工厂类和钩子都讲完了,可能不是很清楚,那也只能这样了,其实看代码最清楚了。下次讲怎么实现控件吧,包括如何利用ATL/WTL的基础架构,那是我最喜欢的部分了

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

下面是CWidgetHook模板类的声明:

///

/// Base class for all widget hook

/// Parameters:

/// T

///      Derived class

/// TBase

///      Widget window wrapper, use WTL wrappers for convenient

template <class T, class TBase = CWindow>

class CWidgetHook : public CWidgetHookBase, public CWindowImpl<T, TBase>

这里用了多重继承,也是ATL里常用的,第一个父类是我们前面定义的CWidgetHookBase接口,我们需要这个接口来实现抽象工厂设计模式。第二个父类是CWindowImp,这是ATL定义的,是所有窗口类的高层父类(虽然还不是顶层)。
CWindowImp接收两个模板参数,同时也是CWidgetHook的模板参数。第一个模板参数是继承类,这也是ATL中常用的技巧,这个技巧使得我们可以在父类中知道继承类的类型,于是,把this指针强制转换成继承类的类型,就可以调用继承类的方法,这种方式实现了类似于虚函数的多态,却不需要付出虚函数的性能代价。这也算把C++模板运用到及至了吧,好像只有在ATL中有这种用法,STL中有很多其他的看起来近似古怪的技巧,这些其实正是C++的魅力所在。后来的语言,如java,C#虽然也在各方面都有新的进步,但比起C++,真正值得人兴奋的地方还真很难找出来。
扯远了,继续说我们的第二个模板参数。追踪ATL的代码,可以看到其实TBase参数最终是作为基类的,通过模板参数改变基类,这也是模板之于OO不同的地方。TBase有一个默认值CWindow,可以在这里传入其他类,但必须是CWindow的继承类,最有价值的参数当然是WTL窗口包装类,这样,在实现控件消息处理类的时候,就可以使用WTL包装类提供的函数,而不需要只依赖windows API了,确实可以带来不少帮助。
才讲完CWidgetHook的声明,真够罗嗦的。下面来看CWidgetHook的定义和实现,首先看几个函数声明:

     void Initialize() {}; //instance initialize

     void Finalize() {};    //instance finalize

     static void InitializeClass() {}; //class initialize

     static void FinalizeClass() {};  //class finalize


上面四个函数都包含了空的实现,在继承类中可以选择的重载它们。重载非虚函数,利用上面讲到的模板技巧实现类似虚函数的多态,这就是模板给我们带来的新概念。Initialize在每个实例生成时被调用,Finalize在实例销毁前被调用;静态函数InitializeClass在类的第一个实例生成时调用,FinalizeClass在类的最后一个实例销毁时调用。
为什么要有两个静态函数呢,因为一个类代表同一种窗口,这些窗口会使用同样的资源,例如checkbox,需要几张不同状态的图片,而这些图片对于每个checkbox来说都是一样的,如果为每个实例保存这样一份资源,就有点浪费内存了。对于一个Skin插件来说,效率还是很重要的,所以我们选择用静态变量保持这些图片,并且在第一个checkbox生成的时候载入这些资源,后续生成的其他checkbox可以重用这些资源,然后在最后一个checkbox消失的时候释放这些资源,这样,内存的使用量被优化到最小。
这两个静态函数的调用在构造函数和析构函数中,另外还有一个实例计数值m_lRef配合,这里就不多说了。下面还剩下OnFinalMessage和Install函数没讲。
OnFinalMessage比较简单,调用Fianlize并删除自己,由于是CWindowImp继承类,OnFinalMessage函数会在窗口销毁的时候被自动调用,这样,就保证了实例会自己释放,不会造成内存泄漏。另外值得一提的是:在调用Finalize等上面提到的四个函数时,都是先把this指针转换成继承类T的指针pT,然后在pT指针上调用。这样才能实现类似虚函数的多态,这种方式据说比使用虚函数的方式效率高,不过我这样写倒不是因为要刻意提高效率,只不过,That's the ATL way
最后来看Install函数,这是在CWidgetHookBase中定义的纯虚函数,在CWidgetHook模板类中,这个函数得以实现。Install函数的主要功能是调用SubclassWindow,从而获得对窗口消息的控制。另外,还实现了控制消息反射和初始化实例的操作。下面是它的代码:

     virtual void Install(HWND hWidget)

     {

         ATLASSERT(::IsWindow(hWidget));

 

         SubclassWindow(hWidget);

        

         //if it is a child window, install a reflector for its parent

         //so that the parent will reflect messages back to me

         if ( (::GetWindowLong(hWidget, GWL_STYLE) & WS_CHILD) == WS_CHILD)

         {

              HWND hWndParent = ::GetParent(hWidget);

              ATLASSERT(::IsWindow(hWndParent));

              //the WM_GETREFLECTOR get the installed reflector, if exists

              CReflectHook* pReflector =

                   (CReflectHook*)::SendMessage(hWndParent, WM_GETREFLECTOR, 0, 0);

              if (!pReflector)

                   new CReflectHook(hWndParent);

         }

 

         T* pT = static_cast<T*>(this);

         pT->Initialize();

     }

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

有过去一个周末了,昨天去看跳水比赛,现场的气氛还是不错的。可惜田亮没有来,否则,光看看观众席的fans也是一种享受啊
废话结束,进入正题,今天讲点以前没说清楚的内容。上次提到了消息反射,但没有深入,这个概念是这样的,许多窗口控件会向父窗口发送一些消息,比如WM_COMMAND消息和WM_NOTIFY消息,通知父窗口一些事件。因为是发给父窗口的,所以控件窗口的过程函数不能捕捉到这些消息。但是,经常我们希望在控件窗口对象中处理这些消息,这样使控件类更加独立。为了实现这个目的,MFC和ATL都提供了消息反射的机制,就是让父窗口在收到这类消息的时候,把它们再发还给控件窗口,这就是消息反射。我们要实现Skin插件,也需要在控件窗口类中收到这些消息,但是,我们不能依赖ATL或者MFC的反射,因为我们希望Skin插件可以被不同的宿主程序使用,而不是局限于ATL和MFC。其他程序可能没有消息反射机制,或者使用了不同的消息反射机制。所以,我们实现了自己的消息反射机制。
CReflectHook类就是用来完成消息反射的,其构造函数接受父窗口的句柄作为参数,然后调用SubclassWindow把对象实例链接到窗口上去,这和控件的实现类似。ProcessWindowMessage是个虚函数,它的定义可以追述到ATL窗口类的最底层,CReflectHook::ProcessWindowMessage实现反射功能,把收到的需要反射的消息发送回控件窗口。但也不是简单的原样返回,而是包装成另一个WM_REFLECT消息,以免和其他消息冲突。当反射消息发会给控件窗口时,控件窗口利用下面三个宏解开WM_REFLECT,并得到原来的消息:
LF_REFLECTED_NOTIFY_CODE_HANDLER
LF_REFLECTED_COMMAND_CODE_HANDLER
LF_REFLECTED_MESSAGE_HANDLER
这几个宏的含义不多说了,熟悉WTL的同学很容易找到答案。另外值得一提的时WM_GETREFLECTOR消息,这个自定义消息可以用来向父窗口查询于其相关联的CReflectHook实例,以避免重复安装反射钩子,因为CReflectHook::ProcessWindowMessage为这个消息返回了this指针。CWidgetHook::Install使用了WM_GETREFLECTOR消息。

值得讲的就这么多了吧,最后说一下怎么写CWidgetHook继承类吧(即控件类),这也是直接影响最终效果的。WTL定义了许多通用控件包装类,把这些类作为CWidgetHook的第二个模板参数可以是后续工作大大简化,当然,如果没有对应的包装类,也可以接收默认参数。
因为借助了ATL/WTL的基础架构,控件类的编写和写一个ATL窗口类非常相似,可以使用ATL/WTL消息映射宏,当然,这些宏需要手工输入,而不象MFC一样提供了Wizard。另外,控件类还可以选择的重载CWidgetHook定义的4个初始化和清理函数,参考Skin技术实现框架(五)。附带的例子中提供了一个Mac按钮类的实现,可以参考,这个按钮例子算是比较复杂的,因为Windows窗口类名为“Button”的窗口实际上包括普通按钮、单选钮和复选框,其他的许多控件实现起来比button容易,当然也有一些比较麻烦的。

关于这个skin框架,基本上应该都讲清楚了,不过肯定比较乱,也许以后有时间整理吧。这里使用了很多WTL的东西,可能熟悉的人并不多,而且最终没有得到微软官方支持,所以要说有多少价值,也说不上,只是喜欢的朋友可以玩一下。我现在也不怎么关注C++的东西了,没办法,新技术发展太快,不得不跟上啊。随便写一点以前的积累,和同学们共勉。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目名称:[精仿]360安全卫士-10.30更新(CSkin Demo) 界面库版本号:10.30 最新版本 下载内容: 精仿360安全卫士源码一份, 可引用至工具箱最新版CSkin.dll一份 实现功能: 1.发光标题。 2.直角边框和阴影。 3.360安全卫士主界面模仿。 4.多系统支持,不需要win8系统,即可实现win8风格的360。 5.自定义控件的美化使用。 界面库更新文档: CC2013-10.30 1.由于SkinForm名字太多人使用,界面库命名正式改为CSkin.dll,官网www.cskin.net。 2.SkinTabControl标签中添加菜单箭头,可点击展开菜单。 3.SkinTabControl添加标签关闭按钮。 4.修复部分中文乱码问题。 5.优化好友列表右键菜单。 6.将窗体自定义系统按钮改为集合模式,可添加无数个自定义系统按钮。自定义系统按钮事件中可以 e.参数 来判断。 7.增加360安全卫士-DEMO案例。 8.增加SkinAnimatorImg控件,用于支持位图动画的播放。如360的动态logo。 9.各种细节BUG优化。 CC2013-10.11 1.添加SkinTabControlEx,加入更加自定义的美化属性和动画效果。 2.添加SkinAnimator,通用动画控件。 3.添加Html编辑器控件 4.修复SkinButton图标和文本相对位置的BUG CC2013-9.26 1.优化好友列表CPU占用 2.好友列表加入好友登录平台属性:安卓 苹果 WEBQQ PC 3.优化标题绘制模式,新添标题绘制模式属性。 4.新添标题偏移度属性。 5.加入圆形进度条控件:ProgressIndicator。 CC2013-9.5.2 1.优化截图控件,截图工具栏加入新功能。 2.解决个人信息卡和天气窗体显示后不会消失的问题。 3.各种细节BUG优化。 CC2013-9.5.1 1.解决贴边左右隐藏的BUG。 2.解决窗体点击事件不能触发的问题。 3.优化SkinButton继承父容器背景色的代码。 4.解决SkinButton异常错误。 CC2013-9.3 1.好友列表右键菜单没反应问题。 2.新增美化控件SkinDatagridview。 3.密码软件盘回删不了文字问题。 4.双击窗体最大化,最大化后再双击恢复原大小,(win7)。 5.部分细节调优。 小编:下载不要分,DEMO教你如何熟练使用CSkin界面库美化自己的窗体。 友情链接: http://bbs.csdn.net/topics/390510544 (精仿QQ2013局域通讯) http://download.csdn.net/detail/lyx_520/5710799 (C#实现Win8窗体)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值