管理应用程序状态

窗口过程只是针对每条消息调用的函数,因此它本质上是无状态的。 因此,需要一种方法来跟踪应用程序从一个函数调用到下一个函数调用的状态。

最简单的方法是将所有内容放入全局变量中。 这足以适用于小型程序,许多 SDK 示例都使用此方法。 但是,在大型程序中,它会导致全局变量的扩散。 此外,你可能有多个窗口,每个窗口都有自己的窗口过程。 跟踪应访问哪些变量的窗口会变得混乱和容易出错。

CreateWindowEx 函数提供了一种将任何数据结构传递到窗口的方法。 调用此函数时,它会将以下两条消息发送到窗口过程:

这些消息按列出的顺序发送。 (这并非 在 CreateWindowEx 期间发送的唯一两条消息,但我们可以忽略此讨论的其他消息。)

WM_NCCREATEWM_CREATE消息在窗口可见之前发送。 这使得它们成为初始化 UI(例如,确定窗口的初始布局)的好位置。

CreateWindowEx 的最后一个参数是 void* 类型的指针。 可以在此参数中传递所需的任何指针值。 当窗口过程处理 WM_NCCREATEWM_CREATE 消息时,它可以从消息数据中提取此值。

让我们看看如何使用此参数将应用程序数据传递到窗口。 首先,定义保存状态信息的类或结构。

// Define a structure to hold some state information.

struct StateInfo {
    // ... (struct members not shown)
};

调用 CreateWindowEx 时,在最终 的 void* 参数中传递指向此结构的指针。

StateInfo *pState = new (std::nothrow) StateInfo;

if (pState == NULL)
{
    return 0;
}

// Initialize the structure members (not shown).

HWND hwnd = CreateWindowEx(
    0,                              // Optional window styles.
    CLASS_NAME,                     // Window class
    L"Learn to Program Windows",    // Window text
    WS_OVERLAPPEDWINDOW,            // Window style

    // Size and position
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

    NULL,       // Parent window    
    NULL,       // Menu
    hInstance,  // Instance handle
    pState      // Additional application data
    );

收到 WM_NCCREATEWM_CREATE 消息时,每条消息的 lParam 参数都是指向 CREATESTRUCT 结构的指针。 CREATESTRUCT 结构又包含传递到 CreateWindowEx 的指针。

显示 createstruct 结构布局的示意图

下面是提取指向数据结构的指针的方式。 首先,通过强制转换 lParam 参数获取 CREATESTRUCT 结构。

CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);

CREATESTRUCT 结构的 lpCreateParams 成员是在 CreateWindowEx 中指定的原始 void 指针。 通过强制转换 lpCreateParams 获取指向自己的数据结构的指针。

pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);

接下来,调用 SetWindowLongPtr 函数,并将指针传递到数据结构。

SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);

最后一次函数调用的目的是将 StateInfo 指针存储在窗口的实例数据中。 执行此操作后,始终可以通过调用 GetWindowLongPtr 从窗口获取指针:

LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);

每个窗口都有自己的实例数据,因此可以创建多个窗口,并为每个窗口提供自己的数据结构实例。 如果定义一个窗口类并创建该类的多个窗口(例如,如果创建自定义控件类),此方法尤其有用。 将 GetWindowLongPtr 调用包装在一个小型帮助程序函数中很方便。

inline StateInfo* GetAppState(HWND hwnd)
{
    LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
    StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);
    return pState;
}

现在,可以按如下所示编写窗口过程。

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    StateInfo *pState;
    if (uMsg == WM_CREATE)
    {
        CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
        pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);
    }
    else
    {
        pState = GetAppState(hwnd);
    }

    switch (uMsg)
    {


    // Remainder of the window procedure not shown ...

    }
    return TRUE;
}

Object-Oriented方法

我们可以进一步扩展此方法。 我们已经定义了一个数据结构来保存有关窗口的状态信息。 为此数据结构提供成员函数 (对数据进行操作的方法) 是有意义的。 这自然会导致设计,其中结构 (或类) 负责窗口上的所有操作。 然后,窗口过程将成为 类的一部分。

换句话说,我们希望从以下方面开始:

// pseudocode

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    StateInfo *pState;

    /* Get pState from the HWND. */

    switch (uMsg)
    {
        case WM_SIZE:
            HandleResize(pState, ...);
            break;

        case WM_PAINT:
            HandlePaint(pState, ...);
            break;

       // And so forth.
    }
}

更改为:

// pseudocode

LRESULT MyWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_SIZE:
            this->HandleResize(...);
            break;

        case WM_PAINT:
            this->HandlePaint(...);
            break;
    }
}

唯一的问题是如何挂接 MyWindow::WindowProc 方法。 RegisterClass 函数要求窗口过程是函数指针。 在此上下文中,不能将指针传递给 (非静态) 成员函数。 但是,可以将指针传递到 静态 成员函数,然后委托给成员函数。 下面是显示此方法的类模板:

template <class DERIVED_TYPE> 
class BaseWindow
{
public:
    static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
        DERIVED_TYPE *pThis = NULL;

        if (uMsg == WM_NCCREATE)
        {
            CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam;
            pThis = (DERIVED_TYPE*)pCreate->lpCreateParams;
            SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);

            pThis->m_hwnd = hwnd;
        }
        else
        {
            pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
        }
        if (pThis)
        {
            return pThis->HandleMessage(uMsg, wParam, lParam);
        }
        else
        {
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
    }

    BaseWindow() : m_hwnd(NULL) { }

    BOOL Create(
        PCWSTR lpWindowName,
        DWORD dwStyle,
        DWORD dwExStyle = 0,
        int x = CW_USEDEFAULT,
        int y = CW_USEDEFAULT,
        int nWidth = CW_USEDEFAULT,
        int nHeight = CW_USEDEFAULT,
        HWND hWndParent = 0,
        HMENU hMenu = 0
        )
    {
        WNDCLASS wc = {0};

        wc.lpfnWndProc   = DERIVED_TYPE::WindowProc;
        wc.hInstance     = GetModuleHandle(NULL);
        wc.lpszClassName = ClassName();

        RegisterClass(&wc);

        m_hwnd = CreateWindowEx(
            dwExStyle, ClassName(), lpWindowName, dwStyle, x, y,
            nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this
            );

        return (m_hwnd ? TRUE : FALSE);
    }

    HWND Window() const { return m_hwnd; }

protected:

    virtual PCWSTR  ClassName() const = 0;
    virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0;

    HWND m_hwnd;
};

BaseWindow 是抽象基类,从中派生特定窗口类。 例如,下面是派生自 BaseWindow的简单类的声明:

class MainWindow : public BaseWindow<MainWindow>
{
public:
    PCWSTR  ClassName() const { return L"Sample Window Class"; }
    LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
};

若要创建窗口,请调用 BaseWindow::Create

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
    MainWindow win;

    if (!win.Create(L"Learn to Program Windows", WS_OVERLAPPEDWINDOW))
    {
        return 0;
    }

    ShowWindow(win.Window(), nCmdShow);

    // Run the message loop.

    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

纯虚拟 BaseWindow::HandleMessage 方法用于实现窗口过程。 例如,以下实现等效于 模块 1 开头显示的窗口过程。

LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(m_hwnd, &ps);
            FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
            EndPaint(m_hwnd, &ps);
        }
        return 0;

    default:
        return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
    }
    return TRUE;
}

请注意,窗口句柄存储在成员变量 (m_hwnd) 中,因此无需将其作为参数传递给 HandleMessage

许多现有的 Windows 编程框架(如 Microsoft 基础类 (MFC) 和活动模板库 (ATL) )使用的方法与此处所示的方法基本相似。 当然,MFC 等完全通用化的框架比这个相对简单的示例更复杂。

下一步

模块 2:在 Windows 程序中使用 COM

BaseWindow 示例