Nella scorsa lezione abbiamo visto come creare una mina personalizza in grado di generare tre eventi: Resize, ClickLeft e ClickRight; gli ultimi due devono essere sfrutti dal progetto Campo Mino per abilitare/disabilitare le mine. Prima di procedere alla sostituzione delle vecchie mine (command button) con le nuove mine (usercontrol activex), aggiungiamo qualche carteristica che può tornarci utile: nella precedente versione del Campo Mino avevamo utilizzo la proprietà Tag dei pulsanti per sapere se nascondevano una mina o no; ora che abbiamo un controllo apposito possiamo aggiungere la proprietà adta per questa informazione.
Nella sezione delle dichiarazioni generali aggiungiamo una variabile priva denomina mlNumeroMine: do che dovrà contenere un numero intero, Long sembra essere il tipo adto:
Option Explicit
Prive mlNumeroMine As Long
Public Event Resize()
Public Event ClickLeft()
Public Event ClickRight()
Aggiungiamo anche una proprietà NumeroMine per rendere accessibile l'informazione al client che userà il controllo:
Public Property Get NumeroMine() As Long
NumeroMine = mlNumeroMine
End Property
Public Property Let NumeroMine(ByVal vNewValue As Long)
mlNumeroMine = vNewValue
End Property
mlNumeroMine conterrà il numero di mine circostanti ogni specifica istanza del controllo; il valore -1 indicherà la presenza di una mina proprio sotto l'istanza considera: la proprietà NumeroMine quindi fa le veci della proprietà Tag in precedenza utilizza. Già che ci siamo, deleghiamo la proprietà Caption del pulsante del nostro controllo ActiveX in modo che sia accessibile dagli utilizzori del componente:
Public Property Get Caption() As String
Caption = cmdMina.Caption
End Property
Public Property Let Caption(ByVal vNewValue As String)
cmdMina.Caption = vNewValue
End Property
Dobbiamo inoltre delegare anche la proprietà Picture, traverso la quale viene visualizza una bandiera sulle mine disabilite:
Public Property Get Picture() As IPictureDisp
Set Picture = cmdMina.Picture
End Property
Public Property Set Picture(ByVal vNewValue As IPictureDisp)
cmdMina.Picture = vNewValue
End Property
Come avevamo già visto in precedenza (e come si può capire consultando il visualizzore oggetti), la proprietà Picture dell'oggetto CommandButton è un'istanza della classe IPictureDisp: dello stesso tipo dovrà pertanto essere la proprietà Picture aggiunta al nostro controllo ActiveX. Trtandosi di un oggetto, la routine di impostazione della proprietà dovrà essere una Property Set e non una Property Let; per lo stesso motivo si utilizza l'istruzione Set nel codice della routine Property Get. Nuralmente il pulsante contenuto nello UserControl dovrà avere la proprietà Style imposta a 1-Graphical.
Prima di sostituire le vecchie mine con le nuove, sarebbe bene fare in modo che il caricamento dei controlli avvenga in modo automico anziché disporli manualmente sul form: finché sono 16 si può anche fare a mano, ma il giorno in cui si decidesse di portarli a 100… La cosa importante è che ci sia almeno un'istanza disegna sul form, con indice zero, come se fosse il primo elemento di una mrice di controlli; poi usando l'istruzione Load se ne possono caricare quanti si vuole. Per semplificarci la vita definiamo nel form Form1 un paio di costanti per memorizzare il numero di "righe" e "colonne" della mrice di pulsanti "Mina": il concetto di righe e colonne ha a che fare con la loro disposizione spaziale nel form, non con le dimensioni della mrice di controlli, poiché tale mrice è unidimensionale; continuando con gli esempi finora fti, i pulsanti saranno 16 divisi in 4 righe e 4 colonne:
Prive Const mlNUMERO_RIGHE As Long = 4
Prive Const mlNUMERO_COLONNE As Long = 4
Nella routine di inizializzazione del form dovremo perciò caricare 15 pulsanti (il primo esiste già e ha indice zero imposto in fase di progettazione) disponendoli in una mrice 4 x 4:
Prive Sub Form_Load()
Dim lContRighe As Long
Dim lContColonne As Long
Dim lContPulsanti As Long
MaxContaMine = 13
lContPulsanti = 0
For lContRighe = 1 To mlNUMERO_RIGHE
If lContPulsanti Then
lContPulsanti = lContPulsanti + 1
Load Mina(lContPulsanti)
With Mina(lContPulsanti)
.Enabled = False
.Visible = True
.Left = Mina(lContPulsanti - mlNUMERO_COLONNE).Left
.Top = Mina(lContPulsanti - 1).Top _
+ Mina(lContPulsanti - 1).Height
End With
End If
For lContColonne = 2 To mlNUMERO_COLONNE
lContPulsanti = lContPulsanti + 1
Load Mina(lContPulsanti)
With Mina(lContPulsanti)
.Enabled = False
.Visible = True
.Left = Mina(lContPulsanti - 1).Left _
+ Mina(lContPulsanti - 1).Width
.Top = Mina(lContPulsanti - 1).Top
End With
Next lContColonne
Next lContRighe
End Sub
Nella routine vengono utilizzi tre contori: uno per le righe, uno per le colonne, uno per i pulsanti; mentre i primi due sono gestiti dai rispettivi cicli, il terzo serve per la mrice dei controlli vera e propria. Il ciclo per riga non fa altro che impostare le coordine del primo pulsante di ogni riga in base a quelle del primo pulsante della riga precedente (il nuovo pulsante viene messo sotto): queste impostazioni sono nuralmente salte a piè pari se l'indice lContPulsanti è ancora a zero, perché in tal caso stiamo trtando il pulsante Mina(0) che abbiamo già inserito in fase di progettazione. All'interno del ciclo per riga c'è il ciclo per colonna, che ripete sostanzialmente le stesse istruzioni con la differenza che le coordine dipendono dal pulsante precedente nella medesima riga (il nuovo pulsante viene messo affianco). Tutti i pulsanti vengono inizialmente disabiliti, poiché l'abilitazione avverrà nel momento in cui il giocore decide di iniziare una nuova partita. Volendo modificare la disposizione dei pulsanti basterà cambiare il valore assegno alle costanti mlNUMERO_RIGHE e mlNUMERO_COLONNE.
Se prove ad avviare il progetto vi dovreste accorgere di una cosa curiosa: tutti i pulsanti sono disabiliti tranne il primo. Ciò avviene perché l'istruzione Enabled=False viene ignora quando lContPulsanti è uguale a zero; per evitare l'inconveniente si può forzare il programma ad eseguire quell'istruzione (utilizzando ad es. la clausola Else nel costrutto If…Then) oppure si può impostare direttamente a False la proprietà Enabled del pulsante Mina(0) in fase di progettazione. In realtà questa seconda procedura non funziona, per un motivo molto semplice: Visual Basic non salva il valore della proprietà modifico dall'utente. Mentre le proprietà gestite direttamente dall'Extender (ad es. Left e Top) sono automicamente salve da Visual Basic quando l'utente le cambia in fase di progettazione, le altre proprietà (come appunto Enabled, che abbiamo delego con le routine Property) necessitano di un salvaggio esplicito: tale salvaggio, e la corrispondente lettura del valore modifico, avvengono rispettivamente negli eventi WriteProperties e ReadProperties dell'oggetto UserControl. In prica, ogni volta che l'istanza del pulsante che abbiamo inserito nel form viene distrutta (ad es. quando si chiude la finestra di progettazione del form), viene genero l'evento WriteProperties che provvede a salvare i valori correnti delle proprietà del componente. Quando invece l'istanza viene ricrea (ad es. ritivando la finestra di progettazione in precedenza chiusa), viene genero l'evento ReadProperties, che assegna i valori delle proprietà secondo le ultime impostazioni salve. Tutti questi valori sono fisicamente memorizzi nel file *.frm che definisce il form dell'applicazione con i controlli disegni al suo interno: è un semplice file di testo che contiene, per ogni controllo, i valori delle proprietà (non tutte) ed eventualmente le variabili e tutto il codice che vediamo nella finestra del codice corrispondente al form. Ai file *.frm per i form corrispondono i file *.ctl per i controlli ActiveX.
Nuralmente il file su disco viene salvo solo alla chiusura del progetto; tutte le modifiche effettue mentre il progetto è ancora aperto sono mantenute in memoria ma non effettivamente scritte sul disco.
Ora, affinché le nostre proprietà personalizze possano "ricordare" l'ultimo valore assegno dall'utente, dobbiamo inserire del codice apposito nei suddetti eventi ReadProperties e WriteProperties, ad es:
Prive Sub UserControl_ReadProperties(PropBag As PropertyBag)
On Error Resume Next
With PropBag
Enabled = .ReadProperty("Enabled", True)
EnabledCmd = .ReadProperty("EnabledCmd", True)
Caption = .ReadProperty("Caption")
End With
End Sub
Prive Sub UserControl_WriteProperties(PropBag As PropertyBag)
With PropBag
Call .WriteProperty("Enabled", UserControl.Enabled, True)
Call .WriteProperty("EnabledCmd", cmdMina.Enabled, True)
Call .WriteProperty("Caption", cmdMina.Caption)
End With
End Sub
L'oggetto PropertyBag, argomento di entrambi gli eventi, è quello che contiene le informazioni sui valori delle proprietà e che consente di leggerle/scriverle tramite i metodi "ReadProperty" e "WriteProperty": il primo vuole come argomenti il nome della proprietà ed eventualmente il valore di default, da usare nel caso in cui non sia disponibile un valore salvo; il secondo invece, oltre al nome della proprietà, vuole anche il valore di essa da salvare, ed eventualmente ancora il valore di default. L'ultimo parametro (valore di default) è importante perché il valore della proprietà è effettivamente salvo solo se è diverso da quello di default, in modo da risparmiare tempo e spazio. L'oggetto PropertyBag è gestito direttamente da Visual Basic, il programmore deve solo preoccuparsi di leggere/salvare le proprietà che gli interessano: un valido aiuto in questo compito è do dalla aggiunta "Creazione guida interfaccia controlli ActiveX" (menù aggiunte), che inserisce automicamente il codice necessario per le proprietà selezione. Nell'evento ReadProperties è sta aggiunta un'istruzione di gestione degli errori come suggerito dalla guida, per evitare problemi nel caso in cui un furbastro abbia modifico manualmente il file *.frm inserendo valori non validi per una certa proprietà. Non tutte le proprietà necessitano di essere salve: ad es. la proprietà NumeroMine non ha bisogno di essere salva, perché il suo valore è genero in fase di esecuzione dall'applicazione client, non è modifico dall'utente in fase di progettazione. C'è poi un terzo evento, InitProperties, che viene genero quando l'istanza del componente è crea per la prima volta (ad es. quando il pulsante è disegno sul form): infti in questo caso non esistono valori delle proprietà precedentemente salvi, perciò più che "leggere" le proprietà occorre "inizializzarle": anche in questo caso torna molto utile il valore di default, che è opportuno specificare sempre, se possibile.
Ora, impostando a False la proprietà Enabled del pulsante Mina(0), questo sarà disabilito come gli altri all'avvio del progetto, perché all'avvio del progetto l'istanza di progettazione viene distrutta, generando l'evento WriteProperties che salva il valore corrente della proprietà Enabled; quando viene crea l'istanza di esecuzione, l'evento ReadProperties assegna a tale proprietà il valore appena salvo.
Tornando alla sostituzione delle mine vecchie con le nuove, un'altra routine da modificare è quella dell'inizio di una nuova partita (mnuNew_Click): pricamente si trta solo di sostituire la proprietà Tag con la proprietà NumeroMine, e la parola chiave "True" con il valore -1 (anche se in realtà è la stessa cosa). Già che ci siamo approfittiamone anche per usare le costanti per il numero di righe e colonne anziché i valori letterali come 4 o 16:
Prive Sub mnuNew_Click()
'variabile temporanea per eseguire i controlli
Dim t As Integer
'contore
Dim i As Integer
'coordine del pulsante con la mina
Dim X As Integer, Y As Integer
'coordine dei pulsanti circostanti quello con la mina
Dim x1 As Integer, y1 As Integer
Randomize Timer
PosMine(0) = Int(Rnd * mlNUMERO_RIGHE * mlNUMERO_COLONNE)
Mina(PosMine(0)).NumeroMine = -1
'Estrai:
t = Int(Rnd * mlNUMERO_RIGHE * mlNUMERO_COLONNE)
If t = PosMine(0) Then
GoTo Estrai
Else
PosMine(1) = t
Mina(PosMine(1)).NumeroMine = -1
End If
'EstraiDiNuovo:
t = Int(Rnd * mlNUMERO_RIGHE * mlNUMERO_COLONNE)
If t = PosMine(0) Or t = PosMine(1) Then
GoTo EstraiDiNuovo
Else
PosMine(2) = t
Mina(PosMine(2)).NumeroMine = -1
End If
'aggiorna le proprietà NumeroMine dei pulsanti
For i = 0 To (mlNUMERO_RIGHE * mlNUMERO_COLONNE - 1)
Mina(i).NumeroMine = 0
Next i
For i = 0 To 2
Mina(PosMine(i)).NumeroMine = -1
'riga in cui si trova la mina
X = Int(PosMine(i) / mlNUMERO_COLONNE)
'colonna in cui si trova la mina
Y = (PosMine(i) Mod mlNUMERO_COLONNE)
For x1 = IIf(X = 0, 0, X - 1) To _
IIf(X = mlNUMERO_RIGHE - 1, mlNUMERO_RIGHE - 1, X + 1)
For y1 = IIf(Y = 0, 0, Y - 1) To _
IIf(Y = mlNUMERO_COLONNE - 1, mlNUMERO_COLONNE - 1, Y + 1)
If Mina(mlNUMERO_COLONNE * x1 + y1).NumeroMine > -1 Then
Mina(mlNUMERO_COLONNE * x1 + y1).NumeroMine =
Mina(mlNUMERO_COLONNE * x1 + y1).NumeroMine + 1
End If
Next y1
Next x1
Next i
For i = 0 To (mlNUMERO_RIGHE * mlNUMERO_COLONNE - 1)
With Mina(i)
.Caption = ""
.Enabled = True
.EnabledCmd = True
Set .Picture = LoadPicture()
End With
Next i
ContaMine = 0
lblTempo.Caption = ""
lblMine.Caption = "3"
ContaSecondi = 0
ContaBandiere = 0
Timer1.Enabled = True
End Sub
Un analogo trtamento va fto con le routine Mina_Click (che ora diventa Mina_ClickLeft) e Mina_MouseDown (che ora diventa Mina_ClickRight):
Prive Sub Mina_ClickLeft(Index As Integer)
'coordine del pulsante premuto
Dim X As Integer, Y As Integer
'coordine dei pulsanti circostanti quello premuto
Dim x1 As Integer, y1 As Integer
If (ContaMine < MaxContaMine) And _
(Mina(Index).Picture.Handle = 0) Then
'nessuna mina esplosa, almeno una mina circostante
If Mina(Index).NumeroMine > 0 Then
If Len(Mina(Index).Caption) = 0 Then
ContaMine = ContaMine + 1
End If
Mina(Index).Caption = CStr(Mina(Index).NumeroMine)
'mina esplosa
ElseIf Mina(Index).NumeroMine = -1 Then
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).NumeroMine=0 (nessuna mina _
esplosa, nessuna mina circostante)
'riga in cui si trova il pulsante premuto
X = Int(Index / mlNUMERO_COLONNE)
'colonna in cui si trova il pulsante premuto
Y = Index Mod mlNUMERO_COLONNE
For x1 = IIf(X = 0, 0, X - 1) To _
IIf(X = mlNUMERO_RIGHE - 1, mlNUMERO_RIGHE - 1, X + 1)
For y1 = IIf(Y = 0, 0, Y - 1) To _
IIf(Y = mlNUMERO_COLONNE - 1, mlNUMERO_COLONNE - 1, Y + 1)
If Len(Mina(mlNUMERO_COLONNE * x1 + y1).Caption) = 0 Then
ContaMine = ContaMine + 1
End If
Mina(mlNUMERO_COLONNE * x1 + y1).Caption =
CStr(Mina(mlNUMERO_COLONNE * x1 + y1).NumeroMine)
Next y1
Next x1
End If
If ContaMine = MaxContaMine Then
MsgBox "HAI VINTO!"
Timer1.Enabled = False
End If
End If
End Sub
Prive Sub Mina_ClickRight(Index As Integer)
With Mina(Index)
If .Picture.Handle = 0 Then 'nessuna bandiera sul pulsante
Set .Picture = LoadPicture("BandieraCampoMino.ico")
ContaBandiere = ContaBandiere + 1
.EnabledCmd = False
Else 'il pulsante ha già la bandiera
Set .Picture = LoadPicture()
ContaBandiere = ContaBandiere - 1
.EnabledCmd = True
End If
End With
lblMine.Caption = CStr(3 - ContaBandiere)
End Sub
Ora che sembra finalmente tutto a posto, sorge però un problema: lo vedremo la prossima lezione.