CueBanner: come sfruttarne le potenzialità nel mondo managed

Di Mauro Servienti - Microsoft MVP

In questa pagina

Introduzione Introduzione
Supportare i Cuebanner Supportare i Cuebanner
Procediamo Procediamo
IExtenderProvider IExtenderProvider
Conclusioni Conclusioni

Introduzione

Durante l’uso di un software ci sono tutta una serie di piccoli particolari che spesso ci sfuggono perchè siamo abituati a certe funzionalità e quindi tendiamo a darle per scontate senza notarle.

Una di queste funzionlità sono proprio i CueBanner. Ne vediamo un esempio nella seguente immagine:

*

Nello specifico ci stiamo riferendo alla scritta “Start Search” presente all’interno della casella di testo del menu start di Microsoft Windows Vista.

La scritta, di seguito CueBanner, offre delle funzionalità simili a quelle di un ToolTip con la differenza che non è necessario posizionare il mouse sul controllo per far sì che venga visualizzata. Se iniziamo a scrivere all’interno della casella di testo, il testo del CueBanner scomparirà e quando la casella di testo perderà il focus, se non abbiamo scritto nulla, il testo del CueBanner ricomparirà automaticamente.

Cerchiamo quindi di capire quali sono i vantaggi che l’uso di un CueBanner può portare nello sviluppo di un’interfaccia grafica per applicazioni per Windows.

Lo scopo ultimo di un CueBanner è quello di fornire supporto all’utente durante l’uso dell’applicazione; i vantaggi rispetto ad un ToolTip sono che non è necessario portare il mouse sopra il controllo e soprattutto non esiste un timeout, come invece c’è per il ToolTip, nella visualizzazione.

Un indubbio vantaggio, anche se questa volta è più per lo sviluppatore/designer, è che ci consente di risparmiare molto spazio per dedicarlo a controlli che ne hanno sicuramente maggior bisogno.

Nell’esempio del menu start di Microsoft Windows Vista, se avessimo utilizzato una Label per ottenere uno scopo simile avremmo sacrificato quasi il 50% dello spazio della casella di testo per una semplice scritta. L’eventuale localizzazione avrebbe ulteriormente ridotto lo spazio a disposizione.

Vediamo subito un esempio che chiarisce bene quest’ultimo punto:

*

Come si evince dallo screenshot, il sistema di ricerca che utilizza i CueBanner (a sinistra) è decisamente più compatto di quello che non li utilizza (a destra) senza per questo essere più complesso da utilizzare per l’utente finale.

 

Supportare i Cuebanner

Ci sono vari modi per ottenere il risultato che abbiamo presentato: uno, ad esempio, potrebbe essere quello di sfruttare gli eventi Enter e Leave del controllo per inserire e/o rimuovere a runtime il testo del CueBanner, eventualmente impostando anche un Font diverso in base al tipo di visualizzazione.

In questa sede però cercheremo di ottenere l’effetto sopra riportato sfruttando il supporto nativo dei controlli Win32 per i CueBanner, facendo uso di ereditarietà e P/Invoke per realizzare una TextBox custom che supporti i CueBanner, dando la possibilità allo sviluppatore di editare le proprietà relative direttamente dall’ambiente di sviluppo.

Facciamo subito una precisazione doverosa: il supporto per i CueBanner è disponibile da Microsoft Windows XP in avanti, quindi anche su Microsoft Windows Server 2003 e naturalmente su Microsoft Windows Vista.

 

Procediamo

Realizzeremo una TextBox personalizzata ereditando dalla TextBox nativa del .NET Framework e aggiungeremo il supporto per i CueBanner, il codice in sè è decisamente semplice.

Iniziamo creando la nostra classe TextBox:

[ToolboxBitmap( typeof( System.Windows.Forms.TextBox ) )]
public class TextBox : System.Windows.Forms.TextBox
{

}

Aggiungiamo quindi le proprietà che ci interessa poter manipolare dall’ambiente di sviluppo e che serviranno per controllare il comportamento del nostro controllo:

private IntPtr cueBannerTextHandle = IntPtr.Zero;
private string _cueBannerText;

[Description( "The cue banner text associated with the control." )]
[Category( "Appearance" )]
public string CueBannerText
{
get { return this._cueBannerText; }
set
{
if( value != this.CueBannerText )
{
this._cueBannerText = value;

if( IsCueBannerSupported )
{
if( this.cueBannerTextHandle != IntPtr.Zero )
{
Marshal.FreeBSTR( this.cueBannerTextHandle );
}

this.cueBannerTextHandle = Marshal.StringToBSTR( 
value );
API.SendMessage( 
this.Handle, 
API.EM_SETCUEBANNER, 
IntPtr.Zero, 
this.cueBannerTextHandle );
}
}
}
}

private Font _cueBannerFont;

[Description( "The font used to display the cue banner text in the control." )]
[Category( "Appearance" )]
public Font CueBannerFont
{
get
{
if( this._cueBannerFont == null && this.Parent != null )
{
return this.Parent.Font;
}

return this._cueBannerFont;
}
set 
{ 
this._cueBannerFont = value; 
API.SendMessage( 
this.Handle, 
API.WM_SETFONT, 
this.CueBannerFont.ToHfont(), 
IntPtr.Zero );
}
}

Abbiamo aggiunto una proprietà CueBannerText che sarà il testo da visualizzare come CueBanner e una proprietà CueBannerFont che rappresenta il Font con cui vogliamo che questo testo venga visualizzato.

Vediamo nel dettaglio cosa succede quando manipoliamo le proprietà in oggetto.

Quando settiamo la proprietà CueBannerText verifichiamo, in primis, che il nuovo valore sia diverso da quello attuale e in caso di variazione, se il CueBanner è supportato dal sistema corrente, lo impostiamo.

L’impostazione del CueBanner fa uso di P/Invoke attraverso l’API SendMessage a cui passiamo l’Handle del controllo su cui impostare il CueBanner, una costante (EM_SETCUEBANNER) che definisce l’operazione da eseguire e l’Handle alla locazione di memoria unmanaged in cui abbiamo inserito il testo da visualizzare come CueBanner.

E’ fondamentale evidenziare che, utilizzando P/Invoke e avendo quindi a che fare con il mondo unmanaged, dobbiamo assicurarci di liberare le eventuali risorse precedentemente allocate. Questo è il motivo per cui prima di impostare il CueBanner occorre liberare l’allocazione di memoria che abbiamo eventualmente utilizzato nell’impostazione precedente, utilizzando i metodi statici FreeBSTR e StringToBSTR della classe Marshal.

Quando invece settiamo la proprietà CueBannerFont ci limitiamo a chiamare la stessa API passando sempre l’handle del controllo su cui vogliamo che l’operazione venga eseguita, la costante WM_SETFONT che indica al sistema l’operazione che desideriamo venga eseguita e l’handle al Font che deve essere applicato.

Ci si potrebbe chiedere perchè non ci siamo limitati ad impostare il Font della TextBox stessa, impostazione che avrebbe portato allo stesso identico risultato. Il problema è che operando direttamente sul Font della TextBox avremmo reso pressochè inutilizzabile la proprietà Font dal designer.

Per determinare se i CueBanner siano supportati o meno ci limitiamo a verificare la versione del sistema operativo corrente in questo modo:

private bool IsCueBannerSupported
{
get
{
Version v = Environment.OSVersion.Version;
return ( ( v.Major == 5 && v.Minor >= 1 ) || v.Major > 5 );
}
}

Tutto quello che abbiamo visto sino ad ora è più che sufficiente per realizzare una TextBox personalizzata con supporto per i CueBanner. Se però proviamo il nostro nuovo controllo ci accorgiamo subito che c’è qualcosa che non va, l’eventuale Font che decidiamo di applicare al CueBanner viene ignorato dopo che il cursore esce dalla TextBox. Per completare l’opera non ci resta quindi che istruire la nostra TextBox su quando e come cambiare il Font:

protected override void OnEnter( EventArgs e )
{
base.OnEnter( e );

if( IsCueBannerSupported )
{
API.SendMessage( 
this.Handle, 
API.WM_SETFONT, 
base.Font.ToHfont(), 
IntPtr.Zero );
}
}

protected override void OnLeave( EventArgs e )
{
base.OnLeave( e );

if( this.Text.Length == 0 && this.IsCueBannerSupported )
{
API.SendMessage( 
this.Handle,
API.WM_SETFONT, 
this.CueBannerFont.ToHfont(), 
IntPtr.Zero );
}
}

Per raggiungere il nostro scopo dobbiamo sapere quando il cursore entra ed esce dal nostro controllo, lo facciamo intercettando gli eventi Enter e Leave, nello specifico eseguendo l’override dei metodi protected OnEnter e OnLeave. All’interno di questi due metodi, dopo aver chiamato il corrispondente metodo della classe di base, per garantire che gli eventi vengano correttamente scatenati anche all’esterno, ci limitiamo a impostare il Font che desideriamo venga utilizzato in base al contesto corrente.

Abbiamo visto quanto sia semplice realizzare una TextBox con pieno supporto per i CueBanner. C’è però un altro controllo che incapsula al suo interno una TextBox che ci piacerebbe usare allo stesso modo: ToolStripTextBox .

 

IExtenderProvider

Nel caso di un controllo ToolStripTextBox le cose si complicano non poco. Il primo problema è che la strada dell’ereditarietà si dimostra subito poco percorribile, certo non impossibile, ma sicuramente non così agevole come per la semplice TextBox.

Anche in questo caso fortunatamente il .NET Framework ci viene incontro dimostrando ancora una volta quanto sia potente e flessibile.

Per raggiungere il nostro scopo realizzeremo un ExtenderProvider che ci permetterà di aggiungere al controllo ToolStripTextBox la funzionalità che desideriamo senza toccare il controllo stesso. Per capire cosa sia un extender provider vediamo subito un esempio, proviamo a trascinare su una form un controllo ToolTip; notiamo che viene aggiunto un nuovo componente, e non un nuovo controllo:

*

Notiamo anche che adesso ogni controllo presente sulla nostra form ha una nuova proprietà:

*

Questa funzionalità è disponibile proprio grazie al concetto di extender provider. La realizzazione di un extender provider è abbastanza semplice, si inizia con il realizzare una classe che deriva dalla classe base Component e implementa l’interfaccia IExtenderProvider:

[ProvideProperty( "CueBannerText", typeof( ToolStripTextBox ) )]
[ProvideProperty( "CueBannerFont", typeof( ToolStripTextBox ) )]
public class CueBannerProvider : Component, IExtenderProvider
{
}

L’attributo applicato in testa alla classe serve per specificare al designer quali debbano essere le proprietà da aggiungere ai controlli presenti sulla form.

Nel caso del ToolTip extender provider la dichiarazione della classe assomiglierà a qualcosa del tipo

[ProvideProperty( "ToolTip", typeof( Control ) )]
public class ToolTip : Component, IExtenderProvider

Il primo step è quindi quello di implementare l’interfaccia IExtenderProvider che definisce un solo metodo:

bool IExtenderProvider.CanExtend( object extendee )
{
return extendee is ToolStripTextBox;
}

Il metodo CanExtend viene chiamato dal designer per sapere se un determinato controllo, quello passato come parametro, debba o meno supportare l’estesione che il nostro provider fornisce. Nel nostro caso noi vogliamo supportare solo ed esclusivamente le ToolStripTextBox.

Il designer si aspetta poi che un extender provider esponga una serie di metodi che servono per impostare e recuperare i valori della proprietà specifica negli attributi in testa alla classe. Questi metodi devono rispecchiare una nomenclatura e una firma ben precisa: deve esserci un metodo GetNomeProprietà e SetNomeProprietà per ogni proprietà che desideriamo aggiungere ai controlli e la firma di questi metodi deve essere la seguente:

public PropertyDataType GetNomeProprietà( ExtendedClass ctrl );
public void SetNomeProprietà( ExtendedClass ctrl, PropertyDataType value );

questi metodi verranno chiamati dal designer, rispettivamente, per impostare le proprietà di un controllo e per leggere il valore di un proprietà. Da questo capiamo subito che una singola istanza di un extender provider deve poter gestire n controlli su una Form. Sarà quindi necessario trovare un sistema per mantenere una lista dei controlli che stiamo estendendo e dei valori delle relative proprietà. Per raggiungere questo scopo facciamo uso di un semplice Dictionary<K, V>:

Dictionary<ToolStripTextBox, IntPtr> controlsTextHandles;
Dictionary<ToolStripTextBox, String> controlsText;
Dictionary<ToolStripTextBox, Font> controlsFont;

EventHandler enterHandler = null;
EventHandler leaveHandler = null;

public CueBannerProvider()
{
controlsTextHandles = new Dictionary<ToolStripTextBox, IntPtr>();
controlsText = new Dictionary<ToolStripTextBox, String>();
controlsFont = new Dictionary<ToolStripTextBox, Font>();

this.enterHandler = new EventHandler( OnToolStripTextBoxEnter );
this.leaveHandler = new EventHandler( OnToolStripTextBoxLeave );
}

Nel costruttore del nostro provider procederemo quindi con l’istanziare 3 Dictionary necessari per tenere traccia del testo assegnato al CueBanner di ogni controllo, del Font assegnato al CueBanner di ogni controllo e dell’handle alla memoria unmanaged, necessario per liberare le risorse quando non sono più necessarie. In questo frangente istanziamo anche i 2 delegati che ci serviranno per gestire gli eventi Enter e Leave della TextBox necessari per impostare il Font del CueBanner.

Quando l’utente cerca di impostare a design-time il valore di una delle “nuove” proprietà esposte dal controllo il designer non fa altro che chiamare i corrispondenti metodi Get e Set:

public void SetCueBannerText( ToolStripTextBox ctrl, String value )
{
if( value == null )
{
value = String.Empty;
}

if( value.Length == 0 )
{
if( controlsText.ContainsKey( ctrl ) )
{
this.FreeMemory( ctrl );

controlsTextHandles.Remove( ctrl );
controlsText.Remove( ctrl );

this.UnwireEvents( ctrl );
}
}
else
{
if( controlsTextHandles.ContainsKey( ctrl ) )
{
this.FreeMemory( ctrl );
}

if( !controlsText.ContainsKey( ctrl ) )
{
/*
 * Agganciamo gli eventi solo ed
 * esclusivamente la prima volta
 * che aggiungiamo un controllo
 */
this.WireEvents( ctrl );
}

this.controlsTextHandles[ ctrl ] = Marshal.StringToBSTR( value );
this.controlsText[ ctrl ] = value;

if( IsCueBannerSupported )
{
API.SendMessage( 
ctrl.TextBox.Handle, 
API.EM_SETCUEBANNER, 
IntPtr.Zero, 
this.controlsTextHandles[ ctrl ] );
}
}
}

La chiamata al metodo “Set” fa in primis una serie di controlli necessari per assicurarsi che il valore in arrivo sia coerente, quindi se il valore in arrivo è una stringa vuota si limita semplicemente a rimuovere il controllo da quelli di cui stiamo tenendo traccia; se invece il valore è diverso da una stringa vuota, per prima cosa liberiamo la memoria eventualmente utilizzata dal precedente valore del CueBanner, dopodichè impostiamo il nuovo valore e ci assicuriamo di gestire gli eventi per il controllo di cui stiamo impostando il valore.

public String GetCueBannerText( ToolStripTextBox ctrl )
{
if( this.controlsText.ContainsKey( ctrl ) )
{
return this.controlsText[ ctrl ];
}
return String.Empty;
}

La chiamata al metodo “Get” si limita invece a ritornare il valore corrente, qualora sia presente, associato al controllo.

 

Conclusioni

Lo sviluppo del nostro extender provider si è concluso. Trascinando su una form il nostro nuovo componente otteniamo lo stesso identico comportamento che abbiamo con il componente ToolTip; un nuovo componente viene aggiunto alla form:

*

e le proprietà dei ToolStripTextBox vengono estese con le nuove funzionalità esposte proprio dal nostro provider:

*

Abbiamo visto quanto sia semplice estendere le funzionalità dei controlli Windows Forms che il .NET Framework ci mette a disposizione. Abbiamo inoltre avuto modo di apprezzare le potenzialità del .NET Framework che ci ha permesso di ottenere lo stesso risultato percorrendo strade radicalmente diverse.

Per ulteriori esempi di codice: