1 開發語言抉擇
1.1 關於開發Win32 程序的語言選擇 C還是C++
在決定拋棄MFC,而使用純Win32 API 開發Window桌面程序之后,還存在一個語言的選擇,這就是是否使用C++。C++作為C的超集,能實現所有C能實現的功能。其實反之亦然,C本身也能完成C++超出的那部分功能,只是可能需要更多行的代碼。就本人理解而言,
- 對於巨大型項目,還是使用純C來架構更加穩妥;
- 對於中小型項目來說,C++可能更方便快捷。由於目前做的是中小項目,所以決定把C++作為主要開發語言。
1.2 關於C++特性集合的選擇
在決定使用C++之后,還有一個至關重要的抉擇,那就是C++特性集合的選擇。C++實在是太復雜了,除了支持它的老祖先C的所有開發模式,還支持基於對象開發(OB)、面向對象開發(OO)、模板技術。可以說,C++是個真正全能型語言,這同時也造成了C++的高度復雜性。使用不同的開發模式,就相當於使用不同的編程語言。就本人而言,對C++的模板編程也根本沒有任何經驗。綜合過去的經驗教訓和本人對C++的掌握程度,決定:
- 使用基於對象和面向對象兩種開發模式,如果一個功能兩種都可以實現,則優先選擇基於對象。傾向於OB的技術觀點來自對蘋果Object-C開發經驗。
- 盡量避免多繼承,此觀點來自Java和.net開發經驗。
- 數據結構和容器,使用C++標准模板庫(STL),模板編程本身復雜,但是使用STL卻非常容易。
2 Windows窗口對象的封裝類
對Windows桌面程序而言,Window和Message的概念是核心。首先需要封裝的就是窗口,例如MFC就是用CWnd類封裝了窗口對象。我們當初拋棄MFC的原因,就是因為它太復雜不容易理解,所以對基本窗口對象的封裝一定要做到最簡單化。
2.1 封裝原則
首要的原則就是“簡單”。能用一個Win32API直接實現的功能,絕不進行二次包裝,如移動窗口可以使用 MoveWindow()一個函數實現,類中就不要出現同樣功能的MoveWindow()函數。MFC里有很多這種重復的功能,其實只是可以少寫一個hwnd參數而已,卻多加了一層調用。我就是要讓HWND句柄到處出現,絕不對其隱藏,因為這個概念對於Windows來說太重要了,開發者使用任何封裝類都不應該對其視而不見。
其次,同樣功能多種技術可以實現時,優先選擇容易理解的技術,“可理解性”比“運行效率”更重要。
2.2 源碼
頭文件 XqWindow.h
- #pragma once
- #include <vector>
- class XqWindow
- {
- public:
- XqWindow(HINSTANCE hInst);
- ~XqWindow();
- private:
- HWND hWnd; // 對外只讀,確保安全
- HINSTANCE hInstance;
- public:
- // 返回窗口對象句柄
- HWND GetHandle();
- // 消息處理。需要后續默認處理則需要返回0;停止該消息后續處理,則返回1
- virtual int HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
- private:
- // 原始窗口過程
- static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
- private:
- // 已注冊過的類集合
- static std::vector<void*> registeredClassArray;
- public:
- // 創建窗口
- void Create();
- };
實現文件 XqWindow.cpp
- #include "stdafx.h"
- #include "XqWindow.h"
- std::vector<void*> XqWindow::registeredClassArray;
- // 創建窗口
- void XqWindow::Create()
- {
- wchar_t szClassName[32];
- wchar_t szTitle[128];
- void* _vPtr = *((void**)this);
- ::wsprintf(szClassName, L"%p", _vPtr);
- std::vector<void*>::iterator it;
- for (it = registeredClassArray.begin(); it != registeredClassArray.end(); it++) // 判斷對象的類是否注冊過
- {
- if ((*it) == _vPtr)
- break;
- }
- if (it == registeredClassArray.end()) // 如果沒注冊過,則進行注冊
- {
- //注冊窗口類
- WNDCLASSEX wcex;
- wcex.cbSize = sizeof(WNDCLASSEX);
- wcex.style = CS_HREDRAW | CS_VREDRAW;
- wcex.lpfnWndProc = XqWindow::WndProc;
- wcex.cbClsExtra = 0;
- wcex.cbWndExtra = 0;
- wcex.hInstance = this->hInstance;
- wcex.hIcon = NULL;
- wcex.hCursor = ::LoadCursor(NULL, IDC_ARROW);
- wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
- wcex.lpszMenuName = NULL;
- wcex.lpszClassName = szClassName;
- wcex.hIconSm = NULL;
- if (0 != ::RegisterClassEx(&wcex)) // 把注冊成功的類加入鏈表
- {
- registeredClassArray.push_back(_vPtr);
- }
- }
- // 創建窗口
- if (this->hWnd == NULL)
- {
- ::wsprintf(szTitle, L"窗口類名(C++類虛表指針):%p", _vPtr);
- HWND hwnd = ::CreateWindow(szClassName,
- szTitle,
- WS_OVERLAPPEDWINDOW,
- 0, 0, 800, 600,
- NULL,
- NULL,
- hInstance,
- (LPVOID)this
- );
- if (hwnd == NULL)
- {
- this->hWnd = NULL;
- wchar_t msg[100];
- ::wsprintf(msg, L"CreateWindow()失敗:%ld", ::GetLastError());
- ::MessageBox(NULL, msg, L"錯誤", MB_OK);
- return;
- }
- }
- }
- XqWindow::XqWindow(HINSTANCE hInst)
- {
- this->hWnd = NULL;
- this->hInstance = hInst;
- }
- XqWindow::~XqWindow()
- {
- if ( this->hWnd!=NULL && ::IsWindow(this->hWnd) ) // C++對象被銷毀之前,銷毀窗口對象
- {
- ::DestroyWindow(this->hWnd); // Tell system to destroy hWnd and Send WM_DESTROY to wndproc
- }
- }
- HWND XqWindow::GetHandle()
- {
- return this->hWnd;
- }
- // 消息處理。需要后續默認處理則需要返回0;停止該消息后續處理,則返回1
- int XqWindow::HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
- {
- return 0;
- }
- // 原始窗口過程
- LRESULT CALLBACK XqWindow::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
- {
- XqWindow* pObj = NULL;
- if (message == WM_CREATE) // 在此消息收到時,把窗口對象句柄賦給C++對象成員,同時把C++對象地址賦給窗口對象成員
- {
- pObj = (XqWindow*)(((LPCREATESTRUCT)lParam)->lpCreateParams);
- pObj->hWnd = hWnd; // 在此處獲取HWND,此時CreateWindow()尚未返回。
- ::SetWindowLong(hWnd, GWL_USERDATA, (LONG)pObj); // 通過USERDATA把HWND和C++對象關聯起來
- }
- pObj = (XqWindow*)::GetWindowLong(hWnd, GWL_USERDATA);
- switch (message)
- {
- case WM_CREATE:
- pObj->HandleMessage(hWnd, message, wParam, lParam);
- break;
- case WM_DESTROY:
- if (pObj != NULL) // 此時,窗口對象已經銷毀,通過設置hWnd=NULL,來通知C++對象
- {
- pObj->hWnd = NULL;
- }
- break;
- default:
- pObj = (XqWindow*)::GetWindowLong(hWnd, GWL_USERDATA);
- if (pObj != NULL)
- {
- if (pObj->HandleMessage(hWnd, message, wParam, lParam) == 0) // 調用子類的消息處理虛函數
- {
- return DefWindowProc(hWnd, message, wParam, lParam);
- }
- }
- else
- {
- return DefWindowProc(hWnd, message, wParam, lParam);
- }
- break;
- }
- return 0;
- }
2.3 使用舉例
基本用法為,創建一個TestWindow類,繼承自XqWindow,然后重新虛函數 HandleMessage()。所有業務處理代碼都要在HandleMessage()里調用,由於該函數是成員函數,所有里面可以直接使用this來引用TestWindow類對象的成員。一個例子代碼如下:
TestWindow.h
- #pragma once
- #include "XqWindow.h"
- class TestWindow :
- public XqWindow
- {
- public:
- TestWindow(HINSTANCE);
- ~TestWindow();
- protected:
- int HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
- private:
- // 業務數據部分
- int rectWidth;
- int rectHeight;
- };
TestWindow.cpp
#include "stdafx.h"
#include "TestWindow.h" TestWindow::TestWindow(HINSTANCE hInst) :XqWindow(hInst) { rectWidth = 300; rectHeight = 200; } TestWindow::~TestWindow() { } int TestWindow::HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_PAINT: hdc = ::BeginPaint(hWnd, &ps); ::Rectangle(hdc, 0, 0, this->rectWidth, this->rectHeight); ::EndPaint(hWnd, &ps); return 1; default: break; } return 0; }
調用部分:
- pTest = new TestWindow(theApp.m_hInstance);
- pTest->Create();
- ::ShowWindow(pTest->GetHandle(), SW_SHOW);
- ::UpdateWindow(pTest->GetHandle());
運行效果:
2.4 技術要點
這個XqWindow類對窗口對象做了最小的封裝,主要實現了消息處理函數和C++對象的關聯。內存布局如下:
需要說明的幾點:
(1)C++類和窗口類的一一對應。由於VC++默認不啟用RTTI,同時考慮到代碼兼容性和運行效率,也不提倡啟用RTTI,在沒有RTTI支持的情況下,如何才能在運行時把同一個類的所有實例與其他類的實例進行區分呢?這里我們采用了C++的虛表指針,每一個有虛函數的類都擁有自己獨立的虛表,而這個虛表指針又在每個實例中存儲。同一個類的不同實例共享一個虛表,所以這給了我們區分對象所屬C++類的機會。當然這種技術只能用到有虛函數的類中,對於沒有虛函數的類的對象,不存在虛表。對於我們的情況,XqWindow類有一個HandleMessage虛函數,從而其他所有繼承此類的子類孫類也就都有自己的虛表了。
在RegisterClass()之前,首先判斷當前C++對象所屬類的虛表指針是否存在vptrAraay鏈表中。如果沒有,則注冊窗口類,並把虛表指針存放到vptrArray鏈表中;如果存在,則直接使用該虛表指針對應的窗口類。
需要注意的是,獲取對象虛表指針值的操作不能在XqWindow::XqWindow()構造函數里進行,因為在執行此函數時,C++對象的虛表指針成員尚未被設置到指向派生類的虛表地址(因為尚未調用子類的構造函數)。所以必須在對象構造完成之后才能獲取虛表指針值,這也是為什么Create()不能在XqWindow()構造函數里調用的原因。(我曾經為了簡化調用把Create()放到XqWindow()里,導致了所有對象的虛表指針都相同的后果!)
(2)C++對象與窗口對象的關系。C++對象創建以后,調用Create()是唯一可以和窗口對象綁定到一起的途徑。在舊窗口銷毀之前,C++對象不能再創建新窗口,調用Create()多次也沒用。
C++對象生存壽命也大於對應的窗口壽命,否則窗口過程中使用C++對象就會出現非法訪問內存問題。這兩種對象的生命序列為: C++ 對象出生 -- 調用Create()產生窗口對象--某種原因窗口對象銷毀--C++對象銷毀。
為防止C++對象在窗口對象之前銷毀,在XqWindow類的析構函數中,先通過DestroyWindow()銷毀窗口對象。窗口對象銷毀時,也會設置C++對象的hWnd為NULL,來通知C++對象窗口的銷毀。
形象一點的說法:C++對象和窗口對象則是一夫一妻制、且只能喪偶不能離異條件下的夫妻關系,而且C++對象是壽命長的一方,窗口對象則是壽命短的一方。只有一個窗口對象死掉后,C++對象才能重新生成新窗口。而且C++對象死掉之前,需要先把窗口對象殺死陪葬。
(3)C++對象和窗口對象的彼此引用。C++對象通過成員變量hWnd引用窗口對象,窗口對象則通過GWL_USERDATA附加數據塊指向C++對象。另外為了及時捕獲WM_CRATE消息並在HandleMessage里處理,C++成員hWnd的賦值並沒有在CreateWindow()之后,而是在原始窗口過程函數處理WM_CREAT消息時。這主要與CreateWindow()原理有關。
CreateWindow()
{
HWND hwnd = malloc(..);
初始化窗口對象;
WndProc(hwnd, WM_CRATE, ..); // 此時已經創建了窗口
其他操作;
return hwnd;
}
同理,DestroyWindow()的原理為.
DestroyWindow(hwnd)
{
窗口對象清理工作;
WndProc(hwnd, WM_DESTROY, ..); // 此時窗口已經不可見了
其他操作;
free(hwnd);
}
2.5 存在問題
雖然XqWindow類可以很好的工作,但也存在一些問題:
(1)由於Window對象靠USERDATA引用C++對象,所以如果其他代碼通過SetWindowLong(hwnd, GWL_USERDATA, xxx)修改了這個數據塊,那么程序將會崩潰。如何防止這種破壞,需要進一步研究。
(2)使用C++對象的虛表指針,而這個指針的具體內存布局並沒有明確的規范標准,一旦將來VC++編譯器修改虛表指針的存放位置,程序將會出問題。不過由於考慮到二進制的兼容性,VC++作出這種改變的可能性不大。
3 一點感受
XqWindow類的源碼一共不到150行,卻花了我2天的業余時間來完成。這里涉及到對C++對象內存布局,窗口創建、銷毀、消息處理過程的深入理解。寫一個小小類就如此不易,寫一個健壯的類庫真是難上加難,想想MFC也真的挺不容易的。
關於這個類,大家有什么好的想法,歡迎交流探討。