アプリケーションの状態の管理

ウィンドウ プロシージャは、すべてのメッセージに対して呼び出される関数に過ぎないため、本質的にステートレスです。 そのため、ある関数呼び出しから次の関数呼び出しまでのアプリケーションの状態を追跡する方法が必要です。

最も簡単な方法は、すべてをグローバル変数に配置することです。 これは小規模なプログラムに対して十分に機能し、SDK サンプルの多くはこのアプローチを使用します。 しかし、大規模なプログラムでは、グローバル変数の急増につながります。 また、複数のウィンドウがあり、それぞれに独自のウィンドウ プロシージャが含まれます。 どのウィンドウがどの変数にアクセスすべきかを追跡すると、混乱を招き、エラーが発生しやすくなります。

CreateWindowEx 関数は、任意のデータ構造をウィンドウに渡す方法を提供します。 この関数が呼び出されると、次の 2 つのメッセージがウィンドウ プロシージャに送信されます。

これらのメッセージは、一覧表示された順序で送信されます。 ( これらは CreateWindowEx の間に送信される 2 つのメッセージだけではありませんが、この説明では他のメッセージは無視できます)。

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 を呼び出すときは、最後の 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 に渡したポインターが含まれています。

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

オブジェクト指向アプローチ

このアプローチをさらに拡張できます。 ウィンドウに関する状態情報を保持するデータ構造が既に定義されています。 このデータ構造には、データを操作するメンバー関数 (メソッド) を提供するのが理にかなっています。 これは当然、ウィンドウ上のすべての操作を構造体 (またはクラス) が担当する設計につながります。 その後、ウィンドウ プロシージャはクラスの一部になります。

言い換えると、私たちはここから行きたいと思います:

// 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のパラメーターとして渡す必要はありません。

Microsoft Foundation Classes (MFC) や Active Template Library (ATL) など、既存のWindows プログラミング フレームワークの多くは、基本的に次に示すようなアプローチを使用します。 もちろん、MFC などの完全に一般化されたフレームワークは、この比較的単純な例よりも複雑です。

次へ

Module 2: Windows プログラムでの COM の使用

BaseWindow サンプル