標籤:
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;停止該訊息後續處理,則返回1virtual 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;停止該訊息後續處理,則返回1int 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也真的挺不容易的。
關於這個類,大家有什麼好的想法,歡迎交流探討。
重溫WIN32 API ------ 最簡單的Windows視窗封裝類