視窗程序只是每個訊息都會被調用的函式,因此本質上是無狀態的。 因此,你需要一種方法來追蹤應用程式從一個函式呼叫到下一個函式呼叫的狀態。
最簡單的方法是把所有東西放在全域變數裡。 這種方法對於小型程式來說相當有效,許多 SDK 範例也採用了這種方法。 然而,在大型計畫中,這會導致全域變數的大量增加。 此外,你可能有多個視窗,每個視窗都有自己的視窗程序。 要追蹤哪個視窗應該存取哪些變數會變得混亂且容易出錯。
CreateWindowEx 函式提供了一種將任何資料結構傳遞給視窗的方法。 當呼叫此函式時,會向你的視窗程序發送以下兩則訊息:
這些訊息依照列出的順序發送。 (這不是 CreateWindowEx 中唯一的兩則訊息,但其他的我們可以忽略。)
WM_NCCREATE和WM_CREATE訊息在視窗顯示前就已發送。 這讓它們成為初始化使用者介面的好地方——例如,用來決定視窗的初始佈局。
CreateWindowEx 的最後一個參數是一個類型為 void* 的指標。 你可以在這個參數中傳遞任何指標值。 當視窗程序處理 WM_NCCREATE 或 WM_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_NCCREATE 和 WM_CREATE 訊息時,每個訊息的 lParam 參數是指向 CREATESTRUCT 結構的指標。 CREATESTRUCT 結構則包含你傳入 CreateWindowEx 的指標。
以下是如何提取指向資料結構的指標的方法。 首先,透過 lParam 參數來取得 CREATESTRUCT 結構。
CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
CREATESTRUCT 結構中的 lpCreateParams 成員就是你在 CreateWindowEx 中指定的原始 void 指標。 透過 casting 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;
}
物件導向方法
我們可以將這種方法進一步擴展。 我們已經定義了一個資料結構來儲存關於視窗的狀態資訊。 因此,提供這個資料結構以成員函式(方法)來操作資料是合理的。 這自然而然會形成這樣的設計:由結構(或類別)負責所有對視窗的操作。 視窗程序接著會成為該類別的一部分。
換句話說,我們希望從以下內容出發:
// 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, PWSTR, 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 Foundation Classes(MFC)和 Active Template Library(ATL),採用的方法基本上與此處所示相似。 當然,像MFC這樣完全通用的框架比這個相對簡單的例子更為複雜。