Gianni Giaccaglini

Tricks & mini applics on WPF
posts - 46, comments - 0, trackbacks - 0

Torri di Hanoi implementato con pulsanti in una Grid

 

La figura precedente mostra questo una fase iniziale del ben noto giochino da me realizzato in WPF sfruttandone la Grid. Le operazioni da compiere procedono per coppie di clic, il primo su un anello origine, il secondo su quello destinazione. Fino a ottenere la situazione seguente:

in cui compare un pulsante dall'etichetta eloquente. Cliccandolo, tutti gli anelli sono impilati sulla prima colonna (della Grid).

La Grid di una finestra Wpf si può prestare a operazioni dinamiche relative agli oggetti contenuti nelle sue celle. L’ho  già fatto con due giochino: il Gioco del 15, descritto nel mio articolo introduttivo a WPF su WpfItalia.it  e in una simulazione di Finali Campionato descritta in questo blog. L’idea è quella di surrogare in qualche modo una DataGridView se non un foglio di lavoro. Pretesa, però, alquanto impropria e fonte di equivoci, soprattutto perché la Grid che il convento Wpf passa NON si compone di celle come oggetti indirizzabili: sono gli oggetti a godere di proprietà Grid.RowProperty e Grid.ColumnProperty, per cui individuarne uno in base alle coordinate è problematico e oltretutto una cella del genere ne può contenere più d’uno. Si deve insistere nel dire che individuare o “selezionare” una cella vuota è privo di senso e nel far presente che una Grid non possiede eventi propri: gli eventi definiti nella tag <Grid ... > (es. <Grid Click=”MiaRoutine” ...> accomunano quelli dei figli della Grid.

Comunque ci si arrangia, evitando l’appesantimento di add-in più flessibili. Questo terzo esempio didattico è stato realizzato con qualche pena ma ritengo sia non solo curioso ma costituisca utili ammaestramenti e salutari riflessioni  magari con possibili varianti rispetto alle soluzioni qui adottate.

Il gioco delle Torri di Hanoi

Per comodità questo modellino è zippato assieme a quello del Gioco del 15. Tale file compresso si può scaricare da

http://www.giannigiaccaglini.it/download/HanoiGioco15.zip

 

La sezione XAML

Tutti i controlli in gioco sono dei Button, inclusi quelli di colore verde della riga in basso, denominati Base1, Base2 e Base3. Questi non sono anelli del gioco ma si sono resi indispensabili affinché l’evento Click per quanto detto sopra non si scatena su una cella vuota. Va poi sottolineata una scelta, opportuna, per cui i controlli di base sono tutti e tre scritti nelle righe XAML terminali. In tal modo quelli degli anelli “veri e propri”, i cui nomi dall’alto verso il basso vanno da Anello1 ad Anello2 fino ad Anello5, sono a loro volta disposti in modo da avere indici da 0 a 1 fino a 4.

Ma ecco tutto il codice XAML, all’inizio del quale va evidenziata la direttiva ButtonBase.Click =”Hanoi” che associa tutti i pulsanti contenuti nella nostra Grid dal prosaico nome “Griglia” a una routine comunitaria di nome “Hanoi”.

<Window x:Class="MainWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

  Title="Torri di Hanoi" Height="425" Width="525">

  <Grid Name="Griglia" ButtonBase.Click ="Hanoi"  Height="409" Width="512">

    <Grid.ColumnDefinitions>

      <ColumnDefinition />

      <ColumnDefinition />

      <ColumnDefinition />

    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>

      <RowDefinition />

        <RowDefinition />

        <RowDefinition />

        <RowDefinition />

        <RowDefinition />

        <RowDefinition />

    </Grid.RowDefinitions>

    <Button Height="68" HorizontalAlignment="Left" Margin="57,1,0,0" Name="Anello1" VerticalAlignment="Top" Width="51" Grid.RowSpan="2" Background="#FFE20E0E" />

    <Button Grid.Row="1" Height="68" HorizontalAlignment="Left" Margin="44,0,0,0" Name="Anello2" VerticalAlignment="Top" Width="77" BorderBrush="Black" Background="#FF0C1DE8" />

    <Button Grid.Row="2" Height="69" HorizontalAlignment="Left" Margin="29,0,0,0" Name="Anello3" VerticalAlignment="Top" Width="107" Background="#FFD6E517" />

    <Button Grid.Row="3" Height="69" HorizontalAlignment="Left" Margin="12,0,0,0" Name="Anello4" VerticalAlignment="Top" Width="141" Grid.RowSpan="2" Background="#FF585F0A" />

    <Button Grid.Row="4" Height="68" HorizontalAlignment="Left" Name="Anello5" VerticalAlignment="Top" Width="171" Background="#FF830C0C" />

    <Button Grid.Column="0" Grid.Row="5" Height="54" HorizontalAlignment="Left" Name="Base1" VerticalAlignment="Top" Width="171" Background="#FF1EF509" />

    <Button Grid.Column="1" Grid.Row="5" Height="54" HorizontalAlignment="Left" Name="Base2" VerticalAlignment="Top" Width="171" Background="#FF31F210" />

    <Button Grid.Column="2" Grid.Row="5" Height="54" HorizontalAlignment="Left" Name="Base3" VerticalAlignment="Top" Width="171" Background="#FF31F210" />

    <Button Grid.Column="2" Grid.Row="5" Name="PulsRipeti" Click="Ripeti" Visibility="Hidden" Margin="0,0,0,14.167" >

      Nuovo Gioco

    </Button>

  </Grid>

</Window>

 

Si perdoni la rozzezza degli aspetti estetici (che NON mi stavano a cuore per nulla, semmai può essere valida una variante basata su stili). A quanto anticipato aggiungo solo l’invito a notare come l’ultimo Button denominato PulsRipeti si trova sopra Base3 ma essendo inizialmente occultato con Visibility=”Hidden” nel corso del gioco non è attivabile dall’utente che vede e può cliccare solo su Base3. Scatenando la routine “Hanoi”. Ma quando al termine appare il button resuscitato il clic su di esso attiva invece la “PulsRipeti” in quanto la routine definita a livello Button predomina su quella comunitaria.

Il codice Visual Basic

Prima di riportare il listato, che, come confido, la brava gente che frequenta questo sito saprà comprendere anche da sola, ritengo importanti due osservazioni:

·         L’indice di un pulsante nella famiglia dei “figli” della Grid – Griglia.Children nel nostro caso – resta immutato e si può dire che tale indice costituisce l’identità stessa del controllo, di cui nel corso del programma vengono modificate riga e colonna;

·         La regola del gioco di fatto non considera (come si potrebbe pensare in un primo tempo) la larghezza  dei vari anelli ma proprio l’indice di ciascuno, che grazie alla (oculata) digitazione del codice XAML, procede in senso decrescente dall’alto verso il basso.

Ma ecco tutto il codice.

Class MainWindow

    Dim Pila1() As Integer = {0, 1, 2, 3, 4, 9}

    Dim Pila2() As Integer = {9, 9, 9, 9, 9, 9}

    Dim Pila3() As Integer = {9, 9, 9, 9, 9, 9}

    Dim RigheTop() As Integer = {0, 5, 5}

    Dim PulsCliccato As Button

    Dim IndPulsCliccato As Integer = 0

    Dim ColOrigine As Integer = 0

    Dim PilaOrig As Array = Nothing

    Private Sub Hanoi(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)

        Dim Sorgente = e.Source

        ' Assumi la colonna dell’origine

        Dim Col As Integer = e.Source.GetValue(Grid.ColumnProperty)

        Dim Vett() = {Pila1, Pila2, Pila3}

        Dim IndAnello = Vett(Col)(0)

        Dim RigaTop = RigheTop(Col)

        If PulsCliccato Is Nothing Then ' Il controllo funziona come Switch!

            If RigaTop = 5 Then Exit Sub

            PulsCliccato = Griglia.Children(IndAnello)

            IndPulsCliccato = IndAnello

            ColOrigine = Col

            PilaOrig = Vett(Col)

        Else

            If Col = ColOrigine Then Exit Sub ' Esci se il clic è su colonna origine

            Dim Msg = "L'anello sovrapposto dev'essere più piccolo!"

            If Not IndPulsCliccato < Vett(Col)(0) Then

                MessageBox.Show(Msg, "Ricorda...", MessageBoxButton.OK, MessageBoxImage.Warning)

                PulsCliccato = Nothing ' Come non detto...

                Exit Sub

            End If

            PulsCliccato.SetValue(Grid.ColumnProperty, Col)

            PulsCliccato.SetValue(Grid.RowProperty, RigaTop - 1)

            PulsCliccato = Nothing

            ' Pop sulla PilaOrig

            PopPila(PilaOrig)

            ' ReDim Preserve PilaOrig(PilaOrig.Length - 2) ' Rigettata!

            ' Aggiorna RigheTop(ColOrigine)

            RigheTop(ColOrigine) += 1

            ' Push su Vett(Col)

            PushPila(Vett(Col))

            ' Aggiorna RigheTop(Col)

            RigheTop(Col) -= 1

            ' Verifica risultato finale

            Msg = "Gioco concluso con successo"

' Le righe seguenti, ridondanti e più chiare, sono poi state

' sostituite con la successiva If RigheTop(2) Then  ecc.

            ' Dim strPila3 = Pila3(0).ToString & Pila3(1).ToString & Pila3(2).ToString _

            '  & Pila3(3).ToString & Pila3(4).ToString

            'If strPila3 = "01234" Then ' (Pila3 completa)

            'MessageBox.Show(Msg, "", MessageBoxButton.OK, MessageBoxImage.Exclamation)

            If RigheTop(2) = 0 Then ' (Pila3 completa)

                MessageBox.Show(Msg, "", MessageBoxButton.OK, MessageBoxImage.Exclamation)

                PulsRipeti.Visibility = Windows.Visibility.Visible

            End If

        End If

    End Sub

    Private Sub PopPila(ByRef Pila As Array)

        Dim IndMax = Pila.Length - 1

        ' Dim MinElem = Pila(0)

        For i = 0 To IndMax - 1

            Pila(i) = Pila(i + 1)

        Next

        Pila(IndMax) = 9

        ' ReDim Preserve Pila(3) 'Rigettata!

    End Sub

    Private Sub PushPila(ByRef Pila As Array)

        Dim IndMax = Pila.Length - 1

        ' Dim MaxElem = Pila(IndMax)

        For i = IndMax To 1 Step -1

            Pila(i) = Pila(i - 1)

        Next

        Pila(0) = IndPulsCliccato

    End Sub

    Private Sub Ripeti()

        For i = 0 To 4

            With Griglia.Children(i)

                .SetValue(Grid.ColumnProperty, 0)

                .SetValue(Grid.RowProperty, i)

            End With

        Next

        Dim P1() As Integer = {0, 1, 2, 3, 4, 9}

        Pila1 = P1

        Dim P2() As Integer = {9, 9, 9, 9, 9, 9}

        Pila2 = P2

        Dim P3() As Integer = {9, 9, 9, 9, 9, 9}

        Pila3 = P3

        Dim RT() As Integer = {0, 5, 5}

        RigheTop = RT

        PulsRipeti.Visibility = Windows.Visibility.Hidden

    End Sub

End Class

 

Commenti essenziali. Li limito ai punti più particolari, che entrambi corrispondono a mie personali scoperte ma che forse solo ai più esperti vengono in mente.

·         Le variabili “generali”, ossia definite nelle Dichiarazioni, Pila1, Pila2 e Pila3 sono vettori che “fotografano” gli indici delle tre pile di anelli, inizializzate coi valori 0, 1, 2, 3, 4, 9 la prima e con tutti 9 le altre, con 9 che funge da “tappo”. Sono soggette a operazioni di pop e push ottenute coi metodi PopPila e rispettivamente PushPila atte a farne uscire e rientrare opportunamente l’indice iniziale dell’insieme.  Il bello della storia è l’utilizzo di un vettore di vettori, con le istruzioni un po’ speciali Dim Vett() = {Pila1, Pila2, Pila3} e Dim IndAnello = Vett(Col)(0), la prima che fissa in Vett le tre pile (mano a mano modificate dal programma), la seconda che assai sinteticamente pone in IndAnello il primo termine della pila individuata da Vett(Col) ove Col è la colonna (da 0 a 2) di una pila. In tal modo ho evitato di ricorrere una struttura Select Case .

·         La variabile delle Dichiarazioni PulsCliccato, inizializzata a Nothing, sul primo clic registra il Button appunto cliccato dall’utente con l’istruzione cruciale
PulsCliccato = Griglia.Children(IndAnello)

Come indica il commento, PulsCliccato funge da switch con Is Nothing, per alternare i casi If ed Else e poteva essere sostituito con una variabile booleana, ma l’ho lasciato perché forse non tutti pensano che una variabile Button (o altro controllo) di fatto coincide con il pulsante via via registrato, in particolare le modifiche alle sue proprietà si applicano all’”originale” (diciamo così), ne nostro caso alle Grid.RowProperty e Grid.ColumnProperty modificando il posizionamento nella Grid.  Direi che è una specie di gemello o, probabilmente, un puntatore.

Discorso sulle possibili alternative

Anziché procedere con commenti dettagliati preferisco solo aggiungere due osservazioni. La prima è la constatazione, denunciata da commenti inseriti, che istruzioni di Redim Preserve relativi ad insiemi indirizzati indirettamente sono rifiutate. Qui non mi dilungo sul motivo di questa faccenda di cui comunque va preso atto. Come riflettendo si comprende, è da questo fatto che è derivato l’utilizzo di insiemi Pila1, Pila2 e Pila3 di lunghezza fissa (con i “tappi” 9 scorrevoli). Peccato, perché il vettore di vettori suggerito consente insiemi di dimensioni diversificate, ma ahimé sempre non ridimensionabili.

Ma non si poteva ricorrere al tipo Stack? La risposta è negativa, come indicano le righe di codice seguenti:

' Variabili a liv. Dichiarazioni NON accettano valori iniziali come

' Dim Pila1 = {0} o Pila1 As Stack = {0}

'  per la natura stessa degli Stack

 

' Uniche dichiarazioni lecite:

Dim Pila1 As Stack(Of Integer)

Dim Pila2 As Stack(Of Integer)

Dim Pila3 As Stack(Of Integer)

Private Sub Prova()

 

. . . .

' Istruzioni a livello routine

Sub MiaSub()

  Dim Vett = {Pila1, Pila2, Pila3} 'Dà errore su Vett

' Dim Vett As Array = {Pila1, Pila2, Pila3} ' Idem c.s.,

' idem con As New ArrayList

  Dim Vett() = {Pila1, Pila2, Pila3} ' E' accettato

  Vett(1).Push(4) ' Accettato dal compilatore, dà errore a run-time

  Dim x = Pila2.Pop ' Idem c.s.

End Sub

La seconda osservazione è relativa all’insieme RigheTop(). Inizializzato coi valori 0, 5 e 5, nel corso del programmino spazia opportunamente sulle righe “di testa” da affibbiare a ciascuna pila, una scelta che differisce da quella delle tre pile. Di qui l’altra domanda: non si poteva fare altrettanto con queste ultime?

Stavolta la risposta è affermativa, in quanto se si riflette non conta tanto l’intera situazione delle pile bensì solo l’indice di testa. Si tratterebbe di un analogo insieme diciamo IndiciTop anch’esso inizializzato coi valori 0, 5 e 5 (anziché 9...). che evita il ricorso alle Sub di pop e di push viste sopra.  Ma ho preferito lasciare la cosa per esercizio a chi ne ha voglia, anzitutto per  pigrizia ma anche ritenendo che la soluzione “vettoriale” meritasse di essere illustrata, perché curiosa e da tenere a mente  per altre occasioni.

Per tutto il resto non voglio togliere a chi legge il piacere di esaminare in dettaglio il procedimento, che alla luce di quanto appena detto e con un po’ di pazienza e il debug passo-passo, conto sia chiaro a tutti.

 

Print | posted on mercoledì 19 gennaio 2011 12:15 |

Feedback

No comments posted yet.

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 3 and 2 and type the answer here:

Powered by:
Powered By Subtext Powered By ASP.NET