Trentesima lezione - Gli Oggetti 2 parte (Campo Mino)

Per mettere in prica qualcuna delle nozioni viste nella lezione precedente torniamo a un vecchio progetto: il campo mino. Tempo fa mi è sto fto notare un errore (di cui probabilmente si saranno accorti in molti, ma che solo un lettore si è degno di segnalarmi): l'errore consiste nel fto che quando si scopre una mina in effetti la partita non termina, ma può essere proseguita dal giocore, benché il programma visualizzi la posizione delle altre mine e interrompa il timer. In realtà di errori ce ne sono anche altri: ad es. la partita non si interrompe neppure quando il giocore vince, e per di più il timer continua a scorrere. Per correggere questi bug occorre intervenire sull'evento Click dei pulsanti "mina", che controlla appunto se una mina sta per esplodere oppure no; questa è la routine originaria:

Prive Sub Mina_Click(Index As Integer)
Dim x As Integer, y As Integer 
'coordine del pulsante premuto
Dim x1 As Integer, y1 As Integer 
'coordine dei pulsanti circostanti quello premuto
If Mina(Index).Tag > 0 Then 'nessuna mina esplosa, 
	almeno una mina circostante
    If Len(Mina(Index).Caption) = 0 Then
        ContaMine = ContaMine + 1
    End If
    Mina(Index).Caption = Mina(Index).Tag
ElseIf Mina(Index).Tag = -1 Then 'mina esplosa
    Mina(Index).Caption = "M"
    For x = 0 To 2
        Mina(PosMine(x)).Caption = "M"
    Next x
    Timer1.Enabled = False
Else 'mina(index).tag=0 (nessuna mina esplosa, nessuna mina circostante)
    x = Int(Index / 4) 'riga in cui si trova il pulsante premuto
    y = Index Mod 4 'colonna in cui si trova il pulsante premuto
    For x1 = IIf(x = 0, 0, x - 1) To IIf(x = 3, 3, x + 1)
        For y1 = IIf(y = 0, 0, y - 1) To IIf(y = 3, 3, y + 1)
            If Len(Mina(4 * x1 + y1).Caption) = 0 Then
                ContaMine = ContaMine + 1
            End If
            Mina(4 * x1 + y1).Caption = Mina(4 * x1 + y1).Tag
        Next y1
    Next x1
End If

If ContaMine = MaxContaMine Then
    MsgBox "HAI VINTO!"
End If
End Sub

Per fare in modo che il giocore non possa ulteriormente continuare a premere i pulsanti dopo aver vinto o perso, occorre in qualche modo "inibire" l'evento click, in modo che le precedenti istruzioni non siano eseguite: il modo più semplice per fare ciò è adottare un flag del tipo bEseguiClick che valga true se la partita è ancora in corso o false se la partita è termina. In realtà una variabile del genere l'abbiamo già utilizza, anche se non è un booleano: si trta della variabile ContaMine, che quando raggiunge il massimo segnala la vittoria del giocore. Nessuno ci impedisce di assegnarle il medesimo valore anche quando il giocore perde, e condizionare l'esecuzione della routine al fto che tale variabile sia minore o uguale al valore massimo raggiungibile:

Prive Sub Mina_Click(Index As Integer)
Dim x As Integer, y As Integer 
'coordine del pulsante premuto
Dim x1 As Integer, y1 As Integer 
'coordine dei pulsanti circostanti quello premuto
If ContaMine < MaxContaMine Then
    If Mina(Index).Tag > 0 Then 
	'nessuna mina esplosa, almeno una mina circostante
        If Len(Mina(Index).Caption) = 0 Then
            ContaMine = ContaMine + 1
        End If
        Mina(Index).Caption = Mina(Index).Tag
    ElseIf Mina(Index).Tag = -1 Then 'mina esplosa
        Mina(Index).Caption = "M"
        For x = 0 To 2
            Mina(PosMine(x)).Caption = "M"
        Next x
        Timer1.Enabled = False
        ContaMine = MaxContaMine
        Exit Sub
    Else 
	'mina(index).tag=0 _
	 (nessuna mina esplosa, nessuna mina circostante)
        x = Int(Index / 4) 'riga in cui si trova il pulsante premuto
        y = Index Mod 4 'colonna in cui si trova il pulsante premuto
        For x1 = IIf(x = 0, 0, x - 1) To IIf(x = 3, 3, x + 1)
            For y1 = IIf(y = 0, 0, y - 1) To IIf(y = 3, 3, y + 1)
                If Len(Mina(4 * x1 + y1).Caption) = 0 Then
                    ContaMine = ContaMine + 1
                End If
                Mina(4 * x1 + y1).Caption = Mina(4 * x1 + y1).Tag
            Next y1
        Next x1
    End If
    
    If ContaMine = MaxContaMine Then
        MsgBox "HAI VINTO!"
        Timer1.Enabled = False
    End If
End If

End Sub

Quando il giocore scopre una mina, oltre ad arrestare il timer il programma provvede ad assegnare:

ContaMine = MaxContaMine

dopodiché termina forzamente la routine per evitare la comparsa del messaggio "hai vinto!" (anche se il giocore ha perso…); una volta che ContaMine ha il suo massimo valore concesso, qualunque ulteriore pressione delle mine non sortisce effetti, perché la condizione iniziale risulta falsa e la routine termina subito. In realtà il vero campo mino si comporta in modo leggermente diverso, impedendo la pressione stessa dei pulsanti: ciò può essere facilmente ottenuto disabilitando tutti i pulsanti "mina", ovvero impostando la proprietà Enabled su False per tutti gli elementi della mrice di CommandButton "Mina". Se invece il contore ContaMine ha un valore minore di MaxContaMine, significa che la partita è ancora in corso, pertanto la routine esegue tutti i controlli del caso ed eventualmente mostra il messaggio di vittoria; anche in tal caso il timer viene interrotto. Ciò non è ancora sufficiente, perché se una nuova partita viene comincia subito dopo, l'etichetta lblTempo riprende da dove si era interrotta anziché ricominciare daccapo; il problema si risolve semplicemente inserendo l'istruzione:

lblTempo.Caption = ""

alla fine della routine mnuNew_Click, prima dell'abilitazione del timer, e quest'altra istruzione all'inizio dell'evento Timer1_Timer:

If ContaMine = 0 Then i = 0

Poiché la variabile ContaMine viene inizializza a zero ad ogni nuova partita, il timer controllando il valore della variabile riesce a capire se deve continuare a contare da dove era arrivo (ricordiamo che la variabile "i" usa dal timer è di tipo stic) o se deve ricominciare da zero. L'unico inconveniente è che finché il giocore non comincia effettivamente a giocare (cioè non prova a premere qualche "mina") la variabile ContaMine resterà sempre a zero, e quindi il timer continuerà a inizializzare la variabile i e l'etichetta lblTempo continuerà a mostrare il valore "1" anche se la partita è comincia da un bel pezzo. A questo punto si potrebbero escogitare vari trucchi, ma la soluzione più elegante è usare una variabile a livello di modulo anziché una variabile stica ma locale rispetto all'evento timer di Timer1:

Dim ContaSecondi As Long
'numero di secondi trascorsi dall'inizio della partita

E modificare di conseguenza la routine del timer:

Prive Sub Timer1_Timer()
    ContaSecondi = ContaSecondi + 1
    lblTempo.Caption = CStr(ContaSecondi)
End Sub

Ovviamente la variabile ContaSecondi dovrà essere inizializza a zero tutte le volte che comincia una nuova partita. Questo per quanto riguarda la correzione dei bug.
Già che ci siamo vediamo di migliorare il gioco implementando la visualizzazione di una bandierina sui pulsanti sui quali il giocore clicca col tasto destro del mouse. Per fare ciò occorre innanzitutto impostare a 1 la proprietà Style dei pulsanti "mina": corrisponde allo stile grafico, che consente appunto di visualizzare icone sui pulsanti; la modalità di default è quella standard (Style=0), senza icone. Purtroppo la proprietà è di sola lettura in fase di esecuzione, per cui è necessario modificare manualmente la proprietà di tutte le mine in ambiente di progettazione: si può fare in un colpo solo selezionando tutte le mine insieme (come si fa con un gruppo di file in gestione risorse) e cambiando il valore della proprietà. A questo punto dobbiamo intercettare l'evento "click col tasto destro" per mostrare o nascondere l'icona della bandiera. L'evento "click", come sapete, è associo per default al tasto sinistro del mouse, e l'oggetto CommandButton non supporta un evento "RightClick" genero dalla pressione del tasto destro; esso supporta però l'evento "MouseDown", che viene genero ogniqualvolta l'utente preme un pulsante del mouse (non importa quale) sull'oggetto: è questo evento che dobbiamo intercettare. Questa è la dichiarazione dell'evento:

Prive Sub Mina_MouseDown _
(Index As Integer, _
 Button As Integer, _
 Shift As Integer, _
 X As Single, _
 Y As Single)

End Sub

Il primo parametro, come per l'evento Click, indica su quale "mina" è sto premuto un tasto del mouse; il secondo invece specifica quale dei tasti è sto premuto: perciò è su questo parametro che dovremo fare gli opportuni controlli; il terzo parametro specifica quali dei tasti control, alt o shift sono premuti mentre viene genero l'evento: è molto utile per riconoscere particolari sequenze (ad es. ctrl+shift+tasto destro); gli ultimi due parametri indicano le coordine del puntore del mouse nel momento in cui viene genero l'evento, e per ora non ci interessano.
Come si è detto, il nostro compito è individuare la pressione del tasto destro del mouse per visualizzare la bandiera sulla mina corrispondente; la chiave è in una semplice If:

With Mina(Index)
    If Button = vbRightButton Then 
	'click col tasto destro
        If .Picture.Handle = 0 Then  
		'nessuna bandiera sul pulsante
            .Picture = LoadPicture("BandieraCampoMino.ico")
        Else 'il pulsante ha già la bandiera
            .Picture = LoadPicture()
        End If
    End If
End With

Se il tasto del mouse premuto è il destro, il parametro Button avrà il valore 2, che è il valore della costante vbRightButton (fa parte del gruppo MouseButtonConstants, insieme a vbLeftButton e vbMiddleButton): in tal caso, se il pulsante non ha alcuna bandiera essa viene visualizza traverso la proprietà Picture, altrimenti significa che il giocore ha premuto il tasto destro su una mina che aveva già la bandiera: perciò essa sarà tolta. L'impostazione della proprietà Picture avviene tramite la funzione LoadPicture: dei vari parametri che accetta, qui è sto uso solo il primo, ovvero il nome del file. Come potete appurare controllando sul visualizzore oggetti, la funzione restituisce un riferimento a un'istanza della classe IPictureDisp, che dispone di una proprietà Handle: tale proprietà assume valore zero se non è associa ad alcuna immagine, altrimenti assume il valore dell'handle dell'icona; l'handle è un numero a 32 bit che fa riferimento univocamente all'immagine carica in memoria dalla funzione LoadPicture. Per eliminare un'immagine associa al pulsante, basta usare ancora la funzione LoadPicture senza alcun parametro: infti il nome del file immagine è facoltivo e, se assente, indica di rimuovere l'immagine correntemente associa al pulsante. Intercetto il click col tasto destro, occorre poi fare in modo che facendo click col tasto sinistro non accada nulla se sulla mina premuta è posta una bandiera: si trta di fare una piccola estensione alla condizione che abbiamo posto all'inizio dell'evento Mina_Click:

If (ContaMine < MaxContaMine) And (Mina(Index).Picture.Handle = 0) Then

L'istruzione If controlla non solo che il gioco sia ancora in corso confrontando il valore di ContaMine con MaxContaMine, ma anche che la mina che il giocore ha premuto non sia una di quelle "blocce" con la bandiera, altrimenti l'evento click viene sostanzialmente ignoro. A differenza del vero campo mino, tuttavia, i pulsanti appaiono effettivamente "premuti" quando si fa click, anche se non succede nulla: non è come se i pulsanti "Mina" fossero disabiliti. In effetti si potrebbe impostare Enabled=False quando il giocore fa click col tasto destro del mouse, approfittando anche del fto che ai pulsanti disabiliti è possibile associare un'icona tramite la proprietà DisabledPicture: ogniqualvolta un pulsante è disabilito Visual Basic provvede a mostrare l'immagine associa alla proprietà, senza bisogno di usare la funzione LoadPicture. Il problema è che poi una volta disabilito il pulsante non è più possibile intercettare l'evento mousedown per eventualmente reimpostare Enabled=True; non è un problema insormontabile, ma sembra più conveniente la strada illustra sopra. È opportuno invece disabilitare le mine all'avvio dell'applicazione, abilitandole solo quando il giocore sceglie di iniziare una nuova partita:

Prive Sub Form_Load()
Dim lContore As Long

MaxContaMine = 13
For lContore = Mina.LBound To Mina.UBound
    Mina(lContore).Enabled = False
Next lContore

End Sub

Mina.LBound e Mina.UBound equivalgono rispettivamente a Lbound(Mina) e Ubound(Mina), ovvero 0 e 15. Per lo stesso motivo, come si è detto più sopra, risulta conveniente disabilitare i pulsanti quando il giocore termina (vincendo o perdendo) la partita, anziché utilizzare la variabile ContaMine per inibire il click sulle mine: dipende anche dai gusti del programmore scegliere una delle tante strade disponibili.
Resta ancora una cosa da fare: eliminare tutte le bandiere eventualmente esistenti quando si comincia una nuova partita; l'ultima parte della routine mnuNew_Click diventerà quindi:

For i = 0 To 15
    With Mina(i)
        .Caption = ""
        .Enabled = True
        .Picture = LoadPicture()
    End With
Next i
ContaMine = 0
lblTempo.Caption = ""
ContaSecondi = 0
Timer1.Enabled = True
Infine, una carteristica prevista ma non ancora implementa riguardava l'etichetta lblMine, che nel campo mino dovrebbe indicare quante mine restano ancora da scoprire: ora che abbiamo capito come intercettare il click col tasto destro possiamo anche aggiornare questa etichetta. Si trta semplicemente di sottrarre, dal numero complessivo di mine (3 nel nostro esempio), il numero di pulsanti su cui sventola la bandiera; il luogo più adto in cui effettuare questo calcolo è l'evento mousedown, do che è lì che si decide se issare o ammainare la bandiera. Intanto dichiariamo un nuovo contore per le bandiere a livello di modulo nel form:

Dim ContaBandiere As Long 'numero di bandiere isse

Poi incrementiamo o decrementiamo il contore ogni volta che una bandiera viene aggiunta o tolta, e aggiorniamo l'etichetta:
Prive Sub Mina_MouseDown _
(Index As Integer, _
Button As Integer, _
Shift As Integer, _
X As Single, _
Y As Single)
With Mina(Index)
    If Button = vbRightButton Then 'click col tasto destro
        If .Picture.Handle = 0 Then  
		'nessuna bandiera sul pulsante
            .Picture = 
			LoadPicture("c: ... BandieraCampoMino.ico")
            ContaBandiere = ContaBandiere + 1
        Else 'il pulsante ha già la bandiera
            .Picture = LoadPicture()
            ContaBandiere = ContaBandiere - 1
        End If
    End If
End With

lblMine.Caption = CStr(3 - ContaBandiere)

End Sub
Infine, inizializziamo il contore ad ogni nuova partita:
Prive Sub mnuNew_Click()

[…]

ContaMine = 0
lblTempo.Caption = ""
lblMine.Caption = "3"
ContaSecondi = 0
ContaBandiere = 0
Timer1.Enabled = True
End Sub

In teoria dovremmo usare una costante per indicare il numero di mine, ma per ora non formalizziamoci troppo. Ci sarebbe anche un piccolo bug: se il giocore mette più bandiere di quante sono le mine, l'etichetta mostra nuralmente un numero negivo; cosa che peraltro accade anche col vero campo mino. Se il programmore lo ritiene opportuno, può imporre dei controlli per evitare che ciò accada, ma a me sembra tutto sommo più conveniente lasciarlo così: l'etichetta ha solo una funzione indicrice, e se il giocore ha voglia di mettere bandiere in eccesso, che lo faccia pure.
Ora il campo mino sembra funzionare a dovere: ma molte cose possono essere ancora migliore, come vedremo. Buon divertimento.



Note sul corso:
I diritti di ognuna delle lezioni presente in queste pagine appartengono all'autore Giorgio Abraini. La riproduzione e la divulgazione delle stesse sono consentite solamente dietro citazione di fonte ed autore.
Per suggerimenti, consigli o richieste conttare giorgio102@libero.it.
Fonte : VBItalia.it