65.9K
CodeProject 正在变化。 阅读更多。
Home

ATL 内部机制 第五部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (39投票s)

2002 年 10 月 27 日

CPOL

13分钟阅读

viewsIcon

465516

downloadIcon

1489

使用 ATL 创建用户界面元素

引言

很多人认为 ATL 仅用于创建 COM 组件。但事实上,您可以使用 ATL 的窗口类,利用 ATL 创建功能完善的基于 Windows 的应用程序。虽然您可以将 MFC 项目转换为 ATL,但 ATL 对 UI 组件的支持非常有限,因此您需要自己编写大量代码。例如,ATL 中没有文档/视图模型,如果您想实现它,则需要自己实现。在本文中,我们将探讨窗口类。我们还将尝试探索 ATL 用于此目的的技术。WTL(窗口模板库)在 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 放在该类中。这个类只不过是对 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。这个类的唯一优点是,现在您不必为窗口 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 指针。编译器生成的错误消息也表明了这一点,即编译器无法将成员函数转换为全局函数。

那么,如果我们想使用成员函数作为回调函数,我们应该怎么办?如果我们以某种方式告诉编译器不要向函数传递第一个参数,那么我们就可以使用成员函数作为回调函数。在 C++ 中,如果我们声明成员函数为静态的,那么编译器就不会传递 this 指针,这实际上是静态和非静态成员函数之间的区别。

因此,我们将 `ZWindow` 类中的 `WndProc` 声明为静态的。此技术也用于多线程场景,当您想将成员函数用作线程函数时,您需要将成员函数声明为静态的。

以下是使用 `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”。但是,为什么要在所有这些都应该是 ZWindow 类的成员函数(因为它们都有 `HWND` 作为第一个参数)时,使用 `BeginPaint`、`GetClientRect` 和 `EndPaint` API 呢?

因为所有这些函数都不是静态的。您不能从静态成员函数调用非静态成员函数。为什么?区别在于 this 指针,非静态成员函数有 this 指针,而静态函数没有。如果我们以某种方式将 this 指针传递给静态成员函数,那么我们就可以从静态成员函数调用非静态成员函数。让我们看下面的程序。

程序 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` 对象的地址,然后从该指针调用非静态成员函数。以下是上一程序的更新版本,其中我们不再直接调用窗口 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;
}

此程序的输出显示相同的消息框,无论您单击哪个窗口。

无论您单击哪个窗口,都会收到相同的消息框。这意味着消息没有正确地传播到适当的窗口。事实上,每个窗口都有自己的窗口过程,它处理该窗口的所有消息。但在这里,我们将第二个派生类的回调函数与第一个窗口一起使用,因此我们无法执行第一个窗口的消息处理程序。

这里我们的主要问题是将窗口的回调函数与适当的窗口关联起来。意思是 `HWND` 应该与适当的派生类地址关联。因此,消息应该转到正确的窗口。这个问题可能有多种解决方案,让我们逐一看看每种解决方案。

首先想到的一个显而易见的解决方案是创建一个全局结构,该结构存储 `HWND` 和适当的派生类地址。但这种方法有两个主要问题。首先,当程序中添加的窗口越来越多时,结构会越来越大。第二个问题是,搜索该全局结构当然需要时间,并且当结构变得非常大时,搜索它会耗费时间。

ATL 的主要目的是尽可能小、尽可能快。而这种技术在两个标准上都失败了。这种方法不仅慢,而且在程序涉及大量窗口时还会消耗大量内存。

另一个可能的解决方案是使用 `WNDCLASS` 或 `WNDCLASSEX` 结构中的 `cbWndExtra` 字段。仍然有一个问题,为什么不使用 `cbClsExtra` 而使用 `cbWndExtra`?答案很简单,`cbClsExtra` 存储每个类的额外字节,而 `cbWndExtra` 存储来自类的每个窗口的额外字节。您可以从同一个类创建多个窗口,因此如果您使用 `cbClsExtra`,您将无法区分不同的窗口回调函数,因为它是由同一个类创建的所有窗口的相同数据。并将适当派生类的地址存储在此字段中。

这似乎是一个不错的解决方案,至少比第一个好。但这个解决方案仍然存在两个问题。第一个是,如果用户想使用 `cbWndExtra`,他/她可能会覆盖使用此技术写入的数据。因此,此类库的客户端在使用 `cbWndExtra` 时必须小心,以免丢失该信息。好的,您已决定并记录在案,在使用您的库时不要使用 `cbWndExtra`,但仍然存在另一个问题。这种方法不是很快速,再次违反了 ATL 的规则,即 ATL 应该是尽可能小、尽可能快速的。

ATL 既不使用第一种方法也不使用第二种方法。ATL 使用的方法称为 Thunk。Thunk 是一小组用于执行某些工作的代码,并且该术语在不同上下文中使用。您可能听说过两种 Thunking。

通用 Thunking

通用 Thunking 允许从 16 位代码调用 32 位函数。在 Win 9x 和 Win NT/2000/XP 上都可用。这也称为 Generic Thunking。

通用 Thunking

通用 Thunking 允许从 32 位代码调用 16 位函数。它仅在 Win 9x 上可用,因为 Win NT/2000/XP 是真正的 32 位操作系统,所以从 32 位代码调用 16 位函数没有逻辑意义。这也称为 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` 存储在该内存地址的紧邻位置,整数 `i` 存储在内存地址 4683580 上,为什么不是 4683578,因为该地址不能被 4 整除。现在有一个问题,内存地址 4683578 和 4683579 上有什么?答案是,如果变量是局部变量,则为垃圾;如果变量是静态或全局变量,则为零。让我们看下面的程序来更好地理解这一点。

程序 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)
// 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 执行所需函数。

程序 77
#include <iostream>
#include <windows.h>
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` 是通过 `Init` 成员函数中初始化的 Thunk 调用。程序的执行过程如下:

  • 回调函数
  • 初始化(初始化 Thunk)
  • 获取 Thunk 的地址
  • 执行 Thunk
  • Thunk 代码调用 `StaticFun`

ATL 使用相同的技术来调用正确的回调函数,但在调用函数之前还做了一件事。现在 `ZWindow` 有一个额外的虚函数 `ProcessWindowMessage`,它在这个类中什么也不做。但是 `ZWindow` 的每个派生类都会重写它来处理自己的消息。过程是相同的,我们将 `ZWindow` 派生类的地址存储在一个指针中,以调用派生类的虚函数,但现在 `WindowProc` 的名称是 `StartWndProc`。这里 ATL 使用该技术来替换 `HWND` 为此指针。但是 `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;    // 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 代码会调用 `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 的其他神秘之处。

© . All rights reserved.