duilib基本框架

最近我一個同學在專案中使用到了duilib框架,但是之前並沒有接觸過,他與我討論這方面的內容,看著官方給出的精美的例子,我對這個庫有了很大的興趣,我自己也是初學這個東東,我在網上花了不少時間來找相關的資料,但是找到的不多,官方給的文件又不全面,但是我還是找到了一些博主貢獻的優秀的博文,現在我是通過博文上的講解加上自己檢視原始碼的一些心得,正在艱難的前行。現在正在看的是博主Alberl在部落格園中的duilib基礎教程中的內容,下面的程式碼都是在他部落格中給出程式碼的基礎上做了一點小小的修改。點選這裡跳轉到對應的部落格,以及博主夜雨無聲的部落格,部落格地址

duilib的簡介

國內首個開源 的directui 介面庫,它提供了一個所見即所得的開發工具——UIDesigner,它只有主框架視窗,其餘的空間全部採用繪製的方式實現,所以對於控制元件來說沒有控制代碼和視窗類等內容,它通過UIDesigner工具將使用者定義的視窗儲存在xml檔案中,在建立視窗時讀取xml檔案中的內容,來繪製相應的控制元件。目前有許多介面採用duilib編寫,大家可以去網上搜集相關資料。

環境的配置

首先我們去github上獲取相關的原始碼,這個是對應的專案地址:https://github.com/duilib/duilib
下載完後,在目錄中找到一個.sln結尾的檔案,使用visual studio編譯器開啟,開啟後發現有一個duilib的專案,以及其他,其實真正有用的就是這個duilib,其餘的都是官方給出的例子程式碼。一般只需要編譯這個duilib專案就可以了,當初沒注意直接點了編譯全部的,結果報了一堆錯誤,其實都是沒有對應的lib和dll檔案造成的。在VS環境下有一個編譯選項,如下圖所示
這裡寫圖片描述
上面有4個編譯選項,最好將所有的都編譯一遍,這樣在對應專案的bin目錄下會生成四個dll檔案,這幾個檔案分別是debug下的UNICODE編碼檔案、ANSI檔案以及Release版本下的UNICODE編碼檔案、ANSI檔案。u代表unicode d代表debug。另外在lib目錄下會生成對應的lib檔案。
在新建的工程中,點選屬性在屬性對話方塊中選擇VC 目錄,在原始檔,庫檔案,包含檔案中將對應的路徑新增進去,分別是專案目錄和lib檔案目錄。如下圖(剛開始有點問題所以新增的內容有點多,但是不影響正常使用):
這裡寫圖片描述
最後可以在環境變數的Path變數中新增對應的dll路徑,這樣就不需要將dll檔案拷貝到自己專案的exe檔案所在位置處。
其實上述的環境可以不用設定,如果不設定,在編寫程式包含相關路徑時就需要給定完整的路徑。
到此處為止,整個開發環境就已經搭建好了,剩下的就是程式碼的編寫了。

基本的框架視窗

首先新建一個Win32型別的專案,新增主函式。然後建立一個新類,我們叫做CDuiFrameWnd,下面是類的原始碼

//標頭檔案
#include <DuiLib\UIlib.h>
using namespace DuiLib;
class CDuiFrameWnd :
public CWindowWnd
{
public:
CDuiFrameWnd();
~CDuiFrameWnd();
virtual LPCTSTR GetWindowClassName() const;
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
protected:
CPaintManagerUI m_PaintManager;
};
//cpp檔案
LPCTSTR CDuiFrameWnd::GetWindowClassName() const
{
return _T("DuiFrameWnd");
}
LRESULT CDuiFrameWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
LRESULT lRes = 0;
if (WM_CLOSE == uMsg)
{
::CloseWindow(m_hWnd);
::DestroyWindow(m_hWnd);
}
if (WM_DESTROY == uMsg)
{
::PostQuitMessage(0);
}
return __super::HandleMessage(uMsg, wParam, lParam);
}

為了能夠使用對應的dll檔案,還需要引入對應的lib檔案,我們在公共的標頭檔案中加入如下程式碼

#ifdef _DEBUG
#   ifdef _UNICODE
#       pragma comment(lib, "Duilib_ud.lib")
#   else
#       pragma comment(lib, "Duilib_d.lib")
#   endif
#else
#   ifdef _UNICODE
#       pragma comment(lib, "Duilib_u.lib")
#   else
#       pragma comment(lib, "Duilib.lib")
#   endif
#endif

在主函式中的程式碼如下:

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR    lpCmdLine,
_In_ int       nCmdShow)
{
CPaintManagerUI::SetInstance(hInstance);
CDuiFrameWnd duiFrame;
//#define UI_WNDSTYLE_FRAME      (WS_VISIBLE | WS_OVERLAPPEDWINDOW)
duiFrame.Create(NULL, _T("測試"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
duiFrame.ShowWindow();
CPaintManagerUI::MessageLoop();
return 0;
}

這些程式碼就可以幫助我們生成基本的框架視窗,另外我們需要時刻記住的是duilib是對win32 API的封裝,所以可以直接使用win32的程式設計方式,如果以後有不會用的地方完全可以使用win32 的API來完成相關的功能的編寫。

框架的剖析

既然它能夠生成單文件的框架視窗,那麼程式碼中所做的幾步基本上與用純粹的win32 API相同,所以我們沿著這個思路來進行框架的簡單剖析。
主函式中首先是程式碼CPaintManagerUI::SetInstance(hInstance);至於類CPaintManagerUI到底有什麼作用,這個我也不太清楚,現在我還沒有仔細看關於這個類的相關程式碼,這句話主要還是獲取了程序的例項控制代碼。現在先不關心這個。下面的幾步主要是在類CDuiFrameWnd中完成或者說在它的基類CWindowWnd中完成。

建立視窗類

主函式中的第二段程式碼主要完成的是類CDuiFrameWnd物件的建立,我們跟到對應的建構函式中發現它並沒有做多餘的操作,現在先不管它是如何構造的,它下面就是呼叫了類的Create函式建立了一個視窗,這個函式的程式碼如下:

HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, int x, int y, int cx, int cy, HMENU hMenu)
{
if( GetSuperClassName() != NULL && !RegisterSuperclass() ) return NULL;
if( GetSuperClassName() == NULL && !RegisterWindowClass() ) return NULL;
m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);
ASSERT(m_hWnd!=NULL);
return m_hWnd;
}

我們主要來看第二個if中的程式碼,首先獲得了父視窗的字串為NULL,然後執行RegisterWindowClass,我們進一步跟到RegisterWindowClass中,它的程式碼如下:

bool CWindowWnd::RegisterWindowClass()
{
WNDCLASS wc = { 0 };
wc.style = GetClassStyle();
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hIcon = NULL;
wc.lpfnWndProc = CWindowWnd::__WndProc;
wc.hInstance = CPaintManagerUI::GetInstance(); //之前設定的例項控制代碼在這個地方使用
wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName  = NULL;
wc.lpszClassName = GetWindowClassName();
ATOM ret = ::RegisterClass(&wc);
ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS);
return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
}

我們發現首先進行的是視窗類的建立,在建立視窗類時主要關心的是視窗類的lpfnWndProc成員和lpszClassName 。lpszClassName 呼叫了函式GetWindowClassName,這個函式我們在派生類中進行了重寫,所以根據多型它會呼叫派生類的GetWindowClassName函式,將我們給定的字串作為視窗類的類名

註冊視窗類

從上面的程式碼可以看出註冊的程式碼也是放在RegisterWindowClass中。在最後呼叫了RegisterClass函式完成了註冊。

建立視窗

當RegisterWindowClass執行完成後,會接著執行下面的程式碼,也就是 m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);完成建立視窗的任務。

顯示視窗

Create函式執行完成後,會接著執行下面的duiFrame.ShowWindow();我們跟到這個函式中,函式程式碼如下:

void CWindowWnd::ShowWindow(bool bShow /*= true*/, bool bTakeFocus /*= false*/)
{
ASSERT(::IsWindow(m_hWnd));
if( !::IsWindow(m_hWnd) ) return;
::ShowWindow(m_hWnd, bShow ? (bTakeFocus ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE) : SW_HIDE);
}

函式ShowWindow預設傳入引數為bShow = true bTakeFocus = false;在最後進行ShowWindow函式的呼叫時,根據bShow和bTakeFocus來進行值得傳入,根據程式碼我們發現,當不傳入引數時呼叫的其實是這樣的程式碼ShowWindow(m_hWnd, SW_SHOWNOACTIVATE);

訊息迴圈

訊息迴圈其實是通過程式碼CPaintManagerUI::MessageLoop();完成,我們跟到MessageLoop函式中看

    MSG msg = { 0 };
while( ::GetMessage(&msg, NULL, 0, 0) ) {
if( !CPaintManagerUI::TranslateMessage(&msg) ) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}

在這個函式中完成了訊息迴圈。

回撥函式

上面我們留了一個lpfnWndProc函式指標沒有說,現在來說明這個部分,跟進到對應的建構函式中,發現類本身不做任何操作,但是父類的建構函式進行了相關的初始化操作,下面是對應的程式碼

CWindowWnd::CWindowWnd() : m_hWnd(NULL), m_OldWndProc(::DefWindowProc), m_bSubclassed(false)
{
}

這樣就將lpfnWndProc指向了__WndProc,用於處理預設的訊息。
這是一個靜態的處理函式,下面是它的程式碼:

LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CWindowWnd* pThis = NULL;
if( uMsg == WM_NCCREATE ) {
LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
pThis = static_cast<CWindowWnd*>(lpcs->lpCreateParams);
pThis->m_hWnd = hWnd;
//當開始建立視窗將視窗類物件的指標放入到對應的GWLP_USERDATA欄位中
::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(pThis));
} 
else {
//取出視窗類物件的指標
pThis = reinterpret_cast<CWindowWnd*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
if( uMsg == WM_NCDESTROY && pThis != NULL ) {
LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam);
::SetWindowLongPtr(pThis->m_hWnd, GWLP_USERDATA, 0L);
if( pThis->m_bSubclassed ) pThis->Unsubclass();
pThis->m_hWnd = NULL;
pThis->OnFinalMessage(hWnd);
return lRes;
}
}
if( pThis != NULL ) {
return pThis->HandleMessage(uMsg, wParam, lParam);
} 
else {
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}

上述的程式碼,在建立視窗時將視窗類物件指標存入到對應的位置便於在其他位置取出並使用。通過return pThis->HandleMessage(uMsg, wParam, lParam);這句話呼叫的具體物件的HandleMessage,我們在對應的派生類中定義了相應的虛擬函式,所以根據多型它會呼叫我們重寫的虛擬函式來處理具體訊息,至於我們不關心的訊息,它會呼叫LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam);或者DefWindowProc,通過對基類的建構函式的檢視,我們發現其實m_OldWndProc就是DefWindowProc。

總結

上面我們說明了duilib的基本框架,下面來總結一下:
1. CPaintManagerUI::SetInstance(hInstance);設定程序的例項控制代碼,這個值會在註冊視窗類時使用
2. 在CWindowWnd類中由Create函式完成視窗類的建立於註冊,以及視窗的建立工作
3. CWindowWnd類中的ShowWindow函式用於顯示視窗
4. 訊息迴圈由CPaintManagerUI::MessageLoop();程式碼完成
5. 最後需要重寫MessageHandle函式用於處理我們感興趣的訊息。並且在最後需要呼叫基類的MessageHandle函式,主要是為了呼叫DefWindowProc處理我們不感興趣的訊息。