m20. windows presentation foundation wpf

Transcript

m20. windows presentation foundation wpf
M20. WINDOWS PRESENTATION FOUNDATION – WPF
Introduzione
WPF è un modo per creare interfacce grafiche un po’ belline. Windows Form usa infatti il
motore GDI che risale ai tempi di “Hanno ucciso l’uomo ragno”: per quanto faccia comunque la sua porca figura, ha delle limitazioni legate al modello retrostante. Non è infatti supportato il concetto di semitrasparenza, mentre alcune cose – come le animazioni
– sono implementate introducendo un botto di cicli di CPU.
Windows Presentation Foundation
Microsoft così nel 2006 si è sforzata di cercare una
seconda strada a GDI, utilizzando il motore grafico
delle DirectX, che fa sintetizzare alla GPU un insieme di operazioni. Questa strada è Windows Presentation Foundation, tramite cui è possibile costruire interfacce grafiche atte a una pluralità di
applicazioni (documenti, contenuti multimediali,
grafica 2D/3D). Facilita l’integrazione di legacy code, cioè codice preesistente che sfruttava le API base di Windows.
Contiene al suo interno sia una porzione applicativa visibile all’utente (PresentationFramework e PresentationCore) e la milcore, ovvero una parte nativa Win32 che vive sotto
il CLR e fa da bridge tra il CLR stesso e le DirectX.
In GDI si usava immediatamente l’algoritmo di Bresenham per “accendere” i vari pixel di
un elemento grafico, andando dunque a utilizzare la grafica raster che non dà buoni risultati in caso di zoom. WPF invece è interamente basato grafica vettoriale, utilizzando
linee, archi di cerchio/ellisse e curve di Bézier che vengono salvati e passati alla GPU
che sintetizza alla miglior risoluzione possibile.
Per funzionare, WPF mette insieme due cose: la logica applicativa (“quando premo questo bottone succede questo”, scritta in C# o in quello che vogliamo) e l’interfaccia grafica (che con Windows Forms diventava codice da eseguire, mentre qui è esplicitata con
una descrizione dichiarativa scritta in un dialetto XML). Questo facilita molto la separazione dei ruoli tra grafico e sviluppatore.
Architettura WPF
Internamente, WPF è fatto di tanti blocchi: alla base di
tutto c’è la capacità di disegnare le primitive visuali
(linee, triangoli, ellissi, curve di Bèzier...) e la cosa è
demandata alla scheda video.
Sopra questo strato c’è uno strato di animazione. Fornisce un meccanismo dichiarativo
per dire cosa dovrà succedere nei prossimi n secondi, basandosi su un “coreografo”. Sullo strato di animazione si appoggiano gli elementi necessari per costruire le primitive
percepite dall’utente (grafica 2D/3D, testo, video, audio, effetti per “storpiare” primitive
esistenti, immagini).
Mentre tutto quello che abbiamo presentato si occupa di far apparire qualcosa sullo
schermo, la colonna laterale si occupa della raccolta degli eventi. Sulla base di questi
due blocchi fondanti si costruiscono i servizi con cui ci interfacciamo: le librerie dei controlli (bottoni e quant’altro), il data binding per legare valori di variabili a caratteristiche
degli elementi grafici, e così via.
Visual rendering
WPF introduce innovazioni dal punto di vista del rendering di oggetti grafici.
Modalità immediate vs. modalità retained
GDI (ma anche Java, librerie C++, Qt e compagnia bella) utilizzano, nel disegnare, una modalità immediate: ogni volta
che il sistema capisce che il rettangolo in cui è ospitata la finestra dell’utente si è “sporcato”, viene mandato un messaggio per ridisegnarla. Questo perché il sistema non ricorda
cosa ciascuna finestra stia visualizzando, quindi ogni finestra
è responsabile di mantenere il suo stato: in Windows questo
sistema è basato sulla coda dei messaggi WM_PAINT che scatenano l’azione di una serie
di routine del programma.
WPF invece usa l’approccio retained: ogni oggetto grafico descrive come vuole apparire
in una modalità dichiarativa. Ciascun oggetto quindi contiene dati sul proprio rendering,
che possono essere dati vettoriali, immagini, glifi (testo) e video, piazzando tutto in una
struttura dati serializzata in modo opportuno.
Il sistema grafico mantiene in una struttura ad albero i dati sul rendering degli oggetti
che compongono la scena da visualizzare. Le strutture dati sono costruite in modo da
essere ottimizzate nella costruzione di primitive di disegno da parte della GPU: quando
la finestra diventa non valida e invia il messaggio di
WM_PAINT, il motore di WPF non va più a scomodare i singoli componenti perché sa già cosa deve fare declinandola opportunamente in base ai parametri correnti e facendola eseguire alla CPU.
Il grosso vantaggio è che non c’è più bisogno di occuparsi
del ridisegno, enunciando solo “che cosa si vuole essere”.
C’è più efficienza perché si può fare del caching delle operazioni di rendering, sfruttando
al meglio la scheda video.
Grafica vettoriale
Consente di descrivere un elemento grafico in base ad una serie di primitive grafiche ed
è una soluzione più sofisticata della tradizionale grafica “bitmap”. Un’immagine ridimensionata viene ridisegnata e non perde qualità rispetto all’originale, perché viene “scalata” anziché “stirata.
Unità di misura
Due fattori determinano la dimensione fisica del testo e oggetti grafici sullo schermo
• risoluzione dello schermo (numero di pixel visualizzati)
• densità dei pixel DPI: dimensione di un pollice ideale espressa in pixel
WPF supporta schermi con differenti densità e usa come unità di misura primaria i “device independent pixel” anziché i pixel hardware e consente di riadattare automaticamente testo ed elementi grafici a diverse risoluzioni e DPI mantenendo costante la dimensione.
Sviluppo in WPF
Come facciamo a sviluppare applicazioni in WPF? Due possibilità:
• siccome tutto ciò che è fatto in WPF è istanza di una classe, posso creare programmaticamente le mie cose
• uso l’approccio dichiarativo utilizzando un linguaggio descrittivo basato su XML
In alcune situazioni è possibile usare un misto dei due linguaggi.
XAML
Usato in WPF per creare e inizializzare oggetti secondo una data gerarchia. Tutte le classi in WPF hanno solo un costruttore privo di argomenti: si utilizzano le proprietà per configurare il comportamento e l’aspetto degli oggetti, facilitando l’integrazione con un linguaggio descrittivo.
Il codice C# rimane così compatto e il codice XML generato è leggibile facilmente anche
dai non programmatori. Se volessi disegnare una finestra e fare tutto via software dovrei
scrivere una cosa del genere
public partial class Window1 : Window {
public Window1() {
InitializeComponent();
StackPanel stackPanel = new StackPanel();
/* Layout */
this.Content = stackPanel;
this.Background = new LinearGradientBrush(Colors.Wheat,Colors.White,
new Point(0,0), new Point(1,1));
TextBlock textBlock = new TextBlock();
textBlock.Margin = new Thickness(20);
textBlock.FontSize = 20;
textBlock.Foreground = Brushes.Blue;
textBlock.Text = "Hello World";
stackPanel.Children.Add(textBlock);
Button button = new Button();
button.Margin= new Thickness(10);
button.Height = 25; button.Width = 80;
button.HorizontalAlignment = HorizontalAlignment.Right;
button.Content = "OK";
stackPanel.Children.Add(button);
}
}
La stessa cosa si può scrivere in XAML
<Window x:Class="hello.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="145" Width="298" >
<Window.Background>
<LinearGradientBrush StartPoint="0 0" EndPoint="1 1" >
<GradientStop Offset="0" Color="Wheat"/>
<GradientStop Offset="1" Color="White"/>
</LinearGradientBrush>
</Window.Background>
<StackPanel>
<TextBlock Margin="20" FontSize="20" Foreground="Blue">Hello World </TextBlock>
<Button Margin="10" HorizontalAlignment="Right" Height="25" Width="80">OK</Button>
</StackPanel>
</Window>
Reattività
Quando creiamo un programma basato su WPF ci sono sempre (almeno) due thread. Noi
interagiamo col thread principale chiamato UI thread, che parte quando l’applicazione
viene lanciata e riceve gli eventi dell’utente, gestendo tutto ciò che è l’interazione base.
Può creare – anche se non è il massimo della vita – uno o più thread secondari per eseguire compiti in base all’input, il che è utile in tutte quelle situazioni in cui i compiti da
fare sono particolarmente spessi e bloccherebbero l’interfaccia.
Nel momento in cui l’applicazione si accorge di dover usare WPF crea comunque un thread secondario chiamato rendering thread che prende l’albero costruito nel thread principale e “fa i disegni” a seconda di quello che gli chiede il thread principale.
Dispatcher
Internamente, tutte le applicazioni WPF hanno l’equivalente di una coda produttore-consumatore, già sincronizzata. Quando vogliamo che succeda qualcosa basta scrivere dei
messaggi all’interno in modo che il thread di rendering li macini.
La coda è incapsulata in un oggetto chiamato dispatcher, conosciuto da tutti gli oggetti
che compongono un’applicazione WPF (perché tutti estendono DispatcherObject). La
coda non è ordinata e riconosce che ci sono
alcune cose più prioritarie.
Sia UIThread che RenderingThread fanno accesso al dispatcher. Il primo inserisce richieste a fronte del verificarsi di un evento e legge i messaggi di notifica da parte degli altri
thread, mentre il secondo riceve richieste di inizio di un’attività e inserisce messaggi
sull’esito delle attività svolte.
Lo UIThread può inserire messaggi nella coda con i metodi Invoke() (“chiedo di fare
questo mestiere e mi blocco finché non è stato fatto”) e BeginInvoke() (“chiedo di fare
questa roba e a limite passo un delegato che viene chiamato quando il mestiere è
fatto”).
Classi base in WPF
Derivano da Object. A parte VisualTreeHelper che aiuta
a gestire l’albero, tutte le altre classi derivano da DispatcherObject per conoscere l’identità del dispatcher
corrente.
La maggior parte, inoltre, sono anche DependencyObject: le proprietà degli oggetti sono
collegate tra loro in modo particolarmente efficiente. Posso ad esempio dire che il bottone X sia sempre 10 pixel più a destra del bottone Y. Un DependencyObject è anche un Visual, nel senso che hanno un’apparenza grafica in quanto sanno generare un insieme di
primitive.
La classe Visual si tripartisce in un certo numero di sottoclassi, specializzate nel creare
elementi visivi di qualche tipo.
La classe System.Threading.DispatcherObject
Vive nel namespace System.Threading perché è un oggetto di sincronizzazione. Dentro
di sé contiene il riferimento al dispatcher e garantisce che tutte le operazioni effettuate
dalle proprie istanze avvengano nel contesto del thread responsabile della gestione del
Dispatcher cui è associato
Dipendenze tra proprietà: classe System.Windows.DependencyObject
Ogni componente, come in C# base, può avere delle proprietà. Queste proprietà possono anche essere vincolate alle proprietà di altri oggetti: una volta che due proprietà
sono collegate, il sistema garantisce il forwarding dei valori attraverso l’albero logico
della scena.
Il valore della proprietà non viene duplicato finché non cambia, anche se appartiene a
due oggetti diversi di una certa classe: si ha così una minore occupazione di memoria.
Devo allocare memoria solo se la proprietà è “indipendente” e diversa da quella di default.
Proprietà aggiunte
<Canvas>
<Button Canvas.Top="20" Canvas.Left="20" Content="Ok"/>
</Canvas>
Quando costruiamo l’albero grafico del WPF bisogna fare attenzione al fatto che nel singolo tag ci sono attributi propri del tag (il bottone contiene la scritta Ok) e attributi di un
oggetto contenitore (il bottone ha una certa posizione nel Canvas), che riconosciamo
perché hanno il punto in mezzo.
Visualizzazione di oggetti: classe System.Windows.Media.Visual
Un oggetto graficamente rappresentabile deve essere istanza di una sottoclasse di System.Windows.Media.Visual, che è la base per implementare nuovi controlli. Introduce
un meccanismo di “caching” delle istruzioni di ridisegno per massimizzare le prestazioni.
Offre supporto per output su display, trasformazioni (es.: scritte ruotate), clipping (tagli),
hit test (ho fatto un click col mouse, sono dentro l’oggetto Visual?) e calcolo dei margini. Non offre nessun supporto interattivo: sebbene sappia rispondere alla domanda
dell’hit test, non ha meccanismi per reagire. Inoltre non gestisce stili, layout e data binding.
I Visual sono combinabili tra loro in una gerarchia di elementi grafici, in cui è bene distinguere i VisualTree dai LogicalTree. Quando scriviamo uno XAML produciamo un LogicalTree, ma alla scheda grafica comunichiamo un albero molto più ciccione, ovvero il
VisualTree. In certe situazioni dobbiamo sapere com’è fatto il VisualTree, al fine di applicare effetti speciali.
La classe Object.VisualTreeHelper
Utilizzata per passare dal LogicalTree al VisualTree, camminarci e scoprire una serie di
elementi al suo interno per poterli manipolare.
La classe DrawingContext
Ogni Visual ha un DrawingContext, che permette di popolare un Visual con un contenuto grafico vero e proprio, offrendo dei metodi che in apparenza disegnano qualcosa, ma
in realtà memorizza un insieme di informazioni sul disegno utilizzate in seguito.
Non viene istanziato direttamente, ma acquisito.
*** Inizio appunti dell’AA 2013-2014 ***
/* Crea un oggetto DrawingVisual che conterrà un rettangolo */
private DrawingVisual CreateDrawingVisualRectangle(Point p, Size s) {
DrawingVisual drawingVisual = new DrawingVisual();
/* Si richiede il DrawingContext associato */
DrawingContext drawingContext = drawingVisual.RenderOpen();
/* Crea un rettangolo e lo inserisce nel DrawingContext. */
Rect rect = new Rect(p, s);
drawingContext.DrawRectangle(Brushes.LightBlue, (Pen)null, rect);
/* Affida il contenuto del DrawingContext al sotto-sistema grafico */
drawingContext.Close();
return drawingVisual;
}
La classe FrameworkElement
Aggiunge dei vincoli affinché un controllo sia un “bravo cittadino” nel mondo di WPF.
All’interno si trovano strumenti per la gestione delle animazioni, per il data binding e per
gli stili.
Layout
Una delle difficoltà iniziali nella creazione di interfacce con WPF è data dal comprendere
come sono posizionati gli elementi. Con Windows Forms il posizionamento è assoluto e
si vive bene, mentre qui c’è lo sforzo di non rendere le dimensioni assolute in modo da
costruire interfacce fluide.
Ci sono dunque tanti elementi del framework per gestire lo spazio. Il framework infatti
presuppone che ciascun componente operi con lo spazio circostante per capire dove andarsi a collocare: in una prima fase di measure ciascun elemento del LogicalTree viene
interrogato per sapere quanto vorrebbe essere grande, mentre in una seconda fase di
arrange il contenitore ricalcola la posizione e la dimensione degli elementi contenuti secondo i propri criteri.
Con questo meccanismo di misurazione posso ad esempio mettere immagini nei bottoni.
Input ed eventi
L’input si origina sempre nel kernel del sistema operativo, dove il driver responsabile di
una certa periferica intercetta un evento essenziale, propagato alla coda principale del
WindowManager posseduta dal sottosistema User32 e inviato al thread che ha creato
l’interfaccia grafica.
Routing degli eventi
Supponiamo di aver cliccato alle coordinate (100, 100). In Windows Form, se alle coordinate (100, 100) c’è il rettangolo di un bottone, questo si sarebbe preso l’evento del
click.
WPF invece fa una cosa più sofisticata: il bottone fa parte di uno StackPanel che fa parte
di una Window. Comincio a dire alla Window che alle coordinate (100, 100) è avvenuto un
click: se questa è interessata a stopparlo bene, altrimenti si prosegue a catena fino al
bottone (e se ha un listener fa delle cose). Se l’evento è arrivato fino al bottone, risale
nuovamente la gerarchia fino alla Window.
Eventi preview
Si parte dall’elemento root scendendo fino all’elemento target. Quest’operazione è detta
tunnel e consente a tutti gli elementi intermedi dell’albero di filtrare o reagire all’evento.
Eventi effettivi
Se – come accade il più delle volte – il preview non fa niente, l’evento arriva sull’elemento che lo ha generato e da questo viene inoltrato fino all’elemento root. L’operazione avviene dopo la ricezione di un evento preview ed è detta bubble.
Templating
Nella classe System.Windows.Controls.Control (che contiene tutto ciò che ha una presentazione grafica ed è interattivo) troviamo la definizione di alcuni template di stili, che
permettono la resa grafica in maniera facile. E’ possibile cambiare completamente
l’aspetto e il comportamento dei nostri controlli.
Tra i controlli ci sono alcuni contenitori, che sono dei blocchi logici che usano il proprio
spazio per ripartirlo tra i figli (tipo lo StackPanel o la Canvas).
***