Display: Presentation

Valuta questo articolo 0 Voti

Autori: Lorenzo Maiorfi e Gianluca Ruta

Lo strato di più alto livello per la realizzazione di interfacce utente prende, all'interno del .NET Micro Framework, il nome di ‘Presentation’. Esso è distribuito in realtà all'interno di un assembly specifico, denominato "Microsoft.SPOT.TinyCore", che definisce al suo interno tutte le classi relative ai namespaces "Microsoft.SPOT.Presentation.*". A differenza di quanto visto per il layer di basso livello, lo strato ‘Presentation’ si occupa anche della gestione dell'input dell'utente, sia per quanto concerne l'uso di pulsanti che per quanto riguarda l'utilizzo di display con funzionalità "touch".

Prima di addentrarci nei meandri di ‘Presentation’, vale la pena sottolineare che l'ambiente di sviluppo prevede un apposito template per la creazione di applicazioni che ne utilizzano le funzionalità. Tale template prende il nome di "Window Application". L'applicazione così creata dall'ambiente di sviluppo ci dà modo di esaminare alcune delle classi più importanti ed in particolare del modo in cui collaborano, nell'ambito di un'applicazione di questo tipo, gli oggetti di tipo "Window", "Program" e "GPIOButtonInputProvider".

Premesso che le funzionalità relative al firmware di esempio che verrà illustrato si limitano al riportare sul display una stringa caricata da risorse e ad intercettare la pressione dei pulsanti del dispositivo per riportarne l'identificativo all'interno della finestra di debug, vale la pena di "sezionare" le parti principali dell'applicazione per identificarne responsabilità e peculiarità. L'interfaccia dell'applicazione in questione è illustrata in figura che segue:

 

L'entry point dell'applicazione, stavolta derivata dalla classe "Microsoft.SPOT.Application", è ancora una volta il metodo statico "Main", nel quale vengono creati un oggetto Window ed un oggetto GPIOButtonInputProvider, come illustrato nel frammento di codice seguente:

1: public class Program : Microsoft.SPOT.Application

2: {

3:     public static void Main()

4:     {

5:         Program myApplication = new Program();

6:  

7:         Window mainWindow = myApplication.CreateWindow();

8:  

9:         // Crea l'oggetto che mappa i GPIO come ingressi di tipo "Button"

10:         GPIOButtonInputProvider inputProvider = new GPIOButtonInputProvider(null);

11:  

12:         // Avvia l'applicazione a partire dalla finestra appena creata

13:         myApplication.Run(mainWindow);

14:     }

15:  

16:     // [...]

17: }

L'oggetto Window rappresenta un contenitore logico e visuale per la generazione dell'interfaccia utente. Durante il normale funzionamento dell'applicazione ne vengono spesso create più istanze, la cui "sovrapposizione" viene gestita dal motore grafico del framework. La creazione della prima (ed unica) finestra della nostra applicazione di esempio avviene grazie al codice riportato nel frammento seguente:

1: public Window CreateWindow()

2: {

3:     Window wnd;

4:  

5:     // Creazione di una finstra con le stesse dimensioni del display

6:     wnd = new Window();

7:     wnd.Height = SystemMetrics.ScreenHeight;

8:     wnd.Width = SystemMetrics.ScreenWidth;

9:  

10:     // Creazione di un oggetto di tipo Text

11:     Text text = new Text();

12:  

13:     text.Font = Resources.GetFont(Resources.FontResources.small);

14:     text.TextContent = Resources.GetString(Resources.StringResources.String1);

15:     text.HorizontalAlignment = Microsoft.SPOT.Presentation.HorizontalAlignment.Center;

16:     text.VerticalAlignment = Microsoft.SPOT.Presentation.VerticalAlignment.Center;

17:  

18:     // Aggiunta dell'oggetto Text alla gerarchia della finestra

19:     wnd.Child = text;

20:  

21:     // Aggiunta di un gestore di evento per l'evento ButtonUp

22:     wnd.AddHandler(Buttons.ButtonUpEvent, new ButtonEventHandler(OnButtonUp), false);

23:  

24:     // La finestra viene effettivamente mostrata

25:     wnd.Visibility = Visibility.Visible;

26:  

27:     // L'input di tipo "Button" viene ridirezionato su questa finestra

28:     Buttons.Focus(wnd);

29:  

30:     return wnd;

31: }

Le sezioni degne di nota sono senz'altro quelle relative alle righe  11-19, in cui viene creato e configurato un oggetto grafico di tipo "Text" (paragonabile ad un controllo grafico di tipo "etichetta") e alla riga 22, in cui vediamo come configurare il sistema di gestione dell'input in modo tale che gli eventi di tipo "ButtonUp" provenienti dal sottosistema "Buttons" vengano gestiti dal metodo denominato (arbitrariamente) OnButtonUp.

Tale metodo, implementato nella nostra applicazione come semplice funzione di debug che riporta il codice del pulsante premuto, è definito con il codice riportato nel frammento seguente:

1: private void OnButtonUp(object sender, ButtonEventArgs e)

2: {

3:     // Visualizza il codice del pulsante nella finestra di debug

4:     Debug.Print(e.Button.ToString());

5: }

Quello che non appare evidente dal codice appena visto è come sia possibile che gli eventi relativi alla pressione dei pulsanti presenti sul dispositivo target siano effettivamente legati agli ingressi digitali corrispondenti ai pulsanti stessi, o anche come siano coniugati lo stato logico dell'ingresso digitale con lo stato logico relativo alla pressione del pulsante, collegabile  con una resistenza di pull-up o di pull-down rispettivamente ai livelli di tensione alto e basso. La risposta alla nostra domanda viene dall'analisi di quanto avviene alla riga 10 del metodo Main() illustrato in precedenza. La classe "GPIOButtonInputProvider" si occupa della trasformazione degli interrupt di basso livello generati dalle variazioni di stato degli ingressi digitali collegati ai pulsanti in eventi "instradati" all'interno del sistema di gestione dell'input del layer ‘presentation’ del Framework.

Questo avviene come illustrato nel frammento di codice seguente:

1: public sealed class GPIOButtonInputProvider

2: {

3:     public readonly Dispatcher Dispatcher;

4:  

5:     private ButtonPad[] buttons;

6:     private DispatcherOperationCallback callback;

7:     private InputProviderSite site;

8:     private PresentationSource source;

9:  

10:     public GPIOButtonInputProvider(PresentationSource source)

11:     {

12:         // memorizziamo la sorgente dell'input

13:         this.source = source;

14:  

15:         // registriamo questo oggetto come sorgente di "input" e abbiamo

16:         // indietro un "InputProviderSite", che inoltra le segnalazioni

17:         // di input all'InputManager, che a sua volta colloca l'input nell'

18:         // area di "staging"

19:         site = InputManager.CurrentInputManager.RegisterInputProvider(this);

20:  

21:         // Creiamo un metodo di callback che, a seguito di un "report"

22:         // di segnalazione di basso livello proveniente da un interrupt,

23:         // inoltra la notifica all'InputProviderSite

24:         callback = new DispatcherOperationCallback(delegate(object report)

25:             {

26:                 InputReportArgs args = (InputReportArgs)report;

27:                 return site.ReportInput(args.Device, args.Report);

28:             });

29:         Dispatcher = Dispatcher.CurrentDispatcher;

30:  

31:         // Creiamo un provider hardware

32:         HardwareProvider hwProvider = new HardwareProvider();

33:  

34:         // definiamo i pin relativi ai GPIO collegati ai pulsanti

35:         // con il default dell'emulatore

36:         Cpu.Pin pinLeft = Cpu.Pin.GPIO_Pin0;

37:         Cpu.Pin pinRight = Cpu.Pin.GPIO_Pin1;

38:         Cpu.Pin pinUp = Cpu.Pin.GPIO_Pin2;

39:         Cpu.Pin pinSelect = Cpu.Pin.GPIO_Pin3;

40:         Cpu.Pin pinDown = Cpu.Pin.GPIO_Pin4;

41:  

42:         // Se non siamo sull'emulatore, usiamo la mappatura del

43:         // HardwareProvider appena definito

44:         if ((pinLeft = hwProvider.GetButtonPins(Button.VK_LEFT)) ==

45:             Cpu.Pin.GPIO_NONE)

46:             pinLeft = Cpu.Pin.GPIO_Pin0;

47:         else

48:         {

49:             pinRight = hwProvider.GetButtonPins(Button.VK_RIGHT);

50:             pinUp = hwProvider.GetButtonPins(Button.VK_UP);

51:             pinSelect = hwProvider.GetButtonPins(Button.VK_SELECT);

52:             pinDown = hwProvider.GetButtonPins(Button.VK_DOWN);

53:         }

54:  

55:         // Definiamo un insieme di oggetti ButtonPad come array

56:         // di oggetti che legano il GPIOButtonInputProvider, il codice "virtuale"

57:         // del pulsante e il pin associato

58:         ButtonPad[] buttons = new ButtonPad[]

59:         {

60:             // Associate the buttons to the pins as discovered or set above

61:             new ButtonPad(this, Button.VK_LEFT  , pinLeft),

62:             new ButtonPad(this, Button.VK_RIGHT , pinRight),

63:             new ButtonPad(this, Button.VK_UP    , pinUp),

64:             new ButtonPad(this, Button.VK_SELECT, pinSelect),

65:             new ButtonPad(this, Button.VK_DOWN  , pinDown),

66:         };

67:  

68:         this.buttons = buttons;

69:     }

70:  

71:     internal class ButtonPad : IDisposable

72:     {

73:         private Button button;          // Codice del pulsante

74:         private InterruptPort port;     // Porta che genera gli interrupt di notifica

75:         private GPIOButtonInputProvider sink;

76:         private ButtonDevice buttonDevice;

77:  

78:         public ButtonPad(GPIOButtonInputProvider sink, Button button,

79:             Cpu.Pin pin)

80:         {

81:             this.sink = sink;

82:             this.button = button;

83:             this.buttonDevice = InputManager.CurrentInputManager.ButtonDevice;

84:  

85:             // Se stiamo su un hardware vero e non emulato

86:             // creiamo una porta interrupt che notifica

87:             // di entrambi i tipi di variazione

88:             if (pin != Cpu.Pin.GPIO_NONE)

89:             {

90:                 // When this GPIO pin is true, call the Interrupt method.

91:                 port = new InterruptPort(pin, true,

92:                     Port.ResistorMode.PullUp,

93:                     Port.InterruptMode.InterruptEdgeBoth);

94:                 port.OnInterrupt += new NativeEventHandler(this.Interrupt);

95:             }

96:         }

97:  

98:         protected virtual void Dispose(bool disposing)

99:         {

100:             if (disposing)

101:             {

102:                 if (port != null)

103:                 {

104:                     port.Dispose();

105:                     port = null;

106:                 }

107:             }

108:         }

109:  

110:         public void Dispose()

111:         {

112:             Dispose(true);

113:             GC.SuppressFinalize(this);

114:         }

115:  

116:         void Interrupt(uint data1, uint data2, DateTime time)

117:         {

118:             // Intercetto la varizione dell'ingresso digitale

119:             // e identifico il tipo di azione effettivamente svolta

120:             RawButtonActions action = (data2 != 0) ?

121:                 RawButtonActions.ButtonUp : RawButtonActions.ButtonDown;

122:  

123:             // Creao un nuovo report di segnalazione

124:             RawButtonInputReport report = new RawButtonInputReport(

125:                 sink.source, time, button, action);

126:  

127:             // accodo l'evento relativo all'interrupt verso il nostro InputProviderSite

128:             // utilizzando l'oggetto Dispatcher per poter effettuare

129:             // la chiamata della callback da un altro thread

130:             // (il thread dell'interrupt e quello che gestisce la

131:             // interfaccia utente sono diversi)

132:             sink.Dispatcher.BeginInvoke(sink.callback, new InputReportArgs(buttonDevice, report));

133:         }

134:     }

135: }

La complessità della struttura della classe GPIOButtonInputProvider può effettivamente spiazzare ad una prima analisi, ma la cosa effettivamente importante da notare è l'effettiva responsabilità di ciascuna delle classi utilizzate, riassunta nell'elenco che segue:

  • PresentationSource è una classe che agevola l'utilizzo di sistemi di rendering indipendenti dall'hardware, attraverso l'esposizione della proprietà RootUIElement, radice logica di una sorgente di contenuti
  • Dispatcher è una classe che si occupa della gestione della una coda di eventi di un thread
  • InputManager è una classe che coordina tutti i sistemi di input attivi nel sistema, trasformando gli eventi di basso livello in eventi consumabili dal sottosistema "Presentation"
  • HardwareProvider fornisce informazioni sulla mappatura delle funzionalità per uno specifico hardware. Nel caso appena visto ad esempio viene utilizzata per conoscere la mappatura tra GPIO di ingresso e codici pulsante
  • ButtonDevice rappresenta un generico dispositivo hardware dotato di pulsanti
  • InputProviderSite rappresenta una classe con funzioni di mediazione tra un input provider ed il InputManager
  • RawButtonInputReport rappresenta l'oggetto che veicola l'informazione sull'attivazione di un pulsante che viene passato da un input provider all'InputProviderSite e ancora all'InputManager
 
Pattern nell'uso del Presentation Layer

Per approfondire la conoscenza sugli strumenti disponibili all'interno del vasto mondo del layer ‘Presentation’, è opportuno prendere in esame alcuni esempi notevoli che illustrano soluzioni articolate per la rappresentazioni di informazioni anche complesse. Gran parte di quanto vedremo in questa sezione  è riassunto nel sample denominato "SimpleWPFApplication", contenuto nel .NET Micro Framework SDK, di cui la figura che segue rappresenta l'interfaccia utente principale. Per dare accesso a ciascun sotto-demo illustrato, l'applicazione in questione utilizza un menù scorrevole animato, azionabile attraverso i tasti "destra", "sinistra" e "centrale", mappati sulla scheda TahoeII che stiamo utilizzando all'interno del tastierino direzionale riportato nella parte inferiore destra della scheda stessa. Ciascuna finestra relativa ad una parte del programma dimostrativo deriva da una classe comune, definita all'interno della medesima applicazione, denominata ‘PresentationWindow’. La derivazione comune di più tipi di finestra da una stessa base permette, come avviene in generale per mezzo dell'ereditarietà dei linguaggi orientati ad oggetti, di condividere caratteristiche e comportamenti comuni, come il fatto di inizializzarsi con la massima dimensione disponibile sul display e supportare l'operazione comune relativa alla pressione di un pulsante, nel caso del codice dimostrativo in questione.

I vari sotto-demo illustrati mostrano ciascuno un aspetto peculiare dello sviluppo di interfacce utente attraverso gli strumenti del framework. A titolo di esempio, prendiamo in esame il sotto-demo relativo all'utilizzo del controllo ‘Stack Panel’, illustrato nella figura successiva.

Il codice relativo a quanto illustrato è riportato nel frammento di codice seguente:

1: internal sealed class StackPanelDemo : PresentationWindow

2: {

3:     private sealed class Cross : Shape

4:     {

5:         public Cross() { }

6:  

7:         // Ridefiniamo il metodo OnRender della classe "UIElement"

8:         // ereditata tramite Window

9:         public override void OnRender(DrawingContext dc)

10:         {

11:             // Linea diagonale 1

12:             dc.DrawLine(base.Stroke, 0, 0, Width, Height);

13:  

14:             // Linea diagonale 2

15:             dc.DrawLine(base.Stroke, Width, 0, 0, Height);

16:         }

17:     }

18:  

19:     // Costruttore

20:     public StackPanelDemo(MySimpleWPFApplication program, Orientation orientation)

21:         : base(program)

22:     {

23:         StackPanel panel = new StackPanel(orientation);

24:         this.Child = panel;

25:         panel.Visibility = Visibility.Visible;

26:  

27:         // Array delle forme geometriche da disegnare

28:         Shape[] shapes = new Shape[] {

29:          new Ellipse(0, 0),

30:          new Line(),

31:          // Quadrato

32:          new Polygon(new Int32[] { 0, 0,    50, 0,    50, 50,    0, 50 }), 

33:          new Rectangle(),

34:          // Forma personalizzata (croce)

35:          new Cross() 

36:       };

37:  

38:         // Aggiungo le forme alla gerarchia visuale

39:         for (int x = 0; x < shapes.Length; x++)

40:         {

41:             Shape s = shapes[x];

42:             s.Fill = new SolidColorBrush(ColorUtility.ColorFromRGB(0, 255, 0));

43:             s.Stroke = new Pen(Color.Black, 2);

44:             s.Visibility = Visibility.Visible;

45:             s.HorizontalAlignment = HorizontalAlignment.Center;

46:             s.VerticalAlignment = VerticalAlignment.Center;

47:             s.Height = Height - 1;

48:             s.Width = Width - 1;

49:  

50:             if (panel.Orientation == Orientation.Horizontal)

51:                 s.Width /= shapes.Length;

52:             else

53:                 s.Height /= shapes.Length;

54:  

55:             panel.Children.Add(s);

56:         }

57:     }

58: }

Vediamo sinteticamente gli aspetti salienti che costituiscono la soluzione appena illustrata:

  • (riga 1) La finestra di questa sotto-demo eredita dalla ‘PresentationWindow’ illustrata in precedenza
  • (righe 3-17) Viene definita una classe annidata che eredita dalla classe ‘Shape’; l'ereditarietà consente alla nostra forma personalizzata di ereditare tutto ciò che è comune alle altre forme, pur mantenendo la flessibilità di ridefinire l'aspetto in cui è specializzata, effettuando quello che viene descritto come "overriding" del metodo OnRender(), in cui avviene la produzione del disegno vero e proprio
  • (righe 20-36) Nel costruttore della classe StackPanelDemo, viene creato uno StackPanel, ossia un contenitore grafico che implementa un layout secondo il quale gli oggetti figli vengono riportati uno sotto l'altro (in caso di orientamento verticale) o uno accanto all'altro (in caso di orientamento orizzontale), seguendo l'ordine di inserimento; il pannello viene poi aggiunto come primo ed unico figlio dell'oggetto Window corrente. Nella seconda parte del costruttore viene poi popolato un array di oggetti di tipo Shape, in realtà costituito da forme di tipo diverso (un ellisse, una linea, un quadrato, un rettangolo ed un oggetto del nostro tipo personalizzato, ossia la croce). Come ultima operazione viene scorso l'array in questione per aggiungere ciascuna Shape al pannello. Lo StackPanel è solo uno dei contenitori inclusi nel framework e altri possono essere aggiunti anche attraverso la creazione di classi derivate da Panel; l'aspetto più rilevante da tenere a mente è che nell'ambito della gerarchia del layer Presentation alcuni oggetti grafici non consentono l'inclusione di oggetti figli, altri consentono l'inclusione di un unico elemento figlio (come la Window) ed altri consentono invece l'inclusione di un numero arbitrario di figli (come per tutti gli oggetti derivati da Panel)

Il codice appena preso in esame viene utilizzato anche nel caso del sotto-demo "Horizontal Stack Panel", in cui gli oggetti Shape sono riportati in orizzontale anziché in verticale, sfruttando la proprietà dello StackPanel valorizzata alla riga 23 del codice appena esaminato.

Per un esempio personalizzazione di un controllo contenitore di tipo "Panel", è possibile fare riferimento al sotto-demo "Scollable Panel", la cui interfaccia utente è illustrata in figura seguente. Tale applicazione illustra come creare una superficie grafica virtualmente molto più estesa del display fisico che la visualizza attraverso la definizione di un controllo personalizzato denominato "TextScrollViewer".

 

 

Touch Screen

La gestione del touch screen all'interno del sistema ‘Presentation’ è estremamente semplificato dal modello ad eventi di chiara ispirazione Windows. In particolare, come è ad esempio analizzabile nel demo denominato "StylusCapture", anch'esso installato dal setup del .NET micro Framework SDK, gli oggetti grafici definiti all'interno della gerarchia visuale espongono una serie di eventi relativi alle singole azioni rilevabili, quali ad esempio "TouchDown" o "TouchUp", rispettivamente sollevati da un elemento visuale quando questo viene "toccato" o "rilasciato".

Per sottoscrivere la nostra applicazione alla notifica di questo tipo di eventi è sufficiente utilizzare i comandi riprodotti nel frammento di codice seguente.

1: Text text1 = new Text();

2: text1.TouchDown += new TouchEventHandler(Text_TouchDown);

3: text1.TouchUp += new TouchEventHandler(Text_TouchUp);

4:  

5: void Text_TouchDown(object sender, TouchEventArgs e)

6: {

7:     // ...

8: }

9:  

10: void Text_TouchUp(object sender, TouchEventArgs e)

11: {

12:     // ...

13: }

14:  

 

Una volta intercettato l'evento è possibile compiere qualsiasi tipo di azione, come cambiare lo stato dell'elemento visuale "toccato" o attivare delle specifiche funzioni hardware. Qualora fosse necessario, come avviene spesso, che il controllo che rileva il primo evento di TouchDown continui a ricevere notifiche dal touch screen anche quando l'area impegnata dal tocco non si sovrappone più con la superficie del controllo, è possibile utilizzare la cosiddetta funzionalità di "cattura", come illustrato nel frammento di codice seguente.

1: // Cattura...

2: TouchCapture.Capture(text);

3:  

4: // Rilascio...

5: TouchCapture.Capture(text, CaptureMode.None);

 

 

 

In maniera analoga, è possibile intercettare gli eventi relativi al touch screen all'interno di un oggetto Window o derivato ridefinendo tramite "override" i metodi OnTouchUp() e OnTouchDown(), come illustrato nel frammento di codice che segue.

 

1: protected override void OnTouchDown(TouchEventArgs e)

2: {

3:    base.OnTouchDown(e);

4:  

5:    // ...

6:  

7:    e.Handled = true;

8: }

9:  

10: protected override void OnTouchUp(TouchEventArgs e)

11: {

12:    base.OnTouchDown(e);

13:  

14:    // ...

15:  

16: }

L'assegnazione fatta a titolo di esempio a riga 7 consente di intervenire nel processo di "bubbling" degli eventi gestito dal sistema ‘Presentation’ . Per bubbling si intende la politica di gestione delle notifiche degli eventi relativi all'interazione con elementi visuali; poiché infatti è possibile che un evento venga rilevato da un elemento ma debba essere notificato anche al suo contenitore, o al contenitore ancora più esterno o all'oggetto Window che fa da involucro "root" della gerarchia, il framework utilizza il concetto di RoutedEvent. Un RoutedEvent è un oggetto la cui principale responsabilità è quella di veicolare una notifica legata ad una qualche tipo di azione effettuata su un elemento visuale secondo una delle seguenti tre "strategie":

  • Bubble: in cui l'evento sale a galla della gerarchia, dall'elemento più interno al contenitore più esterno
  • Direct: in cui l'evento non viene effettivamente instradato ma arriva solo al target principale
  • Tunnel: in cui l'evento scende nella gerarchia, dall'elemento più esterno a quello più interno

Mentre lo scopo delle prime due strategie è abbastanza semplice da intuire, la strategia Tunnel ha una finalità più difficile da cogliere; essa viene utilizzata prevalentemente nelle notifiche di tipo "Preview" in cui a ciascun contenitore viene data l'opportunità di intercettare la notifica dell'evento del proprio "sottoalbero". L'interazione con il processo di "bubbling" o di "tunnelling" avviene tramite la proprietà Handled, esposta dalla classe base che descrive il parametro che accompagna ogni RoutedEvent: RoutedEventEventArgs.

Come ultimo esempio, vale la pena di soffermarci sulle immagini relative all'applicazione dimostrativa della più potente libreria disponibile per .NET Micro Framework in merito allo sviluppo di interfacce utente: MFRichMediaExtensions della Innobedded, società fondata da Jens Kühner, autore del miglior testo in circolazione in materia di .NET Micro Framework, giunto alla sua seconda edizione. Tale libreria, scaricabile all'indirizzo http://www.innobedded.com/download.html, è utilizzabile in modalità demo senza limiti di tempo, seppur con frequenti interruzioni da parte di una bitmap che agisce come "nag screen" raffigurante il logo della Innobedded. La licenza d'uso della libreria ha un prezzo molto vantaggioso se utilizzata per scopi non commerciali ed in tutti i casi semplifica radicalmente lo sviluppo di un interfaccia utente che sia al contempo funzionale ed esteticamente appetibile, come illustrato nelle immagini seguenti.