1、通过 XML 创建界面-对象的动态创建以及属性的设置为了界面可配置化和换肤,需要界面元素可以根据 XML 动态创建和设置属性。在 BKLib 中,CBkObject 类就完成了这样的功能,主要负责类的创建和属性的设置。因为对象都是从 XML 动态创建的,动态的创建是一个类最基本的属性,所以其他类都从CBkObject 派生。来看看这个类的四个方法:BOOL IsClass(LPCSTR lpszName):判断是不是这个类的对象。纯虚方法,也就是从 CBkObject继承来的类都要实现这个方法才行,同时,这个方法在不同的类上面会有不同的表现。所以上层定义接口,下层提供实现。这个方法可以在运行时
2、检测类实例的实际类型。LPCSTR GetObjectClass():同上一个方法,用于获取类的名字。BOOL Load(TiXmlElement* pXmlElem):从 XML 中获取属性并将属性设置到对象中,在基类中仅仅是将一个 XML 元素的属性设置到对象中。当然,如果子类对象有更复杂的实现,比如一个对象对应的 XML 元素还有子节点,就需要 Load 其子节点,这些都可以在子类中通过覆盖父类方法来实现。HRESULT SetAttribute(CStringA strAttribName, CStringA strValue, BOOL bLoading):设置属性的方法,CBkOb
3、ject 是纯虚类,在 XML 中不会有对应的节点,自然也没有相应的属性,所以其实现仅仅放回了一个 E_FAIL,没有其他操作。接着我们就看到了在 bkobject.h 里面一大堆的宏定义:宏定义一般是为了简洁,而这些宏的用途多为子类使用。BKOBJ_DECLARE_CLASS_NAME:获取类名,判断是否是某个类的对象,还有CheckAndNew,用于动态创建以下的宏定义主要用于属性的设置和映射(XML 节点属性和对象属性的对应)BKWIN_DECLARE_ATTRIBUTES_BEGINBKWIN_DECLARE_ATTRIBUTES_ENDBKWIN_CHAIN_ATTRIBUTEBKW
4、IN_CUSTOM_ATTRIBUTEBKWIN_INT_ATTRIBUTEBKWIN_UINT_ATTRIBUTEBKWIN_DWORD_ATTRIBUTEBKWIN_STRING_ATTRIBUTEBKWIN_TSTRING_ATTRIBUTEBKWIN_HEX_ATTRIBUTEBKWIN_COLOR_ATTRIBUTEBKWIN_FONT_ATTRIBUTEBKWIN_ENUM_ATTRIBUTEBKWIN_ENUM_VALUEBKWIN_ENUM_ENDBKWIN_STYLE_ATTRIBUTEBKWIN_SKIN_ATTRIBUTE现在我们看一个例子,继承自 CBkObject 的
5、控件对象 CBkProgress 是如何完成从 XML 动态创建的。首先,在类的定义中包含宏 BKOBJ_DECLARE_CLASS_NAME(CBkProgress, “progress“),将宏展开如下:public: static CBkProgress* CheckAndNew(LPCSTR lpszName) if (strcmp(GetClassName(), lpszName) = 0) return new CBkProgress; else return NULL; /通过传入名称创建对应的类,在解析 XML 中按照节点名字创建对应类的实例static LPCSTR GetC
6、lassName() return “progress”; virtual LPCSTR GetObjectClass() return “progress”; /覆盖父类方法,返回类实例对应的 XML 节点名字virtual BOOL IsClass(LPCSTR lpszName) return strcmp(GetClassName(), lpszName) = 0; /覆盖父类方法,根据 XML 节点名字检查类实例是否是此节点另外,包含设置节点属性的宏,如下BKWIN_DECLARE_ATTRIBUTES_BEGIN()BKWIN_SKIN_ATTRIBUTE(“bgskin“, m_
7、pSkinBg, TRUE)BKWIN_DWORD_ATTRIBUTE(“min“, m_dwMinValue, FALSE)BKWIN_UINT_ATTRIBUTE(“showpercent“, m_bShowPercent, FALSE)BKWIN_DECLARE_ATTRIBUTES_END()将宏展开如下:public: virtual HRESULT SetAttribute( CStringA strAttribName, CStringA strValue, BOOL bLoading) /添加 SetAttribute 方法,在 Load 中循环调用设置属性 HRESULT h
8、Ret = _super:SetAttribute( /首先设置父类定义的属性strAttribName, strValue, bLoading ); if (SUCCEEDED(hRet) return hRet; if (“bgskin“ = strAttribName) /属性名称是 strAttribName m_pSkinBg = BkSkin:GetSkin(strValue); /属性的值是 strValuehRet = TRUE ? S_OK : S_FALSE; /是否全部重绘 else if (“min“ = strAttribName) m_dwMinValue = (D
9、WORD):StrToIntA(strValue); hRet = FALSE ? S_OK : S_FALSE; else if (“showpercent“ = strAttribName) m_bShowPercent = (UINT):StrToIntA(strValue); hRet = FALSE ? S_OK : S_FALSE; else return E_FAIL; return hRet; 现在我们来看看 CheckAndNew 和 SetAttribute 这两个方法是如何被调用的,看调用栈:1.在实窗口的 Create 方法中(DoModal 中调用 Create)调用
10、实窗口的 Load 和 SetXml 方法装载XML,在 SetXML 方法中查找 XML 中存在的“header“、“body“、“footer“节点调用各自的Load 方法并设置相应属性。这三个节点开始就是虚窗口了,调用其 Load 方法就进入了虚窗口的创建体系。2.在这三者的调用中会调用 CBkPanel:Load 方法virtual BOOL Load(TiXmlElement* pTiXmlElem)if (!CBkWindow:Load(pTiXmlElem) /调用父类load方法,主要进行自身属性设置return FALSE;return LoadChilds(pTiXmlEl
11、em-FirstChildElement(); /load子节点在 CBkPanel:LoadChilds 方法中,顺序调用每个子节点的创建方法并调用 Load 方法。3. 在 CBkPanel:LoadChilds 方法中进行了如下调用:CBkWindow *pNewChildWindow = _CreateBkWindowByName(pXmlChild-Value();在_CreateBkWindowByName 函数根据从 XML 解析出的节点名称调用pNewWindow = CBkDialog:CheckAndNew(lpszName); / CBkDialog 为需要动态创建类的名
12、称创建出对应节点对象。安装我们展开的 CheckAndNew 方法,如果节点名称相同,创建类对象并返回,否则返回空。至此,按照 XML 节点名称动态创建类对象的过程就完成了。统一的资源管理为了对界面资源进行管理,同时也为了方便替换,需要对使用的资源进行统一的管理。在 BKLib 中,资源管理主要由以下几种:BkBmpPool:HBITMAP 资源池,用于统一管理 HBITMAP,单例。BkFontPool:FONT 资源池,用于统一管理 FONT,单例。BkPngPool:PNG 图片资源池,用于统一管理 PNG 图片,单例,使用 GDI+。BkString:CString 资源池,用于统一管
13、理 String,单例,从 XML 中获取。BkColor:HLS / 加载字符串BkFontPool:SetDefaultFont(BkString:Get(IDS_APP_FONT), -12); / 设置字体BkSkin:LoadSkins(IDR_BK_SKIN_DEF); / 加载皮肤BkStyle:LoadStyles(IDR_BK_STYLE_DEF); / 加载风格/其中输入参数为 XML 文件的名称在程序关闭后应释放对应资源。使用时只要按照 ID 在资源池中查找对应资源即可,如if (“bgskin“ = strAttribName) /属性名称是 strAttribName
14、 m_pSkinBg = BkSkin:GetSkin(strValue); /属性的值是 strValuehRet = TRUE ? S_OK : S_FALSE; /是否全部重绘 根据 ID(strValue)就可在 BkSkin 中获取相应的 Skin。真实窗口的封装以及实窗口到虚窗口的转化所谓的 DUI 库, windowless 都是在一个窗口体系内虚拟出来虚窗口概念,并且通过接管界面布局、消息传递和分发以及界面绘制来完成更优秀的界面效果。不过这些的根基却又都要落到真实的窗口上,所以在界面库中需要对真实窗口进行封装,并将真实窗口纳入到我们创建的控件体系当中,并在这个过程中完成 win
15、dows 消息的传递,鼠标键盘事件的分发处理,实窗口上的虚窗口的布局排版和绘制操作。首先我们看看 BKLib 中的实窗口体系:BKLib 中并没有自己对 windows 窗口进行封装,而是使用了 WTL 的 CWindowImpl 类,这个类对于 windows 窗口,以及消息分发、窗口属性设置等进行了封装。对于 windows 消息分发中有一个重要的地方就是注册的窗口过程是按照窗口句柄进行处理,但是在我们的程序中是窗口类的一个成员方法,如何将窗口句柄和 C+类实例之间进行映射就成了一个重要的话题,WTL 中主要使用 trunk 技术,而 MFC 则使用链表进行查找,具体的细节大家可以在网上查
16、询,这里就不赘述了。这里我们看一下上面类图中其他几个类的作用:CBkDialog:无窗口控件,它是我们虚窗口体系的一部分CBkViewImpl:这个算是一个接口类,不过它提供了我们需要包含虚窗口体系的实窗口所必须具备的一些方法(这句话真绕 )CBkDialogViewImpl:它是一个实窗口了,因为他从封装了实窗口的 CWindowImpl 类继承而来,同时他也具有包含虚窗口所必须实现的方法(从 CBkViewImpl 继承而来),同时他还聚合了我们虚窗口体系的一部分,也就是 CBkDialog,但是我们实际使用的类并不是它。CBkDialogView:这个类在我们的程序中会创建一个真正的窗口
17、的,从 CBkDialogViewImpl 继承而来,具有了所有的能力,算是这里的中坚力量了。CBkDialogImpl:它也是一个实窗口,我们自定义窗口就是从此继承创建,同时它还聚合了上面的包含容器窗口(CBkDialogView),所以在 BKLib 中创建的窗口是有两层窗口的,上面的用于承载控件体系,下面的则是我们需要自定义的窗口。为什么要创建两个窗口我们之后再研究,这里就先不解释了。了解了上面的窗口体系后,我们就来看看在一个窗口创建过程中,各种消息、排版、绘制是如何从我们封装的实窗口想我们的虚窗口控件体系上转化的吧。创建过程:布局过程(WM_SIZE 消息处理)绘制过程(WM_PAIN
18、T 消息处理)在实窗口到虚窗口的转化过程中,主要在于 CBkDialogViewImpl 类中包含三个成员变量:m_bkHeader,m_bkBody,m_bkFooter,这三者的类型均是 CBkDialog,也就是我们控件体系的一部分。BkLib 中将一个实窗口划分为三个虚窗口:head、body、foot,对于界面的创建、布局、绘制等操作也由这三者传递到虚窗口体系中,并通过递归调用来对所有成员进行处理。如何创建一个模态对话框我们创建的窗口类从 CBkDialogImpl 继承而来,这个窗口就是一个模态的窗口,我们需要调用其 DoModal 方法,但是在界面库里面是如何实现的一个模态的对话
19、框呢。核心就在这个类中的_ModalMessageLoop 方法,我们来研究一下。void _ModalMessageLoop()BOOL bRet;MSG msg;for(;)if (:PeekMessage(if (m_bExitModalLoop | NULL = m_hWnd | !:IsWindow(m_hWnd)break;bRet = :GetMessage(if (bRet = -1)continue; / error, dont processelse if (!bRet)ATLTRACE(L“Why Receive WM_QUIT here?rn“);break; / WM
20、_QUIT, exit message loop:TranslateMessage(:DispatchMessage(在这个函数里面建立了一个消息处理循环。首先进行 WM_QUIT 消息的检测,并且采用 PM_NOREMOVE 的方式,如果获得了这个消息,那么退出消息循环,在之后的 DoModal 函数中模态窗口就被销毁了。然后检测 bExitModalLoop 标志,如果这个标志位为 TRUE 的话那么也退出处理。我们来看一下退出模态的函数:void EndDialog(UINT uRetCode) m_uRetCode = uRetCode;m_bExitModalLoop = TRUE;
21、/ DestroyWindow里面直接Send了WM_DESTROY,所以不会跑到DoModal的消息循环里,所以有了下面那行代码/ DestroyWindow();/ 这句非常重要,可以让DoModal消息循环再跑一次,防止卡死在GetMessage,泪奔:PostThreadMessage(:GetCurrentThreadId(), WM_NULL, 0, 0);可见就是设置 m_bExitModalLoop 这个标志位,使窗口退出。之后就是 GetMessage, TranslateMessage 和 DispatchMessage 了,就完成了常见的消息处理。当我们创建窗口并运行在
22、ModalMessageLoop 函数当中时就形成了模态窗口的效果,也就是接管了消息的分发和处理,其他的窗口就被模住了。无窗口模式-逻辑树结构的建立对于无窗口的模式,各种控件之间的关系需要我们自己来维护,因为我们要进行消息传递,排版布局,创建等操作时都需要沿着各种包含关系来逐个调用,这样对于窗口中的控件就形成了一个逻辑上的树形结构。我们看一下 CBkPanel 类,这个类的名字就表示它是一个有包含功能的类,可以有自己的子节点。这个类有一个成员 CAtlList m_lstWndChild,也就是每个 CBkPanel 类都有一个链表,在这个链表中存储了它的子节点控件。那么这个链表是何时被填充的
23、呢,我们看一下 LoadChilds 方法,就是在这个函数中进行的填充操作,从前面我们知道,这个函数是在用 xml 初始化界面是调用的。其中对于当前节点下的所有 xml 子节点进行下面的处理:CBkWindow *pNewChildWindow = _CreateBkWindowByName(pXmlChild-Value(); /创建控件if (!pNewChildWindow)continue; /创建失败,不进行后续设置pNewChildWindow-SetParent(m_hBkWnd); /设置父节点HBKWNDpNewChildWindow-SetContainer(m_hWndC
24、ontainer); /设置容器窗口的HWNDpNewChildWindow-Load(pXmlChild); /Load这个节点的子节点m_lstWndChild.AddTail(pNewChildWindow); /将自己加入到父节点的链表中在 CBkPanel 的 OnDestroy 函数中进行子节点的销毁操作POSITION pos = m_lstWndChild.GetHeadPosition();while (pos != NULL)CBkWindow *pBkWndChild = m_lstWndChild.GetNext(pos);pBkWndChild-BkSendMessa
25、ge(WM_DESTROY);delete pBkWndChild;m_lstWndChild.RemoveAll();之后进行消息传递,绘制,排版操作都可以使用这个树来进行处理,循环调用所有的节点。如在 OnPaint 函数中:POSITION pos = m_lstWndChild.GetHeadPosition();BOOL bDisabled = IsDisabled(), bIsChildDisabled = FALSE;while (pos != NULL)/此处略去pBkWndChild-BkSendMessage(WM_PAINT, (WPARAM)(HDC)dc);/此处略去
26、 可见也是循环向每个子节点发送 WM_PAINT 消息。同时,在界面库中,还使用了 BkWnds 来记录所有的控件,以便可以便捷地进行获取操作:CBkWindow 类有 HBKWND 类型的成员变量 m_hBkWnd,这是一个虚拟的窗口句柄。BkWnds 是一个控件池,创建的控件会在这里注册,并通过一个 HBKWND 类型变量标示,以后就可以通过控件的 m_hBkWnd 来获取这个控件。消息和事件的传递、分发、相应既然没有真实的窗口,那么也就不能使用 windows 的根据句柄来分发消息的方式了,我们需要创建自己的消息和事件分发体系。主要应该包括这几个方面:1. 接收真实窗口的消息,并将其转化
27、虚窗口体系中的处理2. 虚窗口体系内有一套独立的消息分发机制,可以讲系统消息发至该接收的控件3. 虚窗口需要能够抛出事件的能力,因为虚窗口之间也需要有相互的通知和相应的能力,那么就需要虚窗口可以对于指定窗口抛出事件4. 对于 3 中所抛出的事件,可以传递到指定的控件,并且控件内部应该有一套指定额相应体系在 CBkDialogViewImpl 中使用 WTL 的消息分发方法用于系统消息的分发:BEGIN_MSG_MAP_EX(CBkDialogViewImpl)MESSAGE_RANGE_HANDLER_EX(WM_MOUSEFIRST, WM_MOUSELAST, OnToolTipEvent)MSG_WM_SIZE(OnSize)MSG_WM_PRINT(OnPrint)MSG_WM_PAINT(OnPaint)/从略NOTIFY_CODE_HANDLER_EX(BKINM_INVALIDATERECT, OnBKINMInvalidateRect)REFLECT_NOTIFY_CODE(NM_CUSTOMDRAW)