Display: Bitmap mode

Valuta questo articolo 1 Voti

Autori: Lorenzo Maiorfi e Gianluca Ruta

Anche se parlare di interfaccia utente di un'applicazione non significa necessariamente fare riferimento ad argomenti legati alla visualizzazione di informazioni, è innegabile che il mezzo di gran lunga più efficace in tal senso è senz'altro costituito dall'insieme delle funzionalità e dei dispositivi finalizzati alla rappresentazione grafica e testuale dei dati manipolati da un'applicazione. L'avvento dell'espressione "multimediale" ha in realtà fatto sì che l'attenzione degli utenti ricadesse anche su rappresentazioni diverse da quella visuale, come quella sonora ad esempio, ma è indubbio che il ruolo giocato dai media non visuali è assolutamente marginale. Per questo motivo, gran parte dell'evoluzione subita dalle tecnologie che orbitano intorno al mondo dell'Information Technology, ivi compreso il mondo embedded, si è concentrata sul potenziamento delle capacità di visualizzazione delle informazioni e della gestione dei comandi impartiti dagli operatori. Attenti alle tendenze del panorama tecnologico e agli evidenti segnali che provengono dal mondo dell'elettronica consumer, il team dei designer del .NET Micro Framework ha speso molte energie nell'ambito della realizzazione di un sistema per la realizzazione di interfacce utente ricche. Tale sistema, assolutamente inedito in questo settore, è caratterizzato dalla possibilità di esprimere gerarchie complesse di oggetti grafici attraverso un modello astratto ed indipendente dalla geometria del display utilizzato. Inoltre, il .NET Micro Framework supporta in modo nativo la gestione delle funzionalità "touch", supportate ovviamente dai dispositivi abilitati in tal senso, tra i quali figura la scheda di sviluppo "Tahoe II" di Device Solutions, utilizzata e descritta già nelle precedenti puntate.

 

Bitmap mode

Addentrandoci nelle classi del .NET Micro Framework dedicate alla gestione degli oggetti grafici, scopriamo che esistono in realtà due diversi "strati" che separano il nostro firmware dal driver che di fatto pilota, tipicamente attraverso un'interfaccia parallela, il controller del display: un primo strato si occupa del trasferimento (detto "blitting") di un oggetto Bitmap sulla superficie del display, mentre lo strato superiore, denominato "Presentation", si occupa dell'astrazione degli oggetti grafici, ottenuta attraverso la definizione di una struttura gerarchica di contenitori ed elementi di interfaccia utente che alimentano il motore di layout e di rendering attraverso l'uso di un'apposita "pipeline" gestita dal framework stesso.

Iniziamo la nostra sperimentazione a partire dal layer di basso livello. Il paradigma utilizzato in questo modello di sviluppo prevede che all'interno del firmware venga definita un'istanza della classe Bitmap caratterizzata dalla stessa geometria del display disponibile sul dispositivo. Tale Bitmap agisce come un vero e proprio display "offline", sul quale vengono disegnate delle primitive grafiche attraverso appositi metodi della classe Bitmap, fino a quando, a seguito dell'esecuzione del metodo Flush(), i pixel che  descrivono la superficie della bitmap offline vengono effettivamente disegnati sul display del dispositivo.

Se ad esempio vogliamo disegnare un rettangolo rosso che copra l'intera superficie del display, è sufficiente utilizzare codice riportato nel frammento che segue:

1: int w,h,d,r;

2:            

3: HardwareProvider.HwProvider.GetLCDMetrics(out w,out h,out d,out r);

4:  

5: Bitmap bmp = new Bitmap(w, h);

6:  

7: bmp.DrawRectangle(

8:    Color.White,  // colore bordo ininfluente

9:    0,            // senza bordo

10:    0, 0, w, h,   // parte da 0,0 (in alto a sinistra) ed ha le stesse dimensioni del display

11:    0, 0,         // raggio degli angoli "stondati"

12:    ColorRoyalBlue, 0, 0,    // colore e coordinate di partenza del gradient

13:    ColorRoyalBlue, 0, 0,    // colore e coordinate di arrivo del gradient

14:    Bitmap.OpacityOpaque);   // contenuto opaco (non trasparente)

15:  

16: bmp.Flush();

Analogamente, è possibile utilizzare le numerose primitive grafiche per disegnare ellissi, bitmap, testo e linee. Facendo riferimento alla figura 1, in cui è illustrato l'output del firmware che prenderemo a breve in esame, vale la pena sottolineare che ciascuna primitiva comporta delle peculiarità di cui occorre essere al corrente per evitare sorprese al momento del loro utilizzo.

 

Testo

Nella parte in alto a sinistra dell'immagine vediamo due testi realizzati mediante la primitiva "DrawText". Tale metodo può essere utilizzato secondo quanto illustrato nel frammento di codice seguente, in cui "bmp" rappresenta, come nel frammento precedente, la Bitmap relativa al display "offline":

1: bmp.DrawText("Testo con Font NinaB",

2:      Resources.GetFont(Resources.FontResources.NinaB),

3:      ColorUtility.ColorFromRGB(255, 0, 0),

4:      10, 5);

5:  

6: bmp.DrawText("Testo con Font Small", 

7:      Resources.GetFont(Resources.FontResources.small),

8:      ColorUtility.ColorFromRGB(0, 255, 0),

9:      10, 20);

 

 

Alle righe 2 e 7 è possibile notare come selezionare il font da utilizzare per il rendering del testo; a differenza di quanto avviene infatti all'interno di Windows, nella runtime del .NET Micro Framework non sono presenti dei font residenti. Questo si riflette sulla impossibilità di istanziare la classe Font a partire, ad esempio, dal nome della famiglia del carattere e dalla dimensione. L'unica via prevista è infatti quella in cui il font viene caricato attraverso le "risorse" definite all'interno dell'applicazione. Una "risorsa" è definita dal framework come un oggetto (binario o testuale) incapsulato all'interno di un'apposita area dell'assembly prodotto come risultato della compilazione di un file sorgente con suffisso ".resx". Tale file, strutturato come file xml, riporta il riferimento ai file che verranno effettivamente inclusi come risorsa. In fase di editing del progetto, l'ambiente di sviluppo gestisce le risorse con un apposito designer, dal quale è possibile includere file generici o di tipo specifico, quali immagini, font, testi, ecc. L'editor di risorse genera inoltre un file sorgente per ciascun file .resx presente nel progetto. Tale file, denominato con l'estensione ".Designer.cs", contiene la dichiarazione della classe parziale "Resources" attraverso la quale è possibile accedere all'effettivo contenuto di una risorsa, utilizzando metodi specializzati per il tipo di risorsa richiesta. All'inclusione di un file di tipo ".tinyfnt", che descrive un tipo di carattere tipografico utilizzabile nel .NET Micro Framework, viene ad esempio generato il metodo GetFont() che prevede un parametro di tipo enumerato denominato FontResources, anch'esso definito dal designer, in cui sono elencati tutti i font utilizzabili dall'applicazione. L'installazione del .NET Micro Framework SDK crea una cartella denominata "Fonts", tipicamente contenuta in "C:\Program Files\Microsoft .NET Micro Framework\v4.0", in cui sono presenti due file con suffisso tinyfnt: ‘NinaB’ e ‘small’, illustrati nella citata figura 1 rispettivamente in rosso e in verde. Altri font possono essere aggiunti mediante l'utility TFConvert, contenuta nella cartella "Tools". Tale utility consente infatti la conversione di un font TrueType in un .tinyfnt, con l'unica limitazione rappresentata dal fatto che, essendo i font del .NET Micro Framework non scalabili, in fase di conversione occorre specificare la dimensione del font da convertire.

 

Aree Rettangolari

La primitiva sicuramente più sviluppata è senz'altro quella relativa alla produzione di aree rettangolari. Il motivo è legato al fatto che tale primitiva costituisce il principale strumento per la realizzazione di oggetti grafici complessi quali pulsanti, liste, pannelli, ecc. Analizzando i cinque rettangoli blu riportati in figura 1 è possibile notare come siano previste diverse varianti implementate dallo stesso metodo DrawRectangle(). Nell'ordine da sinistra verso destra nell'applicazione demo sono disegnati rispettivamente un rettangolo pieno con colore uniforme, un rettangolo vuoto con spessore maggiore di un pixel, un rettangolo vuoto con angoli arrotondati, un rettangolo colorato con riempimento ‘gradient’, ossia sfumato tra due colori estremi e in ultimo ancora un rettangolo con riempimento ‘gradient’ ma con direzione verticale. La direzione del riempimento ‘gradient’ è liberamente impostabile specificando i due punti che delimitano gli estremi della interpolazione. Il rendering di aree rettangolari può inoltre avvenire utilizzando una modalità semitrasparente, in cui, attraverso l'ultimo parametro, è possibile specificare il livello di opacità tra 0 e 255. Un esempio di utilizzo di tale funzionalità è riportato nell'area centrale dell'immagine in figura 1, prodotta tramite i comandi riportati nel frammento di codice che segue:

 

1: // Test trasparenza

2: for (ushort opacity = 0; opacity < 200; opacity += 16)

3: {

4:     bmp.DrawRectangle(Color.White,

5:         0,

6:         10 + opacity, 100 + opacity / 8, 50, 50,

7:         0, 0,

8:         ColorPapayaWhip, 0, 0,

9:         ColorPapayaWhip, 0, 0,

10:         opacity);

11: }
 
Cerchi ed ellissi

Attraverso la primitiva DrawEllipse() è possibile disegnare all'interno di una Bitmap cerchi ed ellissi, analogamente a quanto avviene per le aree rettangolari. A causa però di una limitazione nella attuale implementazione del .NET Micro Framework, non è possibile utilizzare tale primitiva per disegnare figure piene, ma solo in modalità "outline" (limitazione aggirabile attraverso l'uso di opportune risorse di tipo "Image", come illustrato nel paragrafo seguente).

L'uso della primitiva DrawEllipse() è riportato nel frammento di codice che segue, in cui viene riprodotta la parte in basso a sinistra dell'immagine nella figura seguente.

 

1: // Cerchi

2: bmp.DrawEllipse(ColorUtility.ColorFromRGB(255, 0, 0), 30, 190, 25, 25);

3: bmp.DrawEllipse(ColorUtility.ColorFromRGB(0, 255, 0), 40, 210, 25, 25);

4: bmp.DrawEllipse(ColorUtility.ColorFromRGB(0, 0, 255), 50, 190, 25, 25);

5:  

6: // NON IMPLEMENTATO IN .NET MF 4.0

7: // bmp.DrawEllipse(

8: //    ColorYellow, 0,90,200,25,25,

9: //    ColorYellow,90,200,ColorMediumSpringGreen,100,210,Bitmap.OpacityOpaque);

 
Immagini bitmapped

L'ultima primitiva che prendiamo in considerazione è anche la più potente. Il metodo DrawImage() consente infatti di disegnare all'interno della superficie grafica "offline" delle immagini descritte attraverso istanze della classe "Bitmap". Oltre al rendering semplice, è possibile effettuare anche la scalatura, riportando l'immagine con dimensioni diverse da quelle originali, ed utilizzare diversi livelli di trasparenza, come per le aree rettangolari. Le immagini da disegnare possono essere definite attraverso il loro contenuto "binario", espresso in formato Bmp, Gif, Jpeg o TinyCLRbitmap (un formato personalizzato utilizzato all'interno del Framework), oppure, in alternativa, estratte dalle risorse dell'applicazione. Nell'esempio riportato nel frammento di codice seguente viene ad esempio riportata sul display l'immagine raffigurante il logo del .NET Micro Framework, precedentemente aggiunto al progetto come risorsa:

// Immagini
Bitmap bmpIcon1 = Resources.GetBitmap(Resources.BitmapResources.netmf);
bmp.DrawImage(100, 190, bmpIcon1, 0, 0, bmpIcon1.Width, bmpIcon1.Height);

 

 

Come esempio conclusivo della parte riguardante l'approccio a basso livello per la generazione di interfacce utente, ci proponiamo di realizzare un semplicissimo oscilloscopio, attraverso lo sviluppo di un'applicazione che, utilizzando l'ADC presente a bordo della scheda di sviluppo TahoeII, permetta il monitoraggio grafico del livello dell'ingresso analogico "AUX", in una scala 0-3.3V. L'immagine prodotta dal firmware in questione è riprodotta nella figura che segue. Il codice relativo al sorgente del firmware è riportato nel frammento seguente, in cui è possibile notare l'utilizzo della primitiva DrawLine(), non trattata in precedenza, per la tracciatura di linee con spessore e colore parametrici.

 

 

1: // Divisione del reticolo...

2: int xdivs = 10;

3: int ydivs = 10;

4: int oldx = 0, oldy = 0;

5:  

6: while (true)

7: {

8:     oldx = -1;

9:     oldy = -1;

10:  

11:     // Pulisco la bitmap

12:     bmp.Clear();

13:  

14:     // Bordo...

15:     bmp.DrawRectangle(ColorGrey, 1, 0, 0, w, h, 

16:             0, 0, Color.White, 0, 0, Color.White, 0, 0,

17:             Bitmap.OpacityTransparent);

18:  

19:  

20:     // reticolo...

21:     for (int xi = 0; xi < xdivs; xi++)

22:     {

23:         bmp.DrawLine(ColorGrey, 1, xi * (w / xdivs), 0, xi * (w / xdivs), h);

24:     }

25:  

26:     for (int yi = 0; yi < ydivs; yi++)

27:     {

28:         bmp.DrawLine(ColorGrey, 1, 0, yi * (h / ydivs), w, yi * (h / ydivs));

29:     }

30:  

31:     bmp.DrawText("AUX", 

32:             Resources.GetFont(Resources.FontResources.small), 

33:             ColorUtility.ColorFromRGB(255, 0, 0), 294, 6);

34:  

35:     bmp.Flush();

36:  

37:     // Scansione e lettura ADC

38:     for (int xi = 0; xi < w; xi++)

39:     {

40:         float scaledtemp=(TahoeII.Tsc2046.ReadAux()) / 3.3f;

41:         

42:         int yt = h-(int)(scaledtemp*h);

43:  

44:         if (oldx != -1)

45:         {

46:             // Linea che unisce gli ultimi 2 punti rilevati...

47:             bmp.DrawLine(ColorUtility.ColorFromRGB(255, 0, 0), 1, oldx, oldy, xi, yt);

48:  

49:             bmp.Flush();

50:         }

51:  

52:         oldx = xi - 1;

53:         oldy = yt;

54:  

55:         Thread.Sleep(10);

56:     }

57:  

58: }