Capire il ciclo dei messaggi e l’intera struttura di gestione dei messaggi sotto windows è essenziale per scrivere qualsiasi tipo di programma. Ora che abbiamo visto l’handling dei messaggi andiamo a guardare meglio all’interno dell’intero processo, in modo da evitare confusioni in seguito; per capire perchè le cose accadono e in che modo.

Cos’è un messaggio?

Un messaggio è un valore intero (int). Se diamo un’occhiata ai files header (che è una buona pratica comune quando si investiga sulle API) potremo trovare che:

#define WM_INITDIALOG                   0x0110
#define WM_COMMAND                      0x0111

#define WM_LBUTTONDOWN                  0x0201

…e così via. I messaggi sono usati per comunicare praticamente tutto alle finestre, per lo meno a livello base. Se volete che una finestra o un controllo (che è fondamentalmente una finestra più specializzata) facciano qualcosa, dovete mandargli un messaggio. Se un altra finestra vuole che facciate qualcosa, vi manda un messaggio. Quando accade un evento, tipo premere i tasti sulla tastiera, muovere il mouse, fare click con il mouse, viene inviato un messaggio alla finestra coinvolta; se questa è la vostra finestra, potete scegliere di processare il messaggio e intraprendere azioni.

Ogni messaggio può avere fino a due parametri, wParam e lParam. In origine wParam era a 16 bit e lParam era a 32 bit, ma in Win32 sono entrambi a 32 bit. Non tutti i messaggi usano questi parametri e ogni messaggio li usa in modo differente. Per esempio il messaggio WM_CLOSE non usa nessuno dei due e quindi dovete ignorarli. WM_COMMAND invece li usa entrambi, wParam contiene due valori, HIWORD(wParam) è il messaggio di notifica (se applicabile) e LOWORD(wParam) è il controllo o l’id del menu che invia il messaggio. lParam è l’HWND (handle della finestra) del controllo che ha inviato il messaggio oppure NULL se il messaggio non proviene da un controllo.

HIWORD() e LOWORD() sono macro definite da windows che isolano rispettivamente la word alta (High Word) di un valore a 32 bit (0xFFFF0000) e la word bassa (0x0000FFFF). In Win32 una WORD è un valore a 16bit, e DWORD (oppure Word Doppia) un valore a 32bit.

Per inviare un messaggio potete usare PostMessage() oppure SendMessage(). PostMessage() mette il messaggio all’interno della coda (Message Queue) e ritorna immediatamente. Questo vuol dire che dopo aver chiamato PostMessage() il messaggio potrebbe essere stato già processato o meno. SendMessage() invece, invia il messaggio direttamente alla finestra e non ritorna finchè questa non ha finito di processarlo. Se volessimo chiudere una finestra potremmo inviarle un messaggio WM_CLOSE del tipo: PostMessage(hwnd, WM_CLOSE, 0, 0); che potrebbe avere lo stesso effetto di fare click sul bottone di chiusura x posizionato nella parte superiore della finestra. Notate che wParam e lParam sono entrambi a 0. Questo perchè, come già detto, non viene passato nessun parametro a WM_CLOSE.

Dialogs

Quando iniziate ad usare i dialog boxes, avete bisogno di inviare i messaggi ai controlli in modo da comunicare con loro. Potete farlo sia usando prima GetDlgItem() per ottenere l’handle del controllo usando l’ID e poi chiamando SendMessage(), OPPURE potete usare SendDlgItemMessage() che non fa altro che combinare i due passaggi. SendDlgItemMessage() e API simili, del tipo GetDlgItemText() possono essere applicate a tutte le finestre, non solo ai dialog boxes.

Cos’è una coda di messaggi (Message Queue)

Diciamo che il vostro programma è occupato a processare il messaggio WM_PAINT e contemporaneamente l’utente scrive una serie di cose con la tastiera. Cosa dovrebbe accadere? Interrompere la funzione e processare i tasti premuti oppure ignorare i tasti premuti? Ovviamente nessuna delle due opzioni ha senso; è proprio per questo che esiste la coda dei messaggi. Quando un messaggio viene “Postato”, esso viene aggiunto alla coda dei messaggi e da qui viene rimosso dopo essere stato processato. Questo ci assicura che nessun messaggio verrà mai perso mentre se ne sta processando un altro.

Cos’è un ciclo di messaggi (Message Loop)

while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&Msg);
    DispatchMessage(&Msg);
}
  1. Scrivo tra parentesi il corrispondente inglese perchè il termine Message Loop è comunemente utilizzato anche in Italia. Il ciclo di messaggi chiama GetMessage(), che “guarda” all’interno della coda dei messaggi. Se essa è vuota l’esecuzione viene bloccata (In termini più tecnici si dice che GetMessage Blocca, finchè…) non riceve un nuovo messaggio.
  2. Quando in seguito ad un evento viene inviato un messaggio alla finestra (per esempio il sistema registra un click del mouse) GetMessage() restituisce un valore positivo ** indicando che c’è un messaggio da processare, e che tutte le informazioni riguardanti il messaggio stesso sono state inserite all’interno della struttura MSG che abbiamo passato alla funzione stessa. In caso di WM_QUIT, GetMessage() ritorna 0, oppure ritorna **un valore negativo in caso di errore.
  3. Noi prendiamo il messaggio(nella variabile Msg) e lo passiamo a TranslateMessage(), che processa il messaggio ulteriormente, trasforma i messaggi generati dai caratteri virtuali (alt,ctrl…) in messaggi generati da caratteri. Questo passaggio non è obbligatorio ma in alcuni casi, molte cose potrebbero non funzionare o funzionare male se questa funzione non viene chiamata.
  4. Dopo aver fatto tutto questo passiamo il messaggio a DispatchMessage() che non fa altro che prendere il messaggio, controlla la finestra di destinazione, si ricava la Window Procedure (procedura che gestisce i messaggi della finestra di destinazione) ed esegue una chiamata ad essa passandole come parametri l’handle della finestra, il messaggio, e i wParam e lParam.
  5. Nella vostra procedura controllate il messaggio con i suoi parametri, e fate quello che ritenete opportuno con essi! Se non processate un messaggio in particolare potete quasi sempre chiamare DefWindowProc() che eseguirà le azioni predefinite al posto vostro (che spesso significa fare niente).
  6. Dopo che avete finito di processare il messaggio, la vostra procedura ritorna, DispatchMessage() ritorna a sua volta, e il ciclo ricomincia dall’inizio.

Quello espresso sopra è un concetto molto importante nei programmi per windows. La Window Procedure non viene chiamata magicamente dal sistema, la chiamate voi indirettamente tramite la chiamata DispatchMessage(). Analoghi risultati infatti si ottengono chiamando GetWindowLong() sull’handle della finestra a cui era destinato il messaggio e chiamando la Window Procedure direttamente! Ossia:

while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
    WNDPROC fWndProc = (WNDPROC)GetWindowLong(Msg.hwnd, GWL_WNDPROC);
    fWndProc(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
}

Perché usiamo DispatchMessage() allora? Beh, sicuramente perchè è quello descritto sopra è solo in sintesi ciò che fa DispatchMessage(); in realtà questa funzione assolve a determinati compiti di traduzione ANSI/Unicode per assicurare la portabilità del codice su altre piattaforme e su sistemi operativi che utilizzano una tabella di caratteri diversa dalla nostra; oltre che effettuare la chiamata di alcune callbacks ed altre piccole operazioni che se non eseguite possono portare all’instabilità del programma. Per questo motivo il codice sopra riportato usatelo solamente per provarne il funzionamento, ma mai in una applicazione reale :)

Notiamo l’uso di GetWindowLong() per ricavare la Window Procedure associata con la finestra. Perchè non chiamiamo semplicemente WndProc() direttamente? Beh, il nostro ciclo di messaggi è responsabile di tutte le finestre del nostro programma, incluse cose tipo botton e list boxes che hanno la loro propria Window Procedure (altrimenti non potrebbero essere visualizzate così come noi le vediamo dato che come abbiamo già detto i controlli sono nient’altro che semplici finestre con una propria Window Procedure), per questo motivo dobbiamo essere sicuri di chiamare la procedura esatta per la finestra. Poi, siccome più di una finestra potrebbe usare la stessa procedura, il primo parametro (handle della finestra) è usato per specificare alla procedura stessa di quale finestra ci stiamo occupando.

Come potete vedere, l’applicazione consuma la maggiorparte del tempo a fare giri e giri nel ciclo dei messaggi, è da qui che partono i messaggi per tutte le finestre. Ma cosa succede quando il programma esce? Siccome usiamo un ciclo while(), se GetMessage() ritorna FALSE (aka 0), il ciclo finisce e raggiungiamo la fine del nostro WinMain() che ci fa uscire dal programma. Questo è esattamente quello che PostQuitMessage() assolve. Esso piazza un messaggio WM_QUIT all’interno della coda e invece di ritornare un valore positivo, GetMessage() riempie la struttura Msg e ritorna 0. A questo punto, il membro wParam di Msg contiene il valore che avete passato a PostQuitMessage() e voi potete ignorarlo o farlo ritornare da WinMain() e quindi usarlo come codice di uscita del processo.

IMPORTANTE: GetMessage() ritorna **-1** se incontra un errore. Ricordatevi questo, oppure potrebbero verificarsi cose inaspettate… anche se in alcuni casi GetMessage() è definito come funzione che ritorna un valore BOOL, ricordatevi che può ritornare valori diversi da TRUE o FALSE, dato che BOOL è definito come UINT (unsigned int). Seguono degli esempi di codice che apparentemente funziona ma in realtà non verrà processato in modo esatto in alcune condizioni.

    while(GetMessage(&Msg, NULL, 0, 0))
    while(GetMessage(&Msg, NULL, 0, 0) != 0)
    while(GetMessage(&Msg, NULL, 0, 0) == TRUE)

Tutti gli esempi sopra sono sbagliati! Il corpo del while verrebbe processato anche in caso di errore e questo potrebbe portare a effetti indesiderati.

    while(GetMessage(&Msg, NULL, 0, 0) > 0)

Questa, o con codice equivalente è la forma da utilizzare SEMPRE.

Spero che ora abbiate capito meglio il Message Loop di Windows. Se questo non è ancora accaduto, non abbiate paura, tutto avrà più senso dopo che l’avrete utilizzato per un pò.