Managing Application State 管理应用程序状态
窗口过程(window procedure)只是一个因各个消息而调用的函数,因此它本质上是无状态的。因此,您需要一种方法来跟踪应用程序从一个函数调用到下一个函数调用的状态。
最简单的方法就是把所有东西都放在全局变量(global variables)中。这对于小程序来说已经足够好了,而且许多SDK示例都使用这种方法。然而,在大型程序中,它会导致全局变量的激增。此外,您可能有几个窗口,每个窗口都有自己的窗口过程。跟踪哪个窗口应该访问哪些变量变得混乱和容易出错。
CreateWindowEx函数提供了将任何数据结构(data structure)传递到窗口的方法。当调用此函数时,它会向您的窗口过程发送以下两个消息:
这些消息是按所列出的顺序发送的。(这并不是CreateWindowEx期间发送的唯一两条消息,但是我们可以忽略其他消息进行讨论。)
WM_NCCREATE和WM_CREATE消息在窗口可见之前发送。这使它们成为初始化UI的好地方,例如,确定窗口的初始布局(initial layout)。
CreateWindowEx 的最后一个参数是void*类型的指针。您可以在这个参数中传递任何您想要的指针值。当窗口过程(window procedure)处理WM_NCCREATE或WM_CREATE消息时,它可以从消息数据中提取这个值。
让我们看看如何使用这个参数将应用程序数据(application data)传递到窗口(window)。首先,定义一个包含状态信息的类或结构体。
// 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_NCCREATE和WM_CREATE消息时,每个消息的lParam参数是指向 CREATESTRUCT结构的指针。反过来,CREATESTRUCT结构体包含您传入CreateWindowEx的指针。
以下是如何提取数据结构的指针。首先,通过转换lParam参数得到CREATESTRUCT 结构。
CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
CREATESTRUCT结构的lpCreateParams成员是在CreateWindowEx中指定的原始空指针。通过转换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);
每个窗口(window)都有自己的实例数据(instance data),因此可以创建多个窗口,并为每个窗口提供数据结构的实例。如果您定义一个windows类并创建该类的多个窗口(例如,如果您创建一个自定义控件类(custom control class)),那么这种方法尤其有用。将GetWindowLongPtr调用封装到一个小的助手函数中是很方便的。
inline StateInfo* GetAppState(HWND hwnd) { LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA); StateInfo *pState = reinterpret_cast<StateInfo*>(ptr); return pState; }
现在您可以将窗口过程(window procedure)编写为如下所示。
C++
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; }
An Object-Oriented Approach 面向对象的方法
我们可以进一步扩展这种方法。我们已经定义了一个数据结构(data structure)来保存关于窗口的状态信息(state information)。为这个数据结构提供操作数据的成员函数(方法)是有意义的。这自然会导致如何设计结构(或类)负责窗口上的所有操作。然后窗口过程(window procedure)将成为类的一部分。
换句话说,我们从这里开始:
C++
// 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; } }
唯一的问题是如何连接(hook up)MyWindow::WindowProc方法。RegisterClass 函数期望窗口过程(window procedure)是一个函数指针。在此上下文中,您不能传递一个指向(非静态)成员函数的指针。但是,可以将指针传递给静态成员函数(static member function),然后委托给成员函数。下面是一个类模板,展示了这种方法:
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类是一个抽象基类(abstract base class,),派生特定的窗口类(specific window classes)。例如,下面是派生自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方法用于实现窗口过程(window procedure)。例如,下面的实现相当于Module 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; }
注意,窗口句柄(window handle)存储在一个成员变量(m_hwnd)中,因此我们不需要将它作为一个参数传递给HandleMessage。
许多现有的Windows编程框架,如Microsoft Foundation Classes (MFC)和Active Template Library (ATL),使用的方法基本上与这里显示的方法相似。当然,像MFC这样的完全通用的框架比这个相对简单的例子要复杂得多。