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