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!
?>
?>