1. 程式人生 > >Duilib學習之基礎

Duilib學習之基礎

一個Win32視窗程式

建立一個空的Win32工程,然後輸入以下程式碼。

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

執行效果

所用的Windows API在MSDN中都可以查到,這裡重點說一下TranslateMessage和DispatchMessage。

TranslateMessage函式將虛擬鍵訊息轉換成字元訊息。比如:

訊息WM_KEYDOWN和WM_KEYUP組合產生一個WM_CHAR或WM_DEADCHAR訊息
訊息WM_SYSKEYDOWN和WM_SYSKEYUP組合產生一個WM_SYSCHAR或 WM_SYSDEADCHAR 訊息
然後放在佇列中,等待下一次執行緒呼叫GetMessage或PeekMessage時被讀出

// 主訊息迴圈: 
    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            if (msg.message==WM_KEYDOWN)
            {
                MessageBox(0, L"KeyDown1", 0, 0);
            }
            /*
            WM_KEYDOWN和WM_KEYUP組合產生一個WM_CHAR或WM_DEADCHAR訊息。
            訊息WM_SYSKEYDOWN和WM_SYSKEYUP組合產生一個WM_SYSCHAR或 WM_SYSDEADCHAR 訊息
            放在佇列中,等待下一次執行緒呼叫GetMessage或PeekMessage時被讀出

            將虛擬鍵訊息轉換為字元訊息。
            字元訊息被送到呼叫執行緒的訊息佇列中,在下一次執行緒呼叫函式GetMessage或PeekMessage時被讀出。
            */
            TranslateMessage(&msg);    
            GetMessage(&msg, NULL, 0, 0);
            if (msg.message == WM_KEYDOWN)
            {
                MessageBox(0, L"KeyDown2", 0, 0);
            }
            if (msg.message == WM_CHAR){
                MessageBox(0, L"Translate", 0, 0);
            }

            /*
            該函式排程一個訊息給視窗程式。通常排程從GetMessage取得的訊息。
            訊息被排程到的視窗程式即是MainProc()函式
            */
            DispatchMessage(&msg);
        }
    }

會發現按下一個鍵後會產生WM_KEYDOWN訊息,經過TranslateMessage翻譯後組合生成WM_CHAR訊息

然後投放到訊息佇列中,使用getMessage取出剛剛投放的訊息,判斷後,發現產生的是一個WM_CHAR訊息

TtanslateMessage函式僅為那些由鍵盤驅動器對映為ASCII字元的鍵產生WM_CHAR訊息

但是TranslateMessage不會丟棄原來的WM_KEYDOWN訊息。依舊可以繼續呼叫函式:  WndProc(HWND, UINT, WPARAM, LPARAM)處理這個訊息

case WM_KEYDOWN:
        MessageBox(0, L"Key Down", 0, 0);
        break;

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

使用DuiLib的CWindowWnd

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

建立一個空的Win32工程,配置好Duilib標頭檔案和庫檔案,然後輸入以下程式碼。

#include "UIlib.h"
using namespace DuiLib;

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

可以發現在呼叫CreateWindowEx這個windows API前會先呼叫RegisterWindowClass註冊視窗類。 這裡的例項控制代碼傳入的是CPaintManagerUI::GetInstance(),因為此前並未用CPaintManagerUI::SetInstance(HINSTANCE hInst)進行設定,所以這裡的例項控制代碼是NULL,但這並不影響視窗的建立。還有一點需要注意的是 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 所以有些額外處理。

三.視窗過程

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對應起來,這也是一種方法,但總歸沒有使用使用者資料來的直接簡便。