轉載必須注明原文轉自C++博客(cppblog),作者畢達哥拉斯半圓,謝謝合作。
上一篇序言得到了很多高人的幫助鼓勵和意見,并且給出了一些框架做參考,我這幾天拼命消化這些信息,比較了一些架構,最終決定以完整的應用框架為主,并不先開發完整的控件庫,可以先采用Windows自帶的控件,或者DX控件,@vczh 的Gac控件也是可以用的嘛,@fzy 提出了快速的迭代開發方法,對我是非常好的建議,但是個人水平問題速度很可能快不起來。^_^
Any Way, 千里之行始于足下,就從最簡單的窗口應用開始,請各位朋友繼續批評指正以及各種吐槽~,哈哈。
Windows是以消息循環為主體,面向過程的軟件結構,這是匯編、C語言對OS開發的必然結果,所以開發框架的第一步就構建面向對象的體系結構,其核心使用了CBThook,下面逐步闡述。
先回顧一個典型的Windows App是這樣的:
1 int APIENTRY _tWinMain(HINSTANCE hInstance,
2 HINSTANCE hPrevInstance,
3 LPTSTR lpCmdLine,
4 int nCmdShow)
5 {
6 UNREFERENCED_PARAMETER(hPrevInstance);
7 UNREFERENCED_PARAMETER(lpCmdLine);
8
9 MSG msg;
10 HACCEL hAccelTable;
11
12 // 初始化全局字符串
13 LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
14 LoadString(hInstance, IDC_TESTWIN32, szWindowClass, MAX_LOADSTRING);
15 //注冊窗口
16 MyRegisterClass(hInstance, szWindowClass, WndProc);
17
18 // 執行應用程序初始化:
19 if (!InitInstance (hInstance, nCmdShow))
20 {
21 return FALSE;
22 }
23
24 while (GetMessage(&msg, NULL, 0, 0))
25 {
26 if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
27 {
28 TranslateMessage(&msg);
29 DispatchMessage(&msg);
30 }
31 }
32
33 return (int) msg.wParam;
34 }
35
然后是很熟悉的WndProc
1 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
2 {
3 int wmId, wmEvent;
4 PAINTSTRUCT ps;
5 HDC hdc;
6 switch (message)
7 {
8 case WM_PAINT:
9 break;
10
11 //message
12 //message
13 //message
14
15 case WM_COMMAND:
16 break;
17 case WM_CREATE:
18 return 0;
19 case WM_DESTROY:
20 PostQuitMessage(0);
21 break;
22 default:
23 return DefWindowProc(hWnd, message, wParam, lParam);
24 }
25 return 0;
26 }
27
上面的MyRegisterClass調用WinApi RegisterClassEx 注冊窗口,InitInstance調用CreateWindow創建窗口,而while循環做消息處理,直到應用退出。
作為WarmGUI的第一個類CWindow,應該像MFC::CWnd那樣中封裝RegisterClass和CreateWindow,這應該是很平凡的事情,照做就可以了,麻煩在于對WndProc封裝時,WndProc必須是一個全局的函數,只能作為CWindow的靜態函數,而static修飾的函數是沒有this指針的,因此在消息循環中,我們只能得到HWND句柄,卻不知道這個句柄是哪一個CWindow的實例。
MFC解決的辦法是使用CBT鉤子,HCBT_CREATEWND類型的CBT鉤子可以在窗口創建時指定回調函數,在這個回調函數中,將HWND和CWnd做一個映射,CWnd::Attach函數完成這個功能,這樣WndProc就可以用FromHandlePermanent函數從hwnd找到CWnd實例,然后可以在消息處理中調用CWnd::On-Message()什么的了。
WARMGUI::CWindow將采用CBT的方法,下面是山寨過程,應該有更好的實現方法,請各位大佬多指教 ^_^
首先類CWindowManager繼承std::map,定義一個HWND-CWindow map,
1 //the CWindowManager is a map of HWND-CWindow
2 class CWindow;
3 //define the HWND-CWindow map
4 typedef map <HWND, CWindow*> CWindowMap;
5 typedef pair<HWND, CWindow*> WindowPair;
6 typedef map <HWND, CWindow*>::iterator WndIterator;
7 typedef pair<WndIterator, bool> IterBool;
8
9 class CWindowManager : private CWindowMap
10 {
11 private:
12 CWindowManager(void);
13 ~CWindowManager(void);
14 public:
15 bool Add(CWindow* pwnd); //add a window to map
16 bool Remove(HWND hwnd); //remove a window by hwnd
17 void Clear(); //remove all items
18 CMyWindow* Find(HWND hwnd); //find the window by hwnd
19
20 public:
21 //get CWindowManager instance as singleton pattern
22 static CWindowManager * GetInstance();
23 };
24
其中的代碼是很平凡的,調用std::map的函數而已,不做舉例了。
用一個全局變量
1 CWindow* gpInitWnd;
記住當前正在創建的CWindow*,并用互斥鎖保證不會對其發生讀寫沖突。事實上只要保證當前線程內不發生讀寫沖突就可以了,因為CBT是以線程為單位創建的,不過第一個版本的CWindow暫時不考慮,以后再說。
這是CBT-hook代碼:
1 static HHOOK ghook = 0;
2
3 //Create a WH_CBT Hook
4 static bool HookCrate()
5 {
6 HANDLE hThread = GetCurrentThread();
7 DWORD dwThreadId = GetThreadId(hThread);
8 if (hThread) {
9 ghook = SetWindowsHookEx(
10 WH_CBT,
11 MyCBTProc, //set the CBT proc
12 0,
13 dwThreadId);
14 if (!ghook)
15 return false;
16 }
17
18 return (0);
19 }
20
21 //Destroy WH_CBT Hook
22 static void HookDestroy()
23 {
24 if (ghook) {
25 UnhookWindowsHookEx(ghook);
26 ghook = 0;
27 }
28 }
29
他的回調函數如下:
1 static LRESULT CALLBACK MyCBTProc(int nCode, WPARAM wParam, LPARAM lParam)
2 {
3 if (nCode == HCBT_CREATEWND) {
4 //GetInitPwnd() return gpInitWnd, the window is creating
5 CWindow * pwnd = GetInitPwnd();
6 if (pwnd) {
7 HWND hwnd = pwnd->GetSafeHwnd();//return pwnd->_hwnd
8 if (!hwnd) {
9 //first time call this proc, the CWindow have no HWND yet,
10 //Attach() will attach the wParam to pwnd, eg. pwnd->_hwnd =
wParam
11 pwnd->Attach((HWND)wParam);
12 //call the PreCreateWindow before WM_CREATE
13 if (!(pwnd->PreCreateWindow((LPCREATESTRUCT)lParam)))
14 return (1);
15 return (0);
16 } else {
17 //second time call this proc, i donw know why for now.
18 //but this is second chance to decide what is the style
19 //of the window, or the window should will be created or not,
20 //if you want create it, return 0, else return non-zero.
21 return (0);
22 }
23 } else {
24 return (1);
25 }
26 } else
27 return CallNextHookEx(ghook, nCode, wParam, lParam);
28 }
29
注釋中我解釋了每個步驟的含義,
MyCBTProc返回0則窗口被創建,返回非零則窗口被銷毀。尤其要注意的是這個回調會被調用兩次,我還不知道為什么,暫時也沒時間搞了,請教一下高人指點。
CWindow的Create函數是這樣的:
1 bool CWindow::Create(TCHAR * szClassName, //NULL for default class name
2 TCHAR * szWindowName,
3 DWORD dwStyle,
4 RECT& rect,
5 CWindow * pParentWnd,
6 LPVOID lpParam)
7 {
8 if (!pParentWnd)
9 return false;
10
11 TCHAR * szcn = (szClassName) ? szClassName : _gszClassName;
12
13 _hInst = pParentWnd->GetInstance();
14 if (!MyRegisterClass(_hInst, szcn)) {
15 DWORD dwErr = GetLastError();
16 if (dwErr != ERROR_CLASS_ALREADY_EXISTS) //0x00000582
17 return false;
18 }
19
20 SetInitWnd(this); //gpInitWnd = this
21 HookCrate();
22
23 HWND hWnd = CreateWindow(szcn,
24 szWindowName,
25 dwStyle,
26 rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
27 pParentWnd->GetSafeHwnd(),
28 NULL, //menu
29 _hInst,
30 lpParam);
31 HookDestroy();
32 UnsetInitWnd(); //gpInitWnd = 0;
33
34 if (!hWnd)
35 return FALSE;
36
37 _pParent = pParentWnd;
38
39 ShowWindow(hWnd, SW_SHOW);
40 UpdateWindow(hWnd);
41
42 return TRUE;
43 }
過程如下:首先注冊窗口類,然后設定gpInitWnd=自己,創建CBTHook,創建窗口,銷毀CBTHook,設定gpInitWnd=0,最后顯示窗口。
CWindow的WndProc如下:
1 LRESULT CALLBACK CWindow::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
2 {
3 //FromHandlePermanent get CWindow* from CWindowManager(a HWND-CWindow* map)
4 //it call CWindowManager::Find(HWND hwnd) function and return CWindow*
5 //it is MFC::CWnd Function
6 CWindow * pwnd = FromHandlePermanent(hWnd);
7 if (!pwnd) return FirstDefaultWndProc(hWnd, message, wParam, lParam);
8
9 //Message Message and Message
10 LRESULT r = 0;
11 switch (message)
12 {
13 case WM_CREATE :
14 r = pwnd->OnCreate((LPCREATESTRUCT)lParam);
15 if (r)
16 return (r);
17 break;
18 case WM_DESTROY :
19 pwnd->OnDestroy();
20 return (0);
21 case WM_PAINT:
22 pwnd->WindowPaint();
23 return (0);
24 case WM_SIZE :
25 pwnd->OnSize((UINT)wParam, LOWORD(lParam), HIWORD(lParam));
26 return (0);
27 case WM_DISPLAYCHANGE :
28 pwnd->WindowPaint();
29 return (0);
30 //message
31 //message
32 //and message
33 default:
34 return DefWindowProc(hWnd, message, wParam, lParam);
35 }
36 return DefWindowProc(hWnd, message, wParam, lParam);
37 }
現在可以對各種消息機制做一個比較好的封裝,比如對WM_PAINT調用WindowPaint函數:
1 void CWindow::WindowPaint()
2 {
3 PAINTSTRUCT ps;
4 HDC hdc = BeginPaint(_hwnd, &ps);;
5
6 OnPaint(hdc, &ps);
7
8 EndPaint(_hwnd, &ps);
9 }
在這個函數內部實現BeginPaint和EndPaint對,保證我們不會因為忙亂而忘記這對冤家(常見錯誤之一呀),CWindow和他的子類只要重寫OnPaint就可以了。
現在可以把消息循環封裝進來了:
1 void CWindow::RunMessageLoop(HACCEL hAccelTable)
2 {
3 MSG msg;
4
5 while (GetMessage(&msg, NULL, 0, 0))
6 {
7 if (hAccelTable) {
8 if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
9 {
10 TranslateMessage(&msg);
11 DispatchMessage(&msg);
12 }
13 } else {
14 TranslateMessage(&msg);
15 DispatchMessage(&msg);
16 }
17 }
18 }
差不多已經做完了,我們看看CWindow的聲明:
1 #define msgfun virtual
2 namespace WARMGUI {
3 class WARMGUI_API CWindow
4 {
5 public:
6 CWindow(void);
7 virtual ~CWindow(void);
8
9 HWND GetSafeHwnd();
10 HINSTANCE GetInstance();
11
12 void Attach(HWND hwnd);
13 void Dettach();
14 virtual BOOL InitInstance( HINSTANCE hInstance,
15 HINSTANCE hPrevInstance,
16 LPTSTR lpCmdLine,
17 int nCmdShow,
18 TCHAR * szClassName,
19 LPTSTR szTitle);
20
21 virtual void RunMessageLoop(HACCEL hAccelTable);
22 virtual BOOL PreCreateWindow(LPCREATESTRUCT cs);
23
24 bool Create(TCHAR * szClassName,
25 TCHAR * szWindowName,
26 DWORD dwStyle,
27 RECT& rect,
28 CWindow * pParentWnd,
29 LPVOID lpParam = 0);
30
32 void WindowPaint();
33
34 protected:
35 HWND _hwnd;
36 HINSTANCE _hInst;
37 CWindow * _pParent;
38
39 protected:
40 ///OnCreate must return 0 to continue the creation of the CWnd object. If the application returns –1, the window will be destroyed.
41 msgfun int OnCreate (LPCREATESTRUCT cs);
42 msgfun void OnDestroy ();
43 msgfun void OnSize (UINT nType, int cx, int cy);
44 //bla bla bla hao duo message
45 //bla bla and bla hai you hao duo message
46
47 protected:
48 static CWindow * FromHandlePermanent(HWND hWnd);
49 static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
50
51 private:
52 virtual ATOM MyRegisterClass(HINSTANCE hInstance, TCHAR * szClassName);
53 };
54 }
現在可以從CWindow繼承,并重載他的各種消息函數,并在WinMain中調用,一個最簡單的應用框架已經有了,顯然,他必須是Hello, World:
1 using namespace WARMGUI;
2
3 class CMyWindow : public CWindow {
4 public:
5 CMyWindow();
6 ~CMyWindow();
7
8 msgfun void OnPaint(LPPAINTSTRUCE ps);x
9 };
10
11 void CMyWindow::OnPaint(HDC hdc, LPPAINTSTRUCT /*ps*/)
12 {
13 HBRUSH brush = CreateSolidBrush(RGB(0, 0, 255));
14
15 TCHAR hello[] = L"Hello, World!";
16 RECT rect;
17 rect.left = 100, rect.top = 100, rect.right = 200, rect.bottom = 200;
18 COLORREF oldBkg = SetBkColor(hdc, RGB(255, 255, 0));
19 COLORREF oldClr = SetTextColor(hdc, RGB(0, 0, 255));
20 DrawText(hdc, hello, _tcslen(hello), &rect, DT_CENTER);
21 SetTextColor(hdc, oldClr);
22 SetBkColor(hdc, oldBkg);
23 DeleteObject(brush);
24 }
25
26 int APIENTRY _tWinMain(HINSTANCE hInstance,
27 HINSTANCE hPrevInstance,
28 LPTSTR lpCmdLine,
29 int nCmdShow)
30 {
31 CMyWindow mainwnd;
32
33 if (mainwnd.InitInstance(hInstance, hPrevInstance, lpCmdLine, nCmdShow, szClassName, szTitle)) {
34 mainwnd.RunMessageLoop(0);
35 } else {
36 return (-1);
37 }
38
39 return (0);
40 }
41