Il blog di Gianni Giaccaglini

Blog su VBA e VSTO
Gianni Giaccaglini

My Links

News

NB - V. anche gli ARTICOLI (in fondo a questa barra)
Solo quesiti validi a: giannigiac@tin.it
Il mio nuovo libro


La mia nipotina ELISA

Foto con dedica a ME di
Bill Gates giovanissimo
nei mitici anni 80!

Categorie Post

Categorie Articoli

Archivio

Immagini

Blog Stats

Gioco del 15 col DatagridView

Gioco del 15 col DatagridView

Questa figura d'apertura illustra subito le tre fasi del giochino, implementato in modo abbastanza originale con il controllo DatagridView

.

Il programmino è quasi uno sfizio o piuttosto una scommessa. Più che altro nasce dall’esplorazione di un controllo, il DataGridView molto popolare ma di cui sono poco note le molte possibilità. La maggioranza lo utilizza per gestire dati provenienti da fonti classiche, tipicamente tabelle di un database associandolo a un controllo per il binding automatico ottenendo così assai facilmente visualizzazioni con tanto di intestazione dei vari campi.

Antefatti

Ma il DataGridView, che d’ora in avanti chiamerò alla buona griglia dati o più semplicemente griglia, fornisce appunto una griglia suddivisa in colonne (Columns), righe (Rows) a loro volta composte da celle (Cells) nelle quali con codice opportuno è dunque possibile inserire qualunque cosa.

Sulla base di tali considerazioni ho in precedenza utilizzato questa griglia per visualizzare dati non strutturati, come una matrice sparsa e un foglio di lavoro Excel. In entrambi i casi non si possono ottenere automatici “arruolamenti” di intestazioni di campi (meglio: non hanno senso). Nel caso Excel occorreva inoltre inserire le intestazioni di righe e colonne. Dopo vari patimenti sono riuscito a implementare un Visore di (fogli) Excel basato su una Windows Form incorporante un controllo DatagridView. Tale utility e la sua descrizione sono fruibili su questo sito, v. l’articolo specifico che precede questo e che consiglio di leggere.

Ai frettolosi concedo comunque questi tre URL:

http://www.giannigiaccaglini.it/download/FigVisoreDiExcel-01.jpg

http://www.giannigiaccaglini.it/download/FigVisoreDiExcel-02.jpg

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

Coi primi due potranno aprire e dare un occhio alle figure che illustrano l’utility, col terzo potranno scaricarla e... dilettarcisi.

Alla scoperta di eventi

Forte di questa esperienza ho cominciato a esplorare gli eventi della nostra griglia, che in effetti ne fornisce una caterva (figura 1).

Figura 1. La marea di eventi del controllo DataGridView.

Si tratta di una quantità impressionante, con quasi-omonimie di varia sottigliezza (come CellClick vs. CellMouseClick che non ho avuto tempo né pazienza di approfondire). Tale figura illustra anche una situazione dei primi tentativi di codice specifico, svolti su una Form1 inglobante una griglia DataGridView1.

Lo scopo fin dall’inizio era quello di realizzare, per lo sfizio didattico annunciato in apertura, il ben noto Gioco del 15.

Nota. V. più avanti una mia precedente realizzazione con Excel per chi proprio ne ignorasse le pur familiari caratteristiche...

Tale tentativo, di primo acchito, è andato incontro a frustrazioni a causa di un fraintendimento di cui non mi compiaccio ma che voglio ugualmente riferire, per onestà intellettuale.

Il listato iniziale

Lo riporto quasi privo di commenti, soprattutto perché questa soluzione imperfetta è stata poi sostituita da quella conclusiva, adeguatamente migliorata. Le operazioni preliminari al tempo di progetto, abbastanza facili, prevedono l’inserimento del DataGridView in una Windows Form con aggiunta di un adeguato numero di colonne, mentre l’aggiunta di righe va fatta a run-time.

Public Class Form1

    Dim DepDato As String = Nothing

    Dim iRiga As Integer = 0, iCol As Integer = 0

    Dim CellaBuco As DataGridViewCell

    Public Sub New()

        InitializeComponent() ' Richiesta da Progettazione Windows Form.

        Dim i = 0, j = 0, N = 3

        Dim Dgv = Me.DataGridView1 ' Fissa DatagridView1 in Dgv

        For j = 0 To N 'Aggiungi N + 1 righe (in più c'è quella default)

            Dgv.Rows.Add()

        Next

        For j = 0 To N ' Intestazioni da 1 a N + 1 delle righe

            Dgv.Rows(j).HeaderCell.Value = (j + 1).ToString

        Next

        Dim NumeriCelle() As Integer = _

           {3, 15, 11, 13, 9, 4, 14, 7, 2, 10, 12, 1, 6, 8, 5, 0}

        j = 0 : i = 0

        Dim k = 0

        For j = 0 To 3

            For i = 0 To 3

                Dgv.Rows(j).Cells(i).Value = NumeriCelle(k).ToString

                k += 1

            Next

        Next

        Dgv.Rows(j - 1).Cells(i - 1).Value = Nothing

        CellaBuco = Dgv.Rows(3).Cells(3)

        Dgv.Show()

    End Sub

 

Il precedente codice anticipa quello del giochino finale e in buona sostanza, inizializza una griglia Con N + 1 righe (insieme Rows) e inserisce come valori (.Value) delle intestazioni di ciascuna riga (Rows(j).HeaderCell) i numeri da 1 a N. I numeri di colonna, dimenticavo, andavano definiti a project-time.

Nota Queste testate, a dire il vero, non sono indispensabili. Le ho solo previste a titolo di richiamo didattico.

 

Trascuriamo per ora le successive istruzioni volte all’inserimento dei contenuti della matrice NumeriCelle() nelle celle della griglia e piuttosto teniamo bene a mente le variabili DepDato, iRiga e iCol nonché CellaBuco di tipo DataGridViewCell (l’oggetto Cell della griglia dati). Definite a livello Dichiarazioni servono allo scambio fra le diverse routine del progetto.

Segue la prima Sub relativa all’evento doppio clic su una cella (CellDoubleClic):

    Private Sub DataGridView1_CellDoubleClick(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles DataGridView1.CellDoubleClick

        Dim Dgv = DataGridView1

        Dim iC = Dgv.SelectedCells(0).ColumnIndex

        Dim iR = Dgv.SelectedCells(0).RowIndex

        Dim Msg = "Hai fatto doppio clic sulla cella in colonna " & _

        iC + 1 & " Riga " & iR + 1 & vbLf & "Contenente "

        Dim Contenuto = Dgv.SelectedCells(0).Value

        If Not Contenuto Is Nothing Then

            DepDato = Contenuto.ToString

            iRiga = iR : iCol = iC

            ' MessageBox.Show(Msg & DepDato) ' Servita per debug

        End If

    End Sub

 

Non ci vuol molto a capire che essa fa sì che quando si dà un doppio clic su una cella all’utente ne viene segnalato l’indice di colonna (ColumnIndex) e di riga (RowIndex) inoltre vengono stoccati nelle variabili comunitarie testé citate il contenuto nonché gl’indici della cella stessa. Faccio solo notare che non esiste una proprietà analoga ad ActiceCell di un foglio Excel, che ho surrogato con SelectedCells(0) che restituisce la prima delle celle selezionate, eventualmente plurime.

N.B. - D'ora in avanti per pigrizia fornisco solo i link delle varie figure

La figura 2 mostra l’effetto della doppia cliccata:

FIGURA 2  http://www.giannigiaccaglini.it/download/FigGioco15-02.jpg

Figura 2. Messaggio a seguito del doppio clic su una cella della griglia.

Ed eccoci a un’altra routine che, stavolta, sfrutta il pulsante Button1 ovviamente inserito da progetto nella Form:

    Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click

        If DepDato Is Nothing Then Exit Sub

        Dim Risp = MessageBox.Show("Copio qui il valore " & DepDato & " ?", "Dimmi", _

        MessageBoxButtons.YesNo)

        If Risp = Windows.Forms.DialogResult.Yes Then

            DataGridView1.SelectedCells(0).Value = DepDato

        End If

    End Sub

 

Il risultato di tale clic è, palesemente, il paste del DepDato precedente nella cella corrente per cui mi limito a mostrarlo nella figura 3:

FIGURA 3 http://www.giannigiaccaglini.it/download/FigGioco15-03.jpg

Figura 3. Situazione successiva al doppio clic su una cella, seleziona di un’altra e clic sul pulsante “Copia qui”.

Segue infine una Sub relativa a un secondo pulsante (non mostrato nelle figure precedenti:

    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click

        If DepDato Is Nothing Then Exit Sub

        Dim Dgv = DataGridView1

        Dim CellaScelta = Dgv.SelectedCells(0)

        If CellaScelta.RowIndex = 4 Then

            MessageBox.Show("Riga extra vietata!", "E' fuori campo del gioco!", MessageBoxButtons.OK, MessageBoxIcon.Warning)

            Exit Sub

        End If

        If Not CellaScelta.Value = Nothing Then Exit Sub

        Dim iC = Dgv.SelectedCells(0).ColumnIndex

        Dim iR = Dgv.SelectedCells(0).RowIndex

        Dim DeltaCol = iC - iCol

        If DeltaCol < 0 Then DeltaCol = -DeltaCol

        Dim DeltaRiga = iR - iRiga

        If DeltaRiga < 0 Then DeltaRiga = -DeltaRiga

        If DeltaCol = 1 And iR = iRiga Or _

           DeltaRiga = 1 And iC = iCol Then

            CellaScelta.Value = DepDato

            Dgv.Rows(iRiga).Cells(iCol).Value = Nothing

        End If

        ' Verifica successo

        Dim k = 1

        For Each Riga In Dgv.Rows

            For Each Cella In Riga.cells

                If Cella.value Is Nothing Or _

                Cella.value <> k.ToString Then Exit For

                If Cella.value = "15" Then

                    MessageBox.Show("Fine del Gioco!", "", _

                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

                End If

                k += 1

            Next

        Next

    End Sub

End Class

 

Tale routine lavora in combutta con quella del doppio clic, per cui l’utente dovrebbe (con qualche pena, in verità) operare in tre mosse: 1. Doppio clic su una cella; 2. Selezione di un’altra cella; 3. Clic sul Button2. Ma qui mi astengo da ogni commento, rimandato alla quasi analoga routine della soluzione conclusiva.

Mi fermo qui con questa soluzione imperfetta, senza riportare altre routine d’evento, diciamo così, “ingannevoli”. L’inganno in cui sono incappato in un primo tempo (e qui c’è l’autocritica anticipata ma l’incidente forse può capitare ad altri...) è l’apparente predominio di certi eventi su altri, qualora vengano sfruttati più eventi nello stesso progetto. In particolare:

§  Il doppio clic che viene di fatto annullato dal clic semplice;

§  Eventi come CellValueChanged e CellEnter si scatenano a tradimento all’apertura della Form.

La spiegazione (col senno di poi? Dite un po’ voi...) è semplice, anche se nel primo caso mi permetto di far notare che in Excel la cosa è meno drastica. Nel secondo la (apparente) anomalia derivava dall’aver dimenticato che la Sub aperturista New prevede giustappunto cambiamenti alle celle, ragion per cui le MessageBox.Show inserite per scopi esplorativi nella routine dell’evento CellValueChanged spuntavano a sorpresa senza che comparisse la finestra di dialogo con la sua brava griglia.

Gioco del 15 con Excel, rivisitato

Anche da questo equivoco derivò il procedimento imperfetto delle tre mosse (doppio clic, altra cella, clic sul Button1) appena intravisto. A quel punto una luce si accese nella mia mente, ricordandomi una vecchia implementazione del Gioco del 15 mediante Excel + macro VBA (figura 4).

FIGURA 4 http://www.giannigiaccaglini.it/download/FigGioco15-04.jpg

Figura 4. Uno sguardo a un mio Gioco del 15 creato a suo tempo con Excel + macro VBA.

Da tale figura, riflettendo, si può evincere l’algoritmo ottimo: solo le celle adiacenti a quella del buco vi si possono spostare e per farlo basta un clic su una di queste.

La descrizione del modellino Excel accennato è fuori tema, ancorché non del tutto fuori luogo. Ad ogni buon conto per comodità degl’interessati & curiosi segnalo due URL:

http://www.giannigiaccaglini.it/download/Gioco%20del%2015.xls

http://www.giannigiaccaglini.it/download/Gioco%20del%2015.pdf

Consentono di scaricare, il primo il modello Excel, il secondo un file che ne descrive le macro.

La soluzione conclusiva

Il cerchio si chiude esaminando la figura 5  e scaricando il giochino del 15 implementato in Visual Studio 2008 dal link seguente:

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

Nota. Il funzionamento “indolore” del piccolo applicativo è garantito al 100% se Visual Studio 2008 è installato o, al più, se il PC è dotato di sistema operativoWindows Vista o almeno XP con service pack 2.

 

FIGURA 5 http://www.giannigiaccaglini.it/download/FigGioco15-05.jpg

CHE POI COINCIDE CON QUELLA POSTA IN CIMA ALL'ARTICOLO... 

Figura 5. Il Gioco del 15 realizzato con una Windows Form dotata di DataGridView nelle sua fasi operative.

La figura 5 mostra tutte assieme la fase iniziale e quella finale del giochino con riapparizione del pulsante etichettato “Nuova partita” che ovviamente permette di iniziarne un’altra.

L’aspetto spartano, criticabile, deriva della scelta di privilegiare la sostanza, trascurando l’estetica. Per migliorarla si può agire sulle proprietà specifiche volte a mutare font, dimensioni e altro, operazioni che richiedono non poca pazienza, purtroppo senza ottenere i risultati del Gioco del 15 fatto con Excel.

Il codice relativo è subito visto, corredato da commenti essenziali.

Il listato finale

Public Class Form1

    Dim CellaBuco As DataGridViewCell, InizioFinito As Boolean

    Dim DepCella As DataGridViewCell, DepVal As String = Nothing

    Sub Inizio()

        InizioFinito = False

        Dim i = 0, j = 0, N = 3

        Dim Dgv = Me.DataGridView1

        For j = 0 To N ' Aggiungi N + 1 righe (in più c'è quella default)

            Dgv.Rows.Add()

        Next

        For j = 0 To N ' Intestazioni da 1 a N + 1 delle righe

            Dgv.Rows(j).HeaderCell.Value = (j + 1).ToString

        Next

        Dim NumeriCelle() As Integer = _

        {3, 15, 11, 13, 9, 4, 14, 7, 2, 10, 12, 1, 6, 8, 5, 0}

        Dim NumeriCasuali(14) As Single

        Randomize()

        For i = 0 To 14

            NumeriCasuali(i) = Rnd() ' Usare System.Math? Troppo complicato...

        Next

        Array.Sort(NumeriCasuali, NumeriCelle)

        j = 0 : i = 0

        Dim k = 0

        For j = 0 To 3

            For i = 0 To 3

                Dgv.Rows(j).Cells(i).Value = NumeriCelle(k).ToString

                k += 1

            Next

        Next

        Dgv.Rows(j - 1).Cells(i - 1).Value = Nothing

        CellaBuco = Dgv.Rows(3).Cells(3)

        InizioFinito = True

        Dgv.Show()

    End Sub

 

    Public Sub New()

        InitializeComponent() ' Chiamata richiesta da Progettazione Windows Form.

        Inizio()

    End Sub

 

In questo incipit si nota subito, rispetto all’analogo caso precedente, la routine Inizio dichiarata a monte della New che la invoca. Questo perché, come si vedrà e come i più svegli già prevedono, Inizio sarà lanciata al termine del gioco anche dal Button1 che ne avvia un altro.

Sempre a livello Dichiarazioni le variabili CellaBuco avente il tipo che identifica una cella della griglia (DataGridViewCell), InizioFinito, DepCella pur’essa del tipo DataGridViewCell e DepVal di tipo stringa servono nell’ordine a registrare il buco, il completamento della Sub Inizio (che non a caso termina con InizioFinito = True), la cella in precedenza depositata e il suo valore (.Value).

Seguono istruzioni che aggiungono 4 righe e relative intestazioni da 1 in avanti (*), caricandole coi numeri da 0 (relativo al buco) a 15 inizializzati nella matrice NumeriCelle secondo una sequenza come un’altra che comunque subito viene rimescolata col metodo Sort avente come chiave una serie NumeriCasuali di valori a caso ottenuti con la classica accoppiata Randomize più il caricamento di valori random Rnd().

Nota (*). Queste etichette laterali come pure quelle delle colonne (definite a project-time) si potevano omettere. Lo si è fatto per motivi didattici, a futura memoria.

 

Vediamo ora la parte iniziale della routine scatenata dal clic su una cella:

    Private Sub DataGridView1_CellClick(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles DataGridView1.CellClick

        ' MessageBox.Show("Ha fatto clic sulla cella") ' Servita per debug

        Dim Dgv = DataGridView1

        Dim CellaScelta = Dgv.SelectedCells(0)

        DepCella = CellaScelta

        DepVal = CellaScelta.Value

        If CellaScelta.RowIndex = 4 Then

            MessageBox.Show("Riga extra vietata!", _

           "E' fuori campo del gioco!", MessageBoxButtons.OK, MessageBoxIcon.Warning)

            Exit Sub

        End If

        ' If CellaScelta.Value = Nothing Then Exit Sub ' Rivelatasi superflua

        If CellaScelta Is CellaBuco Then Exit Sub

        Dim iC = Dgv.SelectedCells(0).ColumnIndex

        Dim iR = Dgv.SelectedCells(0).RowIndex

        Dim DeltaCol = System.Math.Abs(iC - CellaBuco.ColumnIndex)

        Dim DeltaRiga = System.Math.Abs(iR - CellaBuco.RowIndex)

        If DeltaCol = 1 And iR = CellaBuco.RowIndex Or _

           DeltaRiga = 1 And iC = CellaBuco.ColumnIndex Then

            InizioFinito = False ' Istruzione probabilmente pleonastica...

            CellaBuco.Value = CellaScelta.Value

            CellaScelta.Value = Nothing

            CellaBuco = CellaScelta

            InizioFinito = True

        End If

 

Questa routine è decisamente la più importante. Individuata subito la CellaScelta (ottenuta come già visto dalla prime delle celle selezionate) e il suo Value l’una e l’altro sono memorizzati nei rispettivi depositi. Seguono istruzioni volte a vietare, ignorandolo, l’eventuale clic su altre celle vuote (nella riga default, che in verità avrei potuto includere: pazienza...) e soprattutto sulla CellaBuco.

L’analisi del successivo codice volto a definire DeltaCol e DeltaRiga ovvero i valori assoluti delle differenze fra l’uno e l’altro indice della CellaScelta e i corrispettivi della CellaBuco è lasciata per esercizio. Basti sapere che se e solo se tale differenza è unitaria (righe e/o colonne adiacenti nonché (And) l’altro fra i due indici coincide si può procedere allo scambio tra la cella cliccata e quella buca, con opportune registrazioni a livello Dichiarazioni.

Dopo il fatidico scambio la routine in esame prosegue e termina come segue:

        ' Verifica del successo

        Dim j = 0, i = 0

        Dim ValCella = ""

        Dim k = 1

        ' Verifica prime 12 celle

        For j = 0 To 2

            For i = 0 To 3

                ValCella = Dgv.Rows(j).Cells(i).Value

                If ValCella <> k.ToString Then

                    Exit Sub

                End If

                k += 1

            Next

        Next

        ' Verifica quarta riga

        For Each Cella In Dgv.Rows(3).Cells

            If Cella.Value Is Nothing Then Exit For

            Dim TreUltimeCelle As String = ""

            With Dgv.Rows(3).Cells

                TreUltimeCelle &= _

                .Item(0).Value & .Item(1).Value & .Item(2).Value

            End With

            If Not Dgv.Rows(3).Cells(3).Value Is Nothing Then

                ' Il buco dev'essere nell'ultima cella...

                Exit Sub

            End If

            Dim Msg = "Fine del Gioco"

            If TreUltimeCelle = "131415" Then

                MessageBox.Show(Msg, "Terminato con successo", _

                MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

                Me.Button1.Visible = True

                Exit Sub

            Else

                If TreUltimeCelle = "131514" Or _

                   TreUltimeCelle = "141315" Or _

                   TreUltimeCelle = "151413" Then

                    MessageBox.Show(Msg, "(Sequenza iniziale errata...)", _

                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation)

                    Me.Button1.Visible = True

                    Exit Sub

                End If

            End If

        Next

    End Sub

 

I commenti, a questo punto, li lasciati interamente all’esegesi autogestita del lettore al quale fornisco delucidazioni minime ma basilari. Sopratutto si deve sapere in primo luogo che il sort dei numerini non garantisce una sequenza perfettamente “regolare”. Situazioni inattese si possono infatti avere quando gli ultimi tre numeri sistemati a furia di clic sono 13-15-14 oppure 14-13-15 o 15-14-13. Per quanti sforzi (ingenuamente) si facciano è impossibile ricondurli a 13-14-15 (cosa invece possibile con 15-13-14 o 14-15-13).

Nota. Due possibili alternative per evitare ciò: definire una serie abbastanza nutrita di sequenze sparse ma regolari oppure, partendo da una sequenza crescente da 0 a 15, rimescolandola emulando con codice ad hoc quel che vien fatto col clic (o a mano, nella pratica col giochino materiale). Chi ha tempo e pazienza può farlo...

 

Per tale motivo la nostra Sub esamina dapprima la regolare successione da 1 a 12 e, accertatala, discrimina i due casi detti emettendo messaggi distinti.

La classe Form1 si conclude come segue:

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        Button1.Visible = False

        Inizio()

    End Sub

 

    Private Sub DataGridView1_CellValueChanged(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles DataGridView1.CellValueChanged

        If Not InizioFinito Then Exit Sub ' Evento tacitato nella fase iniziale

        Static Bis As Boolean = True ' Impedisce la ripetizione del messaggio...

        Dim Msg = "Hai cambiato il valore!" & vbLf & "Ora lo ripristino"

        If Bis Then MessageBox.Show(Msg, "AZIONE VIETATA", _

        MessageBoxButtons.OK, MessageBoxIcon.Information)

        Bis = False

        DepCella.Value = DepVal

    End Sub

End Class

 

Solo la seconda routine relative all’evento CellValueChanged merita un fuggevole commento: il suo scopo è di impedire all’utente di barare o, peggio, di incasinare il gioco modificando un dato.

Altro non dico, ma solo buono studio!

?>

?>

posted on venerdì 15 gennaio 2010 16.29