duilib開發基礎:創建自定義控件的過程

轉載請說明原出處,謝謝~·http://blog.csdn.net/zhuhongshu/article/details/45362751


       用Duilib開發界面時,很多情況下庫自帶的控件不滿足需求,就需要基於Duilib建立自定義控件(自繪新的控件,或者用來封裝win32的子窗體,來顯示視頻、網頁等)。

       在羣裏經常會有剛接觸Duilib的朋友問題怎麼建立自己的自定義控件,或者建立的控件無法正常創建出來。我簡單寫一篇博客,把創建自定義控件的完整過程,和一些注意事項說明一下。另外說一下如果把win32的子窗體封裝爲控件,希望能有幫助。

       創建自定義控件包含兩個過程:
       1、繼承現有的控件類創建新的控件類
       2、讓程序識別新的控件並可以在xml中使用


創建新的控件類:

       首先從的現有的Duilib控件中選擇一個最合適的控件類,繼承他然後重寫幾個接口。
       我這裏拿仿酷狗的換膚窗體中的一個自繪控件做例子(原文地址:http://blog.csdn.net/zhuhongshu/article/details/38491389
       仿酷狗音樂盒源代碼:http://blog.csdn.net/zhuhongshu/article/details/41037875



      
      


      爲了做出換膚控件,首先選擇CButtonUI爲父類,因爲CButtonUI控件本身就已經包含了normal、hot、pushed等狀態,同時包含單擊事件。

     
#ifndef SKIN_PICKER_PICTURE_ITEM_H
#define SKIN_PICKER_PICTURE_ITEM_H

//xml sample:<SkinPikerPictureItem name="" width="118" height="70" bkimage="UI\BKImage\1small.jpg" bkname="測試" author="Redrain" />
//類名和接口名,在CreateControl函數中會用到
const TCHAR kSkinPickerPictureItemClassName[] =	_T("SkinPikerPictureItemUI");
const TCHAR kSkinPickerPictureItemInterface[] =	_T("SkinPikerPictureItem");

//黑色的前景圖的位置
const TCHAR kSkinPickerPictureItemForeImage[] =	_T("file='UI\\LeftTab\\listitem\\ListBk.png' fade='150'");

//邊框的顏色、圖片名稱的文字顏色、作者信息的文字顏色
const DWORD kBorderColor					  = 0xFF64B0FA;
const DWORD kBkNameColor					  = 0xFFFFFFFF;
const DWORD kAuthorColor					  = 0xFFAAAAAA;

class CSkinPikerPictureItemUI : public CButtonUI
{
public:
	CSkinPikerPictureItemUI();

	LPCTSTR GetClass() const;
	LPVOID GetInterface(LPCTSTR pstrName);

	void SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue);
	void PaintStatusImage(HDC hDC);

private:
	CDuiString m_BkName;
	CDuiString m_Author;
};

#endif // SKIN_PICKER_PICTURE_ITEM_H


CSkinPikerPictureItemUI::CSkinPikerPictureItemUI()
{
	m_Author = _T("作者:");
}
LPCTSTR CSkinPikerPictureItemUI::GetClass() const
{
	return kSkinPickerPictureItemClassName;
}

LPVOID CSkinPikerPictureItemUI::GetInterface(LPCTSTR pstrName)
{
	if( _tcscmp(pstrName, kSkinPickerPictureItemInterface) == 0 ) return static_cast<CSkinPikerPictureItemUI*>(this);
	return CButtonUI::GetInterface(pstrName);
}

void CSkinPikerPictureItemUI::SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue)
{
	if( _tcscmp(pstrName, _T("bkname")) == 0 ) m_BkName = pstrValue;
	else if( _tcscmp(pstrName, _T("author")) == 0 ) m_Author += pstrValue;
	CButtonUI::SetAttribute(pstrName, pstrValue);
}

void CSkinPikerPictureItemUI::PaintStatusImage(HDC hDC)
{
	CButtonUI::PaintStatusImage(hDC);

	if( IsFocused() ) m_uButtonState |= UISTATE_FOCUSED;
	else m_uButtonState &= ~ UISTATE_FOCUSED;
	if( !IsEnabled() ) m_uButtonState |= UISTATE_DISABLED;
	else m_uButtonState &= ~ UISTATE_DISABLED;

	if( (m_uButtonState & UISTATE_PUSHED) != 0 || (m_uButtonState & UISTATE_HOT) != 0) {

		DrawImage(hDC, kSkinPickerPictureItemForeImage) ;

		//計算作者信息文字和背景圖片名字文字的顯示位置,這裏是用了硬編碼,請使用者自己修改
		RECT rcBkName = m_rcItem;
		LONG nTextPadding = (m_rcItem.right - m_rcItem.left  - CRenderEngine::GetTextSize(hDC, GetManager(),\
			m_BkName.GetData(), m_iFont, m_uTextStyle).cx) / 2;
		rcBkName.left += nTextPadding;
		rcBkName.right -= nTextPadding;
		rcBkName.top += 15;
		rcBkName.bottom = rcBkName.top + 20;

		RECT rcAuthor = m_rcItem;
		nTextPadding = (m_rcItem.right - m_rcItem.left - CRenderEngine::GetTextSize(hDC, GetManager(),\
			m_Author.GetData(), m_iFont, m_uTextStyle).cx) / 2;
		rcAuthor.left += nTextPadding;
		rcAuthor.right -= nTextPadding;
		rcAuthor.top += 40;
		rcAuthor.bottom = rcAuthor.top + 20;

		CRenderEngine::DrawText(hDC, m_pManager, rcBkName, m_BkName, kBkNameColor, m_iFont, m_uTextStyle);
		CRenderEngine::DrawText(hDC, m_pManager, rcAuthor, m_Author, kAuthorColor, m_iFont, m_uTextStyle);
		CRenderEngine::DrawRect(hDC, m_rcItem, 2, kBorderColor);
	
	}

}



       新的控件名爲CSkinPickerPictureItemUI。一般來說,建立新控件後,最先應該重寫的兩個函數是GetClass和GetInterface。他們後用來區分控件的類型的虛函數,用於動態識別控件類型和做控件的類型轉換。

       從Duilib的自帶控件上可以看出,比如當前的自定義控件類名爲CSkinPickerPictureItemUI,那麼GetClass函數返回的字符串SkinPickerPictureItemUI。而GetInterface函數是根據傳入的參數,是否與自身的字符串匹配,來決定能否把自己轉換爲需要的控件類型。GetInterface中用來匹配的字符串,應該與xml中的對應的控件的標籤名稱一直,這裏應該是SkinPickerPictureItem。

      比如CButtonUI類,GetClass對應ButtonUI,GetInterface對應Button。這不是強制的,但是保持這個風格很重要!


      理論上,完成這兩個接口就創建好最基本的自定義控件了。但是爲了讓自定義控件的行爲和外觀更豐富,就需要重寫更多的函數了,我這裏把經常會重寫的函數說明一下!

 virtual void DoEvent(TEventUI& event);
 virtual void DoPaint(HDC hDC, const RECT& rcPaint);
 virtual void PaintBkColor(HDC hDC);
 virtual void PaintBkImage(HDC hDC);
 virtual void PaintStatusImage(HDC hDC);
 virtual void PaintText(HDC hDC);
 virtual void PaintBorder(HDC hDC);
 virtual void DoInit();
 virtual void SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue);
 virtual bool IsVisible() const;
 virtual void SetVisible(bool bVisible = true);
 virtual void SetInternVisible(bool bVisible = true); // 僅供內部調用,有些UI擁有窗口句柄,需要重寫此函數
 virtual void SetPos(RECT rc);






       以上列出的函數,是最常被重寫的。
       
       DoEvent函數:控件的核心函數,他是消息處理函數,用來處理Duilib封裝過的各個消息,比如鼠標的移入移出、出現的懸停、單擊雙擊、右擊、滾輪滑動、獲取焦點、設置光標等等。所以如果你的控件需要修改這些行爲,必須重寫這個函數,具體的處理方法可以參考Duilib現有的控件或者仿酷狗程序。


       DoPaint函數:控件的核心函數,他是控件的繪製處理函數,當Duilib底層要重新繪製這個控件,或者控件自己調用Invalidata函數強制自己刷新時,這個函數就會被觸發,在這個函數裏完成了各種狀態下的背景前景繪製,背景色繪製,文本繪製,邊框繪製。而這個函數會調用PaintBkColor、PaintBkImage、PaintStatusImage、PaintText、PaintBorder等函數來完成各個繪製步驟。所以你可以根據需求來決定重寫DoPaint或者只重寫某幾個PaintXXX函數。DoPaint函數經常和DoEvent函數結合使用,DoEvent得到了某個消息後,改變控件的狀態,然後調用Invalidate函數讓控件重繪。


      SetAttribute函數:用於擴展自定義控件的屬性,Duilib的控件本身已經包含name、text、bkimage等屬性,如果要增加新屬性,就需要重寫此函數來擴展屬性,上面的CSkinPickerPictureItemUI例子中已經有用法了。


      DoInit函數:當控件被添加到容器後,由容器調用的函數。在這裏,整個Duilib程序框架已經完成,當需要做一些界面的初始操作時可以重寫此函數,常見的用法就是在此建立Win32子窗體並且封裝它,相關內容我在後面再說。


     IsVisible、SetVisible、SetInternelVisible、SetPos:這幾個函數同樣也是,當控件封裝了Win32子窗口後,重寫這幾個函數來控制子窗口的顯示和隱藏、和位置。


      這樣就創建完成了自定義控件。

識別新控件:


       自定義控件創建完畢後,需要做的就是讓控件可以被xml佈局識別出來。爲此我們需要完成Duilib的IDialogBuilderCallback接口,重寫這個接口中的CreateControl函數。

       通常情況下,可以讓窗體類繼承IDialogBuilderCallback接口並且重寫CreateControl(DuiLib自帶的WindowImplBase窗體類已經繼承了這個接口,如果是繼承WindowImplBase的話就直接重寫CreateControl就可以了)。函數處理方法是比較傳入的字符串,根據字符串來決定返回什麼控件的指針,這個傳入的字符串就是xml文件中控件的標籤,比如<Button />中的字符串Button。

CControlUI* CSkinPickerDialog::CreateControl(LPCTSTR pstrClass) 
{
	if (_tcsicmp(pstrClass, kSkinPickerPictureItemInterface) == 0)
		return	new CSkinPikerPictureItemUI();

	return NULL;
}


      習慣上,在xml中自定義控件的標籤名稱應該和控件的GetInterface中的判斷字符串一致,比如這裏就是SkinPickerPictureItem。這樣,在解析xml過程中,當解析到名爲SkinPickerPictureItem的標籤時,就會創建出CSkinPickerPictureItemUI控件了。


       實際上,誰來繼承IDialogBuilderCallback接口肯定都可以,比如QQDemo和仿酷狗裏面,都給自定義控件本身繼承了這個接口。

      
       當程序響應WM_CREATE消息時,會建立一個CDialogBuilder對象,並且調用他的Create方法來解析xml文件。


CControlUI* CDialogBuilder::Create(STRINGorID xml, LPCTSTR type, IDialogBuilderCallback* pCallback, 
		CPaintManagerUI* pManager, CControlUI* pParent)


        這個函數 的第一個參數指定爲xml文件的路徑;第二個參數一般指定爲NULL,我這裏不詳解了;第三個參數,就是識別自定義控件的關鍵了,這個參數要指定爲繼承了IDialogBuilderCallback接口的類對象的指針,比如窗體類繼承IDialogBuilderCallback,這個參數就填寫窗體類對象的指針。只有填寫了這個參數,自定義控件纔會被識別,經常有人問自己的自定義控件爲什麼無法被識別。多數情況就是這裏沒處理好;第四個參數指定CPaintManagerUI類對象指針,這個肯定會伴隨着窗體類對象一起存在。最後一個參數一般爲NULL。

        這幾步都完成後,你的自定義控件就可以被xml佈局正確的識別並創建了。至此,創建自定義控件的基本過程就完成了!如果有不明白的,可以多看看仿酷狗的代碼、QQDemo等。


封裝Win32控件或者Win32子窗口:


       如果要給Duilib,增加一個視頻播放控件,一般來說視頻播放庫都需要依賴一個子窗口。這時,就應該自定義個控件,並且封裝維護一個子窗口了。

       封裝的子窗口有三種:第一種比較簡單、單純封裝一個子窗口、讓視頻庫一類的庫依賴;第二種麻煩一些、封裝子窗口、並且處理子窗口的消息;第三種和第二種類似、封裝Win32的控件並且處理他的消息。


      單純封裝子窗口:


      這時就需要重寫我之前提到的DoInit函數和SetVisbile等函數了。首先在自定義控件內聲明HWND類型的m_hWnd成員變量來保存子窗體指針。

      在DoInit函數裏,調用CreateWindowEx函數,創建一個win32子窗體,並且用m_hWnd保存句柄。比如:

m_hhWnd = CreateWindow(_T("#32770"), _T("WndMediaDisplay"), WS_VISIBLE | WS_CHILD, 0, 0, 0, 0, m_PaintManager.GetPaintWindow(), (HMENU)0, NULL, NULL);


      然後在SetVisible等函數內控制子窗體的顯示隱藏;在SetPos函數內控制子窗體的位置、限制在本控件的範圍內。

     這樣就封裝好了win32子窗口,然後可以把這個窗體句柄用於視頻播放等。

 
      封裝子窗口並處理他的消息:


      這時就比較麻煩了,參見Duilib的CEditUI控件等。我們需要繼承CWindowWnd另外封裝一個窗體類,窗體類的封裝不屬於本文範圍,我就不細說了。重寫窗體類的HandleMessage函數,來響應各種WM_XXX消息。

      然後在我們的自定義控件內,不再聲明HWND類型m_hWnd變量了,而是自定剛纔的窗體類的對象。然後在DoInit函數內調用這個對應的Create函數函數來創建窗體類。然後同樣還是維護這個窗體的顯示隱藏、和位置。

     關於這種控件的封裝,可以參考我寫的webkit內核瀏覽器控件、裏面是完整的封裝了Win32子窗體、並且處理了他的消息,用於顯示webkit內核渲染的網頁。地址:http://blog.csdn.net/zhuhongshu/article/details/38540711


      封裝Win32控件並處理他的消息:


      這個可以參考CEditUI控件的處理代碼,思想上和封裝子窗口並處理消息是一樣的。同樣也可以參考webkit內核瀏覽器控件代碼。不過與之不同的是,我們需要重寫兩個函數

		LPCTSTR GetWindowClassName() const;
		LPCTSTR GetSuperClassName() const;



      這裏最主要的就是處理GetSuperClassName函數,這個函數的作用就是超類化,而封裝子窗口並處理消息是子類化,這兩個操作恰好相反。在GetSuperClassName函數內,要範圍Win32控件對應的類名、Duilib檢測到GetSuperClassName函數函數後就會創建Win32控件。這時我們處理HandleMessage函數,就可以處理到Win32控件的消息的。具體的處理邏輯請參見CEditUI控件。


總結:

        差不多就說道這裏了,把常見的自定義控件的基本步驟說明了一下,實際開發時還要多看Duilib的源碼,才能稱心如意的開發控件,希望對剛接觸Duilib的朋友有幫助!


Redrain  QQ:491646717  2014.9.19
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章