Lezione 4 – I messaggi crackers
Se le varie "funzioni di comodo" (e relative macro) ci risparmiano un po' di lavoro, e facilitano l'uso dei tipi, per spedire messaggi, non si potrebbe considerare l'uso di qualcosa di analogo anche per il compito di ricevere i messaggi stessi? La risposta è "sì", e lo stesso header file windowsx.h, già visto per le macro di spedizione, ne fornisce una versione: i "message crackers".
Ad esempio, la macro HANDLE_WM_COMMAND, definita in windowsx.h, accetta quattro parametri: i tre classici hwnd, wParam, lParam di un messaggio di command, e, come quarto, il nome di una funzione che deve essere simile, nomi a parte, a:
void Gestisci_Comando(HWND hDialogo,
int idControllo, // id del controllo
HWND hwndControllo, // hwnd del controllo
UINT uCodiceNotifica // sottocodice di comando
);
HANDLE_WM_COMMAND "spezza" il messaggio, nel senso dei vari cast, LOWORD, HIWORD, ecc, ecc, e chiama l'apposita funzione (suo quarto argomento).
Questo ci permette di riscrivere la TreControlli vista al capitolo precedente nella seguente forma:
void On_Comando(HWND hDialogo, int idCliccato, HWND hCliccato, UINT codice)
{
// i soliti controlli
if(codice==BN_CLICKED && (idCliccato==IDC_PRIMO || idCliccato==IDC_SECONDO))
// l'operazione, se i controlli sono OK
SetDlgItemText(hDialogo, IDC_SCRITTA, idCliccato==IDC_PRIMO?"Primo":"Secondo");
}
Questa forma non permette di tornare un codice d'errore (cioè di messaggio non gestito), ma, quando applicabile, è sicuramente più comoda e chiara, permettendo di ragionare al livello semantico un poco più alto di id e codici invece di doverseli ricavare ogni volta a colpi di cast, HIWORD, ecc.
Liberandoci dai dettagli dell'esatta codifica dei campi semanticamente significativi (idCliccato, ecc) nei campi-dati "di trasporto" (wParam, ecc), i message cracker migliorano inoltre la nostra portabilità: originalmente pensati per facilitare la migrazione/coesistenza fra versioni Windows a 16 e 32 bit, potranno, un domani, tornar magari comodi anche nella migrazione 32->64.
windowsx.h comprende anche una macro che facilita la scrittura dell'intera dialog procedure, con l'approccio "semi-ingenuo" precedentemente accennato (mappa messaggio->gestore che è codificata rigidamente nel codice eseguibile, ma gestione di ciascun messaggio in una sua funzione apposita):
#define HANDLE_MSG(hwnd, message, fn) \
case (message): \
return HANDLE_##message((hwnd), (wParam), (lParam), (fn))
L'idea è che si può scrivere direttamente, proprio come "corpo" della dialog procedure:
switch(uMessage)
{
HANDLE_MSG(hDialog,WM_CLOSE,OnClose)
HANDLE_MSG(hDialog,WM_COMMAND,OnCommand)
}
lasciando implicito il test (case) sul messaggio, ed il passaggio dei parametri wParam ed lParam, al cracker più opportuno. Questa soluzione non è particolarmente convincente, per quanto abbiamo già detto sull'approccio "semi-ingenuo", anche se è, ovviamente, molto superiore all'approccio "totalmente ingenuo" (gestire ogni messaggio nel corpo della dialog procedure!), e forse accettabile per casi particolarmente semplici, come questo che abbiamo testè esemplificato.
Se mai, potrebbe essere meglio definirci noi stessi una macro analoga, che metta la corrispondenza messaggio->funzione in una struttura dati, e un'altra che, "frugando" la struttura dati alla ricerca dello specifico messaggio, usi, quando lo trova, il cracker adatto; la dialog procedure userebbe quest'ultima, mentre la struttura verrebbe costruita, ad esempio nel WinMain, come parte dell'inizializzazione del sistema.
windowsx.h contiene parecchie altre macro "di comodo", che si possono usare o meno a seconda dei gusti. Ad esempio, chi non ami il nome poco pronunciabile della importante API GetDlgCtrlID, può usare al suo posto il nome GetWindowID, definito in windowsx.h; normalmente, "rinominare" in questo modo importanti funzioni di sistema per pure considerazioni "estetiche" sarebbe una prassi di valore molto dubbio, e certamente da sconsigliare, ma, in questo caso, essendo il #define a sua volta in un file di sistema, si può ammettere di considerare questo come quasi un "sinonimo" ufficiale.
Più interessante, comunque, la macro di windowsx.h:
SetDlgMsgResult(hwnd, msg, result)
da chiamarsi, di solito, nello specifico contesto:
return SetDlgMsgResult(hDialogo, WM_QUALCOSA, unRisultato);
per uscire dalla dialog procedure con un "risultato", che non sia solo TRUE o FALSE, a fronte di un certo messaggio WM_QUALCOSA. I meccanismi che questa macro utilizza internamente, esaminabili, chiaramente, dalla lettura di windowsx.h, suggeriamo, per il momento, di considerarli "pura magia nera" (e non si andrà troppo lontani dalla realtà...!-); li esamineremo, tuttavia, più avanti nel corso di questo tutorial.
Esempio
Abbiamo trattato, nei recenti capitoli, parecchie questioni che, benché molto importanti, possono parere piuttosto "teoriche"; in questo capitolo ricapitoleremo varie nozioni già viste (e ne introdurremo alcune altre) il tutto, nel contesto di un piccolo esempio. Un "esempio - giocattolo", senza dubbio, non certo un'applicazione di valore commerciale, nondimeno, speriamo che la maggior "concretezza" portata da un esempio rispetto a una pura trattazione teorica possa aiutare il lettore nel compito di apprendimento.
L'obbiettivo è quello di mostrare un dialogo che ha quattro pulsanti (pushbutton), cliccando ciascuno dei quali si ottiene un qualche effetto visibile. Visto che, dopo tutte queste chiacchiere, vi starete certo chiedendo dove stia la "G" di "GUI", vogliamo esagerare: ciascuno dei pulsanti avrà un "look", appunto, "grafico", e, come "effetto visibile", scegliamo lo spostamento di un'altra immagine "grafica" entro la nostra finestra di dialogo.
Dobbiamo, naturalmente, cominciare progettando la finestra di dialogo stessa, nel file di risorse (con identificatori che definiremo come macro, con #define, in un file di header che chiameremo resource.h). Ecco una possibilità, ad esempio:
#include "resource.h"
IDD_DIALOG1 DIALOG 0, 0, 186, 134
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Grafica a go-go!"
FONT 8, "MS Sans Serif"
BEGIN
PUSHBUTTON "Button1",IDC_BUTTON1,130,30,20,20,BS_BITMAP
PUSHBUTTON "Button2",IDC_BUTTON2,150,30,20,20,BS_BITMAP
PUSHBUTTON "Button3",IDC_BUTTON3,140,10,20,20,BS_BITMAP
PUSHBUTTON "Button4",IDC_BUTTON4,140,50,20,20,BS_BITMAP
ICON "",IDC_STICON,68,58,20,20
END
Nulla di nuovo nell'inizio, ma, immagino, vi starete chiedendo cosa siano quei nuovi statement PUSHBUTTON e ICON fra la BEGIN e la END del dialogo...?
Nulla di preoccupante! Sono solo delle "abbreviazioni", potenzialmente comode, che il Resource Compiler RC ci offre, per esprimere cose che potremmo dire anche in altri modi.
PUSHBUTTON non indica altro che un CONTROL, di classe BUTTON, con i tipici bit di stile del pushbutton, qui, inoltre, abbiamo aggiunto, dandolo esplicitamente, il bit di stile BS_BITMAP, che, ricorderete, indica che questo pulsante non mostra del testo, bensì, appunto, una bitmap. L'idea è di sistemare i quattro pulsanti "a croce", diretti "ai 4 punti cardinali" (aggiustate pure le coordinate come vi aggrada, naturalmente!).
Analogamente, ICON indica un CONTROL di classe STATIC, con i tipici bit di stile necessari perché esso mostri una icona.
Naturalmente, non abbiamo ancora detto quali bitmap, o icone, intendiamo mostrare sui nostri controlli, lo faremo "al volo" nel corso dell'inizializzazione della nostra finestra di dialogo, cioè, nella dialog procedure, in risposta al messaggio WM_INITDIALOG. Notare, in particolare, che non abbiamo né bitmap, né icone, fra le risorse del nostro programma dovremo dunque "andarle a prendere" da qualche altra parte, come vedremo.
La nostra procedura WinMain sarà più o meno la solita, cioè, supponendo di avere già definito in modo opportuno una procedura di dialogo, sarà qualcosa del tipo:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), 0, DialogProc, 0);
return 0;
}
avendo, naturalmente, posto l'#include "resource.h" verso l'inizio del file sorgente, per rendere visibile qui la macro IDD_DIALOG1.
Per la struttura base della dialog procedure, accettiamo, ad esempio, l'impostazione offerta da windowsx.h, che abbiamo già esaminato; accertatevi dunque di includere windowsx.h, oltre, naturalmente, al resto di windows.h, con i soliti #define prima dell'include, e un altro che ancora non avevamo visto:
#define OEMRESOURCE
che è necessario per potere accedere ad alcune "risorse standard" che ci faranno comodo.
Sappiamo già che vogliamo gestire i messaggi:
- WM_INITDIALOG, per inizializzare;
- WM_CLOSE, per terminare;
- WM_COMMAND, per rispondere ai clic sui nostri pushbutton
e quindi, la dialog procedure non sarà molto diversa da:
BOOL CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
HANDLE_MSG(hwndDlg,WM_CLOSE,OnDlgClose);
HANDLE_MSG(hwndDlg,WM_INITDIALOG,OnDlgInitDialog);
HANDLE_MSG(hwndDlg,WM_COMMAND,OnDlgCommand);
}
return FALSE;
}
La macro HANDLE_MSG, come abbiamo già accennato, comprende il "case WM_qualcosa:", gli "spezzettamenti" (cracking) veri e propri, ed eventuali cast, necessari per lo specifico messaggio, la chiamata alla funzione il cui nome riceve come terzo parametro, e il return dall'intera dialog procedure. Ci resta, dunque, da scrivere le tre procedure che abbiamo scelto di chiamare OnDlgClose, OnDlgInitDialog, e OnDlgCommand, ciascuna con gli opportuni parametri previsti da windowsx.h. Di queste, la più semplice è certo la OnDlgClose:
void OnDlgClose(HWND hWnd)
{
EndDialog(hWnd,0);
}
Più interessanti, certamente, le altre due funzioni. Vediamone intanto gli "scheletri", ancora vuoti, così da potere, se non altro, mettere insieme un primo abbozzo complessivo di programma, compilarlo, e provarlo:
BOOL OnDlgInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
// qui: azioni di inizializzazione
return TRUE;
}
void OnDlgCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
// qui: azioni in risposta ai comandi
}
Spero che i significati dei vari parametri siano già chiari dai capitoli precedenti.
Eseguendo questa versione "scheletrica", naturalmente, non è che vediamo gran che visto che non abbiamo detto nulla, tra l'altro, su quali bitmap e icone mostrare! Procediamo dunque subito a completare l'inizializzazione.
Dobbiamo caricare e mostrare quattro bitmap sui quattro pulsanti; la ripetitività del compito già ci dice che vorremo scrivere per questo scopo una funzione apposita, e chiamarla quattro volte. Per "simmetria", delegheremo ad un'altra funzione anche il compito di caricare e mostrare l'icona sullo static, se le scriviamo bene, sono funzioni che possono comunque tornarci utili anche in futuro.
L'intera funzione di inizializzazione del dialogo si potrebbe dunque presentare così:
BOOL OnDlgInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
Mostra_BM_bottone(OBM_LFARROW, hwnd, IDC_BUTTON1);
Mostra_BM_bottone(OBM_RGARROW, hwnd, IDC_BUTTON2);
Mostra_BM_bottone(OBM_UPARROW, hwnd, IDC_BUTTON3);
Mostra_BM_bottone(OBM_DNARROW, hwnd, IDC_BUTTON4);
Mostra_Icona_static(IDI_HAND, hwnd, IDC_STICON);
return TRUE;
}
Già, ma cosa sono quelle OBM_eccetera, e IDI_eccetera? Sono gli identificatori delle risorse "OEM" (sigla che sta per "Original Equipment Manufacturer"); in pratica, risorse "di sistema" di Windows, che qualsiasi applicazione può usare. (Le OBM_ecc sono numeri, le IDI_ecc, stringhe).
I quattro valori OBM_ecc che abbiamo usato identificano quattro bitmap di "freccia", nelle quattro direzioni -- proprio quel che vogliamo per i nostri pushbutton; la IDI_HAND è una "icona di errore", che ho scelto solo in quanto sempre "ben visibile".
Oltre all'identificatore della risorsa "OEM" da usare, le due funzioni prendono l'handle della finestra di dialogo, e l'identificatore (numerico) del controllo su cui devono agire; questo garantisce una discreta generalità.
Resta, dunque, da scrivere queste due funzioni. Esse sono, per fortuna, di struttura abbastanza simile e semplice:
void Mostra_Icona_static(const char* ic_cod, HWND hDlg, int sta_cod)
{
HWND hSta = GetDlgItem(hDlg, sta_cod);
HICON hi = LoadIcon(0,ic_cod);
SendMessage(hSta,STM_SETICON,(WPARAM)hi,0);
}
Questa ottiene l'handle del controllo con la già vista API GetDlgItem, l'handle dell'icona con la (per noi nuova) API LoadIcon, e, con un semplice messaggio STM_SETICON, "congiunge" le due nel modo desiderato.
L'API LoadIcon prende due parametri: il primo è una HINSTANCE (tipo che abbiamo già incontrato, e risolutamente ignorato), e, passando il valore 0 per questo parametro, le indichiamo che vogliamo caricare una risorsa di sistema; il secondo parametro, invece, è l'identificatore della risorsa stessa.
void Mostra_BM_bottone(int bm_cod, HWND hDlg, int but_cod)
{
HWND hBut = GetDlgItem(hDlg, but_cod);
HBITMAP hi = LoadBitmap(0,MAKEINTRESOURCE(bm_cod));
SendMessage(hBut,BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)hi);
}
Proprio la stessa logica appena più complicata per dettagli come la necessità di "convertire" l'identificatore di risorsa da int a stringa (cosa che fa per noi la macro MAKEINTRESOURCE, naturalmente), e le complicazioni del messaggio BM_SETIMAGE, che richiede come wParam l'indicazione se stiamo caricando una bitmap o un'icona, e prende l'handle come lParam.
Possiamo dunque compilare nuovamente il nostro piccolo esempio, e, questa volta, dovremmo avere un "look" un poco più soddisfacente. Ci sono ancora dei problemi, naturalmente, tipo il fatto che il look non è del tutto soddisfacente, e la secondaria considerazione che l'intero programma non fa ancora nulla (né mai farà nulla, finché non ci decideremo a farlo rispondere ai comandi).
Quel che vogliamo fare, in risposta ai click dell'utente sui quattro bottoni che abbiamo definito, è "spostare" una finestra il controllo di tipo static che pure abbiamo definito. Come ottenere questo?
Windows non definisce una API che serva veramente a "spostare" una finestra, cioè ad effettuare una modifica relativa alla sua attuale posizione. A prima vista, può sembrare che l'API MoveWindow serva allo scopo, ma, in realtà, nonostante il nome, esse non permette tali "spostamenti", solo il "settaggio" di una nuova posizione (e, sempre in barba al nome, anche il cambio di dimensione di una finestra).
Per usare MoveWindow allo scopo di fare cambiamenti relativi alla posizione di una finestra, dovremmo prima scoprire la sua attuale posizione (e dimensione) nelle coordinate usate da MoveWindow (che, per una finestra-figlia, come è un controllo, sono coordinate relative alla finestra-contenitore, cioè il dialogo); poi, calcolare la nuova posizione; infine, usare MoveWindow per imporre la nuova posizione (e dimensioni eguali alle precedenti).
Non esiste, però, una API che ci dia direttamente le coordinate e dimensioni di una finestra nel sistema di coordinate usate da MoveWindow! Fortunatamente, ci sono vari approcci alternativi. Il più semplice consiste nell'uso di due API che, a differenza di MoveWindow, esistono in effetti in ragionevole "coppia": GetWindowPlacement, che ci dà informazioni sulla posizione di una finestra, e SetWindowPlacement, che ci permette di stabilire questa posizione, esattamente nelle stesse unità di misura, e coordinate, che la precedente GetWindowPlacement ci ha fornito.
Le API di Windows si dividono normalmente in due categorie: quelle che prendono un grande numero di parametri, e quelle che prendono come parametro l'indirizzo di una struttura grossa e complicata.
Quelle del secondo tipo, anche se la cosa può non essere immediatamente evidente, sono generalmente meno scomode, perché una struttura può in genere essere "azzerata in massa" (ad esempio con l'API ZeroMemory, che in realtà è normalmente solo una comoda macro, definita in un header file standard di Windows, che riveste la funzione standard memset), dopo di che possiamo settare i soli campi che interessano; la struttura può essere riempita da una funzione che ci fornisce informazioni, e di essa useremo allora i soli campi che ci interessano; e così via. Le API che prendono un grande numero di parametri distinti non offrono analoghe comodità.
GetWindowPlacement e SetWindowPlacement sono API di quelle che prendono per indirizzo una struttura, specificamente, nel loro caso, una struttura del tipo WINDOWPLACEMENT, entrambe hanno, come primo parametro, l'HWND della finestra che interessa trattare. La struttura WINDOWPLACEMENT è così dichiarata negli header file di Windows:
typedef struct _WINDOWPLACEMENT
{
UINT length;
UINT flags;
UINT showCmd;
POINT ptMinPosition;
POINT ptMaxPosition;
RECT rcNormalPosition;
} WINDOWPLACEMENT;
è piuttosto comune, in questi header, che il nome della struttura sia un typedef di un "vero nome" che si ottiene appiccicando un _, o tag_, o struct_, o altro ancora, al nome-tipo che si usa sempre.
Altrettanto comune è che il primo campo di una struttura sia riservato per dire quanti byte occupa la struttura stessa. Questo serve perché, in certi casi, le future versioni di Windows potranno, nelle stesse API, accettare strutture più estese, per dare o ricevere più informazioni, tuttavia, le nuove versioni di queste API, controllando questo primo campo, saranno in grado di determinare se gli è stata passata una struttura "del vecchio tipo", e comportarsi di conseguenza. Si otterrà così la compatibilità binaria con vecchi programmi già compilati, pur offrendo nuove funzionalità a programmi nuovi.
Per avere informazioni sul posizionamento di una finestra, dunque, la "forma idiomatica" è:
WINDOWPLACEMENT wp;
ZeroMemory(&wp, sizeof(wp));
wp.length = sizeof(wp);
if(!GetPlacement(hwnd, &wp))
{
// errore - diagnosticare, magari con l'aiuto dell'API GetLastError()
}
// qui possiamo consultare i campi di wp
In questo frammento di programma abbiamo anche fatto il doveroso controllo di errore, le API non sono quasi mai funzioni "void", bensì ritornano una indicazione di errore di tipo BOOL (FALSE, cioè zero, in caso di errore), o, se devono tornare normalmente un "valore utile", hanno generalmente un "valore riservato" che viene tornato in caso d'errore (molto spesso il valore tornato in caso di errore è zero, ma non sempre; a volte, ad esempio, per alcune delle API che tornano una HANDLE, l'indicazione di errore è invece il ritorno di INVALID_HANDLE_VALUE). Se un errore si è verificato, il suo codice d'errore è reperibile chiamando l'API GetLastError, e su questo codice si può basare la segnalazione dell'errore all'utente, il tentativo di rimediare all'errore, o che altro.
Vediamo ora i campi della WINDOWPLACEMENT che GetWindowPlacement riempie per noi, e che quindi noi possiamo esaminare dopo averla chiamata con successo. length lo abbiamo assegnato noi all'inizio, e l'API non lo cambia; flags è sempre a zero. showCmd ci indica se la finestra sia ridotta a icona (minimizzata), forzata a riempire lo schermo (massimizzata), o in condizioni di visualizzazione normali; per una finestra-figlia, come il controllo che qui ci interessa, avremo sempre quest'ultima condizione. ptMinPosition e ptMaxPosition danno le coordinate usate se la finestra è rispettivamente iconizzata o massimizzata, e quindi, per la stessa ragione, in questo caso non ci interessano. Tutta l'info di nostro interesse in questo caso, dunque, è contenuta nel campo (di tipo RECT) rcNormalPosition.
RECT è una semplicissima struttura che serve a rappresentare un rettangolo:
typedef struct _RECT
{
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
left e right sono le coordinate dei bordi sinistro e destro del rettangolo, top e bottom quelle dei suoi bordi superiore e inferiore.
La documentazione non è chiarissima su quali coordinate e unità di misura usi la struttura WINDOWPLACEMENT, ma, in pratica, si tratta di pixel, e, per finestre figlie, di coordinate entro l'area-cliente della finestra padre.
Torniamo, dunque, al nostro problema. Windows non ci offre proprio la funzione API che vorremmo:
BOOL WindMove(HWND hwnd, int dx, int dy);
ma ci dà tutti gli strumenti per farcela noi, tutto sommato in modo abbastanza semplice:
{
WINDOWPLACEMENT wp;
ZeroMemory(&wp, sizeof(wp));
wp.length = sizeof(wp);
if(!GetWindowPlacement(hwnd, &wp)) return FALSE;
wp.rcNormalPosition.left += dx;
wp.rcNormalPosition.right += dx;
wp.rcNormalPosition.top += dy;
wp.rcNormalPosition.bottom += dy;
return SetWindowPlacement(hwnd, &wp);
}
Armati di questa funzione general purpose di nostra fattura, la gestione dei comandi nel nostro programmino d'esempio diventa allora semplice:
void OnDlgCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
const int dp = 5; // per esempio
if(codeNotify!=BN_CLICKED) return;
HWND hStatic = GetDlgItem(hwnd, IDC_STICON);
switch(id)
{
case IDC_BUTTON1: WindMove(hStatic,-dp,0); break; // sinistra
case IDC_BUTTON2: WindMove(hStatic,+dp,0); break; // destra
case IDC_BUTTON3: WindMove(hStatic,0,-dp); break; // alto
case IDC_BUTTON4: WindMove(hStatic,0,+dp); break; // basso
}
}
Possiamo finalmente rimettere assieme il programma, ricompilare, e vedere infine un qualche movimento!
Ci sono però ancora almeno un paio di dettagli, nel comportamento di questo minuscolo programma, che non possiamo considerare pienamente soddisfacenti.
Un primo problema emerge quando, nel far "girare" l'icona sulla finestra di dialogo, essa si trova a "interagire" con i pulsanti che si trovano in alto a destra della dialog-box.
Anzitutto, vediamo l'icona passare "sopra" i pulsanti, con l'eccezione di quello che stiamo cliccando, cui invece sembra passare "sotto"; peggio, quando "passa sotto", notiamo che essa sbuca "sporca" dall'altra parte, portando le tracce del bottone che, chiaramente, le è stato "disegnato sopra".
Quando delle "finestre-sorelle" interagiscono "graficamente" in questo modo, è di solito un chiaro sintomo che manca, fra i loro bit di stile, uno in particolare: WS_CLIPSIBLINGS.
In effetti, non abbiamo messo questo bit di stile nel nostro file .RC (e Windows non lo attiva di default perché, di solito, le finestre dei controlli sono "ferme", rispetto al dialogo in cui si trovano; quindi, questo "clipping" non darebbe benefici, però potrebbe "costatare" un sia pur minimo appesantimento del carico di esecuzione). Per attivare questo bit di stile per ciascuna delle finestre dei nostri controlli, basta, cambiare le righe fra il BEGIN ed END del dialogo a qualcosa come:
PUSHBUTTON "Button1",IDC_BUTTON1,130,30,20,20,BS_BITMAP | WS_CLIPSIBLINGS PUSHBUTTON "Button2",IDC_BUTTON2,150,30,20,20,BS_BITMAP | WS_CLIPSIBLINGS PUSHBUTTON "Button2",IDC_BUTTON3,140,10,20,20,BS_BITMAP | WS_CLIPSIBLINGS PUSHBUTTON "Button2",IDC_BUTTON4,140,50,20,20,BS_BITMAP | WS_CLIPSIBLINGS ICON "",IDC_STICON,68,58,20,20,WS_CLIPSIBLINGS
Per l’icona WS_CLIPSIBLINGS risulta l'unico bit di stile che viene esplicitamente settato; per i "pushbutton" esso è "in aggiunta" al già asserito BS_BITMAP, e quindi usiamo l'operatore di "or, bit-per-bit" così che entrambi i bit vengano settati.
Ricompilando questo RC, e rilinkando il tutto per aggiornare le risorse del nostro EXE, vediamo che, in effetti, la situazione è diventata molto più ragionevole; l'icona passa sistematicamente "sotto" tutti i bottoni perché, elencandola per ultima, e quindi facendo sì che venga creata per ultima, implichiamo che è "la più bassa" nell'ordine della "coordinata Z" e comunque non viene "sporcata" da questi passaggi. Se volessimo invece farla passare "sopra", basterebbe naturalmente elencarla, nel file .RC, prima dei pulsanti.
Un altro potenziale difetto del nostro programma è che, a forza di click sui pulsanti, è possibile spingere fuori l'icona, in modo che essa non risulta più visibile sulla finestra. è discutibile se questo sia "giusto" o meno, ma più interessante può essere asserire che non lo è, e studiare come possiamo rimediare a questo "difetto".
Possiamo arricchire la funzione WindMove, che già ci siamo scritti, in modo che, facoltativamente, limiti lo spostamento che impartisce alla finestra che tratta, in modo da mantenerla all'interno di un certo rettangolo. Un buon modo per esprimere questo è con un classico idioma del C: aggiungiamo un parametro, di tipo puntatore a rettangolo (RECT*, o LPRECT che dir si voglia); se questo parametro è nullo, indica che non si desidera alcuna limitazione al movimento della finestra; se non nullo, punta al rettangolo entro il quale la finestra va mantenuta. Le modifiche necessarie alla nostra WindMove sono assai modeste. Il suo prototipo diventa, con un argomento in più:
BOOL WindMove(HWND hwnd, int dx, int dy, LPRECT pRectLimite);
subito prima del return SetWindowPlacement, possiamo inserire un test:
if(pRectLimite)
{
int dd = wp.rcNormalPosition.left-pRectLimite->left;
if(dd<0)
{
wp.rcNormalPosition.left -= dd;
wp.rcNormalPosition.right -= dd;
}
else
{
dd = wp.rcNormalPosition.right - pRectLimite->right;
if(dd>0)
{
wp.rcNormalPosition.left -= dd;
wp.rcNormalPosition.right -= dd;
}
}
// ripetere la stessa logica per top e bottom
}
Resta solo da decidere quale rettangolo passare alla WindMove nella nostra OnDlgCommand e, se passarle effettivamente l'indirizzo di un rettangolo, o, invece, un puntatore 0, in modo che essa non ponga alcun limite allo spostamento dell'icona.
Perchè non lasciare la scelta all'utente interattivo? Tutto sommato, basta aggiungere un pulsante, di tipo "check box", per la scelta di "limitare il movimento dell'icona all'interno del dialogo" e questo si può fare semplicemente aggiungendo una AUTOCHECKBOX fra il BEGIN e l'END del dialogo (questo statement è la forma abbreviata del CONTROL con classe BUTTON e stile da check-box a risposta automatica), ricordiamo che "auto", qui, significa che il segno "spuntatura" verrà automaticamente messo o tolto nel box quando l'utente vi fa click con il mouse, così che il nostro programma non deve preoccuparsene.
Nella OnDlgCommand, possiamo aggiungere una variabile di tipo LPRECT (puntatore a RECT), e una di tipo
RECT:
LPRECT pRectLimit = 0; // default: niente limiti RECT rectLimit;
Prima dell’istruzione di switch, entro la quale vi sono le chiamate a WindMove, dovremo verificare se è necessario imporre i limiti, chiedendo al nuovo auto-checkbox quale sia il suo stato:
if(SendDlgItemMessage(hwnd, IDC_LIMITS, BM_GETCHECK, 0, 0)==BST_CHECKED)
{
// vanno accertati i limiti, poi:
pRectLimit = &rectLimit;
}
else pRectLimit = 0;
Inoltre, tutte le chiamate a WindMove nello switch dovranno avere pRectLimit come ultimo parametro.
Resta solo da capire come ottenere, nella variabile RECT di nome rectLimit, il rettangolo di limitazione del movimento, entro l'if che abbiamo appena scritto. Qui ci torna comoda l'API:
BOOL GetClientRect(HWND hwnd, LPRECT prect);
che deposita, nel RECT il cui indirizzo le è passato come secondo parametro, le coordinate della "area cliente" della finestra la cui handle lo riceve come primo parametro.
Una semplice chiamata:
GetClientRect(hwnd, &rectLimit);
sarà dunque sufficiente.
Stiamo, chiaramente, lasciando come esercizio per il lettore quello di "mettere insieme" tutti gli elementi che stiamo offrendo, allo scopo di ricostruire l'intero programma. Una volta fatto ciò, si potrà tornare a compilarlo e linkarlo, e osservare gli effetti.
Un altro problema, decisamente scocciante per l'utente interattivo di questo programmino, non ammette una soluzione così immediata... l'icona, come da specifiche, risponde ai "click" sui bottoni-freccia; ma se l'utente, come può venire spontaneo, tiene premuto il tasto sinistro del mouse, con il cursore su uno dei pulsanti non succede niente, finché, lasciando il tasto stesso, egli non completa l'azione di click. Sarebbe bello, invece, che i bottoni stessi avessero una azione di "ripetizione automatica", in altri termini, un simile comportamento da parte dell'utente dovrebbe avere come risultato, non la più assoluta immobilità dell'icona, bensì un suo continuo spostamento nella direzione indicata.
Come potremmo ottenere questo tipo di comportamento? Lo vedremo nel prossimo capitolo.











Hello