管理應用程式狀態

視窗程式只是針對每個訊息叫用的函式,因此它原本就是無狀態的。 因此,您需要一種方式,從一個函式呼叫到下一個函式呼叫來追蹤應用程式的狀態。

最簡單的方法是將所有專案放在全域變數中。 這適用于小型程式,而且許多 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時,請在 final 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;
}

pure-virtual 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 Classs (MFC) 和 Active Template Library (ATL) ,都會使用基本上類似此處所示的方法。 當然,MFC 之類的完整一般化架構比這個相對簡單的範例更為複雜。

下一個

課程模組 2:在 Windows 程式中使用 COM

BaseWindow 範例