以金山界面库(openkui)为例思考和分析界面库的设计和实现——代码结构(完)
三年前,准备将金山界面库做一个全面的剖析。后来由于种种原因,这个系列被中断而一直没有更新。时过境迁,现在在windows上从事开发的人员越来越少,关注这块的技术的朋友也很少了。本以为这系列也随着技术的没落而不再被人所关注,所以一直没有更新其的意愿。前些天突然有个朋友对之前《以金山界面库(openkui)为例思考和分析界面库的设计和实现——资源读取模块分析》做了评论,这让我重新燃起一种欲望——将尚未完结的系列写完。于是我打开尘封三年的“草稿箱”里这篇文章,沿着三年前的思路,试着完成这系列博文。(转载请指明出于breaksoftware的csdn博客)
在《问题》一文中,我从一个“无知者”的角度抛出了一系列界面库设计的问题。在《资源读取模块分析》中已经解释了资源的存在形式。本文我们主要分析下整个界面构建的脉络。
以网页为例,我们可以通过html+css+javascript去搭建一个界面。这么设计的好处我在《问题》一文中已经有所阐述。openkui库也是照着这样的思路去设计的,但是它将组成分的更细,以至于让我觉得细的似乎太松散了。
以Sample1的皮肤资源为例,它是由若干xml文件组成了界面描述:
- xmls.xml 它描述了几个关键XML文件的位置。其中IDR_DLG_MAIN对应的XML文件是描述了界面的总体(后文称界面描述文件)。IDR_KSC_STYPE对应的XML文件(后文称样式描述文件)描述了局部界面的一些属性,IDR_KSC_SKINE对应的XML文件(后文称皮肤描述文件)则描述了局部界面对应的类。
- strings.xml 它描述了文字和ID的映射关系。这个非常类似于MFC中的string table。
- images.xml 它描述了图片文件和ID的映射关系。界面中所要用的图片资源都应该在这个文件中被描述。
其中xmls.xml中记录的几个xml和界面的关系最为紧密。而MAIN、STYPE和SKIN的关系也是需要理清的,我们先看界面描述文件内容
<layer title="sample1" width="600" height="470" appwin="1"><header class="mainhead" width="full" height="23"><icon src="ICON_MAIN" pos="5,4"/><text class="dlgtitle" pos="25,6">样例程序1</text><imgbtn id="60003" class="linkimage" skin="minbtn" pos="-105,1"/><imgbtn id="60002" class="linkimage" skin="maxbtn" pos="-73,1"/><imgbtn id="60001" class="linkimage" skin="closeonlybtn" pos="-43,1"/></header><body class="mainbody" width="full" height="full"><dlg pos="0,0,-0,-0" crbg=F7FBBF><text class="hellowordstyle" pos="50,200">hello world!</text></dlg></body><footer class="mainfoot" width="full" height="23" crbg=FFB9B9></footer>
</layer>
它是由header、body和footer三部分组成。每个部分又是由一些子模块组成,如text、imgbtn。以imgbtn为例,我们可以看到它的内部描述了id、class、skin和pos等四个属性。class则是描述该子模块是什么一种样式,我们从linkimage可以猜测出它应该是一个可以点击的图片,于是鼠标移动到上面应该变成手型,这个我们在样式描述文件中可以得到印证<class name=linkimage cursor=hand/>
skin属性则是描述了该子模块的皮肤配图信息,我们在皮肤描述文件中可以找到<png name="minbtn" src="IDP_BTN_SYS_MINIMIZE" subwidth="32"/>
minbtn的src字段指向的是一个图片文件ID,这个我们可以在images.xml中找到<image id="IDP_BTN_SYS_MINIMIZE" path="images/btn_sys_minimize.png" />
如此,我们便将这些XML文件的关系理清楚了。可以想象,相同skin和class的两个模块,它们可能在位置和大小上存在区别,所以“位置”和“大小”两个属性应该是在界面描述文件中,或者说应该以其覆盖其他文件的属性。而诸如手型、字体大小、背景色等则是应该在样式描述文件中描述。至于每个子模块对应的背景图片资源,应该在皮肤描述文件中描述。 上述XML中描述的属性,在界面构建过程中会被读取。可以想象,这个读取操作是每个皮肤模块的基础功能。打个比方,png这个模块它需要读取name、src和subwidth三个属性。它可能存在对应的get_name,get_src和get_subwidth三个方法用于获取上述属性。但是如果一旦增加属性,则需要新增读取函数。而且,属性的值的类型可能也是不同的,比如:
<class name=settingpage crbg=FBFCFD y-margin=10/>
它的属性是十进制数或者16进制数。那么接口的设计类型也无法做到统一。这样的设计存在明显的问题。所以我们应该统一一套获取方法,于是kui设计了如下基础类class KUILIB_API CKuiObject
{
public:......virtual LPCSTR GetObjectClass() = 0;virtual BOOL Load(TiXmlElement* pXmlElem){for (TiXmlAttribute *pAttrib = pXmlElem->FirstAttribute(); NULL != pAttrib; pAttrib = pAttrib->Next()){SetAttribute(pAttrib->Name(), pAttrib->Value(), TRUE);}return TRUE;}virtual HRESULT SetAttribute(CStringA strAttribName, CStringA strValue, BOOL bLoading){return E_FAIL;}......
}
所有皮肤相关的类都继承CKuiObject,它通过load方法遍历XML文件,并通过SetAttribute方法设置不同的属性。我们发现,可以统一这么做的一个非常重要的前提是XML库返回的name和value值都是const char*的。这样就规避了我们之前对数据类型无法统一的担忧。但是有些属性,我们在之后参与计算或者逻辑的时候就是希望它是整形的,那么我们需要怎么处理?从设计的角度说,CKuiObject不应该去关心属性的类型,因为它无法得知属性的类型,且即使得知了属性类型,也无法做到统一的处理(除非使用any类型)。所以,如果真的需要做类型确定,也是应该在不同的子类中做处理,而kui库就是这么做的。我们再来看基类CKuiObject的SetAttribute方法,它没有做任何有意义的事情,那么其有意义的功能是在其子类中实现的。这块的设计和我之前的预想不太一样,我本以为在CKuiObject类中保存一份属性的map结构,并通过SetAttribute方法去填充这个结构。不同的继承类在绘制界面时,则是去读取这个map结构获取需要的信息。这样的设计可以使得属性的保存和获取逻辑变得统一,相比于Kui设计中遍布于各个类的各种属性,明显统一的map结构更加方便和合理。但是有人会说,这样就限制了各个类的属性的类型,使得它们必须是map的value类型(比如string)。其实这个担忧大可不必,我们可以让属性的map是std::map<string,any>的结构,当然这样就得在KuiObject层确定属性值的类型了。我们继续看下各个子类对SetAttribute方法的设计。Kui库使用一组宏定义的方法去设计SetAttribute方法,这样就像MFC中的消息映射表,开发者只要维护好这张表就可以了。这种设计可以方便开发者对代码的修改和扩展。
#define KUIWIN_DECLARE_ATTRIBUTES_BEGIN() \
public: \virtual HRESULT SetAttribute( \CStringA strAttribName, \CStringA strValue, \BOOL bLoading) \{ \HRESULT hRet = __super::SetAttribute( \strAttribName, \strValue, \bLoading \); \if (SUCCEEDED(hRet)) \return hRet; \#define KUIWIN_DECLARE_ATTRIBUTES_END() \return E_FAIL; \\return hRet; \} \
通过KUIWIN_DECLARE_ATTRIBUTES_BEGIN和KUIWIN_DECLARE_ATTRIBUTES_END的组合我们便可以得到一个完整的SetAttribute函数。可以见得,每次设置属性时,我们都需要尝试设置其父类的属性,如果其父类属性设置成功了,则不再在此类中设置属性。对于各个属性,则是使用如下一些宏进行设置#define KUIWIN_CHAIN_ATTRIBUTE(varname, allredraw) \if (SUCCEEDED(hRet = varname.SetAttribute(strAttribName, strValue, bLoading))) \{ \return hRet; \} \else \#define KUIWIN_CUSTOM_ATTRIBUTE(attribname, func) \if (attribname == strAttribName) \{ \hRet = func(strValue, bLoading); \} \else \// Int = %d StringA
#define KUIWIN_INT_ATTRIBUTE(attribname, varname, allredraw) \if (attribname == strAttribName) \{ \varname = ::StrToIntA(strValue); \hRet = allredraw ? S_OK : S_FALSE; \} \else \// UInt = %u StringA
#define KUIWIN_UINT_ATTRIBUTE(attribname, varname, allredraw) \if (attribname == strAttribName) \{ \varname = (UINT)::StrToIntA(strValue); \hRet = allredraw ? S_OK : S_FALSE; \} \else
KUIWIN_CHAIN_ATTRIBUTE宏是为了属性传导的。KUIWIN_CUSTOM_ATTRIBUTE宏是为了设置属性时调用某处理函数的。而KUIWIN_INT_ATTRIBUTE和KUIWIN_UINT_ATTRIBUTE则是将string类型的属性转换成int等其他类型的数据的,我们总览Kui库,可以发现有若干这种类型转换的属性处理宏。这就是我之前所说的,属性的类型是在不同子类中确定的。我们看一个这组宏使用的例子KUIWIN_DECLARE_ATTRIBUTES_BEGIN()KUIWIN_CHAIN_ATTRIBUTE(m_imgSkin, TRUE)KUIWIN_COLOR_ATTRIBUTE("crbg", m_crBg, TRUE)KUIWIN_INT_ATTRIBUTE("left", m_lSkinParamLeft, TRUE)KUIWIN_INT_ATTRIBUTE("top", m_lSkinParamTop, TRUE)KUIWIN_ENUM_ATTRIBUTE("part", UINT, TRUE)KUIWIN_ENUM_VALUE("all", Frame_Part_All)KUIWIN_ENUM_VALUE("top", (Frame_Part_All & ~Frame_Part_Bottom))KUIWIN_ENUM_VALUE("middle", (Frame_Part_All & ~(Frame_Part_Bottom | Frame_Part_Top)))KUIWIN_ENUM_VALUE("bottom", (Frame_Part_All & ~Frame_Part_Top))KUIWIN_ENUM_VALUE("left", (Frame_Part_All & ~Frame_Part_Right))KUIWIN_ENUM_VALUE("center", (Frame_Part_All & ~(Frame_Part_Right | Frame_Part_Left)))KUIWIN_ENUM_VALUE("right", (Frame_Part_All & ~Frame_Part_Left))KUIWIN_ENUM_END(m_uDrawPart)KUIWIN_DECLARE_ATTRIBUTES_END()
不同皮肤类通过上述宏的组合,实现了各自的属性设置方法。其主要实现的功能,就是把属性设置到各自类的成员变量中:要么是直接的成员变量,要么是成员变量的属性中。如上例,KUIWIN_CHAIN_ATTRIBUTE宏就是将属性传递到m_imgSkin的属性中。那什么是m_imgSkin呢?可以想象,每个由图片绘制的皮肤模块都有图片的相关属性,比如图片的地址等,而这些模块则可以作为一个对象存在于皮肤模块类中,以作统一处理。这个就是KUI模块皮肤类的设计思路。但是个人觉得这不是一种好的设计,我觉得图片皮肤类(m_imgSkin对应的类)应该是各个模块图片皮肤类的父类,即应该是继承关系,而不应该是包含关系。打个比方,使用图片方式绘制的按钮和使用图片方式绘制的Frame,应该都是一种图片皮肤类,所以他们应该通过继承的方式体现“是”这层关系。
现在我们来看下m_imgSkin对应的图片皮肤类的设计
class KUILIB_API CKuiImageSkin: public CKuiImage, public CKuiSkinBase
{KUIOBJ_DECLARE_CLASS_NAME(CKuiImageSkin, "imglst")
该类从继承关系上看,它即是一种图片类(CKuiImage),也是一种皮肤类(CKuiSkinBase)。而CKuiImageSkin类这是一个图片组(imglst即image_list)描述的皮肤类。举个例子,我们的按钮一般有三态:普通、按下和悬浮。如果我们将这三态对应的背景图片保存在三张图中,这样会增加文件的读取次数,同时也不利于后期维护。那我们我们就将这三张图片合并为一张图片组,这样一个按钮对应一个图片组,图片数量减少三分之二。当然这儿也不一定是三张图片,也可能是一张,或者是可以表示更多状态的八张。 CKuiImage是一个非常重要的类,它主要完成了图片的读取和绘制工作。其内容非常细节,本文不做分析,有兴趣的同学可以参看KUILib\Include\kuiwin\kuiimage.h。CKuiSkinBase则是皮肤类的绘制的一层封装,它负责表达皮肤被绘制在什么位置。因为不是所有的皮肤都是图片类型的(比如固定底色的),所以使用这层封装也用于涵盖所有皮肤的绘制。我们可以看到,Kui中的皮肤类都继承于CKuiSkinBase
class KUILIB_API CKuiPngSkin: public CKuiSkinBase
{KUIOBJ_DECLARE_CLASS_NAME(CKuiPngSkin, "png")class CKuiSkinButton : public CKuiSkinBase
{KUIOBJ_DECLARE_CLASS_NAME(CKuiSkinButton, "button")class CKuiSkinImgHorzExtend : public CKuiSkinBase
{KUIOBJ_DECLARE_CLASS_NAME(CKuiSkinImgHorzExtend, "imghorzex")
这么零散的皮肤基础类,总得在一个地方进行统筹,现在我们就要讲解皮肤基础类的工厂类——KuiSkin,我们先看看其部分申明
class KuiSkin
{
public:KuiSkin();~KuiSkin();static BOOL LoadSkins(const std::string& strXml){return LoadSkins(strXml.c_str());}static BOOL LoadSkins(LPCSTR lpszXml);static CKuiSkinBase* GetSkin(LPCSTR lpszSkinName){__KuiSkinPool::CPair *pairRet = _Instance()->m_mapPool.Lookup(lpszSkinName);if (pairRet)return pairRet->m_value;elsereturn NULL;}static size_t GetCount();protected:typedef CAtlMap<CStringA, CKuiSkinBase *> __KuiSkinPool;__KuiSkinPool m_mapPool;static KuiSkin* ms_pInstance;static KuiSkin* _Instance(){if (!ms_pInstance)ms_pInstance = new KuiSkin;return ms_pInstance;}void _LoadSkins(TiXmlElement *pXmlSkinRootElem);static CKuiSkinBase* _CreateKuiSkinByName(LPCSTR lpszName){CKuiSkinBase *pNewSkin = NULL;pNewSkin = CKuiImageSkin::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiSkinImgFrame::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiSkinButton::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiSkinImgHorzExtend::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiSkinGradation::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiPngSkin::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;return NULL;}
};
该类读取皮肤描述文件,并对每个name新建对象。之后界面构建过程中,将通过GetSkin的方法获取每个皮肤基础组件。皮肤是界面中一个比较基础的组件,它是一个区域性质的模块。而往往界面中的很多控件是由很多基础的组件组成的,比如一个树形列表。接下来我们再来看下高于皮肤组件层次的界面模块。
界面中,除了单纯的皮肤基础组件,还有一些更简单的组件,比如文字。也有些多个基础组件组合的复杂皮肤模块,比如进度条。以Sample1为例
<icon src="ICON_MAIN" pos="5,4"/>
<text class="dlgtitle" pos="25,6">样例程序1</text>
界面描述文件中的icon和text就是区别于我们上面介绍的图片皮肤类的界面模块。在KUILib\Include\kuiwin\kuiwndcmnctrl.h类中,我们就可以看到一系列这样的类class KUILIB_API CKuiIconWnd : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiIconWnd, "icon")class CKuiCheckBox : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiCheckBox, "check")class CKuiProgress : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiProgress, "progress")class CKuiImageWnd : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiImageWnd, "img")
这些类中最重要的功能是:加载XML文件、绘制和位置计算。于是我们可以发现这些类主要实现了Load、OnPaint和OnNcCalcSize方法。稍微复杂一点的类是进度条类,因为进度条可以分为:进度条外框和进度条填充物两种图片,所以它也将是两个图片基础皮肤类组合而成,我们看下其申明class CKuiProgress : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiProgress, "progress")
protected: CKuiSkinBase *m_pSkinBg;CKuiSkinBase *m_pSkinPos;......KUIWIN_BEGIN_MSG_MAP()MSG_WM_PAINT(OnPaint)MSG_WM_NCCALCSIZE(OnNcCalcSize)KUIWIN_END_MSG_MAP()KUIWIN_DECLARE_ATTRIBUTES_BEGIN()KUIWIN_SKIN_ATTRIBUTE("bgskin", m_pSkinBg, TRUE)KUIWIN_SKIN_ATTRIBUTE("posskin", m_pSkinPos, TRUE)KUIWIN_DWORD_ATTRIBUTE("min", m_dwMinValue, FALSE)KUIWIN_DWORD_ATTRIBUTE("max", m_dwMaxValue, FALSE)KUIWIN_DWORD_ATTRIBUTE("value", m_dwValue, FALSE)KUIWIN_UINT_ATTRIBUTE("showpercent", m_bShowPercent, FALSE)KUIWIN_DECLARE_ATTRIBUTES_END()
}
有兴趣的同学可以参看该类中的OnPaint和OnNcCalcSize方法是如何使用图片基础皮肤类进行绘制的。我们发现这些皮肤组件类都继承于CKuiWindow,目测其是一个窗口控件,但是实际上它并不是
class KUILIB_API CKuiWindow : public CKuiObject
{
......
protected:
......KuiStyle m_style;
......
public:BOOL NeedRedrawParent() {return (m_style.m_strSkinName.IsEmpty() && (m_style.m_crBg == CLR_INVALID));}virtual BOOL Load(TiXmlElement* pTiXmlElem){......};// Set container, container is a REAL windowvirtual void SetContainer(HWND hWndContainer) {m_hWndContainer = hWndContainer;}virtual BOOL IsContainer() {return FALSE;}virtual BOOL NeedRedrawWhenStateChange() {if (!m_style.m_strSkinName.IsEmpty()) {CKuiSkinBase* pSkin = KuiSkin::GetSkin(m_style.m_strSkinName);if (pSkin && !pSkin->IgnoreState())return TRUE;}return (CLR_INVALID != m_style.m_crHoverText) || (NULL != m_style.m_ftHover) || (CLR_INVALID != m_style.m_crBgHover);}
......
protected:KUIWIN_BEGIN_MSG_MAP()MSG_WM_CREATE(OnCreate)MSG_WM_PAINT(OnPaint)MSG_WM_DESTROY(OnDestroy)MSG_WM_WINDOWPOSCHANGED(OnWindowPosChanged)MSG_WM_NCCALCSIZE(OnNcCalcSize)MSG_WM_SHOWWINDOW(OnShowWindow)KUIWIN_END_MSG_MAP_BASE()KUIWIN_DECLARE_ATTRIBUTES_BEGIN()KUIWIN_STYLE_ATTRIBUTE("class", m_style, TRUE)......KUIWIN_DECLARE_ATTRIBUTES_END()
};
我们从SetContainer的注释可以看出,Container类型的类才是真正的窗口类。在CKuiWindow类中,我们看到一个成员变量m_style,它就是我们之前介绍的样式描述文件中的一项。我们还发现m_style中皮肤名的成员变量——m_strSkinName,可以见得皮肤名不仅可以在界面描述文件中确定,也可以在样式描述文件中确定。CKuiWindow内部实现了很多细节功能,本文不作分析,只要知道它主要做了绘制和计算大小和位置的功能即可,而且要记住它是(伪)窗口类的父类。
看过这么多基础类,我们终于要看这些基础类的容器——容器类,以Sample1为例,其header、body和footer三者都是容器类。但是需要注意的是,这些容器类的名字并不是header、body或者footer。我们以headerd为例看下对应的代码
template <class T, class TKuiWin = CKuiDialog, class TBase = ATL::CWindow, class TWinTraits = CKuiDialogViewTraits>
class ATL_NO_VTABLE CKuiDialogViewImpl: public ATL::CWindowImpl<T, TBase, TWinTraits>, public CKuiViewImpl<T>
{friend CKuiViewImpl<T>;public:DECLARE_WND_CLASS_EX(NULL, CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, COLOR_WINDOW)
protected:TKuiWin m_kuiHeader;......BOOL SetXml(LPCSTR lpszXml){......pTiElement = pXmlRootElem->FirstChildElement("header");m_bHasHeader = m_kuiHeader.Load(pTiElement);......}
}
可以见得header对应的类是CKuiDialog,我们查看CKuiDialog的实现class CKuiDialog: public CKuiPanel
{KUIOBJ_DECLARE_CLASS_NAME(CKuiDialog, "dlg")class CKuiPanel : public CKuiContainerWnd
{KUIOBJ_DECLARE_CLASS_NAME(CKuiPanel, "div")......
protected:CAtlList<CKuiWindow *> m_lstWndChild;public:BOOL LoadChilds(TiXmlElement* pTiXmlChildElem){KuiSendMessage(WM_DESTROY);BOOL bVisible = IsVisible(TRUE);for (TiXmlElement* pXmlChild = pTiXmlChildElem; NULL != pXmlChild; pXmlChild = pXmlChild->NextSiblingElement()){CKuiWindow *pNewChildWindow = _CreateKuiWindowByName(pXmlChild->Value());if (!pNewChildWindow)continue;pNewChildWindow->SetParent(m_hKuiWnd);pNewChildWindow->SetContainer(m_hWndContainer);pNewChildWindow->Load(pXmlChild);m_lstWndChild.AddTail(pNewChildWindow);}return TRUE;}void SetContainer(HWND hWndContainer){__super::SetContainer(hWndContainer);POSITION pos = m_lstWndChild.GetHeadPosition();while (pos != NULL){CKuiWindow *pKuiWndChild = m_lstWndChild.GetNext(pos);if (pKuiWndChild){pKuiWndChild->SetContainer(hWndContainer);}}}int OnCreate(LPCREATESTRUCT /*lpCreateStruct*/){POSITION pos = m_lstWndChild.GetHeadPosition();while (pos != NULL){CKuiWindow *pKuiWndChild = m_lstWndChild.GetNext(pos);pKuiWndChild->OnCreate(NULL);}return TRUE;}......
}
class CKuiContainerWnd : public CKuiWindow
{
public:virtual CKuiWindow* FindChildByCmdID(UINT uCmdID);virtual void RepositionChilds();virtual void RepositionChild(CKuiWindow *pKuiWndChild);BOOL IsContainer() {return TRUE;}
};
我们可以看到,主要的类是CKuiPanel。它在其内部维护了一组伪窗口信息,然后所有操作都是遍历这些伪窗口类的处理函数实现消息传递,比如OnCreate方法的实现。而其父类CKuiContainerWnd则主要是定义一些虚方法,并重写了CKuiWindow的IsContainer方法,表明继承于自己的类都是一个容器。
我们还要关注下容器类如何和各个组件进行通信。在MFC的多窗口模式下,消息通过消息泵进行传递。而Kui除了容器类是窗口类,其他组件类则不是窗口,那么它们之间的消息是怎么传递的?我们知道只有窗口才能收到消息,那么可以想到第一步处理消息的地方应该是容器类。以窗口尺寸改变为例,当窗口尺寸改变时,其内部组件也要被调整。首先容器类收到消息
template <class T, class TKuiWin = CKuiDialog, class TBase = ATL::CWindow, class TWinTraits = CKuiDialogViewTraits>
class ATL_NO_VTABLE CKuiDialogViewImpl: public ATL::CWindowImpl<T, TBase, TWinTraits>, public CKuiViewImpl<T>
{
protected:BEGIN_MSG_MAP_EX(CKuiDialogViewImpl)MESSAGE_RANGE_HANDLER_EX(WM_MOUSEFIRST, WM_MOUSELAST, OnToolTipEvent)MSG_WM_SIZE(OnSize)
在容器类的OnSize中,会调用重置组件位置的逻辑void OnSize(UINT nType, CSize size)
{......_RepositionItems();
}void _RepositionItems(BOOL bRedraw = TRUE)
{....WINDOWPOS WndPos = { 0, 0, rcClient.left, rcClient.top, rcClient.Width(), rcClient.Height(), SWP_SHOWWINDOW };if (m_bHasHeader){m_kuiHeader.KuiSendMessage(WM_WINDOWPOSCHANGED, 0, (LPARAM)&WndPos);m_kuiHeader.GetRect(rcHeader);}if (m_bHasFooter){m_kuiFooter.KuiSendMessage(WM_WINDOWPOSCHANGED, 0, (LPARAM)&WndPos);m_kuiFooter.GetRect(rcFooter);WndPos.y = rcClient.bottom - rcFooter.Height();WndPos.cy = rcFooter.Height();m_kuiFooter.KuiSendMessage(WM_WINDOWPOSCHANGED, 0, (LPARAM)&WndPos);}if (m_bHasBody){WndPos.y = rcHeader.bottom;WndPos.cy = rcClient.bottom - rcFooter.Height() - rcHeader.bottom;m_kuiBody.KuiSendMessage(WM_WINDOWPOSCHANGED, 0, (LPARAM)&WndPos);}_Redraw();
}
主界面的header、body和footer都将调用KuiSendMessage方法传递消息,从函数命名上看KuiSendMessage承袭了MFC中消息传递的模式。但是实际上这个只是一种“写法”,和MFC那套不是一套机制。它只是一个函数调用 LRESULT KuiSendMessage(UINT Msg, WPARAM wParam = 0, LPARAM lParam = 0){LRESULT lResult = 0;SetMsgHandled(FALSE);ProcessWindowMessage(NULL, Msg, wParam, lParam, lResult);return lResult;}
而ProcessWindowMessage方法也不是向窗口传递消息,而只是调用各个继承于CKuiWindow类的ProcessWindowMessage方法。 class KUILIB_API CKuiWindow : public CKuiObject
{
......
KUIWIN_BEGIN_MSG_MAP()MSG_WM_CREATE(OnCreate)MSG_WM_PAINT(OnPaint)MSG_WM_DESTROY(OnDestroy)MSG_WM_WINDOWPOSCHANGED(OnWindowPosChanged)MSG_WM_NCCALCSIZE(OnNcCalcSize)MSG_WM_SHOWWINDOW(OnShowWindow)
KUIWIN_END_MSG_MAP_BASE()
......
#define KUIWIN_BEGIN_MSG_MAP() \
protected: \virtual BOOL ProcessWindowMessage( \HWND hWnd, UINT uMsg, WPARAM wParam, \LPARAM lParam, LRESULT& lResult) \{ \#define KUIWIN_END_MSG_MAP() \if (!IsMsgHandled()) \return __super::ProcessWindowMessage( \hWnd, uMsg, wParam, lParam, lResult); \return TRUE; \} \#define KUIWIN_END_MSG_MAP_BASE() \return TRUE; \} \
这个时候消息还是在容器窗口中处理,最终它会遍历容器类所有子模块,并调用子模块的KuiSendMessage方法。class CKuiDialog: public CKuiPanel
{KUIOBJ_DECLARE_CLASS_NAME(CKuiDialog, "dlg")
public:void OnWindowPosChanged(LPWINDOWPOS lpWndPos) {CKuiWindow::OnWindowPosChanged(lpWndPos);_RepositionChilds();}virtual void RepositionChild(CKuiWindow *pKuiWndChild) {......pKuiWndChild->KuiSendMessage(WM_WINDOWPOSCHANGED, NULL, (LPARAM)&WndPos);}
protected:void _RepositionChilds() {POSITION pos = m_lstWndChild.GetHeadPosition();while (pos != NULL) {CKuiWindow *pKuiWndChild = m_lstWndChild.GetNext(pos);RepositionChild(pKuiWndChild);}}
protected:KUIWIN_BEGIN_MSG_MAP()MSG_WM_WINDOWPOSCHANGED(OnWindowPosChanged)KUIWIN_END_MSG_MAP()
};
各个继承于CKuiWindow的类的ProcessWindowMessage方法将被调用,同时“消息”将被传递到处理WM_WINDOWPOSCHANGED的函数中。默认情况下,会调用CKuiWindow的 void OnWindowPosChanged(LPWINDOWPOS lpWndPos){m_rcWindow.MoveToXY(lpWndPos->x, lpWndPos->y);SIZE sizeRet = {lpWndPos->cx, lpWndPos->cy};KuiSendMessage(WM_NCCALCSIZE, TRUE, (LPARAM)&sizeRet);
这个时候消息改成了WM_NCCALCSIZE,为什么要改成这个消息?因为这个消息在CKuiWindow的ProcessWindowMessage方法中不会被处理,从而将会被子类的方法处理,这样就达到了“消息传递”的目的。以第一个需要被重绘的ICON为例class KUILIB_API CKuiIconWnd : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiIconWnd, "icon")......
LRESULT OnNcCalcSize(BOOL bCalcValidRects, LPARAM lParam){LPSIZE pSize = (LPSIZE)lParam;pSize->cx = m_nSize;pSize->cy = m_nSize;return TRUE;}
相对于处理WM_SIZE消息,处理WM_PAINT消息则简单的多:容器类直接调用模块的重绘方法。最后回到总体框架。Kui并没有将这些容器类直接暴露在最外面,而实际通过一系列模板类实现功能
class CKuiDialogView: public CKuiDialogViewImpl<CKuiDialogView>
{
};template <class T, class TKuiView = CKuiDialogView, class TBase = CWindow, class TWinTraits = CControlWinTraits>
class ATL_NO_VTABLE CKuiDialogImpl : public CWindowImpl<T, TBase, TWinTraits>
{
protected:TKuiView m_richView;
}template <class T, class TKuiWin = CKuiDialog, class TBase = ATL::CWindow, class TWinTraits = CKuiDialogViewTraits>
class ATL_NO_VTABLE CKuiDialogViewImpl: public ATL::CWindowImpl<T, TBase, TWinTraits>, public CKuiViewImpl<T>
{
protected:TKuiWin m_kuiHeader;TKuiWin m_kuiBody;TKuiWin m_kuiFooter;
}
至此,Kui界面库主要的脉络给理清了。对于一个完整的界面库,我只是从一些我关心的角度去分析了其实现的大体步骤。其中很多细节处理虽然有待商榷,但是其中的精髓还是不少的。有兴趣的同学可以在源码中挖掘出自己感兴趣的内容。最后附上类图关系。 相关文章:
一包烟钱买到电动剃须刀,小米有品告诉你什么叫性价比
男人身上长得最快的是什么?答案是胡须。一名健康男性的胡须每天都要生长超过 0.4mm,比咱们头发的生长速度还快,这也是男人隔三差五就要剃须的原因之一。男人的一生是与胡子战斗一生,也是被剃须刀拖累的一生。出差办事儿࿰…

Label控件属性AssociatedControlID
可以使用Label控件来标注一个HTML表单字段。Label控件拥有属性AssociatedControlID,可以设置此属性来指向表示表单字段的ASP.NET控件。 例如,代码清单2-3中的页面含有一个简单的表单,表单包含两个字段用于输入名和姓。Label控件用于标注这两个TextBox控件。 Code<% Page Lan…

2015_8_21作业——有自翻译有复制他人的英语太差
date作用:打印或设置系统日期和时间格式:date [OPTION]...[FORMAT]date [u|--utc|--universal] [MMDDhhmm[[CC]YY][.ss]]|是或 多选一的选项 ...代表同类内容可多次选项:注意短选项后不可加号-d,--dateSTRING显示时间字符串但不是立即:如date…
WMI技术介绍和应用——接收事件
时隔两三年,再次更新WMI系列博文。好在功能在三年前就已经实现了,现在只要补充些实例即可。 之前介绍的基本都是查询静态数据,而本文将要介绍非常有意思的事件接收功能。(转载请指明出于breaksoftware的csdn博客) 监控…

WML标签速查手册
WML标签速查手册 结构相关标签语法及属性<wml><wml xml:lang"lang" > content</wml> WML元素的共有属性主要有3个,即id、class和xml:lang属性。WML的所有元素都有两个核心属性,即标识(id)和类(class)属性。它们主要用…
Python 三十大实践、建议和技巧
所有参与投票的 CSDN 用户都参加抽奖活动群内公布奖项,还有更多福利赠送作者 | Erik-Jan van Baaren译者 | 凯隐编辑 | Jane出品 | AI科技大本营(ID:rgznai100)【导读】2020年,你又立了什么新的 Flag?新一年…

点击通知栏后打开Activity,并传参
为什么80%的码农都做不了架构师?>>> Reciver中intent new Intent(context, MessageDetailsaActivity_.class);intent.putExtra("freshMessageId", String.valueOf(push.getObid()));intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Inten…

WMI技术介绍和应用——执行方法
在之前的博文中,我们主要介绍了如何使用WMI查询信息和接收事件。本文将介绍WMI的另一种用法——执行方法。(转载请指明出于breaksoftware的csdn博客) 这块的内容在msdn中有详细的介绍,如果想看原版的可以参阅《Example: Calling a…

动态指定超链接参数的几种方法(Passing a JavaScript variable into href of )
情景:有些环境下我们需要根据页面中JavaScript变量的值来确定某个(某些)超链接的参数,如“http://www.bla.com/test.aspx?var1”中,究竟var1等于多少,要根据JavaScript变量来判定 方法一:很简单…

知乎「致知计划之科学季」颁奖,创作者分享80万元奖金
1月11日,「致知计划之科学季」颁奖典礼在北京798艺术中心举行,近500名创作者到场参加。 为了激励创作者,推动优质内容持续产出,知乎的「致知计划」从四个方面对创作者进行了扶持。一是流量扶持,让专业、优质的内容获得…

Inno Setup制作应用程序安装包
我最近写了一个MFC应用程序,想发给其他的小伙伴玩一玩,直接发了个exe文件过去,结果发现小伙伴那边打不开。原来这个exe文件虽然是MFC静态编译的,但是还依赖了其他几个.dll文件,需要把这几个dll文件和exe文件放在同一个…

WMI技术介绍和应用——事件通知
在《WMI技术介绍和应用——WMI概述》中,我们使用了下图介绍WMI构架(转载请指明出于breaksoftware的csdn博客) 我们之前介绍的使用WMI查询系统、硬件等信息的功能,是通过查询WMI静态数据的空间实现的。这个功能的核心是在上图中2&a…

OpenWebSpider 安装使用
OpenWebSpider 是一个很好用的网络爬虫,也可以叫做“网络蜘蛛”. 安装: 1.使用Vs.net 2003 编译, 记得要copy libmysql.dll到工程里 2.配置文件openwebspider.conf 将已经启动了的 mysql数据库用户名,密码填写好 3.数据库建立 执行 sql_struc…
杂谈 | 当前知识蒸馏与迁移学习有哪些可用的开源工具?
所有参与投票的 CSDN 用户都参加抽奖活动群内公布奖项,还有更多福利赠送作者&编辑 | 言有三来源 | 有三AI(ID:yanyousan_ai)【导读】知识蒸馏与迁移学习不仅仅属于模型优化的重要技术之一,也是提升模型跨领域泛化能力的重要技…

对 Thinking in java 4th Edition I/O DirList.java的疑问
2019独角兽企业重金招聘Python工程师标准>>> 以下原文代码: //: io/DirList.java // Display a directory listing using regular expressions. // {Args: "D.*\.java"} import java.util.regex.*; import java.io.*; import java.util.*; pub…

WMI技术介绍和应用——Instance/Method Provider
在《WMI技术介绍和应用——事件通知》一文中,我们提到了提供者(Provider)这个概念。我们还是要引入WMI的结构图(转载请指明出于breaksoftware的csdn博客) 我们在1这层的Native C/C里可以看到若干Provider,这…

SSH 组建轻量级架构 附录 -- 遇到的问题和解答
action: nulljava.lang.ClassNotFoundException: org.springframework.web.struts.ContextLoaderPlugIn解决方法:加载 spring.jar 包 报 无法初始化at org.apache.struts.action.ActionServlet.initModulePlugIns(解决方法:删除 asm-2.2.3.jar springda…
TIOBE 1月编程语言排行榜:C语言再度「C 位」出道,Python惜败
整理 | 屠敏来源 | CSDN(ID:CSDNnews)【导读】在 2020 年初雪来临之际,TIOBE 官方在最新发布的 1 月编程语言榜单中为我们最终揭开了「 2019 年度编程语言」的神秘面纱,然意料之外情理之中,获此殊荣的并非是…

my项目的总结2015.8.26编
这已经是上上个星期的事了,现在回顾一下: 负责的模块是"my",更精准的说应该是my里面的个人信息管理 由于项目分域,模块已经分好了,涉及到的只是在现有的基础上解决分域后遗留的历史问题 上点图吧:…
WMI技术介绍和应用——Event Provider
在《WMI技术介绍和应用——Instance/Method Provider》一文中,我们介绍了Instance和Method Provider的编写方法。本文我们将介绍更有意思的“事件提供者”。在《WMI技术介绍和应用——事件通知》中,我们曾经提到事件是分为两种:intrinsic eve…

Windows server 2003 IP路由配置
1、静态路由:在静态路由中必须明确指出从源到目标所经过的路径2、默认路由:默认路由是一种特殊的静态路由,为那些在路由表中没有找到明确匹配的路由信息的数据包指定下一步跳地址。在Windows server 2003的计算机上配置默认网关时就为该计算机…
人工智能的下一个前沿:识别“零”和“无”
所有参与投票的 CSDN 用户都参加抽奖活动群内公布奖项,还有更多福利赠送作者 | Max Versace译者 | 夕颜出品 | AI科技大本营(ID:rgznai100)声明:本文为客座文章,仅是为作者的观点,不代表 IEEE Spectrum 或 …

File Operations In Java
2019独角兽企业重金招聘Python工程师标准>>> The “File” class in Java defines many useful methods, here is a program which demonstrates some of these methods. import java.io.*;public class streams {public static void main(String []args){File f1ne…
Cascade RPN,结构的艺术带来极致提升 | NeurIPS 2019
作者 | VincentLee来源 | 晓飞的算法工程笔记(ID: gh_084c810bc839)导读:论文提出Cascade RPN算法来提升RPN模块的性能,该算法重点解决了RPN在迭代时anchor和feature不对齐的问题,论文创新点足,效果也很惊艳…

IIS+PHP+MySQL+Zend Optimizer+GD库+phpMyAdmin安装配置[完整修正实用版]
IISPHPMySQLZend OptimizerGD库phpMyAdmin安装配置[完整修正实用版]IISPHPMySQLZend OptimizerGD库phpMyAdmin安装配置[完整修正实用版][补充]关于参照本贴配置这使用中使用的相关问题请参考关于WIN主机下配置PHP的若干问题解决方案总结这个帖子尽量自行解决,谢谢[url]http://b…

WMI技术介绍和应用——Event Consumer Provider
在《WMI技术介绍和应用——Event Provider》和《WMI技术介绍和应用——接收事件》中,我们展现了如何处理和事件相关的WMI知识。而《WMI技术介绍和应用——接收事件》一文则主要讲解了如何查询事件,这种查询是在我们进程存在时发生的,一旦我们…

shell练习四
2019独角兽企业重金招聘Python工程师标准>>> 模拟linnux登录shell #!/bin/bash echo -n "login:" read name echo -n "passwd:" read passwdif [ $name"aaa" -a passwd"aaa" ]; thenecho "the host and passwd is rig…

WMI技术介绍和应用——总结(完)
断断续续的,历经三年将WMI这个主题给写完了。记得最开始时接触该技术,是因为传统获取CPU序列号的方法总是出错。于是接触了这种已经很老的技术。本着打破砂锅问到底的想法,我决定稍微研究一下,结果越来越深。正好借着年前这点时间…
2020年,大火的Python和JavaScript是否会被取而代之?
作者 | Richard Kenneth Eng 译者 | 明明如月,编辑 | 郭芮 来源 | CSDN(ID:CSDNnews) Python 和 JavaScript 是目前最火的两大编程语言。然而,他们不可能永远屹立不倒。最终,必将像其他编程语言一样跌下神坛…

WaveSwipeRefreshLayout
WaveSwipeRefreshLayout 介绍: 水滴效果的下拉刷新,效果非常不错。 http://itlanbao.com/code/20150815/10000/100423.html 运行效果: 相关代码 android Gallery 图片滚动 BalloonPerformerCountryRankDelightfulMenuDrawableFancyBackground…