Con l'utilizzo della nuova interfaccia di Windows 95 i nostri programmi possono finalmente rinnovare il proprio "look" e sfruttare nuove funzionalità
Tra le numerose innovazioni offerte da Windows 95 e Windows NT 3.51 con il nuovo shell, una delle più importanti
è sicuramente la TaskBar, ovvero la finestra di sistema che contiene l'ormai celebre pulsante Start (o Avvio)
e la lista delle finestre attive. All'interno della taskbar si trova anche una zona che contiene di solito l'ora
corrente, e talvolta altre icone e informazioni. Questa zona, chiamata "tray notification area" (TNA),
è disponibile anche alle applicazioni che necessitano di una icona sempre visibile e che possa essere raggiunta
rapidamente: come vedremo, usarla ed aggiungere un tocco in più ai nostri programmi è tutt'altro
che difficile.
Un esempio di utilizzo della TNA si può avere lanciando Resource Meter, QuickRes oppure stampando un
documento: in tutti i casi appare una piccola icona vicino all'orologio di sistema. La differenza con le normali
icone associate ad un programma sta nel fatto che una icona inserita nella TNA può reagire all'input dell'utente:
per convenzione un click del pulsante destro provoca la comparsa di un menu, mentre un doppio click del pulsante
sinistro esegue un'azione di default (indicata in neretto nel menu). Questo funzionamento non a caso ricorda da
vicino quello del menu di sistema di un programma, ma se dal punto di vista dell'utente il meccanismo è
pressoché identico, ciò che accade "dietro le quinte" è ben diverso. Nel caso della
TNA infatti, i menu non sono gestiti automaticamente e non esiste un menu di default: tutte le azioni sono a carico
dell'applicazione che controlla l'icona. Quest'ultima in pratica si limita a comunicare all'applicazione che al
suo interno è stato premuto il tasto destro del mouse, ed il programma reagisce presentando all'utente un
menu. Naturalmente nulla vieta al programma di effettuare il reboot immediato del sistema... ma di solito si preferisce
attenersi a quella che è l'interfaccia standard di Windows 95.
Trattandosi di Windows, non c'è da stupirsi se l'icona comunica con l'applicazione inviando messaggi. L'icona
però non è una finestra e non possiede quindi una funzione WndProc preposta alla gestione degli eventi,
perciò questi devono essere inviati ad una "vera" finestra. Niente paura, praticamente tutti i
programmi Windows dispongono di una finestra principale e se ce ne fosse bisogno è sempre possibile crearne
una invisibile. Rimane tuttavia il problema di quali messaggi inviare: se l'icona spedisse un semplice WM_RBUTTONDOWN
in seguito ad un click del pulsante destro, la funzione WndProc che lo riceve potrebbe scambiarlo non a torto per
un evento generato nella propria client area. Inoltre la stessa WndProc potrebbe voler gestire più di una
icona, perché porsi dei limiti? La soluzione adottata dai progettisti è tale che al programmatore
viene lasciata la massima libertà: sarà infatti l'applicazione a decidere per ogni icona il valore
del messaggio inviato ed un identificativo che, in caso si voglia usare lo stesso messaggio per diverse icone,
permetta
di riconoscere l'icona che lo ha generato.
L'accesso alla "tray notification area" avviene tramite una sola funzione contenuta nella libreria di sistema SHELL32 (entrata numero 248) e dichiarata come segue:
BOOL APIENTRY Shell_NotifyIcon( DWORD dwMessage, PNOTIFYICONDATA lpData);
Il parametro dwMessage specifica quale azione deve essere effettuata dalla funzione, ovvero se una icona deve essere inserita (NIM_ADD), modificata (NIM_MODIFY) o rimossa dalla taskbar (NIM_DELETE). Tutti i dati necessari all'operazione da eseguire si trovano in una struttura di tipo NOTIFYICONDATA, vedi figura 1, a cui punta il secondo parametro lpData. La funzione restituisce FALSE se c'è stato un errore, oppure TRUE (un qualsiasi valore diverso da zero) in caso di successo.
typedef struct { DWORD cbSize; // Dimensione della struttura (byte) HWND hWnd; // Handle della finestra di gestione UINT uID; // Identificativo dell'icona UINT uFlags; // Flag (costanti NIF_xxx) UINT uCallbackMessage; // Messaggio di notifica HICON hIcon; // Handle dell'icona CHAR szTip[64]; // Testo del tooltip } NOTIFYICONDATA, *PNOTIFYICONDATA; // Messaggi da inviare tramite Shell_NotifyIcon per // inserire, modificare o rimuovere una icona dalla TNA #define NIM_ADD 0x00000000 #define NIM_MODIFY 0x00000001 #define NIM_DELETE 0x00000002 // Flag #define NIF_MESSAGE 0x00000001 #define NIF_ICON 0x00000002 #define NIF_TIP 0x00000004 |
Non è sempre necessario riempire tutti i campi: gli unici tre sempre richiesti sono cbSize, che contiene
la dimensione della struttura, hWnd, che specifica l'handle della finestra che gestisce l'icona e riceve i messaggi
inviati dalla taskbar, e uID, che identifica l'icona con un valore numerico. I restanti campi della struttura sono
opzionali e controllati dal campo uFlags: per esempio se il bit NIF_ICON di uFlags è settato, il campo hIcon
è valido e contiene l'handle di una icona 16x16 da inserire nella TNA. Analogamente se il bit NIF_TIP è
settato il campo szTip contiene il testo del tooltip associato all'icona e gestito direttamente dalla taskbar.
Questo sistema è utile soprattutto per le operazioni di modifica, dato che evita di dover specificare ogni
volta anche i campi che rimarrano invariati.
Una volta inserita, l'icona appare subito a sinistra dell'orologio (se abilitato con l'apposita opzione nella configurazione
della taskbar) o in fondo a destra, ed eventuali icone già presenti vengono spostate per far posto alla
nuova. La posizione assoluta dell'icona cambia inoltre a seconda della posizione e delle dimensioni della taskbar,
ed in genere non è nota all'applicazione. Del resto questa informazione è abbastanza inutile, benché
probabilmente si possa escogitare qualche trucco per ottenerla.
Come si diceva prima, è l'applicazione che decide quale messaggio vuole ricevere dall'icona, riempiendo
il campo uCallbackMessage della struttura e settando il bit NIF_MESSAGE di uFlags (è anche possibile, sebbene
meno utile, non specificare alcun messaggio e creare così una icona "passiva"). La taskbar invia
questo messaggio alla finestra specificata nel campo hWnd ogni volta che una azione del mouse interessa l'area
nella quale si trova l'icona. L'identificativo di quest'ultima, cioè il campo uID della struttura NOTIFYICONDATA,
viene copiato nel parametro wParam, mentre lParam contiene l'evento che ha generato il messaggio, per esempio WM_MOUSEMOVE
o WM_RBUTTONDOWN.
Per esaurimento dei parametri disponibili le coordinate del mouse non vengono trasmesse, ma è comunque possibile
ricavarle attraverso le API GetMessagePos o GetCursorPos. E' facile vedere come lo scopo dei progettisti della
TNA sia pienamente raggiunto: dei quattro parametri di WndProc ben tre (hWnd, wMsg e wParam) sono stati decisi
dall'applicazione, e questo livello di controllo si traduce spesso in codice efficiente e di facile manutenzione.
Data la necessità di passare a Shell_NotifyIcon un puntatore ad una struttura, è di solito abbastanza
scomodo invocare questa funzione direttamente. Considerando anche il numero limitato di azioni che è possibile
intraprendere, l'approccio migliore consiste probabilmente nel realizzare tre diverse funzioni per aggiungere,
modificare e rimuovere icone dalla taskbar. Il codice necessario è molto semplice, ma i benefici in termini
di chiarezza e semplicità d'uso sono notevoli. Volendo, è possibile racchiudere queste funzioni in
una DLL o un una apposita libreria, ma in alcuni casi si può risparmiare il passaggio di qualche parametro
inserendole nell'applicazione stessa ed utilizzando costanti o variabili globali. La scelta, che dipende dalla
situazione o dai gusti personali, è lasciata al lettore. Nel programma di esempio, le funzioni tbarAddIcon,
tbarDelIcon e tbarModifyIcon si trovano all'interno dell'applicazione ed il valore di uCallbackMessage è
stabilito a priori.
Un altro utile accorgimento consiste nel separare la gestione dei messaggi che riguardano le icone in una funzione
apposita. In questo modo non si appesantisce troppo la funzione WndProc che piuttosto di frequente nei programmi
C consiste in una gigantesca istruzione switch di difficile lettura.
La tray notification area serve soprattutto a quei programmi che eseguono del lavoro "in background",
o che comunque non necessitano di una finestra sempre attiva. In alcuni casi è infatti preferibile rinunciare
ad una vera e propria finestra, perché quest'ultima anche se minimizzata andrà a finire nella taskbar,
magari provocando una riduzione nella dimensione dei pulsanti associati alle finestre attive o altri effetti collaterali
che è possibile evitare. Un valido esempio è rappresentato dal Print Manager, che in fase di stampa
inserisce una piccola icona nella TNA e al termine la rimuove e si chiude automaticamente. L'utente ha ancora la
possibilità di attivare una finestra di gestione con un click sull'icona, ma nella maggior parte dei casi
ciò non è necessario: si ha dunque il vantaggio minimizzare l'impatto dell'applicazione sul desktop
senza alcuna perdita di funzionalità.
In qualsiasi momento l'applicazione può modificare il contenuto dell'icona o il testo del tooltip (o addirittura
il valore del messaggio ad essa associato). Queste caratteristiche devono essere tenute presenti e utilizzate il
più possibile, soprattutto dalle applicazioni che non hanno una finestra principale. Come al solito conviene
fare riferimento a qualche programma "originale" ed esaminarne il metodo di interfaccia verso l'utente.
Il Resource Meter controlla costantemente la percentuale di risorse utilizzate dal sistema ed usa una icona per
la presentazione dei dati. L'icona rappresenta un contenitore che si svuota man mano che le risorse disponibili
diminuiscono e benché non fornisca informazioni dettagliate rappresenta una valida sintesi della situazione.
Spostando il mouse sull'icona e tenendolo fermo per qualche decimo di secondo, compare un tooltip nel quale si
trovano informazioni più precise, come "GDI: 94%" o "System: 92%". Infine con un doppio
click sull'icona viene presentata una finestra nella quale i dati sulle risorse vengono visualizzati graficamente
ed in modo completo. Questo triplice livello di presentazione dei dati è probabilmente uno degli usi tipici
per i quali è stata realizzata la TNA, e sicuramente uno degli approcci da considerare quando si scrivono
programmi analoghi.
Un ulteriore considerazione da fare è che in Windows 95 non è più concesso alle applicazioni
di ridisegnare dinamicamente la propria icona. Il metodo usato in Windows 3.x, consistente in pratica nel registrare
la classe principale senza icona e nello gestire poi i messaggi WM_PAINTICON e WM_ICONERASEBKGND, non funziona
più. In effetti, Windows 95 non si prende nemmeno il disturbo di inviare i suddetti messaggi ai programmi
marcati dal linker con versione 4.0 o superiore. Se necessario, rimane comunque possibile cambiare l'icona nella
taskbar mediante la funzione SetClassWord (SetClassLong in Win32), ma questo metodo si usa abbastanza raramente,
sebbene abbia il vantaggio di funzionare anche nelle "vecchie" versioni di Windows. In tutti i casi,
purtroppo, la riduzione delle dimensioni dell'icona a 16x16 pixel non permette di realizzare disegni particolarmente
elaborati, ma del resto la taskbar deve occupare il minor spazio possibile per non ridurre troppo l'area di lavoro
del desktop.
Una realizzazione pratica di quanto visto finora si trova nel programma Cyclops, che segue i movimenti del mouse attraverso una icona inserita nella taskbar. Trattandosi di una applicazione a 32 bit scritta per sfruttare le nuove API disponibili, sarà bene soffermarsi su alcune peculiarità che rendono questo tipo di programmazione leggermente diversa da quella in ambiente Windows a 16 bit.
Il secondo parametro che Windows passa alla funzione WinMain di un programma, chiamato di solito hPrevInstance o hPrevInst, rappresenta l'handle dell'ultima istanza del programma, o NULL se al momento del lancio non ci sono altre istanze in esecuzione. La maggior parte dei programmi Windows, per evitare che vengano avviate più istanze della stessa applicazione, controlla appunto il valore di hPrevInstance ed esce subito se questo è diverso da NULL (o perlomeno tralascia l'inizializzazione: registrare di nuovo la classe per la finestra porterebbe ad un errore). In Win32 però il valore di hPrevInstance è sempre NULL, e un controllo del tipo precedente fa sì che l'applicazione continui tranquillamente la propria esecuzione. Questo comportamento è lecito, perché dal momento che un programma a 32 bit viene eseguito nel proprio address space vengono eliminati possibili problemi. Per fare un esempio, ogni istanza della stessa applicazione potrà chiamare con successo RegisterClass anche usando gli stessi parametri. Proprio a causa dei diversi address space, il problema delle istanze multiple deve essere affrontato in modo differente, ovvero utilizzando una risorsa che sia accessibile a tutti i processi. Nella maggior parte dei casi può essere sufficiente un mutex, ovvero un oggetto che protegge da accessi simultanei una risorsa condivisa (mutex sta per mutual exclusive [object]), ma per il mio programma ho scelto di utilizzare un semaforo, che ha il vantaggio di poter limitare il numero di istanze contemporaneamente in esecuzione ad un valore qualsiasi. La prima istanza crea il semaforo e gli assegna un nome, possibilmente non troppo banale, ed un valore iniziale. Opportunamente la funzione CreateSemaphore non fallisce se il semaforo esiste già, ma restituisce l'handle del semaforo esistente: in questo modo le successive istanze del programma accedono tutte allo stesso semaforo ed ogni accesso, effettuato con la funzione WaitForSingleObject, decrementa il valore associato al semaforo. Quando questo valore si azzera il semaforo diventa "rosso", WaitForSingleObject fallisce ed il programma termina immediatamente.
#define MAX_INSTANCES 2 int PASCAL WinMain( ... ) { HANDLE hSem; DWORD dwWait; hSem = CreateSemaphore( NULL, // Crea il semaforo (o lo apre MAX_INSTANCES, // se ne esiste già uno con lo MAX_INSTANCES, // stesso nome) "MyAppControlSemaphore" ); if( hSem == NULL ) // Operazione fallita, return( 0 ); // meglio terminare il programma dwWait = WaitForSingleObject( hSem, 0L ); if( dwWait != WAIT_OBJECT_0 ) { // Non è possibile accedere al return( 0 ); // semaforo: fine programma ... // In uscita, aggiorniamo il numero di istanze // associato al semaforo ReleaseSemaphore( hSem, 1, NULL ); return( ... ); } |
Come d'obbligo per una applicazione Windows 95, è disponibile un menu che viene attivato con un click del
pulsante destro sull'icona. In Win32 per mostrare a video un menu popup non è più sufficiente chiamare
semplicemente TrackPopupMenu, perché se non ci si ricorda anche di portare in primo piano l'applicazione
mediante la funzione SetForegroundWindow si va incontro ad effetti collaterali orribili a vedersi. La più
comune SetActiveWindow non raggiunge lo stesso risultato, dato che per così dire ha un funzionamento locale
al processo che la chiama, mentre il nostro scopo è quello di estendere l'effetto a tutto il desktop. Per
vedere cosa accade quando manca la chiamata a SetForegroundWindow basta rimuovere la riga corrispondente dal programma,
ricompilare e mandarlo in esecuzione: click destro sull'icona del programma, click sinistro sulla taskbar ed ecco
che il menu scompare sotto la taskbar!
Una volta che il menu popup funziona correttamente, è buona regola stabilire una voce di default (evidenziata
in neretto con la funzione SetMenuDefaultItem) la cui azione possa essere attivata con un doppio click sull'icona.
Nella maggior parte delle applicazioni TNA-oriented l'effetto è solitamente quello di attivare una finestra
vera e propria che consenta di interagire con il programma, ma naturalmente si possono avere diversi funzionamenti
a seconda del tipo di applicazione.
Un'ultima nota va infine spesa per la funzione LoadImage usata per caricare le icone 16x16 da inserire nella taskbar.
Windows95 fa largo uso di icone 16x16 (le cosiddette small icons), e queste ultime vengono spesso ottenute riducendo
le originali icone 32x32. Chiamare la funzione LoadIcon per icone "piccole" provoca comunque l'applicazione
dell'algoritmo di stretching e anti-aliasing standard, e anche se alla fine la dimensione dell'icona non è
cambiata il contenuto può essere ben diverso dall'originale! A questo proposito è bene anche fare
in modo che l'icona principale di un programma contenga al suo interno due versioni della stessa immagine, una
di dimensioni 32x32 ed una 16x16. In questo modo l'Explorer non dovrà utilizzare il precedente algoritmo
e l'aspetto dell'icona rimarrà sotto controllo in tutte le modalità di visualizzazione (inoltre la
gestione di entrambi i tipi di icona è una delle numerose condizioni necessarie per ottenere il "Windows
95 logo" per un programma).
Tutte le funzioni descritte in precedenza sono a 32 bit, e quindi non facilmente accessibili da parte di programmi
a 16 bit. Passeggiare con un debugger nelle librerie di sistema, però, porta ogni tanto alla scoperta di
qualche interessante funzionalità: nel nostro caso l'esame della funzione Shell_NotifyIcon dimostra che
è possibile utilizzare la TNA anche in programmi a 16 bit ed in modo sicuro.
In ambiente Win32, uno dei meccanismi messi a disposizione dal sistema operativo per lo scambio dei dati tra processi
consiste nell'invio di un particolare messaggio, WM_COPYDATA. Seppur con qualche limitazione, questo messaggio
permette ad una applicazione di inviare un blocco di dati ad un processo eseguito in un altro address space, senza
dover ricorrere a meccanismi più potenti ma anche molto più complessi. Ora la funzione Shell_NotifyIcon
deve in qualche modo parlare con la WndProc che gestisce la finestra della taskbar, e non è del tutto sorprendente
che venga usato il messaggio in questione, anche perché il nome del primo parametro (dwMessage) non si presta
a molte interpretazioni. In effetti, la precedente funzione non fa altro che riempire una struttura che differisce
dalla NOTIFYICONDATA standard solo per l'aggiunta di due nuovi campi: nel primo viene posto un valore fisso (34753423h,
utilizzato per convalidare la struttura), mentre nel secondo viene semplicemente salvato il messaggio, ovvero il
primo argomento della funzione. Tutti gli altri valori vengono copiati dalla struttura originale e la nuova struttura
viene infine spedita alla taskbar attraverso un messaggio WM_COPYDATA.
Rimane però un ultimo problema: qual'è l'handle della taskbar? Facile, basta chiamare la funzione
FindWindow specificando il nome della classe della finestra di cui si vuole ricevere l'handle, ovvero "Shell_TrayWnd".
A questo punto qualsiasi programma a 16 bit può accedere tranquillamente alla TNA, riempiendo correttamente
la struttura estesa ed inviando un messaggio WM_COPYDATA alla taskbar. Il file TRAY.PAS mostra una possibile realizzazione
in Turbo Pascal (o Delphi), ma la traduzione in C non dovrebbe presentare alcuna difficoltà.
typedef struct { DWORD dwTag; // Campo di controllo (0x34753423) DWORD dwMessage; // Operazione da effettuare (NIM_xxx) NOTIFYICONDATA stNID; // Struttura NOTIFYICONDATA standard } NOTIFYICONDATA_PRIVATE; |
Questo meccanismo, come del resto accade quando non c'è documentazione ufficiale, andrebbe usato con cautela perché potrebbe cambiare in futuro. Tuttavia esiste già un'applicazione Microsoft che ne fa uso (QuickRes, uno dei PowerToys disponibili gratuitamente sul sito FTP o WWW di Microsoft che probabilmente proprio a causa di questo motivo è stato aggiornato dopo la release ufficiale di Windows 95), perciò è ragionevolmente lecito attendersi che le cose rimangano come stanno ancora per qualche tempo. In ogni caso non dovrebbero esserci complicazioni di nessun tipo sulle versioni di Windows che non supportano la nuova interfaccia: se FindWindow non trova la finestra o se la finestra sbagliata riceve un messaggio WM_COPYDATA che non conosce, ci sono buone probabilità che non accada nulla di irreparabile, anzi che non accada proprio nulla!
Dato il rapido diffondersi di Windows 95, prima o poi dovremo fare tutti i conti con la programmazione a 32 bit e con nuove API e controlli. Fortunatamente il passaggio non deve essere necessariamente traumatico: iniziando ad affrontare piccoli problemi, quali la gestione di una icona nella TNA, è possibile avvicinarsi a questo "nuovo" ambiente in modo graduale ed anche divertente. Nel nostro caso inoltre, poter lavorare a 16 bit consente di utilizzare strumenti di sviluppo già esistenti (Delphi, per esempio) e magari di effettuare il porting a 32 bit quando saranno disponibili strumenti analoghi più aggiornati. Microsoft comunque sta spingendo molto per fare in modo che la maggior parte delle nuove applicazioni sia scritta a 32 bit, in modo da funzionare anche su Windows NT, e se per il momento c'è ancora Windows 95 a fare da raccordo con il "vecchio" mondo a 16 bit, a mio avviso lo studio dell'architettura Win32 e della nuova interfaccia utente può essere un buon investimento fin da ora.
Nota: questo articolo è stato pubblicato sul numero 44 di Computer Programming (Febbraio 1996).
Scarica i sorgenti del programma "Cyclops".
Copyright (c) 1995-2003,2004 Alessandro Scotti. All rights reserved.