Un gioco animato con Visual Basic e Windows Presentation Foundation
a cura di Alessandro Del Sole (requisiti: conoscenze di base di WPF)Introduzione
Una delle caratteristiche più interessanti e importanti di Windows Presentation Foundation è quella delle animazioni. Infatti, in WPF molti oggetti dell'interfaccia possono essere animati sfruttando le potenzialità del rendering delle librerie Microsoft DirectX ®, di cui WPF costituisce una sorta di wrapper, al fine di ottenere effetti grafici davvero entusiasmanti. Quello che è importante sottolineare, a questo proposito, è che anche i controlli utente possono essere animati e non solo figure geometriche o disegni.
In questo articolo ci occuperemo di creare un gioco in cui ci sia una fotografia ricoperta di pulsanti il cui contenuto sia offuscato a intermittenza; per scoprire le parti della fotografia e vincere il gioco sarà necessario cliccare alternativamente su pulsanti che contengano lo stesso testo.Nota: il codice proposto in questo articolo è stato ripreso dalla corrispondente versione in C# prodotta dal team di Windows Presentation Foundation SDK e viene riproposta in lingua italiana e in codice Visual Basic 2008, da parte dell'autore di questo articolo, previa autorizzazione del team menzionato.
Il progetto originale per Visual C# si trova a questo indirizzo del Blog di WPF SDK, mentre il codice sorgente in Visual Basic 2008, che accompagna il presente articolo, è disponibile nell'area download di Visual Basic Tips & Tricks.
Per raggiungere gli obiettivi proposti nell'articolo è necessario avere a disposizione almeno Visual Basic 2008 Express Edition. Questo è infatti l'ambiente di lavoro che ho utilizzato per questo articolo, a dimostrazione del fatto che, per fruire della tecnologia WPF, non è necessario avere a disposizione strumenti di livello avanzato.
Predisposizione del progetto
Aprite Visual Studio 2008 e, tramite il comando Nuovo|Progetto del menu File, create un nuovo progetto vuoto basato su Windows Presentation Foundation in Visual Basic. Chiamate il progetto MatchEm (come il nome originario del gioco) e fate clic su OK, accertandovi che tutto sia come in figura:![]()
Dopo alcuni secondi, Visual Studio mostra il designer WPF e l'editor di codice XAML, che probabilmente già conoscete. Se non avete mai utilizzato questo designer, vi rimando al mio primo articolo introduttivo a WPF.
A questo punto, è necessario aggiungere al progetto un'immagine che costituisca la base del gioco. Nel progetto sorgente a corredo dell'articolo troverete una foto scattata da me, pertanto libera da vincoli di copyright e utilizzabile per la dimostrazione. In ogni caso, per aggiungere un'immagine al progetto è possibile utilizzare il comando Add existing item del menu Project e selezionare l'immagine desiderata.
Ora passeremo a costruire la nostra applicazione. Prima di proseguire nella lettura, vediamo il risultato che dovremo ottenere, al fine di avere un'idea più chiara di cosa andremo a realizzare:![]()
L'offuscamento dei pulsanti e del testo contenuto sarà intermittente; otterremo questo effetto tramite le animazioni. Ci occuperemo, in primo luogo, del lato design, sfruttando il codice XAML. Successivamente, vedremo come rendere operativa l'interfaccia scrivendo codice Visual Basic.
Ossatura dell'interfaccia
Il primo passaggio da eseguire, tramite codice XAML, riguarda la modifica degli oggetti Window e Grid; quest'ultimo è il contenitore di controlli che Visual Studio aggiunge per default alla finestra. Per quanto concerne l'oggetto Window, dobbiamo semplicemente modificarne le dimensioni, al fine di renderla un po' più grande onde consentire una migliore visualizzazione dell'immagine. Ecco il codice della Window, in cui modifichiamo semplicemente le proprietà Width e Height:<Window x:Class="Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="550" Width="550"> </Window>Modifichiamo poi il contenitore Grid. Facendo riferimento alla figura sopra esposta, quella che rappresenta il risultato finale dell'applicazione, risulta subito chiaro che la Grid dovrà contenere un'immagine (controllo Image) e dovrà essere suddivisa in nove celle, quindi tre righe e tre colonne. Ciò posto, riscriviamo il codice del contenitore Grid, proposto per default, nel modo seguente:
<!-- Crea una griglia con tre righe e tre colonne. --> <Grid x:Name="myGrid" VerticalAlignment="Top" HorizontalAlignment="Left" ShowGridLines="True" Width="500" Height="500"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <!-- Crea un'immagine che riempie la griglia. --> <Image Grid.ColumnSpan="3" Grid.RowSpan="3" Source="DSCF0267.jpg"/> </Grid>Le note di rilievo in questo codice sono le seguenti:
- La proprietà ShowGridLines della Grid, impostata a True, permette di visualizzare le righe della griglia;
- L'insieme Grid.ColumnDefinitions permette di definire tante colonne nella griglia quanti sono i suoi elementi ColumnDefinition, quindi, in questo caso, tre colonne la cui larghezza e la cui altezza è determinata automaticamente dal designer; analogo discorso vale per l'insieme Grid.RowDefinitions e i suoi elementi RowDefinition;
- Il controllo Image sfrutta le proprietà Grid.ColumnSpan e Grid.RowSpan che permettono di delimitare la grandezza dell'immagine all'interno delle dimensioni della griglia. A tal proposito, si utilizza il numero massimo di righe e colonne disponibili.
Dopo aver costituito l'ossatura di base, passiamo ad implementare i vari pulsanti che costituiranno il gioco e che verranno definiti, tramite XAML, sfruttando alcuni interessanti effetti di animazione.
Definizione dei pulsanti e delle animazioni
Il gioco richiede l'implementazione di nove pulsanti. Uno che determinerà la fine anticipata del gioco, qualora l'utente lo prema per sbaglio. Gli altri otto devono essere suddivisi a coppie di due: in sostanza, due pulsanti conterranno il testo Sniff, due conterranno il testo Snuff, due conterranno il testo Pickle e due conterranno il testo Poodle. Affinché l'utente sia in grado di scoprire l'immagine, dovrà fare clic sulle coppie di pulsanti corrette, per esempio Snuff - Snuff. Ci occuperemo ora di definire il primo pulsante, tenendo a mente che il codice di tutti i pulsanti che definiremo deve essere scritto di seguito all'oggetto Image e, comunque, all'interno del contenitore Grid.Iniziamo col definire il primo pulsante, al quale, in primo luogo, assegnamo l'effetto sfocato utilizzando la proprietà BitmapEffect:
<!-- Crea pulsanti in ciascuno dei nove quadrati a coprire l'immagine.--> <Button Click="ButtonClicked" Grid.Column="0" Grid.Row="0">Sniff <!-- BlurBitmapEffect offusca il pulsante. --> <!-- Espone una proprietà Radius che viene animata al verificarsi dell'evento Loaded. --> <Button.BitmapEffect> <BlurBitmapEffect KernelType="Box" /> </Button.BitmapEffect>Innanzitutto abbiamo stabilito che, alla pressione del pulsante, venga richiamato il gestore di evento chiamato ButtonClicked e che implementeremo più avanti nell'articolo. Grid.Column e Grid.Row stabiliscono la cella della griglia in cui il pulsante deve essere posizionato. BlurBitmapEffect è un effetto di offuscamento che può essere applicato agli oggetti che derivano dalla classe FrameworkElement e la sua proprietà KernelType permette di specificare il tipo di offuscamento. Box e Gaussian sono i due possibili valori. Box determina un offuscamento piuttosto leggero, al contrario di Gaussian.
Ora passiamo a definire l'animazione del pulsante. È importante prestare attenzione a questo passaggio, poiché ci servirà nell'implementazione di tutti i successivi pulsanti. Se avete letto il capitolo 5 del mio libro su .NET Framework 3.x, saprete già come funzionano le animazioni in WPF; nel qual caso, i seguenti passaggi saranno solo un ripasso.
La prima considerazione da fare è che le animazioni vanno associate a degli eventi: al verificarsi di un particolare evento, viene avviata l'animazione. In XAML, gli eventi di un controllo sono rappresentati da elementi EventTrigger. L'animazione, invece, è costituita da un oggetto BeginStoryBoard, figlio dell'EventTrigger.
Prima di fare confusione, completate il codice relativo al pulsante nel modo seguente, per poi fare ulteriori osservazioni:<Button.Triggers> <!-- Quando ciascun pulsante viene caricato, avvia un'animazione. --> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard> <Storyboard> <!-- Anima la proprietà Radius di qualunque oggetto a cui è applicato un BitmapEffect. --> <DoubleAnimation Storyboard.TargetProperty="BitmapEffect.Radius" From="1.0" To="12.0" Duration="0:0:2" AutoReverse="true" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>Nell'EventTrigger si specifica l'evento (RoutedEvent) da gestire. L'evento Loaded della classe FrameworkElement si verifica al caricamento di tutti gli elementi dell'interfaccia che derivano da FrameworkElement. In questo caso, stiamo dicendo a Visual Basic che la nostra animazione deve essere applicata a tutti i pulsanti. L'elemento BeginStoryBoard contiene, a sua volta, un elemento Storyboard che definisce l'animazione vera e propria. In WPF sono disponibili diversi tipi di animazione, come, per esempio, DoubleAnimation e ColorAnimation. Noi stiamo utilizzando una DoubleAnimation, che ci permette di specificare quale proprietà del pulsante deve essere animata (Storyboard.TargetProperty). Il valore di questa proprietà è impostato come BitmapEffect.Radius. Ciò significa che l'animazione assegnerà al pulsante un effetto di tipo Radius e si rivolgerà a questo. From e To indicano il punto di partenza e il punto di arrivo dell'animazione, mentre Duration indica la durata. In questo caso stiamo dicendo che, nell'arco di due secondi, l'effetto BitmapEffect.Radius deve modificare la propria intensità con valori da 1 a 12 e da 12 a 1, quindi con effetto di ritorno (AutoReverse="true"). Infine, l'animazione deve essere perpetua (RepeatBehavior="Forever").
Una volta capito il meccanismo, possiamo passare a definire il secondo pulsante:<!-- Il seguente pulsante riprende l'idea del primo. Ma viene modificata l'animazione cosicché ciascun pulsante abbia un proprio effetto di offuscamento.. --> <Button Click="ButtonClicked" Grid.Column="0" Grid.Row="1">Sniff <Button.BitmapEffect> <BlurBitmapEffect KernelType="Box" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="BitmapEffect.Radius" From="1.0" To="8.0" Duration="0:0:2.5" AutoReverse="true" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>In questo secondo pulsante, abbiamo modificato l'evento gestito. Button.Loaded si verifica al caricamento del singolo pulsante. Questo per illustrare come sia possibile gestire eventi del singolo controllo o di controlli appartenenti alla stessa famiglia. Abbiamo poi modificato alcuni valori nell'animazione, ma il concetto è sempre lo stesso. Abbiamo così completato la prima coppia di pulsanti.
Ora definiamo la seconda, che riprende, salvo piccole modifiche, la logica della precedente coppia:<Button Click="ButtonClicked" Grid.Column="0" Grid.Row="2">Snuff <Button.BitmapEffect> <BlurBitmapEffect KernelType="Box" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="BitmapEffect.Radius" From="1.0" To="7.0" Duration="0:0:0.7" AutoReverse="true" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> <Button Click="ButtonClicked" Grid.Column="1" Grid.Row="0">Snuff <Button.BitmapEffect> <BlurBitmapEffect KernelType="Box" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="BitmapEffect.Radius" From="1.0" To="10.0" Duration="0:0:1.7" AutoReverse="true" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>Nella terza coppia di pulsanti, che definiamo di seguito, si fa utilizzo del tipo di blurring definito Gaussian e viene ampliato il raggio di valori dell'effetto:
<Button Click="ButtonClicked" Grid.Column="1" Grid.Row="1">Pickle <Button.BitmapEffect> <!-- Utilizziamo l'effetto Gaussian per cambiare un po'. --> <BlurBitmapEffect KernelType="Gaussian" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="BitmapEffect.Radius" From="1.0" To="12.0" Duration="0:0:2" AutoReverse="true" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> <Button Click="ButtonClicked" Grid.Column="1" Grid.Row="2">Pickle <Button.BitmapEffect> <BlurBitmapEffect KernelType="Box" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="BitmapEffect.Radius" From="1.0" To="14.0" Duration="0:0:1.1" AutoReverse="true" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>La quarta coppia di pulsanti, invece, torna a fare uso dell'offuscamento Box:
<Button Click="ButtonClicked" Grid.Column="2" Grid.Row="0">Poodle <Button.BitmapEffect> <BlurBitmapEffect KernelType="Box" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="BitmapEffect.Radius" From="1.0" To="11.0" Duration="0:0:0.5" AutoReverse="true" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> <Button Click="ButtonClicked" Grid.Column="2" Grid.Row="1">Poodle <Button.BitmapEffect> <BlurBitmapEffect KernelType="Box" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="BitmapEffect.Radius" From="1.0" To="11.0" Duration="0:0:0.5" AutoReverse="true" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>Il nono e ultimo pulsante viene definito in maniera analoga ai precedenti, con l'aggiunta della richiesta di gestione dell'evento Loaded, al verificarsi del quale verrà scambiato il testo dei pulsanti in maniera casuale, al fine di dare vita al gioco:
<Button Click="ButtonClicked" Loaded="Shuffle" Grid.Column="2" Grid.Row="2">WHAMMY <Button.BitmapEffect> <BlurBitmapEffect KernelType="Box" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="BitmapEffect.Radius" From="1.0" To="10.0" Duration="0:0:2" AutoReverse="true" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>Dopo la dichiarazione dell'ultimo pulsante, se tutto è andato a buon fine, il designer della finestra si presenta come in figura, con tanto di offuscamento dei pulsanti:
![]()
Avendo applicato l'effetto BlurBitmapEffect a livello di design, i pulsanti appaiono offuscati. Durante l'esecuzione dell'applicazione, l'effetto diminuirà e aumenterà ad intermittenza. È a questo proposito che subentra la scrittura di codice managed in Visual Basic 2008.
Scrittura del codice Visual Basic
Utilizzando la finestra Solution explorer, fate doppio clic sul file di codice chiamato Window1.xaml.vb associato al file Window1.xaml. Quando l'editor è attivo, ci occupiamo di definire in primo luogo, a livello di classe, due campi: uno per tenere traccia di quale pulsante è stato premuto, uno per tenere traccia di quanti pulsanti rimangono da eliminare:' Tiene traccia dell'ultimo pulsante premuto, definito 'pulsante attivo'. Dim m_activeButton As Object = Nothing ' Tiene traccia di quanti pulsanti sono ancora visibili. Se = 0, il giocatore ha vinto. Dim remain As Integer = 8Il primo evento che si verifica, con specifico riferimento al gioco, è quello che riguarda il caricamento dell'ultimo pulsante che abbiamo implementato in precedenza e che ci permetterà di riassegnare il testo ai pulsanti in modo casuale. L'evento Loaded del pulsante, quindi, viene gestito dal gestore Shuffle così come segue:
Private Sub Shuffle(ByVal sender As Object, ByVal e As RoutedEventArgs) Dim r As New Random Dim kids As UIElementCollection = myGrid.Children Dim i As Integer = 0 While i < 20 Dim bSwapA As Button = DirectCast(kids(r.Next(1, 10)), Button) Dim bSwapB As Button = DirectCast(kids(r.Next(1, 10)), Button) Dim sTemp As String = bSwapA.Content.ToString bSwapA.Content = bSwapB.Content bSwapB.Content = sTemp i += 1 End While End SubViene ottenuto l'elenco dei controlli contenuti nella Grid mediante la proprietà Children dell'istanza myGrid. Questo elenco è di tipo UIElementCollection, una speciale collezione di oggetti per l'interfaccia. Il blocco While..End While si occupa di ottenere in sequenza coppie di pulsanti e di invertirne il testo, sfruttando l'istanza r dell'oggetto Random che, come noto, genera numeri casuali.
Il secondo e ultimo metodo da implementare è il gestore dell'evento Click di tutti i pulsanti. Iniziamo col predisporre questo metodo, partendo col primo blocco di codice:
Private Sub ButtonClicked(ByVal sender As Object, ByVal e As RoutedEventArgs) Dim b2 As Button = DirectCast(sender, Button) ' è il pulsante 'Whammy'? If b2.Content.ToString() = "WHAMMY" Then MessageBox.Show("WHAMMY! YOU LOSE! LOSER!!!") ' Fine gioco. Me.Close() End IfIn primo luogo viene ottenuta l'istanza del pulsante cliccato, mediante la tipizzazione dell'oggetto sender come pulsante, necessaria per verificare quale tasto è stato premuto. Se il testo del pulsante è WHAMMY, il gioco viene terminato con un simpatico messaggio che avvisa l'utente della sconfitta.
Proseguiamo con un altro frammento:' se nessun pulsante è attivo, recupera quello chiamante. If m_activeButton Is Nothing Then m_activeButton = sender Exit Sub End If 'Il pulsante attivo è il chiamante ma non può essere cliccato due volte If m_activeButton Is sender Then Exit Sub End IfIl primo blocco If verifica che il pulsante attivo non sia nullo. In questo caso, riceve il riferimento all'oggetto sender chiamante. Il successivo blocco If verifica la corrispondenza tra pulsante attivo e oggetto sender chiamante. Se c'è questa corrispondenza, significa che lo stesso pulsante è stato premuto due volte, quindi esce. Se invece non è stato premuto due volte lo stesso pulsante, si prosegue convertendo l'oggetto corrispondente al pulsante attivo in un nuovo pulsante:
'Cliccati 2 pulsanti diversi: c'è corrispondenza? Dim b1 As Button = DirectCast(m_activeButton, Button)Fatto questo, si verifica l'uguaglianza del contenuto dei pulsanti cliccati e si verifica quanti pulsanti siano ancora visibili. Quando non ci sono più pulsanti, vuol dire che l'utente ha completato il gioco e viene mostrato un messaggio. Per praticità, ho inserito dei commenti nel codice che completa il metodo e che vi aiuteranno nella comprensione di ciascun frammento di codice:
If b1.Content.ToString = b2.Content.ToString Then ' Corrispondenza tra I pulsanti, quindi li nascondiamo. b1.Visibility = Visibility.Collapsed b2.Visibility = Visibility.Collapsed ' sottrae i due pulsanti dal conto totale. remain -= 2 ' se non ci sono più pulsanti, il gioco finisce. If 0 = remain Then ' Elimina il pulsante Whammy nascondendo tutti gli altri. Dim kids As UIElementCollection = myGrid.Children Dim i As Integer = 1 While i < 10 'assume che gli elementi da 1 a 9 siano pulsanti Dim b As Button = DirectCast(kids(i), Button) b.Visibility = Visibility.Collapsed i += 1 End While ' congratulazioni! MessageBox.Show("You win!") Me.Close() End If End If 'Elimina la cache m_activeButton = Nothing End SubOra siete pronti per giocare! Premete F5 per avviare l'applicazione e cercate di fare clic sulle coppie di pulsanti con lo stesso testo per scoprire pian piano l'immagine. A gioco completato, il risultato è il seguente:
![]()
Conclusioni
Le animazioni sono una caratteristica molto potente di Windows Presentation Foundation e possono essere utilizzate per realizzare effetti grafici davvero sorprendenti, essendo applicabili alla maggior parte degli elementi dell'interfaccia grafica.
In questo articolo ne abbiamo visto un piccolo esempio applicato ai controlli utente, ma la documentazione di Microsoft .NET Framework riporta esempi anche più complessi.
Come di consueto potete contattarmi al mio indirizzo visitare il mio blog nel quale potrete trovare numerosi esempi e articoli dedicati a Windows Presentation Foundation.