DirectX/Message loop

Indice del libro

Nel modulo precedente abbiamo visto come creare una finestra. Il problema è che quella finestra è praticamente inutilizzabile. In questo modulo scopriremo come rendere questa finestra viva e come farla interagire con il resto del mondo.

Messaggi modifica

Ogni volta che un evento deve essere segnalato ad una finestra, Windows crea un messaggio per la finestra e lo inserisce in un buffer. L'applicazione ha il compito di prelevare i messaggi l'uno dopo l'altro e di processarli, eseguendo ogni compito che ritiene necessario. Se questi messaggi non vengono interpretati, la finestra non subisce eventi di alcun tipo (creazione, distruzione, mouse, tastiera, ...) e resta un bel rettangolo grigio. Normalmente il codice da eseguire per la gestione dei messaggi è il seguente:

MSG msg = { 0 };
while (msg.message != WM_QUIT)
{
    if (GetMessage(&msg, 0, 0, 0))
    {
        // Gestisci il messaggio
    }
}

Questo codice andrebbe eseguito subito dopo lo ShowWindow del modulo precedente. GetMessage è una API che legge l'ultimo messaggio dal buffer e lo inserisce nella variabile puntata come primo argomento. Il secondo argomento è l'HANDLE della finestra da cui vogliamo ricevere i messaggi. Mettendolo a zero riceveremo anche i messaggi di altre finestre (create sempre dallo stesso processo ovviamente), nel caso in cui pensaste di crearne più di una. Gli ultimi due argomenti settati a zero, sono il codice minimo e massimo dei messaggi da ricevere. Per esempio ci potrebbe piacere leggere solo i messaggi con il tipo da 0x200 a 0x20E, ossia solo quelli legati strettamente alla gestione del mouse. Tutti gli altri verrebbero ignorati. Quando impostati a zero, non viene imposto alcun limite. Tutti i messaggi vengono interpretati. GetMessage ritorna 0 se il messaggio in arrivo è WM_QUIT, ossia il messaggio che chiede la chiusura del programma, e ritorna non-zero per ogni altro messaggio.

GetMessage, nonostante funzioni a dovere in normali applicazioni, purtroppo presenta una grave lacuna per un videogioco. Infatti nel caso in cui non ci fossero messaggi nel buffer, il processo viene messo in pausa fino all'arrivo di un nuovo messaggio. Questo per un videogame è inaccettabile, infatti le scene verrebbero mostrate solo in presenza di un messaggio. Per superare questo impedimento in applicazioni grafiche viene utilizzato PeekMessage. Questa funzione cerca un messaggio nel buffer e ritorna non-zero se un messaggio viene trovato e deve essere interpretato, e zero se non ci sono messaggi da leggere. In questo caso noi mostreremo una scena disegnata in DirectX.

MSG msg = { 0 };
while (msg.message != WM_QUIT)
{
    if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
    {
        // Gestisci il messaggio
    }
    else
    {
        // Disegna con DirectX
    }
}

Se PeekMessage ritorna un valore diverso da zero, dobbiamo a questo punto gestire il messaggio. Questo normalmente si traduce con la chiamata di due funzioni:

if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

TranslateMessage è una comoda funzione che trasforma alcuni codici in altri a noi più comprensibili. Ad esempio può tradurre lo Scancode di un tasto, nel suo corrispondente nella tabella ASCII. DispatchMessage invece chiamerà a sua volta una certa funzione responsabile di gestire i singoli messaggi. Quale funzione? Una creata da noi così:

LRESULT CALLBACK MsgHandler(HWND window, UINT msg, WPARAM wParam, LPARAM lParam)
{
    
}

Questa funzione il cui nome non è rilevante, prende quattro argomenti:

  • L'HANDLE della finestra a cui era destinato il messaggio.
  • Un codice che identifica il messaggio stesso.
  • Due argomenti che possono anche non essere usati (ad esempio per il movimento del mouse potrebbero contenere le nuove coordinate x ed y del cursore)

La funzione viene chiamata da DispatchMessage per ogni messaggio ricevuto. Il valore di ritorno dipende dal tipo di messaggio. Generalmente ritornare zero vuol dire che il messaggio è stato correttamente elaborato (non vale per il 100% dei messaggi).

Associare l'handler alla finestra modifica

Come chiediamo però a Windows di chiamare proprio questa funzione con DispatchMessage? Il puntatore a questa funzione deve essere inserito nel campo lpfnWndProc di WNDCLASSEX al momento della creazione. Questo era l'ultimo elemento della struttura che rimasto in attesa di spiegazioni ed ora WNDCLASSEX non ha più segreti per voi.

Facciamo chiudere la finestra modifica

A questo punto la finestra dovrebbe essere ben visibile, ma non avrete alcuna speranza di riuscire a chiuderla. Infatti il click sulla X rossa, o Alt-F4 inviano un messaggio di tipo WM_DESTROY alla finestra, che fino ad ora noi avevamo completamente ignorato. Vediamo come chiudere la finestra:

LRESULT CALLBACK MsgHandler(HWND window, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}

Ci sono qui due nuove funzioni. PostQuitMessage inserirà nel buffer dei messaggi un messaggio di tipo WM_QUIT. Quando il nostro Message Loop riceverà un WM_QUIT, il loop si interromperà facendo terminare il programma. L'argomento è il parametro da ritornare da WinMain.

DefWindowProc è invece una richiesta che facciamo a Windows di gestire il messaggio. In altre parole, non avendo alcun interesse nel gestire tutti i messaggi esistenti, chiediamo a Windows di gestire in modo standard tutti quelli che noi scartiamo.

Altri messaggi da conoscere modifica

Molti di questi messaggi non li useremo nel nostro libro, ma sono sicuramente utili per un buon videogioco. Evidenzierò in grassetto quelli su cui dovremo lavorare più approfonditamente. Per gli altri si può vedere la libreria MSDN.

WM_ACTIVATE Questo messaggio è inviato alla finestra quando viene selezionata (attivata). Per attivare una finestra è sufficiente cliccarvici su, o selezionarla dalla taskbar dopo averla ridotta ad icona ad esempio. Lo stesso messaggio viene inviato anche quando la finestra viene disattivata. E' possibile riconoscere le due situazioni interpretando il valore in wParam.
WM_KEYDOWN Viene inviato quando viene schiacciato un tasto sulla tastiera. Viene inviato più volte se il tasto resta premuto. Il codice del tasto lo si può trovare in wParam, mentre in lParam si trovano alcune flags (conteggio di ripetizione, scancode OEM, ...)
WM_KEYUP Viene inviato quando viene rilasciato un tasto dalla tastiera. Il formato è uguale a WM_KEYDOWN
WM_CHAR Questo messaggio viene creato da TranslateMessage. Viene generato immediatamente dopo un WM_KEYDOWN. Questa volta in wParam sarà contenuto il codice ASCII del carattere da rappresentare. Generalmente si sceglierà se gestire WM_KEYDOWN o WM_CHAR, ignorando l'altro.
WM_LBUTTONDOWN Segnala lo schiacciamento del tasto sinistro del mouse. In wParam sono contenute delle flag che indicano lo stato di altri tasti, e dei tasti SHIFT, ALT, CTRL della tastiera. In lParam invece troviamo le coordinate del mouse.
WM_LBUTTONUP Pulsante sinistro rilasciato. Stesso formato di WM_LBUTTONDOWN
WM_MBUTTONDOWN, WM_RBUTTONDOWN Schiacciamento del pulsante centrale e destro del mouse.
WM_MBUTTONUP, WM_RBUTTONUP Rilascio del pulsante centrale e destro del mouse.
WM_MOUSEWHEEL Indica l'uso della rotellina di scroll. Nella high word di wParam troviamo un valore (delta) che indica una quantità di spostamento. Nella low word altre flags, e in lParam le coordinate del mouse.
WM_MOUSEHWHEEL Usato per segnalare l'uso della rotellina orizzontale. Stesso formato di WM_MOUSEWHEEL.
WM_MOUSEMOVE Viene inviato ogni volta che il mouse compie un movimento sulla finestra. Il formato è lo stesso di WM_LBUTTONDOWN.
WM_MOUSEHOVER Il mouse entra nella finestra.
WM_MOUSELEAVE Il mouse esce dalla finestra. lParam e wParam non sono usati.
WM_CREATE Viene inviato subito dopo la chiamata a CreateWindow, prima che la finestra sia visibile.
WM_SIZE Viene inviato ogni volta che la finestra subisce un ridimensionamento. Se trascinate le ancore ai lati della finestra, verrà inviato un WM_SIZE per ogni pixel di spostamento.
WM_MOVE Per ogni pixel di spostamento della finestra nello schermo, viene inviato un messaggio WM_MOVE.
WM_ENTERSIZEMOVE Inviato quando l'utente clicca sulle ancore di ridimensionamento ai lati.
WM_EXITSIZEMOVE L'utente ha rilasciato le ancore di ridimensionamento.

Esempi modifica

Proviamo a gestire l'utilizzo della tastiera. Utilizzando il messaggio WM_CHAR, possiamo sapere quando l'utente avrà premuto un tasto. WM_CHAR invia in wParam il codice UTF-16 del carattere Aggiungiamo quindi allo switch:

case WM_CHAR:
    printf("E' stato premuto il tasto: %lc\n", (WCHAR) wParam);
    break;

Ogni volta che la finestra sarà in primo piano ed un tasto verrà premuto, sarà visualizzato sul terminale. Se non avete ancora aperto il terminale seguite le istruzioni nel modulo Terminale di debug.

Sorgente per questo modulo modifica

A questo punto il codice del nostro programma dovrebbe assomigliare a qualcosa di simile:

#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include <fcntl.h>
#include <io.h>

/* Da queste parti ci mettiamo OpenConsole */

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    case WM_CHAR:
        printf("E' stato premuto il tasto: %lc\n", (WCHAR) wParam);
        break;
    default:
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR cmdLine, int cmdShow)
{
    OpenConsole();

    WNDCLASSEX wc;
    ZeroMemory(&wc, sizeof(WNDCLASSEX));

    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = MsgHandler;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
    wc.hCursor = LoadCursor(hInstance, IDC_ARROW);
    wc.hbrBackground = NULL;
    wc.lpszMenuName = NULL;
    wc.lpszClassName = L"DxTutorial1";
    wc.hIconSm = NULL;

    ATOM res = RegisterClassEx(&wc);
    if (!res)
    {
        printf("Impossibile registrare la classe.\n");
        return -1;
    }

    HWND windowHandle = CreateWindow(L"DxTutorial1", L"Titolo della finestra", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);
    ShowWindow(windowHandle, SW_SHOWDEFAULT);

    MSG msg = { 0 };
    while (msg.message != WM_QUIT)
    {
        if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
        {
            // Gestisci il messaggio
        }
        else
        {
            // Disegna con DirectX
        }
    }

    return 0;
}