DuiLib教程–從win32視窗開始

win32視窗程式

大家看到這個標題肯定會問“大哥,你是不是搞錯了,我是來學DuiLib的,你給我扯什麼win32程式”。其實,萬變不離其宗,DuiLib也只是一個ui介面庫,還是得建立在win32程式的基礎上的,還是得從winmain這個狗洞鑽進去。如果不搞懂win32的視窗訊息機制,DuiLib的訊息處理過程肯定得繞暈你。所以我建議大家建立一個空的win32應用程式,新增一個winmain.cpp,把下面的程式碼手打一遍,每一行的作用都搞懂(不會的windows API 一定要自己去查MSDN),就算是為學習DuiLib打下了一個良好的基礎。

#include <Windows.h>
#include <stdio.h>
#include <tchar.h>
// 視窗過程
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam){
switch (message)
{
case WM_LBUTTONDOWN:
MessageBox(hWnd, "你單擊了滑鼠左鍵", "WM_LBUTTONDOWN", MB_OK);
break;
case WM_CHAR:
char szChar[64];
sprintf_s(szChar, 64, "你按下了鍵盤鍵:%c", wParam);
MessageBox(hWnd, szChar, "WM_CHAR", MB_OK);
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此新增任意繪圖程式碼...
SetTextColor(hdc, RGB(255,0,0));
SetBkColor(hdc, RGB(0,255,0));
TextOut(hdc, 200, 200, "Hello World!", strlen("Hello World!"));
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
break;
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR    lpCmdLine, int       nCmdShow){
// 註冊視窗類
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style          = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc    = WndProc;
wcex.cbClsExtra     = 0;
wcex.cbWndExtra     = 0;
wcex.hInstance      = hInstance;
wcex.hIcon          = NULL;
wcex.hCursor        = LoadCursor(NULL, IDC_HAND);
wcex.hbrBackground  = (HBRUSH)(GetStockObject(BLACK_BRUSH));
wcex.lpszMenuName   = NULL;
wcex.lpszClassName  = "mywndclass";
wcex.hIconSm        = NULL;
RegisterClassEx(&wcex);
// 建立視窗
HWND hWnd = CreateWindowEx(0, "mywndclass", "This is a win32 wnd", WS_OVERLAPPEDWINDOW,
100, 100, 800, 600, NULL, NULL, hInstance, NULL);
// 顯示視窗
ShowWindow(hWnd, nCmdShow);
// 視窗訊息迴圈
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)){
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int) msg.wParam;
}

執行效果圖如下:
win32

是不是很醜,怪我咯,我要是做的還看,那公司就不會給我們配個美美的美工了。言歸正傳,大家可以修改下視窗背景刷、字型背景色和前景色,然後再看看效果,加深理解和印象。

是不是SO easy,只需要記住這條主線,註冊視窗類->建立視窗->顯示視窗->視窗訊息迴圈,在接下來學習DUiLib的過程中我們就不會被繞暈了。

使用DuiLib的CWindowWnd

上面的win32視窗程式還是程序導向程式設計,C 是擅長物件導向程式設計OOP的,我們很自然的想到可以用一個類去封裝視窗控制代碼HWND和對應建立視窗、顯示視窗等方法,DuiLib中CWindowWnd就是這個視窗類,分別在UIBase.h和UIBase.cpp中宣告和定義。

拿到一個類,首先就是到標頭檔案中去看她提供的方法,一個封裝良好的類通過方法名就能大致猜出她的作用。所以在看我的DuiLib教程時一定要開啟DuiLib工程find到對應的位置。我只會貼出關鍵的程式碼段講解,其它的你應該到原始碼中去自己有個大概瞭解,有興趣的甚至可以自己嘗試去封裝一個視窗類,再來對照看DuiLib中的CWindowWnd類,這樣可以鍛鍊你的設計類能力。

下面先建立一個空win32工程,在winmain.cpp新增如下程式碼測試下CWindowWnd類:

#include "../../DuiLib/UIlib.h"
using namespace DuiLib;
#ifdef _DEBUG
#   ifdef _UNICODE
#       pragma comment(lib, "../../Lib/DuiLib_ud.lib")
#   else
#       pragma comment(lib, "../../Lib/DuiLib_d.lib")
#   endif
#else
#   ifdef _UNICODE
#       pragma comment(lib, "../../Lib/DuiLib_u.lib")
#   else
#       pragma comment(lib, "../../Lib/DuiLib.lib")
#   endif
#endif
class CFrameWnd : public CWindowWnd
{
public:
virtual LPCTSTR GetWindowClassName() const{
return _T("FrameWnd");
}
virtual void OnFinalMessage(HWND hWnd){
delete this;
}
};
int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, 
LPTSTR lpCmdLine, int nShowCmd ){
// new一個視窗物件
CFrameWnd* pFrame = new CFrameWnd;
// 註冊視窗類、建立視窗
pFrame->Create(NULL, _T("sample01"), UI_WNDSTYLE_FRAME, UI_WNDSTYLE_EX_FRAME,
100, 100, 800, 600, NULL);
// 顯示視窗、進入視窗訊息迴圈
pFrame->ShowModal();
return 0;
}

使用CFrameWnd繼承自CWindowWnd,CWindowWnd必須實現的一個純虛介面是GetWindowClassName來表明她的視窗類名。在OnFinalMessage中delete this是因為DuiLib中需要使用new來生成一個視窗,delete可以防止記憶體洩漏(在後面的DuiLib程式中可以看到都只有new而沒有delete,這是因為DuiLib內部在視窗銷燬時已經做了delete的操作)。編譯生成這段程式碼,執行後也能順利建立顯示一個視窗,讓我們看一下關鍵的幾個方法:

一、建立視窗

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;
}

可以發現在呼叫CreateWindowEx這個windows API前會先呼叫RegisterWindowClass註冊視窗類。 還有一點需要注意的是 CreateWindowEx的最好一個引數將this指標作為引數傳遞了進去,這個玩意在後面可有妙用。

二、顯示視窗並進入訊息迴圈

UINT CWindowWnd::ShowModal()
{
ASSERT(::IsWindow(m_hWnd));
UINT nRet = 0;
HWND hWndParent = GetWindowOwner(m_hWnd);
::ShowWindow(m_hWnd, SW_SHOWNORMAL);
::EnableWindow(hWndParent, FALSE);
MSG msg = { 0 };
while( ::IsWindow(m_hWnd) && ::GetMessage(&msg, NULL, 0, 0) ) {
if( msg.message == WM_CLOSE && msg.hwnd == m_hWnd ) {
nRet = msg.wParam;
::EnableWindow(hWndParent, TRUE);
::SetFocus(hWndParent);
}
if( !CPaintManagerUI::TranslateMessage(&msg) ) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
if( msg.message == WM_QUIT ) break;
}
::EnableWindow(hWndParent, TRUE);
::SetFocus(hWndParent);
if( msg.message == WM_QUIT ) ::PostQuitMessage(msg.wParam);
return nRet;
}

可以發現先呼叫了ShowWindow去顯示視窗,然後進入了我們熟悉的GetMessage訊息迴圈。考慮到可能是子視窗關閉WM_CLOSE 所以有些額外處理,CPaintManagerUI這玩意比較龐大,包攬了繪畫細節和訊息管理, 我們這集還不打算修理它,留到後面解剖。只需要知道TranslateMessage是進行了一個訊息預處理過濾的功能。

三、視窗過程

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;
::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);
}
}

視窗過程因為是回撥函式,所以宣告成static型別,static型別是不能使用非static成員的,那麼問題來了,他怎麼去獲取CWindowWnd物件指標,然後去呼叫CWindowWnd裡面的方法呢。這就是之前CreateWindowEx將this指標傳進去的原因了,在WM_NCCREATE時通過將lParam轉化成LPCREATESTRUCT,裡面的lpCreateParams就是this指標了,然後通過SetWindowLongPtr將this設定為使用者資料,再處理其它的WM_訊息時通過GetWindowLongPtr獲取到this指標,進而可以呼叫CWindowWnd的方法了(比如這裡呼叫了HandleMessage來處理感興趣的WM_訊息),這是我們自己封裝視窗類難以想到的一點吧(小tips:在宣告回撥函式時我們一般將最後一個引數設定為使用者資料)。當然有的同學會說使用map容器將HWND與CWindowWnd對應起來,這也是一種方法,但總歸沒有使用使用者資料來的直接簡便。

這集就先pass了CWindowWnd類,一步一個腳印我們終將走出DuiLib的沙漠。
這裡寫圖片描述