
Come creare un UserForm con un numero dinamico di controlli in grado di reagire a certi eventi? Basta creare un UserForm e un modulo di classe. Infatti, assegnando alla classe i controlli creati dinamicamente nello UserForm, questi si posso fare reagire. Il fine sarà quello di ottenere un solo modulo in modo che la procedura venga semplificata e limitata a due, tre linee di codice. Si creerà, così, uno UserForm e il suo modulo, direttamente come oggetto, quindi grazie al proprio modulo di classe.
Nelle opzioni di Excel bisogna spuntare la casella Considera attendibile l'accesso al modello a oggetti dei progetti VBA. Per poterlo fare accedere a Opzioni di Excel > Centro protezione > Impostazioni Centro protezione > Impostazioni macro. Sarà necessario spuntare altri due riferimenti: Microsoft Forms 2.0 Object Library e Microsoft Visual Basic For Applications Extensibility 5.3, per fare questo nell'Editor VBA cliccare sul menu Strumenti > Riferimenti...
Nell'esempio seguente, creiamo uno UserForm contenente due bottoni che reagiscono al clic e rinviino, nel codice richiamante, la loro Caption.
Creare un modulo di classe nel proprio progetto VBA e nominarlo PrimoEsempio (proprietà Name della classe) e inserirne il seguente codice:
Option Explicit
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'Necessità di spuntare i due riferimenti seguenti (da menu Strumenti > Riferimenti)
'Microsoft Forms 2.0 Object Library
'Microsoft Visual Basic For Applications Extensibility 5.3
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Public myForm As Object 'UserForm
Public WithEvents Button As MSForms.CommandButton 'Bottone
Public Dict As Object 'Objet Dictionnary = la collezione di oggetti
Private Name As String 'Name => consente la costruzione e la distruzione dello UserForm
Private Sub Class_Initialize()
'costruttore della classe
Set Dict = CreateObject("Scripting.dictionary")
End Sub
Public Function Value()
'il metodo Value della Classe consente la costruzione dello UserForm
'e restituire il valore
NewUsf "Il mio primo UserForm" 'creazione dello UserForm
NewButton "myButton1", "Primo Bottone, 120, 30, 5, 5 'creazione del primo bottone
NewBouton "myButton2", "Secondo Bottone", 120, 30, 5, 35 'creazione del secondo bottone
myForm.Show 'visualizzazione dello UserForm
On Error GoTo fin
Value = myForm.Tag 'diamo alla nostra funzione il valore contenuto nel tag dello UserForm
Unload myForm
Exit Function
fin:
End Function
Private Sub NewUsf(myCaption As String)
'procedura di creazione dello UserForm
Set myForm = ThisWorkbook.VBProject.VBComponents.Add(3)
Name = myForm.Name
VBA.UserForms.Add (Name)
Set myForm = UserForms(UserForms.Count - 1)
With myForm
.Caption = myCaption
.Width = 150
.Height = 100
End With
End Sub
Public Sub NewButton(Name As String, Caption As String, Width As Double, Height As Double, Left As Double, Top As Double)
'procedura di creazione di un bottone di controllo
Dim Obj
Set Obj = myForm.Controls.Add("forms.CommandButton.1")
If Obj = True Then Exit Sub
Dim cls As New PrimoEsempio
Set cls.myForm = myForm
Set cls.Button = Obj
With cls.Button
.Name = Name
.Caption = Caption
.Move Left, Top, Width, Height
End With
Dict.Add Name, cls
Set cls = Nothing
End Sub
Private Sub Button_Click()
'procedura evento del clic su bottone
myForm.Tag = Button.Caption
myForm.Hide
End Sub
Private Sub Class_Terminate()
'distruttore classe
Dim VBComp As VBComponent
Set Dict = Nothing 'elimina tutte le istanze della nostra classe => tutti i bottoni
If Name <> "" Then 'se si tratta dello UserForm (l'unica istanza ad avere una proprietà 'Name' riempito)
Set VBComp = ThisWorkbook.VBProject.VBComponents(Name) 'selezionarlo
ThisWorkbook.VBProject.VBComponents.Remove VBComp 'eliminarlo
End If
End Sub
Questo codice è semplificato. Si dispone, grazie al modulo di classe, di uno UserForm e di un metodo Value. Quest'ultimo sarà restituito nel punto desiderato utilizzando il seguente codice:
Sub test() Dim MyForm As New PrimoEsempio MsgBox MyForm.Value Set MyForm = Nothing End Sub
Colui che utilizza lo UserForm o la Classe sa cosa fare con un codice di tipo myForm.Value. Nulla di più facile: basterà inserire questo risultato in una cella, un textBox, o altro.
In questo esempio i tasti saranno creati casualmente, in un Frame che servirà da contenitore proprio dentro lo UserForm. Da notare, in questo esempio, che non verrà creato un metodo Value alla Classe, dato che non restituisce un valore.
Da inserire in un modulo standard:
Sub Usf_Campominato() Dim MyForm As New cCampominato MyForm.Show 0 , False End Sub
I parametri di questo UserForm sono: il primo (obbligatorio), 0.1 o 2, rappresenta la difficoltà (% numero di mine); il secondo parametro (facoltativo), vero o falso, rappresenta la Modalità Cheater attiva o meno.
Creare un modello di classe del proprio progetto su VBA, nominarlo cCampominato (proprietà del Name della classe) e inserire il codice seguente:
Option Explicit
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'Necessità di spuntare i due riferimenti seguenti (da menu Strumenti > Riferimenti)
'Microsoft Forms 2.0 Object Library
'Microsoft Visual Basic For Applications Extensibility 5.3
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'Variabili pubbliche
Public myForm As Object 'UserForm
Public Fram As MSForms.Frame 'Frame = contenitore dei bottoni
Public Dict As Object 'Objet dictionary
Public DictParent As Object 'Objet dictionary
Public Mine As Boolean 'Proprietà Mine se True = bottone piegato
Public Decouverte As Boolean 'Proprietà scoperto se True = "terreno (bottone) sminato"
'variabili private
Private Name As String 'Name => permette la costruzione e la distruzione dello UserForm
Private cVicini() As cCampominato ' Proprietà sotto forma array (tabella) con lista dei bottoni vicini
'variabili pubbliche "eventi"
Public WithEvents Button As MSForms.CommandButton 'bottone
'costanti
Private Const LARG_BTN As Byte = 18 'dimensione dei bottoni
Private Const MIN_ROW As Byte = 7 'minimo di linee
Private Const MAX_ROW As Byte = 30 - MIN_LIN 'massimo di linee
Private Const MIN_COL As Byte = 7 'minimo di colonne
Private Const MAX_COL As Byte = 40 - MIN_COL 'massimo di colonne
Private Const PERCENT_SIMPLE As Byte = 10 '% delle mine in modalità facile
Private Const PERCENT_MEDIUM As Byte = 2 * PERCENT_SIMPLE '% delle mine in modalità media
Private Const PERCENT_HARD As Byte = 3 * PERCENT_SIMPLE '% delle mine in modalità difficile
Private Const COL_MINE As Long = &H188B0 'colore dei bottoni minati (per scoprirli)
Private Const COL_BOTTON As Long = &H8000000F 'colore bottoni
Private Const COL_MINE_POSSIBLE As Long = &HFFFFFF 'colore se bottoni possibilmente minati (bottone visualizzato?) => dubbio
Private Const COL_MINE_PROB As Long = &H8080FF 'colore se bottoni probabilmente minati (bottone visualizzato!) => attenzione pericolo
Property Get Vicini() As cCampominato() 'proprietà di tipo tabella
'proprietà Vicini in lettura
Vicini = cVicini
End Property
Property Let Vicini(ByRef newVicini () As cCampominato)
'proprietà Vicini in Scrittura
cVicini = newVicini
End Property
Private Sub Class_Initialize()
'cosntruttore della classe cCampominato
Set Dict = CreateObject("Scripting.dictionary")
End Sub
Public Sub Show(ByRef Difficult As Long, Optional CheatMode As Boolean = False)
'Metodo Show : permette la visualizzazione dello UserForm
On Error GoTo ErroreParametriMacro ' Verifica se 'Considera attendibile l'accesso al modello a oggetti dei progetti VBA' è stato spuntato nelle opzioni di Excel
With ThisWorkbook.VBProject: End With
Dim Row As Long, Col As Long, NbRows As Long, NbColumns As Long
Dim NbMines As Long, MineAdress() As String, CptMine As Long
Randomize Timer 'inizio generatore numeri aleatori
NbLines = Int(MAX_ROW * Rnd) + MIN_ROW 'Numeri di linee di bottoni
NbColonnes = Int(MAX_COL * Rnd) + MIN_COL 'Numeri di colonne di bottoni
Select Case Difficult 'Numero di mine a seconda la difficoltà scelta
Case 0: Difficult = PERCENT_SIMPLE
Case 1: Difficult = PERCENT_MEDIUM
Case 2: Difficult = PERCENT_HARD
Case Else: Exit Sub
End Select
NbMines = (NbRows * NbColumns) * Difficult \ 100
ReDim MineAdress(NbMines)
For CptMine = 1 To NbMines 'coordinate delle Mine: Col-Row
MineAdress(CptMine) = Int(NbColonnes * Rnd) + 1 & "-" & Int(NbRows * Rnd) + 1
Next
Call Creation_Usf("Campominato", (NbColumns * LARG_BTN) + 5, (NbRows * LARG_BTN) + 22) 'creazione UserFom
Call New_Frame("Fram1", "", NbColumns * LARG_BTN, NbRows * LARG_BTN) 'creazione Frame
For Row = 1 To NbRows 'creazione bottoni
For Col = 1 To NbColumns
'I nomi dei bottoni: Col-Lin
Call Dico("Fram1").New_Button(Col & "-" & Row, "", LARG_BTN * (Col - 1), LARG_BTN * (Row - 1), Dentro(Col & "-" & Row, MineAdress), CheatMode)
Set Dict("Fram1").Dict(Col & "-" & Row).DictParent = Dict("Fram1").Dict
Next Col
Next Row
myForm.Tag = Timer 'memorizza ora inzio partita nella proprietà Tag dello UserForm
myForm.Show 'visualizzazione del campo minato
Exit Sub
ErroreParametriMacro:
MsgBox "Si prega di attivare l'opzione: Considera attendibile l'accesso al modello a oggetti dei progetti VBA"
End Sub
Private Sub Creazione_Usf(Titolo As String, Larghezza As Double, Altezza As Double)
'creazione Userfom
Set myForm = ThisWorkbook.VBProject.VBComponents.Add(3 'aggiunta al progetto di un modello di UserForm
Name = myForm.Name 'selezionare il nome
VBA.UserForms.Add (Name) 'aggiunta dello UserForm al progetto VBA
Set myForm = UserForms(UserForms.Count - 1) 'assegnare questo UserForm alla varibile oggetto
With myForm 'assegnare alcune proprietà
.Caption = Titolo 'titolo
.Width = Larghezza 'larghezza
.Height = Altezza 'altezza
End With
End Sub
Public Sub New_Frame(myName As String, Titolo As String, Larghezza As Double, Altezza As Double)
'creazione Frame
If Dict.Exists(myName) = True Then Exit Sub 'uscire se già esistente
Dim myClass As New cCampominato 'creazione di una nuova istanza della nostra classe
Set myClass.Fram = myForm.Controls.Add("forms.frame.1") 'Creazione di un controllo di tipo Frame
Set myClass.myForm = myForm 'assegnare lo UserForm alla proprietà "myForm" della istanza di classe
With myClass.Fram 'assegnargli alcune proprietà
.Name = myName 'nome
.Caption = Titolo 'titolo
.Move 0, 0, Larghezza, Altezza 'posizione
End With
Dict.Add myName, myClass 'aggiungere la nostra istanza al Dizionario
Set myClass = Nothing
End Sub
Public Sub New_Button(myName As String, Titre As String, Sinistra As Double, Alto As Double, boolMine As Boolean, Optional CheatMode As Boolean)
'creare i Bottoni
If Dico.Exists(myName) = True Then Exit Sub 'uscire se esiste
Dim myClass As New cCampominato 'creare una nuova istanza della classe
Set myClass.Button = Fram.Controls.Add("forms.CommandButton.1") 'Creare un control di tipo Bottone
Set myClass.myForm = myForm 'assegnare lo UserForm alla proprietà "myForm" della istanza della classe
myClass.Mine = boolMine 'definire la proprietà Mine del bottone (True o False)
With myClass.Button 'definire alcune proprietà del bottone
.Name = myName 'nome
.Caption = Titolo 'Caption
.Move Sinistra, Alto, LARG_BTN, LARG_BTN 'posizione
If CheatMode Then 'In cheat mode, DARE COLORE AI BUTTONI MINE
If boolMine Then .BackColor = COL_MINE Else .BackColor = COL_BUTTON
Else
.BackColor = COL_BUTTON
End If
End With
Dict.Add myName, myClass 'aggiungere l'istanza della classe al Dizionario
Set myClass = Nothing
End Sub
Private Function Dentro(adresse As String, Tb) As Boolean
'funzione per cercare un valore in una variabile tabella
Dim i As Long
For i = 0 To UBound(Tb) 'loop su tutta la var tabella
If Tb(i) = adress Then Dentro = True: Exit Function 'se trovato l'elemento cercato => funzione true, uscire
Next i
End Function
Private Sub Button_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
'Procedura evento quando si schiaccia, tasto destro o sinistro del mouse, su un Buttone dello UserForm
If Button = XlMouseButton.xlSecondaryButton Then 'clic destro
Select Case Button.Caption 'a seconda del Caption del buttone 4 possibilità
Case "": Button.Caption = "!": Button.BackColor = COL_MINE_PROB 'se caption è vuoto: visualizzare ! (= attenzione pericolo)
Case "!": Button.Caption = "?": Button.BackColor = COL_MINE_POSSIBLE 'se caption è !: visualizzare? (= dubbio)
Case "?": Button.Caption = "": Button.BackColor = COL_BUTTON 'se caption è ?: visualizzare niente (= levare il dubbio)
Case Else: 'altrimenti (caption = chiffra (Numero di mine vicine)) non fare niente
End Select
ElseIf Button = XlMouseButton.xlPrimaryButton Then 'clic sinistro
If DictParent.Item(Button.Name).Mine Then 'se bottone minato
Call Visualizza_Tutte_Mine 'visualizzare tutte le mine
MsgBox "Hai perso!" 'partita persa
myForm.Hide 'uscire
Else 'se bottone non minato
Button.BackColor = COL_BUTTON 'ripristinare il colore predefinito in caso di clic destro predente
Dim myClass As cCampominato 'Richiamare la procedura di sminamento
Set myClass = DictParent.Item(Button.Name) 'procedura ricursiva di propagazione
Call smina(myClass) 'bottoni i quali vicini non sono minati
End If
End If
If Partita_Vince Then 'avviare la funzione Partita_Vince
Call Visualizza_Tutte_Mine 'se vince : visualizza mine e messaggio:
MsgBox "Bravo!" & vbCrLf & "Hai vinto! Tempo scaduto : " & CInt(Timer - CDbl(myForm.Tag)) & " secondi.", vbOKOnly + vbExclamation, "VINCE!"
myForm.Hide 'uscire dallo UserForm. Ciò che avvia il distruttore della classe
End If
End Sub
Private Sub Visualizza_Tutte_Mine()
'Se partita persa, colore tutti bottoni minati
Dim chiave
For Each chiave In DictParent.keys 'loop su tutte le chiave del DictParent
'quest'ultimo contiene tutte le istanze della classe contenute nel Frame
'se l'istanza di classe è minata => colore
If DictParent.Item(chiave).Mine Then DictParent.Item(chiave).Button.BackColor = COL_MINE
Next
End Sub
Private Sub Smina (Cl As cCampominato)
'procedura ricursiva di propagazione della scoperta dei bottoni non minati
Dim NbMine As Integer
NbMine = CountMine(Cl.Button.Name) 'fare il test del numero delle mine vicine
If NbMines > 0 Then 'se il bottone a al meno una mina tra i vicini
Cl.Bouton.Caption = NbMines 'visualizzare il numero delle mine
Cl.Scoperta= True 'visualizzare il bottone
Else 'altrimenti
If Cl.Scoperta = False Then 'se il bottone non è ancora scoperto
Cl.Scoperta = True 'scoprirlo
Cl.Bouton.Visible = False 'rendere la scoperta visibile dal giocatore (=> il bottone sparisce)
Quale_vicino Cl 'cercare quali bottoni sono i vicini di questo bottone
Dim Tb() As cMine, i As Integer
Tb = Cl.Vicini
For i = 0 To UBound(Tb) 'scoprire tutti i bottoni vicini (RICORSIVITÀ)
Scopri_Mina Tb(i)
Next
End If
End If
End Sub
Private Function ContareMine(Bott As String) As Integer
'funzione per contare le mine contenute nei bottoni vicini
Dim i As Integer, j As Integer, Col As Integer, Lin As Integer
Dim myClass As cMine
For i = -1 To 1 'incrementare la colonna e la linea da -1 a 1 solo per i bottoni vicini a
For j = -1 To 1 'a quello il cui nome è passato in parametro
Col = CInt(Split(Bott, "-")(0)) + i 'incrementare n° di colonna
Lin = CInt(Split(Bout, "-")(1)) + j 'incrementare n° di linea
If DictParent.Exists(Col & "-" & Lin) Then 'se il bottone esiste (non andare fuori lo UserForm)
Set myClass = DictParent.Item(Col & "-" & Lin) 'assegnare alla variabile il bottone vicino
If myClass.Mine Then ContareMine = ContareMine + 1 'se est minato, incrementare la funzioni di 1
End If
Next j
Next i
End Function
Private Sub Quale_vicino(Cl As cMine)
'procedura per assegnare, alla proprietà Vicini() di un bottone, la lista dei bottoni che lo circondano
Dim i As Integer, j As Integer, Col As Integer, Lin As Integer
Dim myClass As cMine, ListaVicini() As cMine, cpt As Byte
For i = -1 To 1 'incrementare la colonna et la linea da -1 a 1 solo per i bottoni vicini a
For j = -1 To 1 'a quello il cui nome è passato in parametro
Col = CInt(Split(Cl.Bottone.Name, "-")(0)) + i 'incrementare n° di colonna
Lin = CInt(Split(Cl.Bouton.Name, "-")(1)) + j 'incrementare n° di linea
'se il bottone esiste e il suo nome è diverso di quello passato in parametro
If DictParent.Exists(Col & "-" & Lin) And Cl.Bouton.Name <> Col & "-" & Lin Then
Set maClass = DicoParent.Item(Col & "-" & Lin) 'assegnare alla variabile il bottone vicino
ReDim Preserve ListaVicini(cpt) 'ridimensionare variabile tabella
Set ListaVicini(cpt) = myClass 'assegnare l'istanza di classe (bottone) alla tabella
cpt = cpt + 1
End If
Next j
Next i
Cl.Vicini = ListaVicini 'assegnare la proprietà Vicini dell'istanza di classe (del bottone)
End Sub
Private Function Partita_Vinta() As Boolean
Dim chiave
For Each chiave In DictParent.keys 'loop su tutte le chiavi del DictParent
'(quindi tutte le istanze della classe, quindi su tutti i bottoni)
'Se il bottone non è "scoperto" e non contiene una mina
If DictParent.Item(chiave).Scoperta = False And DictParent.Item(chiave).Mine = False Then
Partita_Vinta = False 'allora la partita non est finita
Exit Function
End If
Next
Partita_Vinta = True
End Function
Private Sub Class_Terminate()
'distruttore della della classe cMine
Dim VBComp As VBComponent
Set Dico = Nothing 'cancellare tutte le istanze della classe => tutti i bottoni
If Name <> "" Then 'si tratta dello UserForm (unica istanza avendo una proprietà "Name" assegnata)
Set VBComp = ThisWorkbook.VBProject.VBComponents(Name) 'indicarlo
ThisWorkbook.VBProject.VBComponents.Remove VBComp 'cancellarlo
End If
End Sub
Nota Bene: il codice della procedura ricorsiva di propagazione della scoperta dei bottoni non minati è stata semplificata grazie al nostro modulo di classe.
Foto: © Pexels.