Lezione 9 – I controlli LISTBOX



By admin on settembre 12, 2010


I controlli "Listbox", servono a mostrare all'utente elenchi (liste) di stringhe di testo (o, liste di arbitraria grafica, usando il concetto avanzato di "owner-drawn"), eventualmente dando all'utente la possibilità di selezionare uno o più degli elementi elencati.
Il controllo può occupare uno spazio su schermo inferiore a quello necessario per mostrare simultaneamente tutti gli elementi dell'elenco; in questo caso, si potranno avere delle "scroll bar", di cui l'utente potrà servirsi per scorrere avanti e indietro nell'elenco.
Non bisogna confondere i (relativamente semplici) controlli "listbox", con i sofisticati controlli comuni "listview", che svolgono ruoli analoghi ma in modi più complessi. Comunque, anche un "semplice" listbox, così come gli altri controlli "elementari" che abbiamo esaminato sinora, di compiti può svolgerne parecchi, come vedremo, oltre a quello fondamentale di mostrare un semplice elenco di stringhe (con scrolling).
Anche per i controlli Listbox, come per gli altri tipi già visti ai capitoli precedenti, i bit di stile svolgono un ruolo fondamentale. LBS_NOSEL serve per eliminare la possibilità che l'utente effettui una selezione sulla lista; con questo bit di stile, il Listbox è un controllo di "puro output". Al contrario, i bit di stile (mutuamente esclusivi) LBS_MULTIPLESEL e LBS_EXTENDEDSEL permettono entrambi di abilitare sulla lista la selezione multipla, con due diverse modalità di interazione dal punto di vista dell'utente: la prima prevede semplicemente che ogni click su di un elemento lo trasformi da non-selezionato a selezionato, o viceversa; la seconda, più sofisticata, prevede l'uso di tasti speciali come SHIFT e CONTROL per i cambiamenti della selezione, è una modalità di interazione probabilmente familiare agli utenti esperti di Windows.
Il bit LBS_NOTIFY fa sì che il controllo spedisca alla finestra che lo contiene un messaggio di notifica ad ogni click o doppio clic dell'utente su di un elemento della lista (per la precisione, bisognerà porre NOT LBS_NOTIFY, esplicitamente, fra i bit di stile, se non si vogliono avere queste notifiche). Il bit LBS_WANTKEYBOARDINPUT permette un controllo ancora maggiore da parte del programma, poiché fa sì che il listbox spedisca alla finestra che lo contiene uno speciale messaggio WM_VKEYTOITEM ogni volta che l'utente preme un tasto mentre il focus è sul listbox stesso. Normalmente, un listbox elenca i propri elementi uno dopo l'altro, in una singola colonna; si può invece ottenere una molteplicità di colonne usando il bit di stile LBS_MULTICOLUMN.
Normalmente, la scroll-bar verticale appare automaticamente (se la finestra del list-box comprende il bit di stile WS_VSCROLL, naturalmente!), se e quando la lista contiene più elementi di quanti possa mostrarne simultaneamente sullo schermo, e sparisce se il numero di elementi è inferiore a questo; per avere invece una scroll-bar verticale sempre visibile (che sarà disabilitata, se non vi è possibile scorrimento), occorre aggiungere al controllo il bit di stile LBS_DISABLENOSCROLL. Se interessa che l'utente possa effettuare uno scroll orizzontale, si userà il bit di stile WS_HSCROLL (e vedremo inoltre più avanti quale altra precauzione sarà necessaria in questo caso).
Il bit LBS_USETABSTOPS fa sì che il controllo espanda i caratteri di tabulazione (control-I, cioè caratteri di valore numerico 9) presenti nelle stringhe che contiene. LBS_NOREDRAW evita che il controllo si ridisegni automaticamente ad ogni cambiamento (questo bit può essere messo o tolto in qualsiasi momento, spedendo al controllo opportuni messaggi di WM_SETREDRAW, con wParam rispettivamente FALSE o TRUE). LBS_NOINTEGRALHEIGHT evita che la dimensione del controllo venga arrotondata ad un multiplo intero dello spazio necessario per mostrare ciascun elemento; di default, questo arrotondamento avviene automaticamente, mentre, con questo bit, la dimensione del controllo è invece esattamente quella specificata dal programma.
Un controllo listbox mantiene una copia delle stringhe relative agli elementi che contiene, a meno che il listbox non sia "owner-drawn", nel qual caso ciò normalmente non succede; ma si può specificare il bit di stile LBS_HASSTRINGS per far sì che le stringhe vengano memorizzate anche da un controllo listbox owner-drawn.
Il concetto di "owner-drawn" per un controllo listbox è più raffinato che per i controlli che abbiamo visto sinora, poichè il listbox ha molteplici elementi ("item") e il disegno deve avvenire in modo separato per ciascuno di essi. Ad ogni WM_DRAWITEM, lParam sarà un puntatore ad una struttura dati DRAWITEMSTRUCT, che fra i vari campi ne ha uno, itemID, che identifica l'esatto elemento di cui si richiede il disegno, e un altro, itemData, che può portare informazioni arbitrarie che il programma ha scelto di associare al dato elemento (si tratta spesso di un puntatore a una struttura di dati, o di un indice in una tabella mantenuta dal programma):

typedef struct tagDRAWITEMSTRUCT
{
 UINT  CtlType; 
 UINT  CtlID; 
 UINT  itemID; 
 UINT  itemAction; 
 UINT  itemState; 
 HWND  hwndItem; 
 HDC   hDC; 
 RECT  rcItem; 
 DWORD itemData; 
} DRAWITEMSTRUCT; 

 
Il campo CtlID è l’ID del controllo; itemID identifica, per quei controlli che hanno dei "sotto-elementi" (i  listbox ed i combobox), di quale sotto-elemento si parli.
CtlType identifica il tipo di controllo: ODT_STATIC, ODT_BUTTON, ecc. dato, anche questo, che dovrebbe essere già noto sulla base del CtlID; così pure per hwndItem, che è l'HWND del controllo.
itemAction è una maschera di bit che dicono se il controllo debba essere interamente ridisegnato (cioè se ha ricevuto un vero WM_PAINT), se abbia cambiato (guadagnato o perso) il focus, se abbia cambiato stato (bottone da premuto a non, o viceversa, ad esempio).
Così pure per itemState, che definisce lo "stato" attuale del controllo (o suo eventuale sotto-elemento).
itemData è un arbitrario valore di 32 bit che può essere stato associato all'elemento da parte dell'applicazione, ma esso non si applica a bottoni e static.
Il controllo listbox può essere reso owner-drawn da due diversi bit di stile (mutualmente esclusivi): LBS_OWNERDRAWFIXED, se tutti gli elementi hanno la stessa altezza (il programma dovrà allora rispondere a un singolo messaggio WM_MEASUREITEM, speditogli alla creazione della lista, e i dati di dimensione che fornirà in quell'occasione varranno per ogni elemento); LBS_OWNERDRAWVARIABLE, se ogni elemento può avere altezza diversa (in questo caso, il messaggio WM_MEASUREITEM verrà spedito separatamente per ogni elemento da disegnare).
LBS_NODATA, che funziona solo assieme a LBS_OWNERDRAWFIXED, e senza LBS_HASSTRINGS, permette di avere controlli listbox con un numero enorme di elementi (altrimenti, vi è un limite attorno al migliaio), a patto naturalmente che la finestra contenitore si prenda tutte le responsabilità di disegno dell'aspetto di ciascun elemento a fronte della sola informazione sul "numero di riga" dell'elemento stesso (è questo che viene passato, in questo specifico caso, come campo itemID della struttura dati DRAWITEMSTRUCT che accompagna ciascun messaggio WM_DRAWITEM). Al contrario, per il caso molto comune in cui dal listbox non si voglia altro che un elenco di stringhe in ordine alfabetico, si può lasciare l'intera responsabilità dell'ordinamento al controllo stesso, semplicemente dotandolo dei bit di stile LBS_SORT. Questo bit si può usare anche per listbox owner-drawn privi del bit di stile LBS_HASSTRINGS, nel qual caso la finestra che contiene il controllo listbox dovrà rispondere ai messaggi WM_COMPAREITEM che il controllo le lancerà quando necessario per effettuare il sort degli elementi.
Il lettore è, naturalmente, incoraggiato a sperimentare con tutti questi bit di stile, e le loro combinazioni, magari sviluppando per gli stili dei listbox un programmino di sperimentazione analogo a quello che scrivemmo per quelli degli editbox. Si userà il messaggio LB_ADDSTRING, per "popolare" opportunamente il controllo listbox.
Naturalmente, molto dell'effetto dei bit di stile che abbiamo qui esaminato è legato ai vari messaggi inviati al controllo, e ai messaggi di notifica che il controllo a sua volta spedisce al contenitore.
Un controllo listbox può essere "popolato" dinamicamente ma, spesso, potrà anche servire, invece, popolarlo "una volta per tutte", con dati non destinati a cambiare. Purtroppo, non si possono specificare direttamente i contenuti iniziali nel file .RC; bisogna, invece, operare comunque "a runtime", (spesso, all'inizializzazione, cioè rispondendo al messaggio WM_INITDIALOG), eseguendo opportune funzionalità di "riempimento".
Un modo decisamente anomalo di popolare una listbox è quello messo a disposizione dall'API DlgDirList, che riempie un controllo listbox con l'elenco dei file presenti in un certo folder. Non usatela! Al giorno d'oggi, gli utenti si aspettano, quando devono scegliere un file fra tanti, di vedere un'interfaccia utente più sofisticata di una semplice lista di nomi-file -- e hanno ragione, anche perchè non è affatto difficile fornire loro queste migliori interfacce, grazie ai "dialoghi comuni", di cui parleremo più avanti. Quest'API è presente solo per compatibilità con un passato antichissimo, e la nomino solo per avvertirvi di non usarla.
Il modo più normale di "popolare" un controllo listbox è quello di inviargli il messaggio LB_ADDSTRING, con lParam posto all'indirizzo della stringa (che viene accodata alle eventuali altre già presenti); si tratta di una "stringa" nel senso C del termine, cioè un array di caratteri terminato da uno 0 binario. Se il listbox è owner-drawn, e privo del bit di stile LBS_HASSTRING, esso risponde a questo messaggio semplicemente memorizzando l'lParam come dato associato al nuovo elemento. Al messaggio LB_ADDSTRING, la listbox risponde dando come codice di ritorno l'indice (da 0 in sù) della posizione in lista del nuovo elemento (normalmente, come dicevamo, il nuovo elemento è posto in coda agli altri già presenti, ma questo non si applica se il listbox ha il bit di stile LBS_SORT). Questo indice, ad esempio, si può usare come valore di wParam nell'inviare alla listbox un messaggio LB_SETITEMDATA, che farà sì che i 32 bit contenuti nell'lParam siano associati come "dati" dell'elemento stesso (similmente, inviare alla lista un messaggio LB_GETITEMDATA, sempre con l'indice di elemento in wParam, permetterà di recuperare, come valore di ritorno, il dato associato a un certo elemento).
Se si intende aggiungere ad una lista un grande numero di elementi, una piccola ottimizzazione può essere quella di spedirle anzitutto un messaggio LB_INITSTORAGE, con, in wParam, il numero di elementi che si intende inserire, e, in lParam, il numero di byte di cui la lista avrà bisogno per conservare il loro testo; questa operazione non è necessaria, ma può accelerare sostanzialmente le cose.
Se si vuole inserire un elemento ad una posizione particolare, si può usare il messaggio LB_INSERTSTRING al posto di LB_ADDSTRING; si dovrà allora passare l'indice desiderato (-1, per appendere il nuovo elemento in coda a quelli già presenti) come valore di wParam questo blocca, ma solo temporaneamente, la funzionalità di ordinamento automatico di un listbox con stile LBS_SORT; vi consiglio di evitare questa situazione, che può causare alcune anomalie, semplicemente non usando mai LB_INSERTSTRING, bensì soltanto LB_ADDSTRING, per popolare listbox con stile LBS_SORT.
Si può rimuovere dalla lista un elemento col messaggio LB_DELETESTRING, sempre con l'indice dell'elemento d'interesse in wParam, e si avrà come valore di ritorno il numero di elementi restanti. Si può anche chiedere il numero di elementi presenti, senza fare modifiche, col messaggio LB_GETCOUNT; e svuotare completamente un listbox col messaggio LB_RESETCONTENT. La cancellazione di un elemento, se la lista è owner-drawn, provoca la spedizione di un messaggio WM_DELETEITEM al proprietario (su Windows 95/98, questo messaggio è spedito anche per la cancellazione di elementi da liste non owner-drawn).
La lunghezza del testo associato a un dato elemento si può richiedere spedendo al listbox il messaggio LB_GETTEXTLEN (sempre con l'indice dell'elemento in wParam), e il testo stesso con LB_GETTEXT (con l'indirizzo del nostro buffer, che deve avere una lunghezza sufficiente e verrà riempito col testo richiesto, passato come lParam).
Si può trovare un elemento che inizia con un certo testo con il messaggio LB_FINDSTRING (oppure, un elemento che ha esattamente un certo testo, con il messaggio LB_FINDSTRINGEXACT); si passerà l'indirizzo della stringa da cercare (terminata come al solito da 0 binario) in lParam, e, in wParam, -1 per cercare nell'intera lista, o, se no, l'indice del primo elemento da non prendere in considerazione nella ricerca (ma la ricerca riprenderà dall'inizio della lista se non ha successo entro la sua fine; il codice LB_ERR, che indica che nulla è stato trovato, viene restituito come valore di ritorno solo se l'elemento cercato non è presente in nessun punto della lista).
Le caratteristiche di dettaglio di una listbox con stile LBS_USETABSTOPS dipendono da come sono settati i suoi "tab stop", cioè, "arresti di tabulazione". Per default, essi sono ogni 32 "unità di dialogo", l'unità di misura, cui già abbiamo accennato, che corrisponde a circa 1/4 della larghezza media dei caratteri del font in uso; in altri termini, i tab stop di default sono ogni 8 caratteri circa, il che può essere già soddisfacente in molti casi. Se non lo è, si può rimediare spedendo alla listbox un messaggio LB_SETTABSTOPS: in wParam metteremo il numero totale di tab-stop che desideriamo, in lParam l'indirizzo di un array di interi corrispondente ai valori dei tab-stop, in unità di misura "dialog unit" e in ordine strettamente crescente.
Per una listbox multi-colonna (cioè, con il bit di stile LBS_MULTICOLUMN), si può stabilire l'ampiezza delle colonne (che sono tutte di eguale larghezza) con il messaggio LB_SETCOLUMNWIDTH; in wParam, si metterà la desiderata larghezza in pixel delle colonne.
La selezione corrente del listbox (un singolo elemento, ovvero un numero variabile di elementi, secondo lo stile del listbox) viene normalmente stabilita secondo le interazioni dell'utente, ma può anche essere diretta da programma.
Il messaggio LB_SETCURSEL (con l'indice dell'elemento da selezionare in wParam), che funziona solo con liste a selezione singola, seleziona l'elemento indicato (e, se non era fra quelli nella zona visibile della lista, fa scorrere la lista stessa in modo che lo diventi). Alternativamente, si può usare anche LB_SELECTSTRING, che praticamente congiunge le funzioni di LB_FINDSTRING e LB_SETCURSEL (esso ha gli stessi parametri e valore di ritorno di LB_FINDSTRING).
Per liste a selezione multipla, invece, si usa LB_SETSEL: in wParam, 0 per togliere la selezione, 1 per metterla; in lParam, l'indice dell'elemento cui metterla o toglierla (ovvero, -1 per toglierla o metterla a tutti gli elementi simultaneamente). (LB_SELITEMRANGE e LB_SELITEMRANGEEX sono delle piccole ottimizzazioni che consentono di mettere o togliere la selezione a un gruppo contiguo di elementi in un sol colpo).
Per chiedere alla listbox informazioni sulla sua selezione, se la listbox è a selezione singola, si usa il messaggio LB_GETCURSEL, che ritorna l'indice dell'unico elemento selezionato (LB_ERR, se nessun elemento è selezionato).
Per liste a selezione multipla, invece, si usa LB_GETSELCOUNT per ottenere il numero totale di elementi selezionati, e LB_GETSELITEMS per avere l'elenco degli elementi stessi (passando, in wParam, il numero massimo che si è preparati ad accettare, e in lParam l'indice di un nostro buffer, un array di interi, che verrà riempito con l'informazione richiesta); il valore di ritorno è il numero di interi posti nel buffer. Alternativamente, LB_GETSEL, con l'indice dell'elemento di interesse in wParam, permette di sapere se un dato elemento è selezionato o meno (se non lo è, il valore di ritorno sarà zero).
Anche lo stato dello scorrimento (scroll) di una listbox è normalmente modificato dalle azioni dell'utente (in questo caso, dalla sua interazione con la scroll-bar del listbox); anche qui, naturalmente, lo si può stabilire anche da programma, spedendo al listbox il messaggio LB_SETTOPINDEX, avendo in wParam l'indice dell'elemento che si vuole mostrare in cima alla lista. Per chiedere quale sia l'elemento attualmente in cima alla lista, si userà il messaggio LB_GETTOPINDEX, che naturalmente restituisce questa informazione come valore di ritorno.
Per sapere se l'elemento con un certo indice è attualmente visualizzato (e, se sì, a quali coordinate entro l'area del controllo listbox) si usa il messaggio LB_GETITEMRECT, passando, in wParam, l'indice dell'elemento, e, in lParam, l'indirizzo di una nostra struttura di tipo RECT che verrà riempita con le informazioni richieste. Il valore di ritorno da questo messaggio sarà 1 se l'elemento è visualizzato, 0 in caso contrario e -1 (cioè LB_ERR) in caso di errore; nel RECT passato, vengono comunque poste le coordinate-cliente che sarebbero usate se l'elemento fosse mostrato, anche se esse sono in realtà fuori dall'area attualmente in vista. Si noti che la "larghezza" indicata per la visualizzazione corrisponde sempre e comunque all'intera larghezza della listbox (o della sua colonna in cui si trova l'elemento, per listbox a più colonne), anche se l'elemento è in realtà più stretto, ovvero più largo.
Notiamo che, con questi messaggi, abbiamo quanto ci serve per garantire che l'ultima stringa appena aggiunta in coda alla lista sia sempre visualizzata: basterà fare qualcosa tipo:

int idx = SendMessage(hlist, LB_ADDSTRING, 0, (LPARAM)buf);
int shown;
do{
   RECT rr;
   shown = SendMessage(hlist, LB_GETITEMRECT, idx, (LPARAM)&rr);
   if(!shown)
     {
      int itop = SendMessage(hlist, LB_GETTOPINDEX, 0, 0);
      SendMessage(hlist, LB_SETTOPINDEX, itop+1, 0);
     }
} while(!shown);

 

Questo provoca un lento scorrimento della lista, se era stata fatta scorrere verso la cima e gli elementi contenuti sono tanti. Per avere un effetto grafico più rapido, possiamo circondare questo brano di codice fra due messaggi WM_SETREDRAW, che, come abbiamo accennato, fanno commutare il bit di stile LBS_NOREDRAW della lista. Per garantire il "ridisegno" finale, occorrerà anche una chiamata all'API InvalidateRect alla fine di questa sequenza. In alternativa, naturalmente, possiamo scegliere di calcolare quale dovrà essere il primo elemento della lista, al fine di garantire che il nuovo elemento appena aggiunto venga visualizzato all'ultimo posto.





Lascia un Commento