分享
 
 
 

ATL布幔之下的秘密(5)

王朝other·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

原作者:Zeeshan Amjad

原文链接:http://www.codeproject.com/atl/atl_underthehood_5.asp

点这里下载本文配套源代码

介绍

很多人认为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的其它秘密。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有