Gestione dello stato di un'applicazione

Una routine di finestra è solo una funzione che viene richiamata per ogni messaggio, quindi è intrinsecamente senza stato. È quindi necessario tenere traccia dello stato dell'applicazione da una chiamata di funzione alla successiva.

L'approccio più semplice consiste semplicemente nell'inserire tutto nelle variabili globali. Questa operazione funziona abbastanza bene per i programmi di piccole dimensioni e molti esempi di SDK usano questo approccio. In un grande programma, tuttavia, porta a una proliferazione di variabili globali. Inoltre, potresti avere diverse finestre, ognuna con la propria procedura di finestra. Tenere traccia di quale finestra deve accedere alle variabili che diventano confusioni e soggette a errori.

La funzione CreateWindowEx consente di passare qualsiasi struttura di dati a una finestra. Quando questa funzione viene chiamata, invia i due messaggi seguenti alla routine della finestra:

Questi messaggi vengono inviati nell'ordine elencato. Questi non sono gli unici due messaggi inviati durante CreateWindowEx, ma è possibile ignorare gli altri per questa discussione.

Il messaggio WM_NCCREATE e WM_CREATE vengono inviati prima che la finestra diventi visibile. Ciò consente di inizializzare l'interfaccia utente, ad esempio per determinare il layout iniziale della finestra.

L'ultimo parametro di CreateWindowEx è un puntatore di tipo void*. È possibile passare qualsiasi valore del puntatore desiderato in questo parametro. Quando la procedura della finestra gestisce il messaggio WM_NCCREATE o WM_CREATE , può estrarre questo valore dai dati del messaggio.

Verrà illustrato come usare questo parametro per passare i dati dell'applicazione alla finestra. Definire prima di tutto una classe o una struttura che contiene informazioni sullo stato.

// Define a structure to hold some state information.

struct StateInfo {
    // ... (struct members not shown)
};

Quando si chiama CreateWindowEx, passare un puntatore a questa struttura nel parametro void* finale.

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

Quando si ricevono i messaggi WM_NCCREATE e WM_CREATE , il parametro lParam di ogni messaggio è un puntatore a una struttura CREATESTRUCT . La struttura CREATESTRUCT , a sua volta, contiene il puntatore passato in CreateWindowEx.

diagramma che mostra il layout della struttura createstruct

Ecco come estrarre il puntatore alla struttura dei dati. Prima di tutto, ottenere la struttura CREATESTRUCT eseguendo il cast del parametro lParam .

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

Il membro lpCreateParams della struttura CREATESTRUCT è il puntatore void originale specificato in CreateWindowEx. Ottenere un puntatore alla propria struttura di dati eseguendo il cast di lpCreateParams.

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

Chiamare quindi la funzione SetWindowLongPtr e passare il puntatore alla struttura dei dati.

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

Lo scopo di questa ultima chiamata di funzione consiste nell'archiviare il puntatore StateInfo nei dati dell'istanza per la finestra. Dopo aver eseguito questa operazione, è sempre possibile recuperare il puntatore dalla finestra chiamando GetWindowLongPtr:

LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);

Ogni finestra ha i propri dati di istanza, quindi è possibile creare più finestre e assegnare a ogni finestra la propria istanza della struttura dei dati. Questo approccio è particolarmente utile se si definisce una classe di finestre e si crea più finestre di tale classe, ad esempio se si crea una classe di controllo personalizzata. È pratico eseguire il wrapping della chiamata GetWindowLongPtr in una piccola funzione helper.

inline StateInfo* GetAppState(HWND hwnd)
{
    LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
    StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);
    return pState;
}

È ora possibile scrivere la routine della finestra come indicato di seguito.

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

Approccio Object-Oriented

È possibile estendere ulteriormente questo approccio. È già stata definita una struttura di dati per contenere informazioni sullo stato sulla finestra. È consigliabile fornire questa struttura di dati con funzioni membro (metodi) che operano sui dati. Ciò comporta naturalmente una progettazione in cui la struttura (o la classe) è responsabile di tutte le operazioni nella finestra. La procedura della finestra diventerà quindi parte della classe.

In altre parole, vorremmo andare da questo:

// 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.
    }
}

A questo scopo:

// pseudocode

LRESULT MyWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_SIZE:
            this->HandleResize(...);
            break;

        case WM_PAINT:
            this->HandlePaint(...);
            break;
    }
}

L'unico problema è come collegare il MyWindow::WindowProc metodo. La funzione RegisterClass prevede che la procedura della finestra sia un puntatore alla funzione. Non è possibile passare un puntatore a una funzione membro (non statica) in questo contesto. Tuttavia, è possibile passare un puntatore a una funzione membro statica e quindi delegare alla funzione membro. Ecco un modello di classe che mostra questo approccio:

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

La BaseWindow classe è una classe base astratta, da cui derivano classi di finestre specifiche. Ecco ad esempio la dichiarazione di una classe semplice derivata da BaseWindow:

class MainWindow : public BaseWindow<MainWindow>
{
public:
    PCWSTR  ClassName() const { return L"Sample Window Class"; }
    LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
};

Per creare la finestra, chiamare 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;
}

Il metodo pure-virtuale BaseWindow::HandleMessage viene usato per implementare la procedura di finestra. Ad esempio, l'implementazione seguente equivale alla procedura della finestra visualizzata all'inizio del modulo 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;
}

Si noti che l'handle della finestra viene archiviato in una variabile membro (m_hwnd), quindi non è necessario passarlo come parametro a HandleMessage.

Molti dei framework di programmazione Windows esistenti, ad esempio Microsoft Foundation Classes (MFC) e Active Template Library (ATL), usano approcci che sono fondamentalmente simili a quelli illustrati qui. Naturalmente, un framework completamente generalizzato come MFC è più complesso di questo esempio relativamente semplicistico.

Prossima

Modulo 2: Uso di COM nel programma Windows

Esempio di BaseWindow