介紹
很多人認為ATL只是用來編寫COM組件的,其實你也可以使用ATL 中的窗口類來創建基於窗口的應用程序。雖然你可以將基於MFC的程序轉換為ATL ,但是ATL中對於UI(譯注:用戶界面)組件的支持太少了。所以,這就要求你 需要自己編寫很多代碼。例如,在ATL中沒有文檔/視圖,所以在你想使用它的時 候就需要自己實現了。在本篇中,我們將要探究一些關於窗口類的秘密,以及 ATL技術實現的秘密。WTL(Window Template Library,窗口模板庫),雖然到 現在(譯注:本文於2002年10月27日發表在CodeProject)還不為Microsoft所支 持,但是它在制作圖形應用程序方面跨出了一大步。WTL就是基於ATL的窗口類的 。
在開始討論基於ATL的程序之前,讓我們從一個經典的Hello world程序開始吧。這個程序完全用SDK編寫,並且我們中幾乎所有人都已經熟悉它了。
程序66.
#include <windows.h>
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
char szAppName[] = "Hello world";
HWND hWnd;
MSG msg;
WNDCLASS wnd;
wnd.cbClsExtra = NULL;
wnd.cbWndExtra = NULL;
wnd.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wnd.hCursor = LoadCursor(NULL, IDC_ARROW);
wnd.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wnd.hInstance = hInstance;
wnd.lpfnWndProc = WndProc;
wnd.lpszClassName = szAppName;
wnd.lpszMenuName = NULL;
wnd.style = CS_HREDRAW | CS_VREDRAW;
if (!RegisterClass (&wnd))
{
MessageBox(NULL, "Can not register window class", "Error",
MB_OK | MB_ICONINFORMATION);
return -1;
}
hWnd = CreateWindow(szAppName, "Hello world", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
while (GetMessage(&msg, NULL, 0, 0))
{
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
switch (uMsg)
{
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rect);
DrawText(hDC, "Hello world", -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage (0);
break;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
這個程序沒有什 麼新鮮的東西,它就是顯示了一個窗口,並在窗口中央顯示Hello world。
ATL是一個面向對象的開發庫,也就是說你可以用類來完成工作。讓我們 嘗試著自己來做一些相同的工作,編寫一些微小的類來使我們的工作更加簡單吧 。好了,那我們來編寫一些類來簡化工作——但是編寫這些類應該遵 循一個什麼樣的標准呢?換句話說就是,需要編寫多少類,它們的關系是什麼, 以及擁有什麼樣的方法和屬性。在這裡我並不打算討論整個的面向對象理論,我 們這裡只是編寫一個高質量的庫。為了使我的任務相類似,我將相關的API進行 了分組,並將這些相關的API放在了一個類裡邊。我將所有處理窗口的API放在了 一個類裡,並且它可以和其它的API相關聯,例如字體、文件、菜單等等。所以 我編寫了一個很小的類,並將所有第一個參數為HWND的API放在了這個類中。也 就是說,這個類只是簡單地對窗口API進行了一層包裝。我的類名稱為ZWindow, 當然你可以自由地選擇你喜歡的名稱。這個類是類似這個樣子:
class ZWindow
{
public:
HWND m_hWnd;
ZWindow (HWND hWnd = 0) : m_hWnd(hWnd) { }
inline void Attach(HWND hWnd)
{ m_hWnd = hWnd; }
inline BOOL ShowWindow(int nCmdShow)
{ return ::ShowWindow(m_hWnd, nCmdShow); }
inline BOOL UpdateWindow()
{ return ::UpdateWindow (m_hWnd); }
};
在這裡,我只封裝了目前需要的API。你可以向這 個類中添加全部的API。對於這個類來說的唯一優點,就是你不用像API那樣傳遞 HWND參數了,這個類本身會傳遞這個參數。
呃,到現在為止還沒有什麼 特別的。但是,我們的窗口回調函數怎麼辦呢?請記住,這個回調函數的第一個 參數也是HWND,所以對於我們的標准而言,它也應該是這個類中的成員。所以, 我也添加了我們的回調函數。現在,這個類就應該是類似這個樣子了:
class ZWindow
{
public:
HWND m_hWnd;
ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
inline void Attach(HWND hWnd)
{ m_hWnd = hWnd; }
inline BOOL ShowWindow(int nCmdShow)
{ return ::ShowWindow (m_hWnd, nCmdShow); }
inline BOOL UpdateWindow()
{ return ::UpdateWindow(m_hWnd); }
LRESULT CALLBACK WndProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
};
你需要為WNDCLASS或WNDCLASSEX的一個域 提供這個回調函數的地址。並且,你需要在創建ZWindow類對象之後像這樣賦值 :
ZWindow zwnd;
WNDCLASS wnd;
wnd.lpfnWndProc = wnd.WndProc;
但是當你編譯程序的時候,編譯器會給出類似這樣的錯誤 :
cannot convert from ''long (__stdcall ZWindow::*) (struct HWND__ *,
unsigned int,unsigned int,long)'' to ''long (__stdcall *)(struct HWND__ *,
unsigned int, unsigned int,long)
原因是你不能將成員函數作為回調函數來傳遞。為 什麼呢?因為在成員函數的情況下,編譯器會自動傳給成員函數一個參數,這個 參數是指向這個類的指針,或者換句話說是this指針。所以這就意味著當你在成 員函數中傳遞了n個參數的話,那麼編譯器會傳遞n+1個參數,並且那個附加的參 數就是this指針。這條錯誤消息就表明編譯器不能將成員函數轉換為全局函數。
那麼,如果我們想將成員函數作為回調函數的話,應該怎麼辦呢?如果 我們告訴編譯器,不傳遞第一個this指針參數的話,那麼我們就可以將成員函數 作為回調函數了。在C++中,如果我們將成員函數聲明為static的話,那麼編譯 器就不會傳遞this指針了。這就是static和非static成員函數實質上的不同。
所以,我們可以把ZWindow類中的WndProc聲明為static成員函數。這一 技術也可以用在多線程的情況下,比如當你想要使用成員函數作為一個線程函數 的時候,你就可以將一個static成員函數作為線程函數。
下面就是使用 了ZWindow類的更新程序。
程序67.
#include <windows.h>
class ZWindow
{
public:
HWND m_hWnd;
ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
inline void Attach(HWND hWnd)
{ m_hWnd = hWnd; }
inline BOOL ShowWindow(int nCmdShow)
{ return ::ShowWindow (m_hWnd, nCmdShow); }
inline BOOL UpdateWindow()
{ return ::UpdateWindow(m_hWnd); }
LRESULT CALLBACK WndProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,
int nCmdShow)
{
char szAppName[] = "Hello world";
HWND hWnd;
MSG msg;
WNDCLASS wnd;
ZWindow zwnd;
wnd.cbClsExtra = NULL;
wnd.cbWndExtra = NULL;
wnd.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wnd.hCursor = LoadCursor(NULL, IDC_ARROW);
wnd.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wnd.hInstance = hInstance;
wnd.lpfnWndProc = ZWindow::WndProc;
wnd.lpszClassName = szAppName;
wnd.lpszMenuName = NULL;
wnd.style = CS_HREDRAW | CS_VREDRAW;
if (!RegisterClass (&wnd))
{
MessageBox(NULL, "Can not register window class", "Error",
MB_OK | MB_ICONINFORMATION);
return -1;
}
hWnd = CreateWindow(szAppName, "Hello world", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
zwnd.Attach(hWnd);
zwnd.ShowWindow(nCmdShow);
zwnd.UpdateWindow();
while (GetMessage(&msg, NULL, 0, 0))
{
DispatchMessage(&msg);
}
return msg.wParam;
}
這個程序只是簡單示范了一下 ZWindow的用法,說實話,這個類就不會做什麼特別的了。它只是對Windows API 的一層包裝,唯一的優點就是你不需要傳遞HWND參數了,但是你必須得在調用成 員函數的時候輸入對象的名稱。
對於以前,你這樣調用函數:
ShowWindow(hWnd, nCmdShow);
現在,你可以這麼做:
zwnd.ShowWindow(nCmdShow);
到現在為止,這並不是一個明顯的 優點。
我們來看看在WndProc中如何處理窗口消息。在前一個程序中,我 們只處理了一個函數,也就是WM_DESTROY。如果你想要處理更多的消息,那麼可 以在switch語句中加入更多的case。讓我們來修改一下WndProc,處理一下 WM_PAINT。就像這個樣子:
switch (uMsg)
{
case WM_PAINT:
hDC = ::BeginPaint(hWnd, &ps);
::GetClientRect(hWnd, &rect);
::DrawText(hDC, "Hello world", -1, &rect, DT_CENTER | DT_VCENTER DT_SINGLELINE);
::EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
::PostQuitMessage(0);
break;
}
這個代碼很正確,它會在窗口的正中顯示Hello world。但是,為什 麼要用BeginPaint、GetClientRect和EndPaint這些API呢?根據我們的標准,這 些API都應該作為ZWindow的成員函數來使用的——它們的第一個參數 都是HWND。
因為所有這些函數都是非static函數。並且,你不能在 static成員函數中調用非static成員函數。為什麼呢?因為它們的區別就是this 指針,非static成員函數擁有this指針,而static函數沒有。如果我們通過某種 手段將this指針傳遞給了static成員函數,那麼我們就可以在static成員函數中 調用非static成員函數了。讓我們看看下面的程序。
程序 68.
#include <iostream>
using namespace std;
class C
{
public:
void NonStaticFunc()
{
cout << "NonStaticFun" << endl;
}
static void StaticFun(C* pC)
{
cout << "StaticFun" << endl;
pC->NonStaticFunc();
}
};
int main()
{
C objC;
C::StaticFun(&objC);
return 0;
}
程序的輸出為:
StaticFun
NonStaticFun
所以,我們就可以使用和這裡相同的技術,也就是將ZWindow對象的地址存入一 個全局變量,然後利用這個指針調用非static成員函數。下面是前一個程序的更 新版本,在其中我們沒有直接調用窗口的API。
程序69.
#include <windows.h>
class ZWindow;
ZWindow* g_pWnd = NULL;
class ZWindow
{
public:
HWND m_hWnd;
ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
inline void Attach (HWND hWnd)
{ m_hWnd = hWnd; }
inline BOOL ShowWindow(int nCmdShow)
{ return ::ShowWindow(m_hWnd, nCmdShow); }
inline BOOL UpdateWindow()
{ return ::UpdateWindow(m_hWnd); }
inline HDC BeginPaint(LPPAINTSTRUCT ps)
{ return ::BeginPaint(m_hWnd, ps); }
inline BOOL EndPaint(LPPAINTSTRUCT ps)
{ return ::EndPaint(m_hWnd, ps); }
inline BOOL GetClientRect(LPRECT rect)
{ return ::GetClientRect(m_hWnd, rect); }
BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,
HWND hWndParent = 0, DWORD dwStyle = WS_OVERLAPPEDWINDOW,
DWORD dwExStyle = 0, HMENU hMenu = 0)
{
m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, hWndParent, hMenu, hInstance, NULL);
return m_hWnd != NULL;
}
static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
ZWindow* pThis = g_pWnd;
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
switch (uMsg)
{
case WM_PAINT:
hDC = pThis->BeginPaint(&ps);
pThis- >GetClientRect(&rect);
::DrawText(hDC, "Hello world", -1, &rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
pThis- >EndPaint(&ps);
break;
case WM_DESTROY:
::PostQuitMessage(0);
break;
}
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
char szAppName[] = "Hello world";
MSG msg;
WNDCLASS wnd;
ZWindow zwnd;
wnd.cbClsExtra = NULL;
wnd.cbWndExtra = NULL;
wnd.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
wnd.hCursor = LoadCursor (NULL, IDC_ARROW);
wnd.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wnd.hInstance = hInstance;
wnd.lpfnWndProc = zwnd.WndProc;
wnd.lpszClassName = szAppName;
wnd.lpszMenuName = NULL;
wnd.style = CS_HREDRAW | CS_VREDRAW;
if (!RegisterClass (&wnd))
{
MessageBox(NULL, "Can not register window class", "Error",
MB_OK | MB_ICONINFORMATION);
return -1;
}
g_pWnd = &zwnd;
zwnd.Create(szAppName, "Hell world", hInstance);
zwnd.ShowWindow(nCmdShow);
zwnd.UpdateWindow();
while (GetMessage(&msg, NULL, 0, 0))
{
DispatchMessage(&msg);
}
return msg.wParam;
}
那麼,我們終於有了這個可以 工作的程序。現在,讓我們來利用面向對象程序設計。如果我們對於每個消息都 調用函數,並且使這些函數都成為虛函數的話,那麼我們就可以在繼承ZWindow 類之後調用這些函數了。所以,我們可以自定義ZWindow的默認行為。現在, WndProc是類似這個樣子:
static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
LPARAM lParam)
{
ZWindow* pThis = g_pWnd;
switch (uMsg)
{
case WM_CREATE:
pThis- >OnCreate(wParam, lParam);
break;
case WM_PAINT:
pThis->OnPaint(wParam, lParam);
break;
case WM_DESTROY:
::PostQuitMessage (0);
break;
}
return ::DefWindowProc (hWnd, uMsg, wParam, lParam);
}
在這裡,OnCreate和OnPaint是 虛函數。並且,當我們從ZWindow繼承一個類的時候,我們就可以重寫所有我們 想自定義的這些函數。下面是一個完整的程序,它示范了在派生類中WM_PAINT消 息的使用。
程序70.
#include <windows.h>
class ZWindow;
ZWindow* g_pWnd = NULL;
class ZWindow
{
public:
HWND m_hWnd;
ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
inline void Attach(HWND hWnd)
{ m_hWnd = hWnd; }
inline BOOL ShowWindow(int nCmdShow)
{ return ::ShowWindow(m_hWnd, nCmdShow); }
inline BOOL UpdateWindow()
{ return ::UpdateWindow(m_hWnd); }
inline HDC BeginPaint(LPPAINTSTRUCT ps)
{ return ::BeginPaint(m_hWnd, ps); }
inline BOOL EndPaint (LPPAINTSTRUCT ps)
{ return ::EndPaint(m_hWnd, ps); }
inline BOOL GetClientRect(LPRECT rect)
{ return ::GetClientRect(m_hWnd, rect); }
BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,
HWND hWndParent = 0, DWORD dwStyle = WS_OVERLAPPEDWINDOW,
DWORD dwExStyle = 0, HMENU hMenu = 0)
{
m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,
CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, hWndParent, hMenu, hInstance, NULL);
return m_hWnd != NULL;
}
virtual LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
hDC = BeginPaint(&ps);
GetClientRect(&rect);
::DrawText(hDC, "Hello world", -1, &rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
EndPaint(&ps);
return 0;
}
virtual LRESULT OnCreate(WPARAM wParam, LPARAM lParam)
{
return 0;
}
static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
LPARAM lParam)
{
ZWindow* pThis = g_pWnd;
switch (uMsg)
{
case WM_CREATE:
pThis->OnCreate(wParam, lParam);
break;
case WM_PAINT:
pThis ->OnPaint(wParam, lParam);
break;
case WM_DESTROY:
::PostQuitMessage(0);
break;
}
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
};
class ZDriveWindow : public ZWindow
{
public:
LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
hDC = BeginPaint (&ps);
GetClientRect(&rect);
SetBkMode(hDC, TRANSPARENT);
DrawText(hDC, "Hello world From Drive", -1, &rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
EndPaint(&ps);
return 0;
}
};
程序的輸出是一個窗 口中的一條“Hello world from Drive”消息。在我們使用派生類之 前,可以說一切都是順利的。當我們從ZWindow派生出多於一個類的時候,問題 就會發生。這樣,所有的消息就都會流向ZWindow最後繼承的那個派生類。讓我 們看看以下的程序。
程序71.
#include <windows.h>
class ZWindow;
ZWindow* g_pWnd = NULL;
class ZWindow
{
public:
HWND m_hWnd;
ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
inline void Attach(HWND hWnd)
{ m_hWnd = hWnd; }
inline BOOL ShowWindow(int nCmdShow)
{ return ::ShowWindow(m_hWnd, nCmdShow); }
inline BOOL UpdateWindow()
{ return ::UpdateWindow(m_hWnd); }
inline HDC BeginPaint(LPPAINTSTRUCT ps)
{ return ::BeginPaint(m_hWnd, ps); }
inline BOOL EndPaint (LPPAINTSTRUCT ps)
{ return ::EndPaint(m_hWnd, ps); }
inline BOOL GetClientRect(LPRECT rect)
{ return ::GetClientRect(m_hWnd, rect); }
BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,
HWND hWndParent = 0, DWORD dwStyle = WS_OVERLAPPEDWINDOW,
DWORD dwExStyle = 0, HMENU hMenu = 0, int x = CW_USEDEFAULT,
int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT,
int nHeight = CW_USEDEFAULT)
{
m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,
x, y, nWidth, nHeight, hWndParent, hMenu,
hInstance, NULL);
return m_hWnd != NULL;
}
virtual LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
hDC = BeginPaint(&ps);
GetClientRect(&rect);
::DrawText(hDC, "Hello world", -1, &rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
EndPaint(&ps);
return 0;
}
virtual LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)
{
return 0;
}
virtual LRESULT OnCreate(WPARAM wParam, LPARAM lParam)
{
return 0;
}
virtual LRESULT OnKeyDown(WPARAM wParam, LPARAM lParam)
{
return 0;
}
static LRESULT CALLBACK StartWndProc(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
ZWindow* pThis = g_pWnd;
if (uMsg == WM_NCDESTROY)
::PostQuitMessage(0);
switch (uMsg)
{
case WM_CREATE:
pThis->OnCreate(wParam, lParam);
break;
case WM_PAINT:
pThis->OnPaint(wParam, lParam);
break;
case WM_LBUTTONDOWN:
pThis- >OnLButtonDown(wParam, lParam);
break;
case WM_KEYDOWN:
pThis->OnKeyDown(wParam, lParam);
break;
case WM_DESTROY:
::PostQuitMessage(0);
break;
}
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
};
class ZDriveWindow1 : public ZWindow
{
public:
LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
hDC = BeginPaint (&ps);
GetClientRect(&rect);
::SetBkMode(hDC, TRANSPARENT);
::DrawText(hDC, "ZDriveWindow1", -1, &rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
EndPaint (&ps);
return 0;
}
LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)
{
::MessageBox(NULL, "ZDriveWindow1::OnLButtonDown", "Msg", MB_OK);
return 0;
}
};
class ZDriveWindow2 : public ZWindow
{
public:
LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
hDC = BeginPaint(&ps);
GetClientRect (&rect);
::SetBkMode(hDC, TRANSPARENT);
::Rectangle(hDC, rect.left, rect.top, rect.right, rect.bottom);
::DrawText(hDC, "ZDriveWindow2", -1, &rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
EndPaint(&ps);
return 0;
}
LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)
{
::MessageBox(NULL, "ZDriveWindow2::OnLButtonDown", "Msg", MB_OK);
return 0;
}
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
char szAppName[] = "Hello world";
MSG msg;
WNDCLASS wnd;
ZDriveWindow1 zwnd1;
ZDriveWindow2 zwnd2;
wnd.cbClsExtra = NULL;
wnd.cbWndExtra = NULL;
wnd.hbrBackground = (HBRUSH) GetStockObject(GRAY_BRUSH);
wnd.hCursor = LoadCursor (NULL, IDC_ARROW);
wnd.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wnd.hInstance = hInstance;
wnd.lpfnWndProc = ZWindow::StartWndProc;
wnd.lpszClassName = szAppName;
wnd.lpszMenuName = NULL;
wnd.style = CS_HREDRAW | CS_VREDRAW;
if (!RegisterClass (&wnd))
{
MessageBox(NULL, "Can not register window class", "Error",
MB_OK | MB_ICONINFORMATION);
return -1;
}
g_pWnd = &zwnd1;
zwnd1.Create(szAppName, "Hell world", hInstance);
zwnd1.ShowWindow (nCmdShow);
zwnd1.UpdateWindow();
g_pWnd = &zwnd2;
zwnd2.Create(szAppName, "Hello world", hInstance, zwnd1.m_hWnd,
WS_VISIBLE | WS_CHILD | ES_MULTILINE, NULL, NULL, 0, 0, 150, 150);
while (GetMessage (&msg, NULL, 0, 0))
{
DispatchMessage (&msg);
}
return msg.wParam;
}
程序的輸出表明,不管你單擊了哪個窗口,都會彈出相同的 MessageBox。
不管 你單擊了哪個窗口,你都會獲得相同的消息框。這就意味著消息並沒有傳遞給適 當的窗口。事實上每個窗口都擁有自己的窗口過程,這些窗口過程處理窗口的所 有消息。但是在這裡,我們對第一個窗口使用了第二個窗口的回調函數,所以我 們就不能對第一個窗口的消息進行處理了。
現在,我們最主要的問題是 將窗口的回調函數和相應的窗口關聯起來。這就意味著HWND應該和相應的派生類 關聯起來,所以消息應該發送給正確的窗口。解決這個問題可以有若干種方法, 讓我們來一個一個看一看。
首先我想出了一個最明顯的解決方法,我們 可以很容易地實現。方法是創建一個全局的結構,這個結構存儲HWND和相應的派 生類。但是,這個方法有兩個主要的問題。第一,這個結構會在窗口逐漸加入程 序的過程中越變越大;第二,在結構變得很大之後,在這個結構中進行搜索肯定 也會花費大筆時間。
而ATL的最主要目的就是使程序盡可能地小和快。並 且,上述技術對於這兩個標准都達不到。這個方法不單單是慢,還會在程序中包 含大量窗口的情況下占用大量內存。
另一個可能的解決方案是使用 WNDCLASS或WNDCLASSEX結構的cbWndExtra域。還有一個問題是,為什麼不用 cbClsExtra,而要用cbWndExtra呢?答案很簡單,cbClsExtra為每個窗口類存儲 額外的字節,而cbWndExtra為每個窗口存儲額外的字節。並且,你可能會從一個 窗口類創建多個窗口,這樣,如果你使用了cbClsExtra的話,那麼你就不能通過 cbClsExtra區別不同的回調函數了,因為對於這些相同窗口類產生的窗口來說這 個值是一樣的。然後,將相應的派生類地址存儲到cbWndExtra中。
這個 和方法看起來比第一個要好,但是它仍然有兩個問題。第一,如果用戶希望使用 cbWndExtra,那麼他/她就可能會覆蓋著一技術所使用的數據,這樣客戶就需要 在使用cbWndExtra的時候十分注意了,以防丟失信息。那麼好了,你可以在文檔 中寫明在使用你的庫時不要使用cbWndExtra,但是仍然會有一個問題:這個方法 並不是很快,又一次違背了ATL的規則——ATL應該盡可能地小和快。
ATL沒有使用這兩個方法中的任何一個,它使用的方法被稱作Thunk。 Thunk是一個小系列的代碼,並且這一術語被用在不同的地方。你可能曾經聽過 兩種Thunking:
Universal Thunking
Universal Thunking允許在 16位代碼中調用32位的函數,在Win 9x和Win NT/2000/XP下都可以使用,也被稱 作Generic Thunking。
General Thunking
General Thunking允許 在32位代碼中調用16位的函數,它只能用在Win 9x中,因為Win NT/2000/XP是純 32位操作系統,所以在32位代碼中調用16位的函數不合乎邏輯。General Thunking也被稱作Flat Thunking。
ATL沒有使用這兩種方法,因為你不 會在ATL中將16位和32位的代碼混合。事實上,ATL插入了一小段代碼來調用正確 的窗口過程。
在研究ATL的Thunking之前,讓我們先從一些基礎概念開始 。請看下面的簡單程序。
程序72.
#include <iostream>
using namespace std;
struct S
{
char ch;
int i;
};
int main()
{
cout << "Size of character = " << sizeof(char) << endl;
cout << "Size of integer = " << sizeof(int) << endl;
cout << "Size of structure = " << sizeof(S) << endl;
return 0;
}
程序的輸出為:
Size of character = 1
Size of integer = 4
Size of structure = 8
一個整型和一個字符的尺寸 之和應該是5而不是8。那麼讓我們略微修改一下程序,再添加一個成員變量,看 看會發生什麼。
程序73.
#include <iostream>
using namespace std;
struct S
{
char ch1;
char ch2;
int i;
};
int main()
{
cout << "Size of character = " << sizeof(char) << endl;
cout << "Size of integer = " << sizeof(int) << endl;
cout << "Size of structure = " << sizeof(S) << endl;
return 0;
}
程序的輸出和前一個一樣。那麼這裡發生了什麼?再 修改一下程序,看看布幔之下發生了什麼吧。
程序74.
#include <iostream>
using namespace std;
struct S
{
char ch1;
char ch2;
int i;
}s;
int main()
{
cout << "Address of ch1 = " << (int)&s.ch1 << endl;
cout << "Address of ch2 = " << (int)&s.ch2 << endl;
cout << "Address of int = " << (int)&s.i << endl;
return 0;
}
程序的輸 出為:
Address of ch1 = 4683576
Address of ch2 = 4683577
Address of int = 4683580
這是由於結構和聯合成員的字對齊的緣故 。如果你注意觀察的話,你就能推斷出來這個結構外的每個變量都存儲在能被4 整除的地址上,這是為了提高處理器的性能。所以,這裡的結構分配了4的整數 倍的內存空間,也就是4683576,ch1和它有相同的地址。ch2成員存儲在這個位 置之後,而int i存儲在4683580的位置上。這個位置不是4683578的原因是它不 能被4整除。現在的問題是,4683578和4683579的位置上是什麼呢?答案是如果 變量是本地變量,那麼這裡是垃圾值;如果是static或全局變量,那麼是0。讓 我們看看下面這個程序來更好地理解這一點。
程序75.
#include <iostream>
using namespace std;
struct S
{
char ch1;
char ch2;
int i;
};
int main()
{
S s = { ''A'', ''B'', 10};
void* pVoid = (void*)&s;
char* pChar = (char*)pVoid;
cout << (char)*(pChar + 0) << endl;
cout << (char)*(pChar + 1) << endl;
cout << (char)*(pChar + 2) << endl;
cout << (char)*(pChar + 3) << endl;
cout << (int)*(pChar + 4) << endl;
return 0;
}
程序的輸出為:
A
B
…
…
10
程序的輸出清楚地表明,那些空間中是垃圾值,就 像下表一樣。
現在 ,如果我們不想浪費那些空間的話應該怎麼做呢?有兩個選擇:或者使用編譯器 開關/Zp,或者在聲明結構之前使用#pragma語句。
程序 76.
#include <iostream>
using namespace std;
#pragma pack(push, 1)
struct S
{
char ch;
int i;
};
#pragma pack(pop)
int main()
{
cout << "Size of structure = " << sizeof(S) << endl;
return 0;
}
程序的輸出為:
Size of structure = 5
這就意味著現在已經沒有字對齊了。事實上,ATL使用這一技術來制作thunk。 ATL使用了一個結構,這個結構沒有使用字對齊,並且這個結構中直接儲存了微 處理器的機器代碼。
#pragma pack(push,1)
// 存儲機器代碼的結 構
struct Thunk
{
BYTE m_jmp; // jmp指 令的操作碼
DWORD m_relproc; // 相對jmp
};
#pragma pack(pop)
這種類型的結構保存了thunk代碼,它可以在不工 作的時候執行。讓我們來看看下面這種簡單的情況,我們將要使用thunk來執行 我們想要執行的函數。
程序77.
#include <iostream>
#include <windows.h>
using namespace std;
class C;
C* g_pC = NULL;
typedef void(*pFUN)();
#pragma pack (push,1)
// 存儲機器代碼的結構
struct Thunk
{
BYTE m_jmp; // jmp指令的操作碼
DWORD m_relproc; // 相對jmp
};
#pragma pack(pop)
class C
{
public:
Thunk m_thunk;
void Init(pFUN pFun, void* pThis)
{
// 跳轉指令的操作碼
m_thunk.m_jmp = 0xe9;
// 相應函數的地址
m_thunk.m_relproc = (int)pFun - ((int)this+sizeof(Thunk));
FlushInstructionCache(GetCurrentProcess(),
&m_thunk, sizeof(m_thunk));
}
// 這 是回調函數
static void CallBackFun()
{
C* pC = g_pC;
// 初始化thunk
pC->Init (StaticFun, pC);
// 獲得thunk代碼地址
pFUN pFun = (pFUN)&(pC->m_thunk);
// 開始執行thunk代碼 ,調用StaticFun
pFun();
cout << "C::CallBackFun" << endl;
}
static void StaticFun()
{
cout << "C::StaticFun" << endl;
}
};
int main()
{
C objC;
g_pC = &objC;
C::CallBackFun();
return 0;
}
程序的輸出為:
C::StaticFun
C::CallBackFun
在這裡, StaticFun是通過thunk調用的,而thunk是在Init成員函數中初始化的。程序的 執行是類似這個樣子
·CallBackFun
·Init(初始 化thunk)
·獲得thunk地址
·執行 thunk
·Thunk代碼調用StaticFun
ATL 也使用了相同的技術來調用正確的回調函數,但是它在調用函數之前還做了一件 事情。現在ZWindow又有了一個虛函數ProcessWindowMessage,它在這個類中什 麼也不做。但是,ZWindow的每個派生類都需要重寫它來處理自己的消息。整個 的處理過程和我們將ZWindow的派生類地址存入一個指針並調用派生類的虛函數 是相同的,但是現在WindowProc的名字是StartWndProc。在這裡,ATL使用了這 一技術來將HWND參數替換為了this指針。但是,HWND怎麼樣了,我們就這麼失去 它了嗎?事實上,我們已經將HWND存入了ZWindow類的成員變量中了。
要 達到這一點,ATL使用了一個較前一個程序大一點的結構。
#pragma pack (push,1)
struct _WndProcThunk
{
DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
DWORD m_this;
BYTE m_jmp; // jmp WndProc
DWORD m_relproc; // 相對jmp
};
#pragma pack(pop)
並且,在 初始化的時刻,寫入操作碼“mov dword ptr [esp +4], pThis”。 是類似這個樣子:
void Init(WNDPROC proc, void* pThis)
{
thunk.m_mov = 0x042444C7; //C7 44 24 04
thunk.m_this = (DWORD)pThis;
thunk.m_jmp = 0xe9;
thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk));
FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof (thunk));
}
並且,在初始化thunk代碼之後,獲得thunk的地址並 向thunk代碼設置新的回調函數。然後,thunk代碼會調用WindowProc,但是現在 第一個參數就不是HWND了,事實上它是this指針。所以我們可以將它安全的轉換 為ZWindow*,並調用ProcessWindowMessage函數。
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
ZWindow* pThis = (ZWindow*)hWnd;
if (uMsg == WM_NCDESTROY)
PostQuitMessage(0);
if (!pThis->ProcessWindowMessage (pThis->m_hWnd, uMsg, wParam, lParam))
return ::DefWindowProc(pThis->m_hWnd, uMsg, wParam, lParam);
else
return 0;
}
現在,每個窗口正確 的窗口過程就可以被調用了。整個的過程如下圖所示:
由於 代碼長度所限,程序的完整代碼將隨本文配套提供。我希望能在本系列中之後的 文章中繼續探究ATL的其它秘密。
本文配套源碼