WPF: fare animazioni con suoni incorporati (ed usando un thread)
Windows Presentation Foundation soffre a mio avviso di una grave lacuna: nello XAML non è possibile creare animazioni (e relative storyboard) in grado di mandare in play files .wav o .mp3 in determinati momenti. Se questo assunto è sbagliato fermate la lettura di questo post, scorrete fino in fondo e lasciatemi un commento.
Introduzione
Supponiamo di avere una finestra (Window) grande 640×480. Supponiamo di avere una palla colorata in alto a sinistra e supponiamo di creare un’animazione che faccia spostare questa palla verso il basso, fino a quando essa non “rimbalza” contro il bordo inferiore della finestra tornando alla sua posizione originaria…e così via…all’infinito. Possiamo mandare in play il file boeing.wav nel momento in cui la nostra palla rimbalza? Se vogliamo fare in modo dichiarativo nello XAML, la risposta è no. Se invece siamo disposti a scrivere codice, la risposta è sì. Siccome siamo sviluppatori, prendiamo questa seconda strada e vediamo come fare.
In questo momento non ho un progetto demo pronto per essere uploadato se volete dargli un’occhiata. Se a qualcuno di voi dovesse servire, fatemelo sapere con un commento e provvederò.
Andiamo al dunque (un po’ troppo velocemente)
Spiegare da zero in solo post come usare Blend per creare una Window, come posizionare i Panel che si servono, come posizionare i controlli e come creare un’animazione è davvero troppo. Perciò supponiamo di averlo fatto.
La prima cosa da capire è che se vogliamo in qualche modo gestire una storyboard da codice, non possiamo farla partire tramite un trigger di sistema. Ad esempio, quando creiamo un’animazione con Blend, questi ci mette automaticamente un trigger sull’evento Window.Loaded per far partire la storyboard che abbiamo disegnato e che fa spostare la palla (sembra più un CD!) verso il basso. Cancelliamo dallo XAML questo trigger e mettiamo un pulsante che fa partire manualmente la storyboard. Il codice C# è molto semplice:
public Window1() { this.InitializeComponent(); // Insert code required on object creation below this point. story = (Storyboard)this.FindResource("FallCd"); }
Nel costruttore della form otteniamo l’istanza della Storyboard chiamata “FallCd”. L’oggetto story che si vede qui sopra è dichiarato privato della Window. L’evento Click del Button è questo:
private void StartAnimation_Click(object sender, RoutedEventArgs e) { story.Begin(this.cd, true); }
A questo punto possiamo fare una cosa molto semplice. Possiamo sottoscriverci nello XAML all’evento CurrentTimeInvalited dello Storyboard:
<Storyboard x:Key="FallCd" CurrentTimeInvalidated="Storyboard_CurrentTimeInvalidated"> ... ... </Storyboard>
In questo modo siamo notificati ad ogni step di avanzamento del Clock che regola l’animazione stessa. Ad esempio…supponiamo di visualizzare nel Title della Window il tempo che passa:
private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e) { TimeSpan span = story.GetCurrentTime(this.cd).GetValueOrDefault(); this.Title = span.ToString(); }
Ottengo il TimeSpan dalla storyboard e semplicemente la visualizzo. Da qui a fare il play di un file .mp3 al momento opportuno passa davvero poco:
private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e) { if (span.Seconds == 1) { player.Play(); } }
player è un’istanza della classe SoundPlayer già istanziata e caricata con un file .wav nel costruttore della form. Il codice qui sopra manda il play il file non appena siamo ad un secondo dall’inizio dell’animazione? Sì, ma c’è un grandissimo problema. Le animazioni in WPF sono regolate con un clock molto preciso. Un secondo è diviso in 1.000 millisecondi, per cui quel metodo Play() viene chiamato in realtà un’infinità di volte. La cosa è semplicemente risolvibile con una variabile booleana, in modo tale che entriamo nell’if soltanto una volta. Quindi:
private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e) { if (span.Seconds == 1 && !playing) { playing = true; player.Play(); } }
La variabile playing è dichiarata come privata dalla Window e all’inizio vale false.
L’ultimo dubbio è: ma è proprio vero che viene usato un thread secondario???
La classe SoundPlayer espone due metodi che fanno il play del file audio precedentemente caricato. Il metodo Play() ed il metodo PlaySync(). Che differenza c’è fra i due? Ve lo riporto direttamente da come lo dice MSDN.
Metodo Play
Plays the .wav file using a new thread, and loads the .wav file first if it has not been loaded.
Metodo PlaySync
Plays the .wav file using the user interface (UI) thread, and loads the .wav file first if it has not been loaded.
Noi abbiamo usato Play, per cui mi aspettavo di non avere problemi sull’animazione, cosa che invece non avviene. Quando siamo ad un secondo dall’inizio dell’animazione, il SoundPlayer parte, ma l’animazione subisce un rallentamento, uno scatto. E’ proprio vero che viene istanziato un thread per fare quel play? Supponiamo di voler creare noi il nostro thread da lanciare quando è il momento opportuno.
Creiamo un metodo privato chiamato sparo() che non fa altro che fare il play del file:
void sparo() { player.Play(); }
Dichiariamo un’istanza di Thread come privato della Window e definiamolo nel costruttore:
soundThread = new Thread(new ThreadStart(sparo)); soundThread.Priority = ThreadPriority.Highest; soundThread.IsBackground = true;
A questo punto è sufficiente chiamare soundThread.Start() per mandare in play il file:
private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e) { if (span.Seconds == 1 && !playing) { playing = true; soundThread.Start(); } }
Il Play del SoundPlayer viene invocato con il nostro thread, pronto per l’esecuzione e questa volta la nostra animazione rimane fluida e veloce anche quando si sente lo sparo! Evviva!
Sincronizzare un evento con il play del file
Chiudo questo post domenicale mattutino con una piccola osservazione. Nel mio test ho usato semplicemente il tempo – fare il play in un determinato momento temporale – ma nulla vieta di inserire nell’evento CurrentTimeInvalited tutti i controlli del caso, quindi…magari accorgersi quando la palla si sta allontanando dal bordo invece che avvicinarsi e così via.
Buona domenica a tutti!!!