Nella
scorsa lezione abbiamo visto come creare una mina personalizzata
in grado di generare tre eventi: Resize, ClickLeft e ClickRight;
gli ultimi due devono essere sfruttati dal progetto Campo
Minato per abilitare/disabilitare le mine. Prima di procedere
alla sostituzione delle vecchie mine (command button) con
le nuove mine (usercontrol activex), aggiungiamo qualche caratteristica
che può tornarci utile: nella precedente versione del
Campo Minato avevamo utilizzato la proprietà Tag dei
pulsanti per sapere se nascondevano una mina o no; ora che
abbiamo un controllo apposito possiamo aggiungere la proprietà
adatta per questa informazione.
Nella sezione delle dichiarazioni generali aggiungiamo una
variabile privata denominata mlNumeroMine: dato che dovrà
contenere un numero intero, Long sembra essere il tipo adatto:
Option
Explicit
Private
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 considerata: la proprietà
NumeroMine quindi fa le veci della proprietà Tag in
precedenza utilizzata.
Già che ci siamo, deleghiamo la proprietà Caption
del pulsante del nostro controllo ActiveX in modo che sia
accessibile dagli utilizzatori 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, attraverso
la quale viene visualizzata una bandiera sulle mine disabilitate:
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 visualizzatore 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.
Trattandosi 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. Naturalmente il
pulsante contenuto nello UserControl dovrà avere la
proprietà Style impostata 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 automatico 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 disegnata
sul form, con indice zero, come se fosse il primo elemento
di una matrice 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
matrice 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 matrice di controlli,
poiché tale matrice è unidimensionale; continuando
con gli esempi finora fatti, i pulsanti saranno 16 divisi
in 4 righe e 4 colonne:
Private
Const
mlNUMERO_RIGHE As Long =
4
Private 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 impostato
in fase di progettazione) disponendoli in una matrice 4 x 4:
Private
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 utilizzati tre contatori: 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 matrice
dei controlli vera e propria. Il ciclo per riga non fa altro
che impostare le coordinate 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
naturalmente saltate a piè pari se l'indice lContPulsanti
è ancora a zero, perché in tal caso stiamo trattando
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 coordinate dipendono dal pulsante precedente
nella medesima riga (il nuovo pulsante viene messo affianco).
Tutti i pulsanti vengono inizialmente disabilitati, poiché
l'abilitazione avverrà nel momento in cui il giocatore
decide di iniziare una nuova partita. Volendo modificare la
disposizione dei pulsanti basterà cambiare il valore
assegnato alle costanti mlNUMERO_RIGHE e mlNUMERO_COLONNE.
Se
provate ad avviare il progetto vi dovreste accorgere di una
cosa curiosa: tutti i pulsanti sono disabilitati tranne il primo.
Ciò avviene perché l'istruzione Enabled=False
viene ignorata 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à modificato
dall'utente.
Mentre le proprietà gestite direttamente dall'Extender
(ad es. Left e Top) sono automaticamente salvate da Visual Basic
quando l'utente le cambia in fase di progettazione, le altre
proprietà (come appunto Enabled, che abbiamo delegato
con le routine Property) necessitano di un salvataggio esplicito:
tale salvataggio, e la corrispondente lettura del valore modificato,
avvengono rispettivamente negli eventi WriteProperties e ReadProperties
dell'oggetto UserControl. In pratica, 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 generato l'evento WriteProperties che provvede a salvare
i valori correnti delle proprietà del componente.
Quando invece l'istanza viene ricreata (ad es. riattivando la
finestra di progettazione in precedenza chiusa), viene generato
l'evento ReadProperties, che assegna i valori delle proprietà
secondo le ultime impostazioni salvate. Tutti questi valori
sono fisicamente memorizzati nel file *.frm che definisce il
form dell'applicazione con i controlli disegnati 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.
Naturalmente il file su disco viene salvato solo alla chiusura
del progetto; tutte le modifiche effettuate mentre il progetto
è ancora aperto sono mantenute in memoria ma non effettivamente
scritte sul disco.
Ora, affinché le nostre proprietà personalizzate
possano "ricordare" l'ultimo valore assegnato dall'utente,
dobbiamo inserire del codice apposito nei suddetti eventi ReadProperties
e WriteProperties, ad esempio:
Private
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
Private
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 salvato;
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 salvato solo se è diverso da
quello di default, in modo da risparmiare tempo e spazio.
L'oggetto PropertyBag è gestito direttamente da Visual
Basic, il programmatore deve solo preoccuparsi di leggere/salvare
le proprietà che gli interessano: un valido aiuto in
questo compito è dato dalla aggiunta "Creazione
guidata interfaccia controlli ActiveX" (menù aggiunte),
che inserisce automaticamente il codice necessario per le proprietà
selezionate. Nell'evento ReadProperties è stata aggiunta
un'istruzione di gestione degli errori come suggerito dalla
guida, per evitare problemi nel caso in cui un furbastro abbia
modificato manualmente il file *.frm inserendo valori non validi
per una certa proprietà.
Non tutte le proprietà necessitano di essere salvate:
ad es. la proprietà NumeroMine non ha bisogno di essere
salvata, perché il suo valore è generato in fase
di esecuzione dall'applicazione client, non è modificato
dall'utente in fase di progettazione. C'è poi un terzo
evento, InitProperties, che viene generato quando l'istanza
del componente è creata per la prima volta (ad es. quando
il pulsante è disegnato sul form): infatti in questo
caso non esistono valori delle proprietà precedentemente
salvati, 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à disabilitato 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 creata l'istanza di esecuzione, l'evento ReadProperties
assegna a tale proprietà il valore appena salvato.
Tornando alla sostituzione delle mine vecchie con le nuove,
un'altra routine da modificare è quella dell'inizio di
una nuova partita (mnuNew_Click): praticamente si tratta 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:
Private
Sub mnuNew_Click()
Dim t As
Integer 'variabile temporanea
per eseguire i controlli
Dim i As
Integer 'contatore
Dim X As Integer, Y As
Integer 'coordinate del pulsante
con la mina
Dim x1 As
Integer, y1 As Integer
'coordinate dei pulsanti circostanti
quello
' con la mina
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
X = Int(PosMine(i) / mlNUMERO_COLONNE) 'riga
in cui si trova la mina
Y = (PosMine(i) Mod mlNUMERO_COLONNE) 'colonna
in cui si trova la mina
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 trattamento va fatto con le routine Mina_Click (che
ora diventa Mina_ClickLeft) e Mina_MouseDown (che ora diventa
Mina_ClickRight):
Private
Sub Mina_ClickLeft(Index
As Integer)
Dim X As
Integer, Y As Integer
'coordinate del pulsante premuto
Dim x1 As
Integer, y1 As Integer
'coordinate dei pulsanti circostanti
quello
' premuto
If
(ContaMine < MaxContaMine) And
(Mina(Index).Picture.Handle = 0) Then
If Mina(Index).NumeroMine
> 0 Then 'nessuna
mina esplosa, almeno una mina
' circostante
If Len(Mina(Index).Caption)
= 0 Then
ContaMine = ContaMine + 1
End If
Mina(Index).Caption = CStr(Mina(Index).NumeroMine)
ElseIf Mina(Index).NumeroMine
= -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).NumeroMine=0
(nessuna mina esplosa, nessuna mina
'circostante)
X = Int(Index / mlNUMERO_COLONNE) 'riga
in cui si trova il pulsante premuto
Y = Index Mod mlNUMERO_COLONNE 'colonna
in cui si trova il pulsante premuto
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
Private Sub Mina_ClickRight(Index
As Integer)
With
Mina(Index)
If .Picture.Handle = 0
Then 'nessuna
bandiera sul pulsante
Set .Picture = LoadPicture("BandieraCampoMinato.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. |