窗口过程是一个函数,该函数包含了每条消息对应的业务逻辑。它本身是无状态的。然而有些时候你需要跟踪应用程序中每次函数调用的状态信息。

最简单的方法就是将所有东西都放到一个全局变量中,对于简单的程序来说这种方法足够了,许多 SDK 的案例都是用这种方法。然而,对于复杂程序来说,这种方法会导致全局变量的骤增,另外,你的应用程序可能不止一个窗口,每个窗口都有自己的窗口过程,区分哪个窗口使用哪个全局变量有时候并不是一个简单的事情,过多相似属性的全局变量会让程序可读性非常差,书写过程中非常容易造成错误。

CreateWindowEx 函数提供一种方式可以将任何一种数据结构以指针的方式传递给窗口。当这个函数被调用的时候,它会向窗口过程发送下面两条消息:

  • WM_NCCREATE

  • WM_CREATE

消息的顺序和列表中的一致,CreateWindowEx 函数不仅仅发送这两条消息,但是其它的消息暂时被忽略。

WM_NCCREATE 和 WM_CREATE 消息会在窗口显示之前发送,初始化 UI 的逻辑可以放到二者的消息处理函数中。例如,可以在处理函数中处理窗口布局初始化的代码。

CreateWindowEx 函数最后一个参数是一个 void* 类型的指针变量。你可以通过该参数传递任何你想传递的值。当窗口过程在处理 WM_NCCREATE 和 WM_CREATE 消息的时候,它能从消息的附加数据中拿到该值。

让我们展示一下这个功能,首先你需要定义一个结构体保存状态信息。

// Define a structure to hold some state information.

struct StateInfo {

    // ... (struct members not shown)

};

当你调用 CreateWindowEx 函数的时候,将这个结构体的指针传入。

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_NCCREATE 和 WM_CREATE 消息的时候,lParam 参数代表一个指针变量,指向一个 CREATESTRUCT 类型的结构体。该结构体的内部就包含了从 CreateWindowEx 传进来的指针变量。

参数详解

想要获取数据值,你需要先将 lParam 参数转换为 CREATESTRUCT 结构体的指针变量。

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

通过 CreateWindowEx 函数传入的 void* 指针变量被存储在结构体的 lpCreateParams 成员变量中。

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

接下来,你可以通过 SetWindowLongPtr 函数将指针数据存储起来。

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

这个函数会将你存储的数据和窗口绑定,这样当你需要这个数据的时候,可以随时通过 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;
}

按照上边的思路,我们可以使用面向对象的方法进一步优化。现在已经有了一个数据结构来存储窗口相关的状态信息,而为了操作这些状态数据,会提供一些相关的接口。响应的调用这些接口的窗口过程就会逐渐变成了数据结构(类)的一部分。

换句话说,原来的窗口过程代码:

// 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 注册时候传入的窗口过程指针改为 MyWindow::WindowProc 即可。因为窗口过程必须是静态成员函数,所以在窗口过程中调用普通成员函数就需要额外的获取 this 指针,而前文提到的方法正式解决之道,只需要将原来的 StateInfo 结构体指针换位 MyWindow 的 this 指针。

下面是示例代码:

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 是一个抽象的基类,用它可以派生出特定的窗口类。例如:

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 类中有一个纯虚函数,用来实现自定义的窗口过程函数。例如:

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;
}

上面虚函数内部的第一个参数不是窗口句柄(HWND),原因是窗口句柄已经成为了 MainWindow 的成员变量(m_hwnd),并不需要传递就可以直接在函数中获取到。

很多 Windows 程序框架都是使用类似的方法,如 MFC、ATL等等,当然它们是比较完善通用的框架,所以代码要比上边展示的要复杂很多。