以金山界面库(openkui)为例思考和分析界面库的设计和实现——代码结构(完)
? ? ? ? 三年前,準(zhǔn)備將金山界面庫(kù)做一個(gè)全面的剖析。后來(lái)由于種種原因,這個(gè)系列被中斷而一直沒(méi)有更新。時(shí)過(guò)境遷,現(xiàn)在在windows上從事開(kāi)發(fā)的人員越來(lái)越少,關(guān)注這塊的技術(shù)的朋友也很少了。本以為這系列也隨著技術(shù)的沒(méi)落而不再被人所關(guān)注,所以一直沒(méi)有更新其的意愿。前些天突然有個(gè)朋友對(duì)之前《以金山界面庫(kù)(openkui)為例思考和分析界面庫(kù)的設(shè)計(jì)和實(shí)現(xiàn)——資源讀取模塊分析》做了評(píng)論,這讓我重新燃起一種欲望——將尚未完結(jié)的系列寫(xiě)完。于是我打開(kāi)塵封三年的“草稿箱”里這篇文章,沿著三年前的思路,試著完成這系列博文。(轉(zhuǎn)載請(qǐng)指明出于breaksoftware的csdn博客)
? ? ? ? 在《問(wèn)題》一文中,我從一個(gè)“無(wú)知者”的角度拋出了一系列界面庫(kù)設(shè)計(jì)的問(wèn)題。在《資源讀取模塊分析》中已經(jīng)解釋了資源的存在形式。本文我們主要分析下整個(gè)界面構(gòu)建的脈絡(luò)。
? ? ? ? 以網(wǎng)頁(yè)為例,我們可以通過(guò)html+css+javascript去搭建一個(gè)界面。這么設(shè)計(jì)的好處我在《問(wèn)題》一文中已經(jīng)有所闡述。openkui庫(kù)也是照著這樣的思路去設(shè)計(jì)的,但是它將組成分的更細(xì),以至于讓我覺(jué)得細(xì)的似乎太松散了。
? ? ? ? 以Sample1的皮膚資源為例,它是由若干xml文件組成了界面描述:
- xmls.xml 它描述了幾個(gè)關(guān)鍵XML文件的位置。其中IDR_DLG_MAIN對(duì)應(yīng)的XML文件是描述了界面的總體(后文稱界面描述文件)。IDR_KSC_STYPE對(duì)應(yīng)的XML文件(后文稱樣式描述文件)描述了局部界面的一些屬性,IDR_KSC_SKINE對(duì)應(yīng)的XML文件(后文稱皮膚描述文件)則描述了局部界面對(duì)應(yīng)的類。
- strings.xml 它描述了文字和ID的映射關(guān)系。這個(gè)非常類似于MFC中的string table。
- images.xml 它描述了圖片文件和ID的映射關(guān)系。界面中所要用的圖片資源都應(yīng)該在這個(gè)文件中被描述。
? ? ? ? 其中xmls.xml中記錄的幾個(gè)xml和界面的關(guān)系最為緊密。而MAIN、STYPE和SKIN的關(guān)系也是需要理清的,我們先看界面描述文件內(nèi)容
<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三部分組成。每個(gè)部分又是由一些子模塊組成,如text、imgbtn。以imgbtn為例,我們可以看到它的內(nèi)部描述了id、class、skin和pos等四個(gè)屬性。class則是描述該子模塊是什么一種樣式,我們從linkimage可以猜測(cè)出它應(yīng)該是一個(gè)可以點(diǎn)擊的圖片,于是鼠標(biāo)移動(dòng)到上面應(yīng)該變成手型,這個(gè)我們?cè)跇邮矫枋鑫募锌梢缘玫接∽C
<class name=linkimage cursor=hand/>? ? ? ? skin屬性則是描述了該子模塊的皮膚配圖信息,我們?cè)谄つw描述文件中可以找到
<png name="minbtn" src="IDP_BTN_SYS_MINIMIZE" subwidth="32"/>? ? ? ? minbtn的src字段指向的是一個(gè)圖片文件ID,這個(gè)我們可以在images.xml中找到
<image id="IDP_BTN_SYS_MINIMIZE" path="images/btn_sys_minimize.png" />? ? ? ? 如此,我們便將這些XML文件的關(guān)系理清楚了。可以想象,相同skin和class的兩個(gè)模塊,它們可能在位置和大小上存在區(qū)別,所以“位置”和“大小”兩個(gè)屬性應(yīng)該是在界面描述文件中,或者說(shuō)應(yīng)該以其覆蓋其他文件的屬性。而諸如手型、字體大小、背景色等則是應(yīng)該在樣式描述文件中描述。至于每個(gè)子模塊對(duì)應(yīng)的背景圖片資源,應(yīng)該在皮膚描述文件中描述。
? ? ? 上述XML中描述的屬性,在界面構(gòu)建過(guò)程中會(huì)被讀取。可以想象,這個(gè)讀取操作是每個(gè)皮膚模塊的基礎(chǔ)功能。打個(gè)比方,png這個(gè)模塊它需要讀取name、src和subwidth三個(gè)屬性。它可能存在對(duì)應(yīng)的get_name,get_src和get_subwidth三個(gè)方法用于獲取上述屬性。但是如果一旦增加屬性,則需要新增讀取函數(shù)。而且,屬性的值的類型可能也是不同的,比如:
<class name=settingpage crbg=FBFCFD y-margin=10/>? ? ? ? 它的屬性是十進(jìn)制數(shù)或者16進(jìn)制數(shù)。那么接口的設(shè)計(jì)類型也無(wú)法做到統(tǒng)一。這樣的設(shè)計(jì)存在明顯的問(wèn)題。所以我們應(yīng)該統(tǒng)一一套獲取方法,于是kui設(shè)計(jì)了如下基礎(chǔ)類
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;}......
}? ? ? ? 所有皮膚相關(guān)的類都繼承CKuiObject,它通過(guò)load方法遍歷XML文件,并通過(guò)SetAttribute方法設(shè)置不同的屬性。我們發(fā)現(xiàn),可以統(tǒng)一這么做的一個(gè)非常重要的前提是XML庫(kù)返回的name和value值都是const char*的。這樣就規(guī)避了我們之前對(duì)數(shù)據(jù)類型無(wú)法統(tǒng)一的擔(dān)憂。但是有些屬性,我們?cè)谥髤⑴c計(jì)算或者邏輯的時(shí)候就是希望它是整形的,那么我們需要怎么處理?從設(shè)計(jì)的角度說(shuō),CKuiObject不應(yīng)該去關(guān)心屬性的類型,因?yàn)樗鼰o(wú)法得知屬性的類型,且即使得知了屬性類型,也無(wú)法做到統(tǒng)一的處理(除非使用any類型)。所以,如果真的需要做類型確定,也是應(yīng)該在不同的子類中做處理,而kui庫(kù)就是這么做的。我們?cè)賮?lái)看基類CKuiObject的SetAttribute方法,它沒(méi)有做任何有意義的事情,那么其有意義的功能是在其子類中實(shí)現(xiàn)的。這塊的設(shè)計(jì)和我之前的預(yù)想不太一樣,我本以為在CKuiObject類中保存一份屬性的map結(jié)構(gòu),并通過(guò)SetAttribute方法去填充這個(gè)結(jié)構(gòu)。不同的繼承類在繪制界面時(shí),則是去讀取這個(gè)map結(jié)構(gòu)獲取需要的信息。這樣的設(shè)計(jì)可以使得屬性的保存和獲取邏輯變得統(tǒng)一,相比于Kui設(shè)計(jì)中遍布于各個(gè)類的各種屬性,明顯統(tǒng)一的map結(jié)構(gòu)更加方便和合理。但是有人會(huì)說(shuō),這樣就限制了各個(gè)類的屬性的類型,使得它們必須是map的value類型(比如string)。其實(shí)這個(gè)擔(dān)憂大可不必,我們可以讓屬性的map是std::map<string,any>的結(jié)構(gòu),當(dāng)然這樣就得在KuiObject層確定屬性值的類型了。
? ? ? ? 我們繼續(xù)看下各個(gè)子類對(duì)SetAttribute方法的設(shè)計(jì)。Kui庫(kù)使用一組宏定義的方法去設(shè)計(jì)SetAttribute方法,這樣就像MFC中的消息映射表,開(kāi)發(fā)者只要維護(hù)好這張表就可以了。這種設(shè)計(jì)可以方便開(kāi)發(fā)者對(duì)代碼的修改和擴(kuò)展。
#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; \} \? ? ? ? 通過(guò)KUIWIN_DECLARE_ATTRIBUTES_BEGIN和KUIWIN_DECLARE_ATTRIBUTES_END的組合我們便可以得到一個(gè)完整的SetAttribute函數(shù)。可以見(jiàn)得,每次設(shè)置屬性時(shí),我們都需要嘗試設(shè)置其父類的屬性,如果其父類屬性設(shè)置成功了,則不再在此類中設(shè)置屬性。對(duì)于各個(gè)屬性,則是使用如下一些宏進(jìn)行設(shè)置
#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宏是為了屬性傳導(dǎo)的。KUIWIN_CUSTOM_ATTRIBUTE宏是為了設(shè)置屬性時(shí)調(diào)用某處理函數(shù)的。而KUIWIN_INT_ATTRIBUTE和KUIWIN_UINT_ATTRIBUTE則是將string類型的屬性轉(zhuǎn)換成int等其他類型的數(shù)據(jù)的,我們總覽Kui庫(kù),可以發(fā)現(xiàn)有若干這種類型轉(zhuǎn)換的屬性處理宏。這就是我之前所說(shuō)的,屬性的類型是在不同子類中確定的。我們看一個(gè)這組宏使用的例子
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()
? ? ? ? 不同皮膚類通過(guò)上述宏的組合,實(shí)現(xiàn)了各自的屬性設(shè)置方法。其主要實(shí)現(xiàn)的功能,就是把屬性設(shè)置到各自類的成員變量中:要么是直接的成員變量,要么是成員變量的屬性中。如上例,KUIWIN_CHAIN_ATTRIBUTE宏就是將屬性傳遞到m_imgSkin的屬性中。那什么是m_imgSkin呢?可以想象,每個(gè)由圖片繪制的皮膚模塊都有圖片的相關(guān)屬性,比如圖片的地址等,而這些模塊則可以作為一個(gè)對(duì)象存在于皮膚模塊類中,以作統(tǒng)一處理。這個(gè)就是KUI模塊皮膚類的設(shè)計(jì)思路。但是個(gè)人覺(jué)得這不是一種好的設(shè)計(jì),我覺(jué)得圖片皮膚類(m_imgSkin對(duì)應(yīng)的類)應(yīng)該是各個(gè)模塊圖片皮膚類的父類,即應(yīng)該是繼承關(guān)系,而不應(yīng)該是包含關(guān)系。打個(gè)比方,使用圖片方式繪制的按鈕和使用圖片方式繪制的Frame,應(yīng)該都是一種圖片皮膚類,所以他們應(yīng)該通過(guò)繼承的方式體現(xiàn)“是”這層關(guān)系。
? ? ? ? 現(xiàn)在我們來(lái)看下m_imgSkin對(duì)應(yīng)的圖片皮膚類的設(shè)計(jì)
class KUILIB_API CKuiImageSkin: public CKuiImage, public CKuiSkinBase
{KUIOBJ_DECLARE_CLASS_NAME(CKuiImageSkin, "imglst")
? ? ? ? 該類從繼承關(guān)系上看,它即是一種圖片類(CKuiImage),也是一種皮膚類(CKuiSkinBase)。而CKuiImageSkin類這是一個(gè)圖片組(imglst即image_list)描述的皮膚類。舉個(gè)例子,我們的按鈕一般有三態(tài):普通、按下和懸浮。如果我們將這三態(tài)對(duì)應(yīng)的背景圖片保存在三張圖中,這樣會(huì)增加文件的讀取次數(shù),同時(shí)也不利于后期維護(hù)。那我們我們就將這三張圖片合并為一張圖片組,這樣一個(gè)按鈕對(duì)應(yīng)一個(gè)圖片組,圖片數(shù)量減少三分之二。當(dāng)然這兒也不一定是三張圖片,也可能是一張,或者是可以表示更多狀態(tài)的八張。
? ? ? ? CKuiImage是一個(gè)非常重要的類,它主要完成了圖片的讀取和繪制工作。其內(nèi)容非常細(xì)節(jié),本文不做分析,有興趣的同學(xué)可以參看KUILib\Include\kuiwin\kuiimage.h。CKuiSkinBase則是皮膚類的繪制的一層封裝,它負(fù)責(zé)表達(dá)皮膚被繪制在什么位置。因?yàn)椴皇撬械钠つw都是圖片類型的(比如固定底色的),所以使用這層封裝也用于涵蓋所有皮膚的繪制。我們可以看到,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")
? ? ? ? 這么零散的皮膚基礎(chǔ)類,總得在一個(gè)地方進(jìn)行統(tǒng)籌,現(xiàn)在我們就要講解皮膚基礎(chǔ)類的工廠類——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;}
};
? ? ? ? 該類讀取皮膚描述文件,并對(duì)每個(gè)name新建對(duì)象。之后界面構(gòu)建過(guò)程中,將通過(guò)GetSkin的方法獲取每個(gè)皮膚基礎(chǔ)組件。皮膚是界面中一個(gè)比較基礎(chǔ)的組件,它是一個(gè)區(qū)域性質(zhì)的模塊。而往往界面中的很多控件是由很多基礎(chǔ)的組件組成的,比如一個(gè)樹(shù)形列表。接下來(lái)我們?cè)賮?lái)看下高于皮膚組件層次的界面模塊。
? ? ? ? 界面中,除了單純的皮膚基礎(chǔ)組件,還有一些更簡(jiǎn)單的組件,比如文字。也有些多個(gè)基礎(chǔ)組件組合的復(fù)雜皮膚模塊,比如進(jìn)度條。以Sample1為例
<icon src="ICON_MAIN" pos="5,4"/>
<text class="dlgtitle" pos="25,6">樣例程序1</text>? ? ? ??界面描述文件中的icon和text就是區(qū)別于我們上面介紹的圖片皮膚類的界面模塊。在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文件、繪制和位置計(jì)算。于是我們可以發(fā)現(xiàn)這些類主要實(shí)現(xiàn)了Load、OnPaint和OnNcCalcSize方法。稍微復(fù)雜一點(diǎn)的類是進(jìn)度條類,因?yàn)檫M(jìn)度條可以分為:進(jìn)度條外框和進(jìn)度條填充物兩種圖片,所以它也將是兩個(gè)圖片基礎(chǔ)皮膚類組合而成,我們看下其申明
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()
}? ? ? ? 有興趣的同學(xué)可以參看該類中的OnPaint和OnNcCalcSize方法是如何使用圖片基礎(chǔ)皮膚類進(jìn)行繪制的。
? ? ? ? 我們發(fā)現(xiàn)這些皮膚組件類都繼承于CKuiWindow,目測(cè)其是一個(gè)窗口控件,但是實(shí)際上它并不是
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類中,我們看到一個(gè)成員變量m_style,它就是我們之前介紹的樣式描述文件中的一項(xiàng)。我們還發(fā)現(xiàn)m_style中皮膚名的成員變量——m_strSkinName,可以見(jiàn)得皮膚名不僅可以在界面描述文件中確定,也可以在樣式描述文件中確定。CKuiWindow內(nèi)部實(shí)現(xiàn)了很多細(xì)節(jié)功能,本文不作分析,只要知道它主要做了繪制和計(jì)算大小和位置的功能即可,而且要記住它是(偽)窗口類的父類。
? ? ? ? 看過(guò)這么多基礎(chǔ)類,我們終于要看這些基礎(chǔ)類的容器——容器類,以Sample1為例,其header、body和footer三者都是容器類。但是需要注意的是,這些容器類的名字并不是header、body或者footer。我們以headerd為例看下對(duì)應(yīng)的代碼
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);......}
}? ? ? ? 可以見(jiàn)得header對(duì)應(yīng)的類是CKuiDialog,我們查看CKuiDialog的實(shí)現(xiàn)
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。它在其內(nèi)部維護(hù)了一組偽窗口信息,然后所有操作都是遍歷這些偽窗口類的處理函數(shù)實(shí)現(xiàn)消息傳遞,比如OnCreate方法的實(shí)現(xiàn)。而其父類CKuiContainerWnd則主要是定義一些虛方法,并重寫(xiě)了CKuiWindow的IsContainer方法,表明繼承于自己的類都是一個(gè)容器。
? ? ? ? 我們還要關(guān)注下容器類如何和各個(gè)組件進(jìn)行通信。在MFC的多窗口模式下,消息通過(guò)消息泵進(jìn)行傳遞。而Kui除了容器類是窗口類,其他組件類則不是窗口,那么它們之間的消息是怎么傳遞的?我們知道只有窗口才能收到消息,那么可以想到第一步處理消息的地方應(yīng)該是容器類。以窗口尺寸改變?yōu)槔?#xff0c;當(dāng)窗口尺寸改變時(shí),其內(nèi)部組件也要被調(diào)整。首先容器類收到消息
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中,會(huì)調(diào)用重置組件位置的邏輯
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都將調(diào)用KuiSendMessage方法傳遞消息,從函數(shù)命名上看KuiSendMessage承襲了MFC中消息傳遞的模式。但是實(shí)際上這個(gè)只是一種“寫(xiě)法”,和MFC那套不是一套機(jī)制。它只是一個(gè)函數(shù)調(diào)用
LRESULT KuiSendMessage(UINT Msg, WPARAM wParam = 0, LPARAM lParam = 0){LRESULT lResult = 0;SetMsgHandled(FALSE);ProcessWindowMessage(NULL, Msg, wParam, lParam, lResult);return lResult;}? ? ? ? 而ProcessWindowMessage方法也不是向窗口傳遞消息,而只是調(diào)用各個(gè)繼承于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; \} \
? ? ? ? 這個(gè)時(shí)候消息還是在容器窗口中處理,最終它會(huì)遍歷容器類所有子模塊,并調(diào)用子模塊的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()
};? ? ? ? 各個(gè)繼承于CKuiWindow的類的ProcessWindowMessage方法將被調(diào)用,同時(shí)“消息”將被傳遞到處理WM_WINDOWPOSCHANGED的函數(shù)中。默認(rèn)情況下,會(huì)調(diào)用CKuiWindow的
void OnWindowPosChanged(LPWINDOWPOS lpWndPos){m_rcWindow.MoveToXY(lpWndPos->x, lpWndPos->y);SIZE sizeRet = {lpWndPos->cx, lpWndPos->cy};KuiSendMessage(WM_NCCALCSIZE, TRUE, (LPARAM)&sizeRet);? ? ? ? 這個(gè)時(shí)候消息改成了WM_NCCALCSIZE,為什么要改成這個(gè)消息?因?yàn)檫@個(gè)消息在CKuiWindow的ProcessWindowMessage方法中不會(huì)被處理,從而將會(huì)被子類的方法處理,這樣就達(dá)到了“消息傳遞”的目的。以第一個(gè)需要被重繪的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;}? ? ? ? 相對(duì)于處理WM_SIZE消息,處理WM_PAINT消息則簡(jiǎn)單的多:容器類直接調(diào)用模塊的重繪方法。
? ? ? ? 最后回到總體框架。Kui并沒(méi)有將這些容器類直接暴露在最外面,而實(shí)際通過(guò)一系列模板類實(shí)現(xiàn)功能
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界面庫(kù)主要的脈絡(luò)給理清了。對(duì)于一個(gè)完整的界面庫(kù),我只是從一些我關(guān)心的角度去分析了其實(shí)現(xiàn)的大體步驟。其中很多細(xì)節(jié)處理雖然有待商榷,但是其中的精髓還是不少的。有興趣的同學(xué)可以在源碼中挖掘出自己感興趣的內(nèi)容。最后附上類圖關(guān)系。
總結(jié)
以上是生活随笔為你收集整理的以金山界面库(openkui)为例思考和分析界面库的设计和实现——代码结构(完)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 服务器架设笔记——httpd插件支持my
- 下一篇: WMI技术介绍和应用——接收事件