前言:这一系列博客翻译自 The Code Project 上的文章,作者是Zeeshan amjad。
题目:ATL Under the Hood - Part 4
原文链接:http://www.codeproject.com/Articles/3102/ATL-Under-the-Hood-Part-5
介绍
很多人认为ATL仅仅是用来创造COM组件。实际上你可以利用ATL的窗口类,来创建流畅的基于窗口的应用程序。虽然我们可以将你基于MFC的工程转化为ATL,但ATL中包含的UI组件非常少,所以,你需要写大量的代码。比如,ATL中没有文档/试图结构,所以,你想在ATL中用的话,就必须自己实现它了。这部分中,我们来探讨窗口类。同时也探讨ATL窗口类技术。WTL(window template library)到现在还没有被微软支持,实际上是向守卫制作图形应用方向走了一步。WTL就是基于ATL的窗口类构建的。
在讨论任何基于ATL的程序之前,我们首先看看经典的 “hello world”程序。这个程序完全利用windows SDK编写。大家应该都比较熟悉:
程序 66
#includeLRESULT 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放入在一个类中,以便在其他窗口中可以重复使用,比如字体,文件,菜单等。所以我做了一个小的类,将所有第一个参数为HWND的API放入其中。这个类除了对windows 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。这个类唯一的有点是,你不再需要为window 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结果时,你需要将回调函数的地址传递给它,在我们创建了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++中,如果我们将成员函数申明为静态函数,编译器就不会传递this指针了。这也是静态成员函数和非静态成员函数的区别。
所以,我们将WndProc作为ZWindowd的静态成员函数。这个技术在成员函数做线程函数时也用到了。
看下面的例子:
程序 67
#includeclass 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); } static 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参数个API了。但当你调用函数时,你还得键入类名。
比如,之前你这样调用:
ShowWindow(hWnd, nCmdShow);
现在你这样掉:
zwnd.ShowWindow(nCmdShow);
目前为止没什么到不了的,接下来看看WndProc中处理消息的方式。之前我们只响应一个消息:WM_DESTROY。如果我们想处理更多的消息,我们就需要在消息循环函数中添加更多的case 语句。下面的函数添加了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呢?他们第一个参数都是HWND,应该是包含在zwindow中成员变量才对呀?
由于所有这些函数都不是静态函数,而你有不可以在静态的成员函数中调用非静态的成员函数。为什么呢?他们的不同依然是this指针,非静态成员函数含有this指针,而静态成员函数没有。如果我们有办法将这个this指针传递给类的静态成员函数,那我们就可以在其中调用非静态成员函数了。看看下面的例子:
Program 68
#includeusing 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对象的地址,在静态成员函数中,就可以利用这个变量来调用非静态成员函数了。下面程序是对前面程序的改进,其中没有直接调用window的API。
程序 69
#includeclass 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的默认行为了。看看下面的例子:
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类,我们可以重写那些想要特化的函数,下面的程序显示了如何在子类中调用OnPaint函数。
程序 70
#includeclass 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继承不止一个累时,问题就出现了。所有的消息都跑到了最后一个派生类的消息循环里了。我们看下面的例子:
程序 71
#includeclass 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; }
无论你点击哪个窗口,这个程序输出同样的消息框。这就是说,消息没有被合适的传送给相应的窗体。实际上,每个窗口都有自己的消息过程,来处理相应窗口的消息。但这里的第一个窗体也用了第二个派生类的消息循环函数,从而导致第一个窗口的消息无法执行。
这里的关键问题是:我们必须将特定的消息循环与特定的窗口相联系。也就意味着,HWND必须与特定的派生类相关联,从而使消息走到特定的窗口。为实现这个功能,有很多方法可以选择,我们下面分别给予阐述:
第一种简单的思路是:构造一个全局的结构,该结构存储窗口句柄和对应的派生类地址。但有两个重要问题,首先是当程序中窗口越来越多时,这个结构将越来越大;其次是这种方法涉及到全局搜索的问题,而在该结构非常大的情况下,时间开销是个问题。(我补充了下面的例子,已经在vs2005里编译通过,请看下面的程序:)
#include
class ZWindow;
typedef struct _Node{
ZWindow * g_pWnd;
HWND m_hwnd;
_Node * next;
}Node;
Node * pheader = NULL;
Node * pcurrent = NULL;
ZWindow * current_wnd = 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);
Node* pnode = new Node();
pnode->g_pWnd = this;
pnode->m_hwnd = this->m_hWnd;
pnode->next = NULL;
Node * pNode = pheader;
if(pheader == NULL){
pheader = pnode;
pcurrent = pheader;
}
else{
pcurrent->next = pnode;
pcurrent = pcurrent->next;
}
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 = NULL;
Node * pthisnode = pheader;
while(pthisnode != NULL && pthisnode->m_hwnd != hWnd)
pthisnode = pthisnode->next;
if(pthisnode != NULL)
pThis = pthisnode->g_pWnd;
if(pThis == NULL)
pThis = current_wnd;
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;
}
current_wnd = &zwnd1;
zwnd1.Create(szAppName, "Hell world", hInstance);
zwnd1.ShowWindow(nCmdShow);
zwnd1.UpdateWindow();
current_wnd = &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;
}
该程序与前一个程序相比较,有以下不同:
首先,构造了全局的节点,用来存储派生类和窗口的组合,然后构造一个链表,记录其头部和当前位置。同时,还需要一个全局的派生类指针来引导窗口过程进入相应的类,来初始化全局节点。
ypedef struct _Node{
ZWindow * g_pWnd;
HWND m_hwnd;
_Node * next;
}Node;
Node * pheader = NULL;
Node * pcurrent = NULL;
ZWindow * current_wnd = NULL;
(由上面程序可以看出,我们在刚刚创建窗口时,是利用全局的派生类指针作为标识,进行窗口过程和派生类的关联;而在基类的Create函数中(注意该函数不是虚函数,所以子类不可以重写,但子类将继承该函数)进行全局链表的初始化。之后,所有的窗口消息将有这个链表中类和窗口的关联性来连接。)
ATL的主要目标是让代码尽可能少,执行速度尽可能快。但上述的方法却连其中任何一点都做不到。上述的方法不仅很慢,而且在窗口很多的情况下会消耗大量内存。
另一种可能的方案是,利用WNDCLASS 或者 WNDCLASSEX结构中的cbWndExtra字段。同时,我们要注意一个细节,为何没有用cbClsExtra而用cbWndExtra? 答案很简单,cbClsExtra为每个类存储额外字节,而cbWndExtra为类中每个窗口存储附加字节。所以,你无法用cbClsExtra区分统一各类的多个窗口。我们只需要在cbWndExtra中存储适当的窗口类信息。 (我补充了下面的例子,已经在vs2005里编译通过,请看下面的程序:)
#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); }
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);
::SetWindowLong(m_hWnd,GWL_USERDATA,(LONG)this);
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 = NULL;
pThis = (ZWindow*)::GetWindowLong(hWnd,GWL_USERDATA);
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 = sizeof(int);
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;
}
zwnd1.Create(szAppName, "Hell world", hInstance);
zwnd1.ShowWindow(nCmdShow);
zwnd1.UpdateWindow();
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;
}
该程序与程序71相比,仅仅是在基类的Create函数中,添加了下面语句:
::SetWindowLong(m_hWnd,GWL_USERDATA,(LONG)this);
同时,在消息循环中通过下面语句获取当前派生类:
pThis = (Zwindow*)::GetWindowLong(hWnd,GWL_USERDATA);
这种方案比第一种要好一些,但任然有两个问题。第一,如果用户想利用cbWndExtra字段,很可能将导致消息循环的混乱,所以用这种技术的人必须小心谨慎,别丢到这个字段的内容。就算你决定这么做,并且写好文档告诉别人你的库不支持修改cbWndExtra字段,但还有第二个问题,与ATL使用的方法相比,这种方法不够快。
ATL既没有用第一种方法,也没有用第二种方法,而使用了一种叫做“Thunk”的方法。Thunk是执行特定工作的一小段代码,这个术语在一些不同的情况下,你可能听过两种Thunk技术
通用Thunk(Universal Thunking)
通用Thunk(Universal Thunking)可以实现16位代码调用32位函数。在Win9x和WinNT/2000/XP均可用。这种Thunk又被较为泛型Thunk(Generic Thunking).
一般性Thunk(General Thunking)
一般性Thunk可以使16位函数调用32位的代码,仅仅在 Win 9x上使用,因为Win NT/2000 / XP是真正的32位操作系统,所以没有理由用16位代码调用32为程序,这种Thunk也被叫做平面Thunk(Flat Thunking)。
ATL没有使用这两种里面的任意一种,因为我们在ATL中不涉及16位和32位的混合编程,而只是插入一小段代码来调用合理的窗口过程。
在看ATLthunking技术之前,我们先看看基本概念。看看下面简单例子:
程序 72
#includeusing 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
#includeusing 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
#includeusing 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整出的地址处。这是为了提高CPU的性能。所以,这里的结构体存在了4的倍数对应的一个地址 4683576 处,成员变量ch1 当然也是同样的地址. 成员变量 ch2紧挨着这个存储单元存储,而int型的成员变量 I存放在地址为 4683580处,为什么不紧挨着放在 4683578处呢,因为这个地址是不可以被4整除的。 一个自然的问题:在4683578 and 4683579中放置的内容是什么呢? 如果变量是局部的,那存放的就是垃圾数据,如果是全局的或者静态的,就是0。看下面程序有助于理解这里所讲的内容:
程序 75
#includeusing 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
#includeusing 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) // structure to store the machine code struct Thunk { BYTE m_jmp; // op code of jmp instruction DWORD m_relproc; // relative jmp }; #pragma pack(pop)
这种结构可以存储thunk代码,这些代码执行的速度非常快。我们看看用thunk执行函数的例子。
Program 77
#include#include using namespace std; class C; C* g_pC = NULL; typedef void(*pFUN)(); #pragma pack(push,1) // structure to store the machine code struct Thunk { BYTE m_jmp; // op code of jmp instruction DWORD m_relproc; // relative jmp }; #pragma pack(pop) class C { public: Thunk m_thunk; void Init(pFUN pFun, void* pThis) { // op code of jump instruction m_thunk.m_jmp = 0xe9; // address of the appripriate function m_thunk.m_relproc = (int)pFun - ((int)this+sizeof(Thunk)); FlushInstructionCache(GetCurrentProcess(), &m_thunk, sizeof(m_thunk)); } // this is cour call back function static void CallBackFun() { C* pC = g_pC; // initilize the thunk pC->Init(StaticFun, pC); // get the address of thunk code pFUN pFun = (pFUN)&(pC->m_thunk); // start executing thunk code which will call 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中初始化,并且通过其调用。程序执行的思路大致是:
CallBackFun
Init (to initialize the thunk)
Get the address of thunk
Execute thunk
Thunk code call StaticFun
ATL用了同样的技术实现回调函数的调用,但在调之前还做了一件事。ZWindow又加了一个虚函数: ProcessWindowMessage
,这个函数在基类中不做任何事情。但每个派生类都要通过重写这个函数来处理其消息。过程是一样的:我们将ZWindow派生类的地址保存在一个指针中,用这个指针来调用派生类的虚函数,但窗口函数由WindowProc变为StartWndProc。ATL用该技术将HWND参数用this指针来替代,这样,this指针里包含了相应的HWND,所以,this和HWND信息都将得到保存。
为了实现这个,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; // relative 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代码将调用callWindowProc函数,而此时的第一个参数由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; } 现在,每个窗口都会别适当的窗口过程处理。
-------------------------------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------华丽的分割线--本系列结束----------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------