(pubblicato il 16 febbraio 2003)

Introduzione a Visual Basic for Application

Premessa


In questa  parte di corso saranno spiegati i fondamenti del linguaggio Visual Basic in ambito ACCESS: si partirà dalle nozioni di base, dando per scontato che il lettore non abbia alcuna conoscenza di programmazione, e si procederà man mano approfondendo argomenti più complessi.


Visual Basic è chiamato così perché deriva dal linguaggio BASIC (Beginners All-Purpose Symbolic Instruction Code), un tipo di linguaggio semplice e molto diffuso che era implementato, ad es., nei mitici Commodore 64; nel corso dell'evoluzione del BASIC si sono aggiunte molte istruzioni e sono cambiate diverse caratteristiche, la principale delle quali è indicata dal termine "visual": come vedremo tra poco, infatti, per progettare l'interfaccia grafica di un'applicazione si ha a disposizione un ambiente grafico in cui si possono utilizzare numerosi componenti di vario tipo, come i pulsanti, le caselle di testo ecc. In questo modo è possibile creare l'interfaccia grafica in modo semplice e veloce, senza dover ogni volta scrivere le istruzioni necessarie per ordinare al computer di creare le finestre di cui abbiamo bisogno, come invece accadrebbe se utilizzassimo un linguaggio non "visuale". 

I forms rappresentano le finestre che saranno visualizzate dalla vostra applicazione. La "casella degli strumenti" (Fig.2), contiene una serie di icone, ciascuna delle quali rappresenta un particolare oggetto inseribile sul form: ad es., ci sono le label (etichette), le textbox (caselle di testo), i commandbutton (pulsanti), ecc.; selezionando un'icona è possibile disegnare direttamente sul form l'oggetto ad essa associato.

Fig.2: Casella degli strumenti utilizzabili in ACCESS


Fig.3: La finestra delle Proprietà visualizza le proprietà dell'oggetto selezionato sul form (maschera). A seconda dell'oggetto selezionato, le proprietà visualizzate nella tabella saranno differenti.

Ogni oggetto, o controllo, presente nella casella degli strumenti è dotato di un insieme di proprietà, metodi ed eventi.

Proprietà le proprietà rappresentano gli attributi che definiscono l'aspetto e varie funzionalità di ogni controllo; ad es., la proprietà Name indica il nome con cui quel controllo è identificato all'interno del codice; le proprietà Height e Width indicano l'altezza e la larghezza del controllo, ecc. Molte proprietà sono comuni a diversi oggetti (ad es. qualunque controllo dispone della proprietà Name), altre invece sono specifiche di un controllo particolare (ad es., la proprietà Intervallo timer è disponibile solo per il form). Solitamente le proprietà possono essere lette e anche impostate, ovvero è possibile sia leggere il valore della proprietà, sia assegnare ad essa un nuovo valore: in tal caso si dice che la proprietà è di lettura e scrittura; tuttavia esistono anche proprietà di sola lettura, alle quali non è possibile assegnare un nuovo valore, e di sola scrittura.

Metodi un metodo è un'azione che l'oggetto può eseguire: ad es., l'oggetto form dispone del metodo Repaint. Il metodo Repaint completa qualsiasi aggiornamento di schermo in sospeso relativo a una determinata maschera.   Come per le proprietà, alcuni metodi sono comuni a diversi controlli, altri invece sono specifici di un controllo particolare.

Eventi gli eventi sono, come dice il nome, "situazioni" generate dal controllo quando si verificano certe condizioni; solitamente, ma non necessariamente, gli eventi si producono in conseguenza di un'azione dell'utente: per es., quando l'utente fa clic su un pulsante, il controllo che identifica quel pulsante genera un evento click; un esempio di evento non generato dall'utente è l'evento Timer che viene generato dopo un certo numero di millisecondi specificato dalla proprietà "Intervallo Timer" del form.

Cliccando su si apre un'altra finestra, l'editor del codice, in cui il programmatore può scrivere le istruzioni necessarie a far compiere al computer determinate operazioni. L'editor del codice si presenta con un'area bianca in cui va scritto, appunto, il codice, e con due caselle a discesa poste nella parte superiore della finestra (Fig.4): la casella di sinistra fornisce l'elenco degli oggetti disponibili sul form selezionato, oltre alla sezione Generale, che serve per le dichiarazioni delle variabili, come vedremo oltre; la casella di destra fornisce invece un elenco degli eventi associati al controllo selezionato nella casella di sinistra.


Fig.4: Casella destra e sinistra

Ad es., selezionando nella casella di sinistra l'oggetto form e nella casella di destra l'evento click, verrà visualizzata nell'area del codice la sezione relativa all'evento click dell'oggetto form, in cui il programmatore può scrivere le istruzioni che devono essere eseguite quando l'utente fa clic sul form. Per accedere alle proprietà e ai metodi dei vari controlli, bisogna scrivere il nome del controllo seguito da un punto "." e dal nome del metodo o della proprietà (ad es. "me.repaint" oppure "Command1.Caption").



Passiamo ora alle fondamenta del linguaggio Visual Basic. Come ogni linguaggio di programmazione, il Visual Basic è costituito da un insieme di parole chiave, funzioni, istruzioni che seguendo determinate regole sintattiche permettono al programmatore di impartire "ordini" al computer al fine di produrre un certo risultato. Prima di vedere nello specifico quali sono le particolarità del linguaggio Visual Basic, è necessaria un'introduzione sulle principali strutture che caratterizzano un qualunque linguaggio di programmazione:

Istruzioni un'istruzione è un insieme di una o più parole che indicano al computer di eseguire una particolare operazione: ad es., l'istruzione msgbox permette mostrare una certa informazione sullo schermo.

Routine una routine è un insieme di una o più istruzioni, eseguite in risposta ad un'azione dell'utente, come ad es. la pressione di un tasto, oppure in seguito ad una "chiamata" della routine da parte del programmatore.

Variabili le variabili sono dei "contenitori" alle quali è associato un certo valore (non necessariamente numerico); ad es., se la variabile di nome messaggio contiene il valore "questo è un messaggio di prova", utilizzando l'istruzione msgbox seguita dal nome della variabile (cioè scrivendo: msgbox messaggio) potremo vedere visualizzata, ad es. su una finestra, la frase "questo è un messaggio di prova".

Tornando al Visual Basic, cominciamo col vedere come si dichiarano le variabili, ovvero come si fa a ordinare al computer di utilizzare una certa variabile e di assegnare a quest'ultima un certo valore; la sintassi per dichiarare una variabile in Visual Basic è la seguente:

Dim NomeVariabile As TipoVariabile

Ad esempio:

Dim Pippo As Integer

mentre per assegnarle un valore bisogna scrivere il nome della variabile, il simbolo "=" e il valore da assegnare (es.: pippo=10). Il nome della variabile è a completa discrezione del programmatore, tuttavia bisogna osservare qualche restrizione: ad es., il nome non può essere più lungo di 255 caratteri, non può contenere punti o spazi, deve iniziare con una lettera, non può essere uguale a una delle parole-chiave utilizzate da Visual Basic Ad es., non si può fare:

Dim dim As Integer

perché "dim" è una parola riservata per la dichiarazione delle variabili. Il tipo della variabile indica il tipo di valore che è possibile assegnare alla variabile, ed è possibile scegliere tra diverse alternative; i tipi principali supportati da Visual Basic sono:
 

Tipo di dati Spazio occupato in memoria Intervallo di valori
Byte 1 byte 0/255
Integer 2 byte -32768/+32767
Long 4 byte -2147483648 / +2147483647
String variabile da 0 a circa 65000 caratteri
boolean 2 byte vero o falso
double 8 byte 1.79769313486231E308 e -4,94065645841247E-324
single 4 byte 3,402823E38 e -1,401298E-45
Date 8 byte  

Ce ne sono anche altri, ma per ora bastano questi; come si può notare, i tipi di variabili si distinguono sia per la quantità di memoria che richiedono, sia per l'intervallo di valori che possono assumere; i primi tre tipi servono per memorizzare valori numerici, mentre il quarto memorizza delle stringhe, ovvero delle sequenze di caratteri (parole o frasi, come nell'esempio della variabile messaggio, in cui messaggio è una variabile di tipo string). La dichiarazione delle variabili, in Visual Basic, non è obbligatoria: quando Visual Basic trova un nome di variabile non dichiarato, crea automaticamente una variabile con quel nome (è ciò che si chiama dichiarazione implicita); questa può sembrare una bella comodità, ma in realtà è più una fonte di errori che altro: basta infatti un piccolo errore nel digitare il nome di una variabile, e Visual Basic penserà NON che la variabile è sempre la stessa ma con un nome sbagliato, BENSÌ che si tratta di un'altra variabile, che verrà automaticamente creata. Così, se per es. scrivo:

Dim Pippo As Integer
Pippo = 10
Pipo = 5


dove per errore nella terza riga ho scritto "pipo" anziché "pippo", non avrò una sola variabile (pippo) che contiene il valore 5, ma DUE variabili (Pippo e Pipo) che contengono rispettivamente i valori 10 e 5. Per evitare questo tipo di errori, è possibile rendere obbligatoria la dichiarazione esplicita di TUTTE le variabili: per fare ciò, bisogna andare nella sezione "generale" del nostro form ed aggiungere l'istruzione:

Option Explicit

che per l'appunto informa visual basic del fatto che la dichiarazione esplicita è obbligatoria. In questo modo Visual Basic genererà un errore perché riconosce che la variabile "Pipo" non è stata dichiarata.

Seconda lezione - le variabili in Visual Basic

Una cosa importante da tenere presente a proposito delle variabili è la loro area di visibilità: ciò deriva dal fatto che una variabile può essere dichiarata in punti diversi del codice, e cioè nella sezione dichiarazioni di un form oppure all'interno di una routine; nel primo caso, la variabile sarà visibile, e quindi potrà essere utilizzata, in tutto il form (si dice che è una variabile dichiarata a livello di modulo); nel secondo caso la variabile sarà visibile soltanto all'interno della routine in cui è dichiarata (si dice che è una variabile locale). Per capire bene la differenza proviamo ad aprire un nuovo progetto e a scrivere, nella sezione dichiarazioni del form:

Option explicit
Dim VarGen As Integer

Public Sub Prova()
Dim VarLoc As Integer
   VarGen=1
   VarLoc=1
End Sub

e, nella routine Load del form:

Private Sub
Form_Load()
VarGen=2
VarLoc=2
Prova
Debug.Print VarGen, VarLoc
End Sub

Nota: il comando Debug.Print viene utilizzato per visualizzare il valore delle due variabili nella finestra Immediata

In parole povere, abbiamo dichiarato una variabile di nome VarGen visibile in tutto il form, e una variabile di nome VarLoc visibile solo nella routine di nome Prova: all'interno di questa routine alle due variabili viene assegnato il valore 1, mentre quando il form viene caricato si verifica l'evento Load, durante il quale vengono eseguite le istruzioni che abbiamo appena inserito: dapprima vengono eseguite le due istruzioni di assegnamento e poi l a routine Prova, infine il valore delle due variabili viene visualizzato nella finestra "Immediata", di cui si parlerà più avanti. Per visualizzare la finestra basta andare sul menù "Visualizza" e scegliere la voce "Finestra Immediata" (questo nell'ambiente VBA di access).



Ora, apriamo la nostra maschera in "Visualizzazione Maschera": quello che succederà è l'interruzione del programma con la visualizzazione del seguente messaggio di errore:

variabile non definita

dove la variabile è quella evidenziata, cioè la variabile VarLoc. Questo succede proprio perché all'esterno della routine Prova la variabile locale VarLoc non esiste: infatti le variabili locali vengono create all'inizio della routine in cui sono dichiarate, e automaticamente distrutte quando la routine finisce di essere eseguita. La variabile VarGen, invece, è visibile sia nell'evento load del form, sia nella routine prova, proprio perché è stata dichiarata nella sezione delle dichiarazioni generali del form. Faccio notare che se si prova a eliminare l'istruzione Option Explicit che, se avete seguito il mio suggerimento (v. lezione precente), dovrebbe essere inserita automaticamente, quando il progetto viene avviato non si verifica alcun errore e nella finestra immediata viene stampata la riga:

2 2

Questo succede perché senza l'istruzione Option Explicit durante l'esecuzione vengono create DUE variabili VarLoc: una dichiarata esplicitamente nella routine Prova e distrutta alla fine della routine, l'altra dichiarata implicitamente nell'evento load del form e a cui viene assegnato il valore 2, che viene poi correttamente visualizzato nella finestra immediata.

Dato che in una applicazione possono esserci ben più di un unico form, ci si può chiedere se le variabili dichiarate a livello di modulo in un certo form siano visibili anche negli altri form: la risposta è positiva solo se nella dichiarazione delle variabili si usa la parola chiave Public, ad es.:

Public VarGen As Integer

questa parola chiave specifica appunto che la variabile dichiarata è pubblica, cioè è visibile in tutta l'applicazione a cui appartiene il form in cui è dichiarata; utilizzando invece la parola chiave Private o la classica Dim, la variabile sarà privata e quindi visibile solo nel form in cui è dichiarata. Prima ho detto che le variabili locali vengono create all'inizio della routine in cui sono dichiarate e distrutte alla fine: da ciò deriva che se la stessa routine viene richiamata più volte, la variabile viene creata e distrutta altrettante volte, ed ogni volta la variabile creata non ha alcuna connessione con la variabile omonima creata la volta precedente. Se ad esempio modifichiamo la nostra routine Prova in questo modo:

Public Sub Prova ()
Dim VarLoc As Integer
VarLoc=VarLoc + 1
Debug.Print VarLoc
End Sub

E nell'evento Load scriviamo:

Private Sub
Form_Load()
Prova
Prova
End Sub

e proviamo ad avviare il progetto, vedremo che nella finestra immediata viene scritto:

1 1

e non, come qualcuno si potrebbe aspettare:

1 2

Se si desidera che una variabile locale conservi il proprio valore tra una chiamata e l'altra della routine, bisogna dichiararla con la parola chiave Static:

Public Sub
Prova()
Static VarLoc As Integer
   VarLoc = VarLoc + 1
   Debug.Print VarLoc
End Sub

Se proviamo ancora ad eseguire il progetto, vedremo che questa volta nella finestra Immediata appare scritto:

1 2

La variabile VarLoc viene quindi creata solo una volta (la prima volta che la routine viene eseguita) e viene distrutta solo quando chiudiamo la maschera (cliccando ovviamente sulla X in alto a destra nel form, come per qualunque altra finestra); tuttavia questa variabile è ancora una variabile locale, quindi non è utilizzabile all'esterno della routine. Il motivo per cui VarLoc assume il valore 1 dopo l'istruzione:

Varloc = Varloc + 1

è presto detto: in Visual Basic ogni variabile numerica viene automaticamente inizializzata al valore 0 quando viene creata; le variabili stringa, invece, vengono inizializzate con una stringa vuota, ossia con zero caratteri.

Nota: Nel caso in cui si assegnino nomi composti agli elementi del codice la cui denominazione è arbitraria (variabili, costanti, procedure, funzioni), è buona norma mantenere la prima lettera maiuscola di ciascun nome, in modo da operare una separazione intuitiva. Nel corso di questa lezione ad esempio si è trattata la variabile VarLoc il cui nome deriva dall'unione dei nomi Variabile Locale. Lo stesso vale per VarGen, unione di Variabile e Generale

Terza lezione - le routine in Visual Basic

Dopo aver visto in dettaglio le variabili, vediamo meglio le routine. Innanzitutto bisogna dire che le routine, come le variabili, devono essere dichiarate: la dichiarazione avviene specificando la parola chiave Sub (in seguito vedremo che si possono utilizzare anche altre parole chiave) seguita dal nome della routine e da due parentesi tonde, la prima aperta e la seconda chiusa (tra poco vedremo cosa si può mettere all'interno delle parentesi). Ad esempio:

La parola Sub può essere preceduta dalle parole chiave Private o Public (NON da Dim: quest'ultima serve solo per le variabili), a seconda che la routine debba essere visibile solo nel form corrente o in tutto il progetto.  La dichiarazione delle routine serve a indicare dove INIZIA la routine: per indicare dove FINISCE è necessario scrivere End Sub dopo l'ultima istruzione appartenente alla routine. Quindi:

Nella prima lezione avevo detto che le routine sono un'insieme di più istruzioni: esse risultano molto comode quando si ha bisogno di eseguire più volte uno stesso gruppo di istruzioni, in modo che quando si ha necessità di eseguire una certa operazione, è sufficiente richiamare quella routine anziché riscrivere ogni volta le medesime istruzioni. Supponiamo, ad es., di voler calcolare i quadrati dei valori 2, 5, e 8 e di volerli visualizzare nella finestra Immediata: senza usare le routine saremmo costretti a scrivere (ad es. nell'evento Load del nostro form):
 

Private Sub Form_Load()
	'dichiaro la variabile:
	Dim x As Integer
	x = 2 ^ 2 'il simbolo "^" serve a indicare l'elevamento a potenza
	'stampo il risultato:
	Debug.Print x 
	x = 5 ^ 2
	Debug.Print x
	x = 8 ^ 2
	Debug.Print x
End Sub

Nota: il simbolo " ' " serve a indicare un commento al codice: il commento viene del tutto ignorato da Visual Basic, la sua utilità deriva dal fatto che permette a chi legge il codice di comprendere meglio cosa fanno le istruzioni.

Utilizzando invece una routine di nome "Quadrato" che calcola il quadrato di un numero potremmo scrivere, nella sezione delle dichiarazioni:

Dim
x As Integer
___________________________________________________________
Private Sub Quadrato() 'qui inizia la routine
Dim y As Integer'dichiaro una variabile locale
y = x ^ 2 'calcolo il quadrato
Debug.Print y 'stampo il risultato
End Sub 'qui finisce la routine

e, nell'evento Load del form:

Public Sub Form_Load ()
	x = 2 'imposto la variabile
	Quadrato 'calcolo il quadrato e stampo il risultato
	x = 5
	Quadrato
	x = 8
	Quadrato
End Sub

In un esempio così semplice è poco utile ricorrere alle routine, ma se pensate che una routine potrebbe contenere centinaia di istruzioni che vanno ripetute più o meno spesso vi accorgerete della loro grande utilità; un aspetto da non trascurare è infatti anche la leggibilità del codice, ovvero la sua facilità di comprensione: utilizzando le routine per eseguire operazioni complesse è molto più semplice capire come funziona un programma (procedendo con gli esempi ve ne renderete conto sempre meglio).

Nell'esempio qui sopra abbiamo impostato una variabile dichiarata a livello di modulo (la variabile x) in modo che potesse essere utilizzata anche nella routine: in realtà ciò non è necessario, infatti è possibile utilizzare il passaggio di parametri alla routine. Un parametro è una variabile che viene comunicata alla routine, la quale può utilizzarla più o meno come se fosse una sua variabile locale: in questo modo è possibile travalicare i limiti imposti dall'area di visibilità delle variabili (v. lez. precedente). Ad es., se noi avessimo dichiarato la variabile x non nella sezione delle dichiarazioni ma all'interno dell'evento Load, avremmo ottenuto un errore del tipo:

perché la variabile non sarebbe stata visibile nella routine quadrato (a meno che abbiate dimenticato di inserire l'istruzione "Option Explicit": d'ora in poi darò sempre per scontato che sia attivata la dichiarazione obbligatoria delle variabili). Invece utilizzando il passaggio di parametri questo problema non sussiste più. Per indicare a Visual Basic che la nostra routine vuole uno o più parametri bisogna elencare i parametri, o argomenti, all'interno delle parentesi nella dichiarazione della routine:

Private Sub
Quadrato (Param As Integer)
Dim y As Integer
     y = Param ^ 2
     Debug.Print y
End Sub

Come avrete notato, l'indicazione dell'argomento segue le stesse regole della dichiarazione delle variabili, eccezion fatta per la parola chiave Dim (o Private o public) che in questo caso NON va inserita perché non avrebbe senso: l'argomento di una routine è utilizzabile solo all'interno di quella routine. Per indicare più parametri bisogna separarli con la virgola: ad es:

... (Param1 As Integer, Param2 As String ...) ... 

Il nome del parametro non ha nulla a che vedere col nome della variabile passata come argomento (nel nostro caso la variabile x): nel caso in cui i nomi fossero uguali, all'interno della routine verrebbe comunque utilizzato il parametro e non la variabile originaria. All'interno dell'evento load del form potremo ora scrivere:

Public Sub
Form_Load()
Dim x As Integer
    x = 2
   Quadrato x
    x = 5
   Quadrato x
   x = 8
   Quadrato x
End Sub

e nella finestra immediata vedremo:

4
25
64

Naturalmente l'argomento da passare alla routine può essere una variabile dichiarata da noi, ma anche un valore fisso: possiamo quindi fare a meno, nel nostro caso, di utilizzare la variabile x e scrivere direttamente:

Public Sub
Form_Load()
    Quadrato (2) 'oppure Quadrato 2
    Quadrato (5) 'oppure Quadrato 5
    Quadrato (8) 'oppure Quadrato 8
End Sub

ottenendo il medesimo risultato: ogni volta che viene chiamata la routine quadrato, la variabile param assumerà di volta in volta il valore 2, 5, 8. Avrete sicuramente capito che per eseguire una routine è sufficiente scriverne il nome, eventualmente seguito dagli argomenti richiesti dalla routine: gli argomenti andranno separati da virgole nel caso siano più di uno (ad es. quadrato 2, 3, "Pippo" se gli argomenti sono tre: i primi due numerici e il terzo una stringa). E' però possibile anche utilizzare l'istruzione Call, seguita dal nome della routine e dai parametri, questa volta però obbligatoriamente racchiusi tra parentesi:

        Call Quadrato (2)  

oppure:

	Call AreaTriangolo (base, altezza)  

se gli argomenti sono diversi.
L'uso delle parentesi sarà fondamentale nell'uso delle funzioni, che vedremo la prossima volta.

Quarta lezione - le funzioni in Visual Basic

Dopo le routine, tocca alle funzioni: la differenza tra una routine e una funzione è che la prima esegue un certo numero di istruzioni, la seconda esegue un certo numero di istruzioni e in più restituisce un valore, che quindi può essere memorizzato in una variabile.
L'uso delle funzioni è comodo quando è opportuno sapere quale sia il risultato finale delle operazioni eseguite : ad es., la funzione potrebbe restituire un valore di errore se qualcosa è andato storto durante la sua esecuzione, oppure potrebbe restituire il risultato di un'operazione matematica come quella eseguita dalla routine quadrato che abbiamo visto nella lezione precedente.
Per dichiarare una funzione bisogna usare la parola chiave Function al posto di Sub; inoltre è sempre meglio, se possibile, specificare il tipo di dati restituito dalla funzione.
Ad es.:

Private Function Quadrato (Param As Integer) As Long
	Quadrato=Param ^ 2
	Debug.Print Quadrato
End Function

P.S.: il Debug.Print è inserito come istruzione di controllo ma è inutile

Il valore che la funzione deve restituire deve essere assegnato al nome della funzione:

	Quadrato = Param ^ 2  

che viene quindi trattato come se fosse una variabile: perciò rispetto alla routine Quadrato (v.lez. precedente) si può fare a meno di dichiarare una variabile locale a cui assegnare il risultato dell'elevamento a potenza.
Inoltre, possiamo fare a meno di usare l'istruzione Debug.Print all'interno della funzione, perché il valore che vogliamo visualizzare è quello restituito dalla funzione e quindi è visibile anche all'esterno di essa: nell'evento Load del form, anziché scrivere

	Quadrato 2
	Quadrato 5
	Quadrato 8

possiamo quindi scrivere:

	Debug.Print Quadrato(2)
	Debug.Print Quadrato(5)
	Debug.Print Quadrato(8)

In questo caso è necessario usare le parentesi tonde: queste vanno sempre indicate se si vuole che la funzione restituisca effettivamente un valore; infatti è anche possibile richiamare la funzione senza parentesi:

	Quadrato 2  

come se fosse una routine, ma non sarà possibile scrivere:

	Debug.Print Quadrato 2  

perché chiamando la funzione senza usare le parentesi si impedisce che essa restituisca un valore.
Lo stesso accade se si chiama la funzione con l'istruzione Call; pertanto mentre una routine può essere chiamata indifferentemente con o senza le parentesi, una funzione deve essere chiamata con le parentesi (ma senza usare l'istruzione Call) affinché restituisca un valore.
Per quanto riguarda il passaggio di parametri, valgono le stesse regole che abbiamo visto per le routine; a questo proposito c'è da sapere un'altra cosa: i parametri possono essere preceduti dalla parola chiave ByVal o ByRef.
Queste parole chiave specificano, rispettivamente, che il parametro viene passato per valore o per riferimento: senza entrare troppo nei dettagli, è sufficiente dire che nel primo caso la funzione (o la routine) conosce soltanto il valore della variabile passata come argomento, mentre nel secondo caso conosce, per così dire, la variabile stessa, potendo quindi intervenire direttamente su di essa.
Concretamente ciò significa che se la variabile è passata per valore, la funzione (o la routine) potrà eseguire operazioni sul valore della variabile ma non potrà modificarla (alla fine della routine il parametro riassume il valore originale); se invece la variabile è passata per riferimento, la funzione potrà modificare direttamente la variabile.

Un esempio chiarirà tutto: creiamo una funzione con due argomenti, uno passato per valore, l'altro per riferimento e facciamo qualche operazione su di essi:

Private Function Prova (ByVal Valore As Integer, _
		        ByRef Riferimento As Integer) As Integer
	Valore = Valore + 1
	Riferimento = Riferimento + 1
	Prova = Valore + Riferimento
End Function

La funzione non fa altro che incrementare di 1 i due argomenti e restituirne la somma.
Ora dichiariamo, nell'evento Load del form, due variabili e passiamole alla funzione:

Private Sub Form_Load()
	Dim Var1 As Integer, Var2 As Integer
	Dim Risultato As Integer
	Var1 = 3
	Var2 = 10
	Risultato = Prova(Var1, Var2)
	Debug.Print Risultato
	Debug.Print Var1, Var2 
End Sub

Eseguendo il progetto, noteremo che nella finestra immediata compaiono i valori:

15
3        11

15 è il risultato della funzione ((3+1)+(10+1)=15); la variabile Var1, che è stata passata per valore e che quindi non ha potuto essere stata modificata, conserva il valore che aveva prima della chiamata della funzione, cioè 3; invece Var2, che è stata passata per riferimento, è stata effettivamente modificata durante l'esecuzione della funzione, e infatti dopo la chiamata ha assunto il valore 11, cioè il valore originario più 1.
Se non viene specificata né ByVal né ByRef, Visual Basic penserà automaticamente che la variabile è stata passata per riferimento: questa infatti è l'impostazione predefinita. Un'altra parola chiave che può precedere gli argomenti è Optional: questa specifica che l'argomento seguente non è obbligatorio, e in tal caso è possibile indicare un valore di default che l'argomento deve assumere nel caso in cui l'istruzione chiamante la funzione (o la routine) ometta effettivamente il parametro. Ad es.:

Private Function Prova (A As Integer, Optional B As Integer = 1) As Integer
	Prova = A + B
End Function

se nell'evento Load del form scriviamo:

Private Sub Form_Load()
	Debug.Print Prova(2)
	Debug.Print Prova(2, 3)
End Sub

vedremo nella finestra immediata:

3
5

Nel primo caso, infatti, il parametro B è omesso e quindi assumerà per default il valore 1.

Come per le routine, anche le variabili locali di una funzione possono essere dichiarate Static per indicare che il loro valore viene mantenuto tra una chiamata e l'altra della funzione (v.lez. 2): se si vuole che TUTTE le variabili locali siano statiche, si può dichiarare la funzione in questo modo:

Private Static Function Prova() As Integer
	Dim A As String 'variabile statica
	Dim B As Long 'variabile statica
End Function

Le variabili locali saranno statiche anche se nella loro dichiarazione non è stata specificata la parola chiave Static.

Quinta lezione - il primo programma in Visual Basic for Application

Ora che conosciamo le basi fondamentali della programmazione, possiamo avvicinarci alla creazione del nostro primo programma: il classico "Hello World!", ovvero un programma che non fa altro che visualizzare un messaggio di saluto.
Per fare ciò cominciamo col creare un nuovo form contenente una Label, cioè un'etichetta: il controllo Label lo trovate sulla casella degli strumenti  ed è quello indicato dall'icona "Aa".

Una Label è un controllo che permette di visualizzare una stringa: una volta inserita sul form, potremo vedere le sue proprietà nella finestra delle proprietà; vedremo innanzitutto la proprietà Name (Nome elemento) che definisce il nome con cui si può fare riferimento al controllo all'interno del codice, come si può leggere anche nella descrizione fornita nella parte inferiore della finestra delle proprietà. Il nome predefinito, per qualunque controllo, è dato dal nome del tipo di controllo (nel nostro caso Label) più un numero che viene incrementato ogni volta che si aggiunge un altro controllo dello stesso tipo: se provate ad aggiungere un'altra Label sul form, vedrete che il suo nome predefinito sarà Label2, e così via.
Diamo un'occhiata ad altre proprietà interessanti: la proprietà Caption (Etichetta) ad es., assume il valore "Label1": provando a modificare questo valore (basta cliccarci sopra una volta e scrivere) vedrete che cambierà anche la stringa visualizzata sul form (per ora lasciate pure "Label1"); pertanto sarà questa la proprietà che dovremo opportunamente impostare per visualizzare il messaggio di saluto. Prima di vedere come, inseriamo sul form anche un CommandButton, cioè un pulsante, che trovate anch'esso sulla casella degli strumenti. (è quello sotto il check box)

Anche il pulsante ha la proprietà caption. Modificate la proprietà Caption e assegnategli il valore "Saluta!" (senza scrivere anche le virgolette). La posizione dell'etichetta e del pulsante sul form può essere cambiata semplicemente puntando il mouse sul controllo e trascinandolo nella posizione voluta. Ora andate sulla finestra del codice (se non la vedete cliccate su ): selezionate la voce Command1 dall'elenco degli oggetti e la voce click dall'elenco delle routine, e vi troverete, ovviamente, nella parte di codice che verrà eseguita quando si fa click sul pulsante:

P.S. se state usando la versione italiana di Access probabilmente vi appare Comando1 al posto di Command1

ora, all'interno di questa routine scrivete:

	Label1.Caption = "Hello World!"  

oppure:

	Label1.Caption = "Salve Mondo!"  

se non amate la lingua inglese…
Provate a visualizzare il form in "visualizzazione Maschera" e cliccate sul pulsante "Saluta": il testo dell'etichetta diventerà "Hello World!". Quello che succede dovrebbe essere evidente: cliccando sul pulsante viene scatenato l'evento Click relativo ad esso, e viene quindi eseguita l'istruzione che abbiamo inserito noi, e che imposta il valore della proprietà caption dell'etichetta a "Hello World!".
 


Si dice che questa proprietà viene impostata in fase di esecuzione, ovvero mentre il programma è in esecuzione; la proprietà caption del pulsante, invece, è stato impostato in fase di progettazione, ovvero quando abbiamo "disegnato" la nostra applicazione. Volendo, avremmo potuto impostare la proprietà Caption dell'etichetta già in fase di esecuzione: in tal caso non avremmo avuto bisogno del pulsante "Saluta".
Proviamo ora a migliorare un po' la nostra applicazione: di solito è buona norma inizializzare il valore di proprietà o variabili che andranno in seguito modificate; ad esempio, prima di premere il pulsante "Saluta", l'etichetta mostrava la stringa "Label1", cosa non molto bella a vedersi: quindi è meglio annullare la proprietà Caption. E' possibile farlo in fase di progettazione, cancellando "Label1" nella finestra delle proprietà e premendo Invio; in fase di esecuzione, bisogna usare l'istruzione:

	Label1.Caption = ""  

dove tra le virgolette non ci devono essere spazi: è una "stringa nulla", di lunghezza zero, che non contiene alcun carattere. Questa istruzione dovremmo metterla in una routine che viene eseguita subito, non appena l'applicazione viene avviata: il posto più adatto è senz'altro la routine Load del form, quella che abbiamo usato per i nostri esempi nelle lezioni precedenti; infatti l'evento Load del form viene generato prima della visualizzazione della finestra. Se volessimo poter annullare, ossia cancellare, il saluto, dovremmo inserire sul form un altro pulsante: assegnate il valore "Annulla" (sempre senza le virgolette) alla proprietà Caption e, nella routine Click (del Command2, non del Command1!) ripetete l'istruzione:

	Label1.Caption = ""  

E' buona norma, inoltre, inserire un pulsante (esempio Command3) per uscire dal programma, anche se non è necessario in quanto basta premere la X in alto a destra. Per terminare un programma dovremo scrivere dentro lo script di tale bottone:

        Quit
Per chiudere invece il form basta scrivere 
	docmd.close

Questa istruzione determina la generazione dell'evento Unload.

Analogamente, esiste anche l'istruzione che serve per "caricare" un form in memoria.

DoCmd.OpenForm  <NomeForm>

La parola chiave Me indica il form corrente, ossia quello a cui appartiene la riga di codice che sta per essere eseguita. La Caption del pulsante Command3 andrebbe impostata, per esempio, a "Esci". Se proviamo ad eseguire l'applicazione, vedremo inizialmente solo i tre pulsanti "Saluta", "Annulla" ed "Esci": premendo "Saluta", il programma visualizzerà il messaggio di saluto, premendo "Annulla" questo messaggio scomparirà e premendo "Esci" chiuderemo il programma.

Per migliorare ulteriormente il nostro programma potremmo, ad es., cambiare la stringa che compare nella barra del titolo del form (nel nostro caso "Form1"): anche in questo caso si tratta della proprietà Caption (del form); basta andare nella finestra della proprietà, selezionare la maschera selezionare la proprietà Caption e scrivere, ad esempio:
"Programma di esempio Hello World"
 

Nel caso non siano allineati, potete selezionarli e andare sul menù Formato - Allinea e scegliere la voce opportuna. Inoltre, è opportuno modificare i nomi dei vari controlli: ad esempio, il nome dell'etichetta potrebbe essere "lblHelloWorld" (senza spazi, mi raccomando); quello dei pulsanti "btnSaluta", "btnAnnulla", "btnEsci"; naturalmente in tal caso bisogna modificare anche le istruzioni del codice. Così:

	Label1.Caption = ""  

diventa:

	lblHelloWord.Caption = ""  

e così via. Il motivo per cui i nomi sono preceduti da lbl o cmd ve lo spiegherò la prossima volta.

Sesta lezione - convenzioni, algebra e variabili Booleane

Come promesso, cominciamo a vedere cosa sono e come si usano le cosiddette convenzioni di scrittura del codice: tali convenzioni sono regole che ogni programmatore dovrebbe seguire, al fine di scrivere un codice facilmente comprensibile e modificabile per chiunque abbia l'occasione di leggerlo ed eventualmente correggerlo e migliorarlo; rappresentano una sorta di galateo del programmatore, che permette di standardizzare il codice e quindi di renderne più semplice la lettura, la comprensione e la diffusione. Le convenzioni più semplici sono quelle che riguardano la denominazione di oggetti e variabili: secondo tali convenzioni è bene utilizzare un prefisso di due o tre caratteri specifico per ogni tipo di oggetto o variabile: ad es., se utilizziamo un CommandButton dovremmo usare il prefisso btn; per le etichette (c.d. Label) il prefisso è lbl, per i form è frm, e così via. Leggendo un codice di questo tipo:

	frmMain.repaint
	lblFileName.Caption="c:\pippo.txt"
	btnSave_Click  

saremmo in grado di intuire subito che prima di tutto viene visualizzato il form frmMain, dopodiché la caption di un'etichetta viene aggiornata con un nome di file opportuno, e infine viene richiamata la routine corrispondente all'evento Click di un CommandButton. Senza usare i prefissi, non avremmo potuto sapere a priori a quale oggetto si riferisse la proprietà Caption o la routine Click. Analogo discorso vale per le variabili: innanzitutto è bene usare una lettera per indicarne l'area di validità: g per le variabili globali, o pubbliche, m per quelle private ma dichiarate a livello di modulo (quindi nella sezione dichiarazioni di un form, ad esempio); per le variabili locali delle routine non si usa alcun prefisso. Poi bisognerebbe utilizzare un altro prefisso per indicare il tipo di variabile: str per le stringhe, int per gli integer, lng per i long, ecc. Ad esempio:

	gstrUserName  

indica una variabile pubblica che contiene il nome dell'utente (e quindi si tratta di una stringa). Ci sono poi delle convenzioni per la strutturazione del codice, ovvero l'inserimento di commenti e l'uso di una formattazione adeguata del codice: ad esempio, ogni subroutine o funzione dovrebbe includere un commento che ne indichi lo scopo, il significato delle variabili richieste in input, il valore restituito, e ogni altra informazione non direttamente intuibile dal codice ma che serve a capire meglio cosa fa e come funziona la routine. Per quanto riguarda la formattazione del codice, bisognerebbe utilizzare i rientri: consideriamo ad es. queste istruzioni:

Non preoccupatevi del significato delle istruzioni if e for: le vedremo in dettaglio più avanti. La cosa importante è sapere è sapere che queste istruzioni sono nidificate, ovvero contenute l'una nell'altra: il blocco if…end if contiene il blocco for…next, che a sua volta contiene l'istruzione di aggiornamento della label. La nidificazione è appunto immediatamente evidenziata dal rientro delle varie istruzioni: maggiore è il rientro, maggiore è il livello logico di nidificazione; è un po' lo stesso principio utilizzato da Gestione risorse per visualizzare la gerarchia delle directories: più una cartella è "rientrata", meno importante è il suo livello gerarchico. Queste sono alcune delle convenzioni di scrittura del codice: altre le vedremo strada facendo. Non pensate però che queste convenzioni servano solo se il vostro codice deve essere letto da altri: anzi, la loro utilità è rivolta soprattutto all'autore del codice, perché spesso accade che egli non si ricordi più come e perché aveva strutturato in un certo modo il codice (quando avrete un po' più di esperienza ve ne accorgerete!).

Ora introduciamo il discorso delle operazioni logiche e delle variabili booleane (pron. buleàne). Chiunque dovrebbe avere familiarità coi concetti di vero e falso, due condizioni logiche opposte che stanno alla base del nostro modo di ragionare e che sono oggetto di studi filosofici (e successivamente matematici) sin dai tempi di Aristotele. Questi due concetti sono fondamentali per l'intero mondo dei computer, anzi per lo stesso funzionamento dei computer: infatti, come tutti sanno, un computer si basa su una logica binaria, in cui ogni singolo bit può assumere due soli valori: 0 e 1, che possono essere logicamente associati ai valori falso e vero. Il logico e matematico George Boole,

 

vissuto nel XIX secolo, sviluppò un insieme di regole (che formano l'algebra booleana) per eseguire operazioni logiche sui valori vero e falso. Le principali regole, di cui probabilmente avrete già sentito parlare, sono queste:

And (congiunzione), Or (disgiunzione), Not (negazione), Xor (exclusive Or)

Questi operatori logici funzionano come le operazioni matematiche (anzi, a ben guardare SONO operazioni matematiche, pur essendo diverse da quelle che abbiamo studiato alle elementari): legano tra di loro due espressioni booleane (cioè due valori del tipo vero/falso) e forniscono un risultato. In Visual Basic esiste un tipo di dati particolare, il tipo Boolean, che indica proprio una variabile booleana, che può assumere il valore vero o falso, o meglio True o False, che sono le parole chiave utilizzate nell'ambito delle operazioni logiche booleane. Lo schema del risultato degli operatori logici And, Or, Xor è il seguente:
 

True And True = True
True And False = False
False And True = False
False And False = False

True Or True = True
True Or False = True
False Or True = True
False Or False = False

True Xor True = False
True Xor False = True
False Xor True = True
False Xor False = False
 

Come si può notare, la congiunzione (And) di due espressioni è vera solo se sono vere entrambe le espressioni ("io lavoro a Milano e a Torino" è vera solo se è contemporaneamente vero che lavoro a Milano e che lavoro a Torino); la disgiunzione (Or) è vera solo se è vera almeno una delle due espressioni ("io lavoro a Milano o a Torino" è vera se lavoro sia a Milano che a Torino, ma è vera anche se lavoro a Milano e a Brescia o se lavoro a Torino e a Roma; sarà invece falsa solo se non lavoro né a Milano né a Torino). La disgiunzione esclusiva (Xor) non è immediatamente intuibile come la congiunzione e la disgiunzione, ma è un misto di queste due operazioni e della negazione: l'Xor restituisce True solo se le due espressioni sono diverse, mentre restituisce False solo se le due espressioni sono uguali; corrisponde alla "o" disgiuntiva, come quando si dice "io lavoro o a Milano o a Torino": mentre l'Or permette la compresenza di entrambe le alternative, l'Xor ne esclude una delle due. La negazione, infine, coinvolge una sola espressione e restituisce semplicemente il suo opposto:

Not True = False
Not False = True

Questi operatori possono essere raggruppati insieme: così come si può scrivere (5+2)*3, si può anche scrivere
 

(True Or False) And True

il risultato di questo esempio sarà True. Ora potete capire che l'Xor è il risultato di questa espressione (x e y sono due variabili booleane):

x Xor y = ((Not x) And y) Or (x And (Not y))

Ci sono altri due operatori logici disponibili in Visual Basic, che sono utilizzati molto raramente ma che potrebbero comunque essere utili: l'equivalenza (Eqv) e l'implicazione (Imp):

 

True Eqv True = True
True Eqv False = False
False Eqv True = False
False Eqv False = True

True Imp True = True
True Imp False = False
False Imp True = True
False Imp False = True

In parole povere, l'Eqv è la negazione dell'Xor: infatti due espressioni sono equivalenti se sono uguali (entrambe false o entrambe vere); l'Imp corrisponde più o meno alla costruzione "se… allora" con l'uso di una condizione sufficiente ma non necessaria.

Comunque gli operatori logici che è necessario conoscere sono solo And, Or, Not, e sarebbe bene conoscere anche Xor: la prossima volta vedremo degli esempi.

Settima lezione - Utilizzo delle variabili Booleane

Nella scorsa lezione abbiamo visto cosa sono le variabili booleane e quali operazioni si possono fare con esse: ora vediamo come si usano. L'istruzione principale che fa uso di espressioni booleane è il costrutto

"if condizione then istruzione"

che, come dice il nome, esegue una o più 
            istruzioni se si verifica una certa condizione. Ad esempio, 
            l'istruzione: 
If lblName.Visible = True Then
	lblName.Caption = "Pippo"
End if

aggiorna la proprietà Caption dell'etichetta lblName se questa etichetta è visibile: il tipo della proprietà Visible, infatti, è proprio Boolean. Quando Visual Basic deve eseguire un'istruzione come quella riportata, prima di tutto verifica se l'espressione compresa tra le parole chiave If e Then è vera o falsa: se l'etichetta è visibile (cioè se la proprietà visible è uguale a True), la condizione è verificata e quindi sarà eseguita l'istruzione seguente la parola chiave Then; altrimenti la condizione non sarà verificata e l'istruzione sarà del tutto ignorata. Solitamente i programmatori VB ("VB" è ovviamente l'abbreviazione di Visual Basic…) utilizzano una sintassi leggermente diversa, organizzata su più di una riga (cosiddetta a blocco):

If lblName.Visible = True Then lblName.Caption = "Pippo"

In Visual Basic di solito si inserisce una istruzione per ogni riga, poiché è sufficiente il ritorno a capo per far capire a Visual Basic che l'istruzione è terminata; nel caso della If…Then, quindi, è possibile far stare tutta l'istruzione sulla stessa riga, ma se le istruzioni che seguono Then vengono riportate nelle righe successive, è necessario indicare dove finiscono le istruzioni appartenenti al blocco If, e questo si fa appunto con la parola chiave End If, esattamente come si fa con End Sub o End Function per indicare la fine di una subroutine o di una funzione. Personalmente ritengo che la seconda sintassi sia più chiara e comoda, soprattutto quando le istruzioni da eseguire nel blocco sono più di una: infatti, se volessimo utilizzare la sintassi su una singola riga, bisognerebbe separare le varie istruzioni con i ":", ad esempio:

If
lblName.Visible = True Then lblName.Caption = "Pippo":Form1.Caption = "Pippo" ....

(Nota: il carattere " : " può essere utilizzato per riunire più istruzioni sulla stessa riga anche al di fuori di un'istruzione If…Then) L'istruzione If…Then permette di eseguire certe istruzioni anche nel caso in cui la condizione non sia verificata: ciò è possibile grazie alla parola chiave Else, ad esempio:

If strName = "Pippo" Then
                        	msgbox "Il nome è Pippo"
Else
                        	msgbox "Il nome non è Pippo"
End If

Se la variabile stringa strName è uguale a "Pippo", il messaggio "Il nome è Pippo" viene mostrato. In questo modo è possibile ottenere una certa flessibilità nell'esecuzione condizionale di un gruppo di istruzioni; questa flessibilità è ulteriormente aumentata dalla possibilità di utilizzare la parola chiave ElseIf:

If strName = "Pippo" Then
                        	frmProva.lblName.Caption = "Pippo"
            	ElseIf strName = "Topolino" Then
                        	frmProva.lblName.Caption = "Minnie"
ElseIf strName = "Pluto" Then
                        	frmProva.lblName.Caption="Orazio"
Else
                        	docmd.close
            End If

In un blocco di questo tipo, Visual Basic comincia a verificare le condizioni una per una: quando una delle condizioni è verificata, vengono eseguite le istruzioni relative a quella condizione, ignorando le altre; se nessuna delle condizioni è verificata, vengono eseguite le istruzioni comprese nella clausola else, se è presente: altrimenti non viene eseguita alcuna istruzione. Infatti le clausole Else e ElseIf sono facoltative.
All'interno di ogni blocco If…Then è possibile inserire qualunque istruzione, anche un'altra If…Then (si parla dei cosiddetti blocchi annidati); ad esempio si potrebbe fare:

If intX > 10 Then
            	
            	   If intX > 100 Then
                  intY = intX / 2
               	 	 	
            	 	 	 	
            	Else
                  intY = intX + 10
               	 	 	
            	 	 	 	
            	End If
ElseIf intX > 5 Then
               intY = 0
Else
               bValue = True
End If

In questo blocco viene valutato il valore della variabile Integer intX: se è maggiore di 10, viene effettuato un ulteriore controllo: se intX è maggiore di 100, viene posto intY = intX/2, altrimenti intY = intX+10; se invece intX non è maggiore di 10 (cioè intX <= 10), Visual Basic controlla se intX > 5: in questo caso si pone intY = 0. Se infine intX <= 5, la variabile booleana bValue viene impostata a True. E' chiaro quindi che l'istruzione If…Then permette di effettuare confronti flessibili e ramificati per determinare quali istruzioni è opportuno eseguire.
Ma non basta: le condizioni da valutare possono essere espressioni anche molto complesse, utilizzando gli operatori logici che abbiamo visto nella lezione precedente; ad esempio:

If ((intX> 10 And intX < 100) Or (intX > 200 And intX<= intY / 3)) Imp bValue = False Then
                        	...
End If

La valutazione della condizione segue le regole generali per l'uso delle parentesi: dapprima viene confrontato il valore di intX con 10 e con 100: se è compreso tra questi due valori (esclusi gli estremi), l'espressione nella prima sotto-parentesi sarà True;
poi intX viene confrontato con 200 e con intY / 3: se è compreso tra questi due valori, la seconda sotto-parentesi sarà True; se almeno una delle due sotto-parentesi è True, anche la super-parentesi sarà True. Infine viene confrontata la variabile bValue con il valore False: se bValue è False, l'espressione "bValue = False" sarà True, e quindi lo sarà anche l'intera condizione valutata dalla If (infatti l'operatore Imp restituisce sempre True se la seconda espressione è True: v.lezione precedente); se invece bValue = True, la seconda espressione sarà False: in tal caso l'intera condizione sarà True solo se anche la super-parentesi sarà False. Un esempio dovrebbe chiarire tutto: supponiamo che intX = 90, intY = 600, bValue = True; allora avremo:

If ((True And True) Or (False And True)) Imp False Then

che corrisponde a:

If (True Or False) Imp False Then

che corrisponde a:

If True Imp False Then

Siccome True Imp False restituisce False, le istruzioni successive a Then non saranno eseguite. Vi faccio notare che Visual Basic valuta sempre tutte le espressioni, anche se in qualche caso è inutile; supponiamo ad esempio di avere:

If intX > 10 And intX < 20 Then

con intX = 5; sapendo che intX > 10 è False e che il valore della condizione dipende dall'operatore And, si potrebbe anche fare a meno di confrontare intX con 20, perché qualunque sia il risultato di questo confronto la condizione sarà comunque False, dato che la prima espressione è False e l'And restituisce True solo se entrambe le espressioni sono True.
In effetti alcuni linguaggi più "intelligenti" (ad esempio il C++) evitano i confronti inutili; Visual Basic invece li fa sempre tutti, e nel caso di controlli pesanti bisognerebbe tenerne conto, facendo in modo che vengano valutate solo le espressioni strettamente necessarie. Ad es., l'istruzione:

If intX > 10 And intX < 20 Then
               intX = intX * 2
End If

potrebbe essere modificata in questo modo:

If intX > 10 Then
   	 	 	
            	if intX < 20 Then
                  IntX = IntX * 2
   	 	 	
            	End If
End If

In questo modo, se intX=5, Visual Basic non perde tempo a confrontare intX con 20, perché già la prima condizione è falsa, e quindi il secondo blocco If sarà ignorato.

Ottava lezione - Altro ancora sulle variabili Booleane, Bit e Byte

Finora abbiamo visto come vengono valutate le espressioni per verificare se sono true o false, e negli esempi abbiamo sempre usato dei confronti: in realtà questo spesso non è necessario, anzi è ridondante; consideriamo ad esempio questa condizione:

If frmAbout.Visible = True Then

La condizione risulta true se frmAbout.Visible è True, risulta False se frmAbout.Visible è False: in parole povere il valore della condizione è identico al valore di frmAbout.Visible; pertanto non c'è alcun bisogno di effettuare il confronto, ma sarà sufficiente scrivere:

If frmAbout.Visible Then

Infatti la condizione valutata dalla if deve essere interpretabile come un valore Boolean, e frmAbout.Visible è proprio un valore Boolean che può quindi essere usato direttamente come una condizione. E se avessimo bisogno di confrontare questa proprietà con False? E' semplicissimo, basta fare:

If Not frmAbout.Visible = True Then

Se il form non è visible, la condizione sarà verificata e le istruzioni saranno eseguite. Una cosa simile si può fare anche con i valori numerici: bisogna infatti sapere che i valori True e False in realtà non sono altro che numeri, e precisamente la parola chiave true corrisponde al valore -1 e la parola chiave false corrisponde al valore 0. Provate infatti a creare un nuovo progetto, a inserire un CommandButton e a scrivere queste righe nella routine dell'evento click:

Dim blnProva As Boolean
                                    blnProva = -1
                                    Debug.Print blnProva
                                    blnProva = 0
                                    Debug.Print blnProvaa

Una volta avviato il progetto, quando premerete il pulsante vedrete nella finestra immediata:

  True                      
  False

Ora provate a scrivere, al posto di "blnProva = -1", "blnProva = 34" e riavviate il progetto: ripremendo il pulsante, vedrete ancora nella finestra immediata:

  True
              False

Questo accade perché, quando un valore numerico viene convertito in Boolean, esso è interpretato da VB come true se è diverso da 0, anche se il valore predefinito di true è -1, ed è interpretato come false quando è uguale a 0.
Analogamente, è possibile convertire un valore Boolean in un valore numerico:

Dim intProva As Integer
                                    intProva = True
                                    Debug.Print intProva
                                    intProva = False
                                    Debug.Print intProva

Questa volta premendo sul pulsante vedrete nella finestra immediata:

-1
                                 0

Insomma, le parole chiave True e False funzionano quasi come delle variabili numeriche il cui valore è rispettivamente -1 e 0: infatti la guida in linea di Visual Basic afferma che le variabili Boolean sono memorizzate come variabili numeriche a 16 bit (2 byte), ossia come gli Integer. Ma se un numero può essere direttamente interpretato come un valore Boolean, non c'è bisogno di scrivere, ad esempio:

If intProva <> 0 Then '<> significa "diverso da"

ma si può scrivere direttamente:

If intProva Then

La conversione tra valori numerici e Booleani non è invece immediata quando si usano gli operatori logici; proviamo ad esempio a scrivere:

If 4 And 2 Then

Si potrebbe pensare che, essendo 4 <> 0 e 2 <> 0, e quindi entrambi True, la condizione risulti verificata perché True And True = True; invece no.
Il fatto è che gli operatori logici, se applicati a valori o variabili numeriche, eseguono un confronto "bit a bit" dei due operandi. Qui occorre fare una breve digressione sui bit e sui byte. Un bit è l'unità fondamentale su cui si basa il funzionamento di un computer, e può assumere solo due valori: 0 e 1; questa caratteristica deriva dal fatto che i supporti fisici di memorizzazione dei dati possono assumere solo due stati stabili: possono cioè essere magnetizzati in senso positivo oppure negativo. Pertanto i processori eseguono i calcoli in base al sistema numerico binario, cioè in base alle potenze di 2 (perché 2 sono i valori possibili: 0 e 1), mentre noi utilizziamo il sistema decimale, cioè in base alle potenze di 10 (perché 10 sono le cifre da 0 a 9). Solitamente come unità di misura della capacità di memoria dei computer viene preso il byte, che è semplicemente un gruppo di 8 bit contigui: se avessimo un numero di 8 cifre decimali questo sarebbe compreso tra 0 e

10 ^ 8 - 1 = 100.000.000 - 1 = 99.999.999 

Invece un numero di 8 cifre binarie (cioè un byte) è compreso tra 0 e

2 ^ 8 - 1 = 256 - 1 = 255 

infatti il tipo di dati byte memorizza proprio un numero compreso tra 0 e 255 (v.lez. 1). Un numero di 16 bit (cioè 2 byte, come un integer), ad es., è compreso tra 0 e

2 ^ 16 - 1 = 65535 

e così via (il motivo per cui il tipo integer può assumere valori tra -32768 e +32767 lo vedremo un'altra volta). Viceversa, il numero 206 può essere scomposto, in base al sistema decimale, in

200 + 0 + 6 = 2 * 10 ^ 2 + 0 * 10 ^ 1 + 6 * 10 ^ 0 

numerando ogni cifra a partire da destra (cioè considerando 6 la prima cifra, 0 la seconda cifra, 2 la terza cifra) potremo scrivere

206 = 2 * 10 ^ (3 - 1) + 0 * 10 ^ (2 - 1) + 6 * 10 ^ (1 - 1) 

In altri termini, ogni numero può essere espresso come la somma dei prodotti di ogni cifra per 10 elevato alla posizione occupata da quella cifra meno uno. Questa spiegazione può sembrare molto complicata, ma è importante per capire come convertire un numero decimale in base binaria e un numero binario in base decimale; supponiamo ad es. di avere il numero binario 101: per sapere a quale numero decimale corrisponde, dovremo moltiplicare 1 (la prima cifra da destra) per 2^ (1 - 1); poi dovremo moltiplicare 0 (la seconda cifra) per 2 ^ (2 - 1) e infine 1 (la terza cifra da destra) per 2 ^ (3 - 1). In parole povere:

101 = 1 * 2 ^ (3 - 1) + 0 * 2 ^ (2 - 1) + 1 * 2 ^ (1 - 1) =
                                    = 1 * 2 ^ 2 + 0 + 1 * 2 ^ 0 =
                                    = 4 + 0 + 1 =
                                    = 5 

Se invece dovessimo convertire un numero decimale in binario, dovremmo dividere il numero per 2: se il risultato è intero, scriviamo 0, altrimenti 1; poi prendiamo la parte intera della divisione e la dividiamo ancora per 2, e procediamo in questo modo finchè non resta 0; infine si ribalta la serie di 0 e 1 che abbiamo scritto. Ad eempio, per convertire il numero 9 si procede così:

	                            9 / 2 = 4,5
                                    4 / 2 = 2
                                    2 / 2 = 1
                                    1 / 2 = 0,5
                                    0 / 2 = 0  1
                                    0
                                    0
                                    1
                                    0 

La successione che abbiamo ottenuto è quindi 10010, che ribaltata diventa: 01001, cioè 1001: infatti

1001 = 1 * 2 ^ (4 - 1) + 0 + 0 + 1 * 2(1 - 1) = 8 + 1 = 9 

Ora torniamo al confronto bit a bit. Questo confronto, come dice il termine, prende in considerazione ogni singolo bit di un operando e lo confronta con il bit che occupa la stessa posizione nell'altro operando: il risultato del confronto sarà determinato dall'operatore logico utilizzato, tenendo conto che il bit 0 corrisponde a False e il bit 1 corrisponde a True. Supponiamo ad eempio. di fare 23 And 200:

            00010111 (rappresentazione binaria di 23)
                                    11001000 (rappresentazione binaria di 200)
                                    --------
                                    00000000 risultato dell'And bit a bit

Il risultato è 0 perché, per ogni posizione, i bit corrispondenti dei due operandi non sono mai contemporaneamente 1 e 1 (cioè True e True). Facciamo altri esempi:

13 Or 17:

                                    00001101 (13)
                                    00010001 (17)
                                    --------
                                    00011101 (29) 


                                    Not 55::

                                    00110111 (55)
                                    ----------
                                    11001000 (200) 

Nel caso del Not x, vi faccio notare che il risultato è sempre 255 - x, ovvero il valore massimo che può assumere un byte meno il valore dell'operando (nel caso in cui x sia un dato di tipo byte).

Nona lezione - Studiare un'applicazione professionale: il Blocco Note di Windows

Nella precedente lezione abbiamo visto che gli operatori logici, se applicati a variabili numeriche, eseguono un confronto bit a bit dei due operandi (o dell'operando, nel caso si usi l'operatore Not): ora vediamo in quali casi è utile utilizzare questo tipo di confronto.
Supponiamo di avere una serie di attributi relativi a un oggetto: questi attributi possono essere presenti singolarmente, oppure essere presenti contemporaneamente ad altri; ad esempio, gli attributi di un file (archivio, nascosto, sistema, sola lettura) possono essere presenti uno per uno, oppure anche più di uno insieme.
Per sapere quali attributi possiede un determinato file, essi vengono memorizzati come un numero: infatti ad ogni attributo corrisponde un preciso valore; all'attributo archivio corrisponde il valore 32, all'attributo nascosto corrisponde il valore 2, all'attributo sistema corrisponde 4 e all'attributo sola lettura corrisponde 1.
Così, se il valore degli attributi di un file è 2, sapremo che quello è un file nascosto, e viceversa; se invece il file, oltre ad essere nascosto, è anche di sola lettura, allora il valore dei suoi attributi sarà 3, cioè 2+1. Non è un caso, infatti, se i valori dei singoli attributi sono tutti potenze di 2 (anche 1 è potenza di 2: 1 = 2 ^ 0): in questo modo non ci sono possibilità di equivoci nel caso siano presenti più di un attributo, perché la somma di potenze di 2 sarà sempre diversa da una potenza di 2.
Ora, supponiamo che il valore degli attributi del file pippo.txt sia 34: come facciamo a sapere quali attributi corrispondono a 34 ?
Bisogna considerare il corrispondente valore binario:

00100010 (34)
                                    -------------
                                    00100000 (32)
                                    00000010 (2) 

Come si vede, 34 è la somma di 32 e 2 perché sono impostati (cioè valgono 1) il secondo e il sesto bit (a partire da destra, come sempre), che corrispondono appunto alle potenze:

2 ^ 1 = 2 

e :

2 ^ 5 = 32 

In questo caso è molto semplice capire quali sono i bit impostati, ma ovviamente non è sempre così: per capirlo senza fare troppa fatica è sufficiente usare l'operatore And con i valori giusti. Nel nostro caso sappiamo che i valori possibili sono 1, 2, 4, 32 oppure una loro combinazione; ebbene, eseguendo l'And con ognuno di questi valori sapremo subito quali bit sono impostati:

34 And 1 = 0
                                    34 And 2 = 2
                                    34 And 4 = 0
                                    34 And 32 = 32 

I valori diversi da 0 sono proprio 2 e 32, quindi sappiamo che il valore 34 corrisponde agli attributi nascosto e archivio.
Allora, se ad esempio volessimo cercare tutti i file nascosti all'interno di una directory, potremmo eseguire questo controllo (per ogni file):

If intAttr And vbHidden Then

dove intAttr è la variabile che contiene il valore degli attributi del file in esame, e vbHidden è una costante di VB che assume proprio il valore dell'attributo Hidden (nascosto), cioè 2: la condizione

intAttr And vbHidden

sarà diversa da 0 (e quindi True) solo se il file è nascosto; potrebbe anche avere altri attributi, ma a noi interessa solo che sia nascosto.
Altri esempi dell'uso del confronto bit a bit li vedremo man mano che se ne presenterà l'occasione.
 


Ora ricominciamo coi nostri progetti d'esempio: uno dei modi migliori per imparare un linguaggio di programmazione è provare a replicare un'applicazione "professionale", che conosciamo già e che sappiamo già utilizzare; per cominciare, naturalmente, è bene partire da un'applicazione semplice, ad esempio il Blocco Note di Windows.


Innanzitutto bisogna capire da quali controlli è costituita l'applicazione: nel caso del blocco note è facile vedere che esso comprende un menu e un'area bianca in cui scrivere il testo; quest'area bianca non è altro che un TextBox. Cominciamo allora con l'aprire un nuovo form, inseriamo un TextBox sul form e chiamiamolo txtFile, mentre il form lo chiamiamo frmNotePad e come Caption gli diamo "Blocco Note - ACCESS"; trasciniamo txtFile in modo che l'angolo superiore sinistro coincida con l'angolo superiore sinistro di frmNotePad e poi ridimensioniamolo in modo che occupi tutta l'area disponibile in frmNotePad (vedi figura sottostante):
 

Lo stesso risultato si può ottenere selezionando txtFile e impostando a 0 le proprietà Top (Da Margine superiore) e Left (Sinistra) nella finestra delle proprietà; le proprietà Height (altezza) e Width (larghezza)

Impostiamo le proprietà del form e del controllo  txtFile a questi valori:

Non avendo a disposizione un  menù usiamo in alternativa i bottoni inseriti nella zone di intestazione maschera.  Chiamiamo ciascun bottone con il nome MnuNuovo, MnuApri, MnuSalva , MnuEsci, MnuTrova e MnuTrovaAncora (questi ultimi 2 bottoni verranno utilizzati in seguito).



Ora abbiamo disegnato il nostro menù : sul frmNotePad vedremo quindi un qualcosa di questo tipo:

Per sapere quale file aprire o con quale nome salvare il testo che l'utente ha inserito nel textbox txtFile si potrebbe utilizzare la finestra di dialogo standard di Windows, proprio quella che usa lo stesso Blocco Note o qualunque altra applicazione di Windows, ma per ora ci accontentiamo di un semplice inputbox, ovvero una finestrella che chiede l'inserimento di una stringa: nel nostro caso questa stringa sarà il nome del file.
Prima di tutto, però, dobbiamo dichiarare le variabili che ci serviranno: queste saranno il nome del file da aprire o salvare e il contenuto di questo file;
cominciamo dunque a inserire, nella routine mnuApri_Click, le istruzioni:

Dim strNomeFile As String
Dim strTesto As String
                StrNomeFile = Inputbox("Inserisci il nome del file:") 

La funzione Inputbox è quella che fa comparire l'omonima finestra:


Come parametri richiede obbligatoriamente il prompt, ovvero il messaggio visualizzato nella finestra, e facoltativamente il "titolo" della finestra, cioè la stringa che compare nella barra del titolo; gli altri parametri, tutti facoltativi, per ora non ci interessano: comunque sulla guida trovate tutte le informazioni necessarie.
Il valore restituito dalla funzione è proprio ciò che l'utente inserisce nella casella di testo contenuta nella finestra, e noi assegniamo questo valore a una nostra variabile locale.
Una volta saputo il nome del file, dobbiamo aprirlo e leggerne il contenuto: per fare questo dobbiamo utilizzare l'istruzione open e quelle ad essa collegate.
Open serve ad aprire un file in lettura e/o scrittura: questa istruzione sarà analizzata in dettaglio più avanti, ora è sufficiente sapere che per utilizzarla è necessario indicare il nome del file da aprire, la modalità di apertura (in parole povere se vogliamo aprirlo per leggere e/o per scrivere) e un numero di file che verrà utilizzato per identificare univocamente il file che abbiamo aperto; la sintassi è la seguente:

Open nome del file For modalità di apertura As #numero del file 

Noi conosciamo il nome del file, sappiamo che vogliamo aprirlo per leggerlo (quindi la modalità sarà input) e dato che è il primo file che apriamo possiamo usare 1 come numero di file:

Open strNomeFile For Input As #1 

dopodiché dobbiamo leggerne il contenuto: esiste una funzione apposita, Input, che vuole sapere quanti byte deve leggere e da quale file; il numero di byte da leggere è esattamente uguale alla lunghezza del file, che possiamo recuperare attraverso la funzione LOF (Length Of File):

strTesto = Input(LOF(1),1) 

Il testo letto da Input() viene così memorizzato in strTesto.
Ora che abbiamo letto il contenuto del file, possiamo anche chiuderlo:

Close

Ricordate di chiudere sempre i file aperti che non vi servono più, perché finché restano aperti non saranno accessibili da altre applicazioni.
Ora non ci resta che visualizzare ciò che abbiamo letto:

txtFile.Text = strTesto 

Se volete, potete già provare ad avviare il progetto e ad aprire qualche file: per chiuderlo non possiamo ancora usare il menù File->Esci perché non abbiamo inserito le istruzioni necessarie, però è sufficiente cliccare sulla "x" nel form.

Decima lezione - Studiare un'applicazione professionale: il Blocco Note di Windows - Seconda parte


Dopo aver inserito il codice per il menù mnuApri, scriviamo quello per mnuSalva: questa volta ci serve una sola variabile, e cioè strNomeFile, perché il testo che dobbiamo scrivere sul file è quello contenuto in txtFile, a cui possiamo accedere attraverso la proprietà Text; quindi non abbiamo bisogno di una variabile per il testo. Per sapere con quale nome salvare il file, usiamo ancora la funzione InputBox:

Dim strNomeFile As String
                strNomeFile = InputBox("inserisci il nome del file:") 

Ora dobbiamo nuovamente aprire il file (questa volta per scriverci sopra, quindi in modalità output) e scrivere il contenuto del TextBox:

Open strNomeFile For Output As #1
Print #1, txtFile
Close 1

Per scrivere sul file abbiamo usato l'istruzione Print, che ha bisogno di conoscere il numero del file su cui scrivere e i valori da scrivere: questi valori possono essere separati da "," o da ";": la virgola indica che i valori saranno separati, all'interno del file, da una tabulazione, mentre il punto e virgola indica che i valori saranno scritti uno di seguito all'altro. Infine dobbiamo scrivere il codice per il menù Esci: qui basterebbe scrivere docmd.close per chiudere il form oppure Quit se intendiamo chiudere l'applicazione.
E' vero che ogni file viene chiuso dopo aver letto/scritto su di esso, ma questo è quello che avviene in circostanze normali: è possibile che in certi casi si verifichi un qualche errore che impedisca la chiusura del file nelle routine mnuApri e mnuSalva; il programmatore deve essere abbastanza previdente da includere ulteriori controlli al momento di uscire dall'applicazione. E' vero anche che quando un'applicazione termina, Visual Basic si preoccupa di chiudere tutti i file rimasti aperti, ma un controllo in più non fa mai male: è un po' come spegnere le luci e chiudere il gas quando si esce di casa.
Pertanto possiamo scrivere:

      Close
                  docmd.close

L'istruzione Close serve a chiudere il file specificato dal numero passato come argomento all'istruzione (ad es., close 1 chiude il file numero 1); se si usa senza argomenti, close chiude tutti i file, aperti con l'istruzione Open, che non sono ancora stati chiusi; dopo aver chiuso tutti i file eventualmente ancora aperti possiamo uscire dall'applicazione con l'istruzione Unload.
Il nostro editor personale è così del tutto funzionante, tuttavia è ancora ampiamente migliorabile: ad esempio, potremmo inserire la voce di menù "Nuovo" per creare un nuovo file; questo non significa altro che ripulire il txtFile in modo che l'utente possa scrivere il testo del nuovo file: basterebbe quindi fare:

txtFile = "" 

In realtà questo non è sufficiente, perché bisogna controllare se sono state effettuate modifiche al testo dall'ultimo salvataggio: in questo caso, infatti, il programma dovrebbe chiedere gentilmente all'utente se vuole salvare le modifiche. Per realizzare questo controllo abbiamo bisogno di una variabile che ci dica se il testo è cambiato dall'ultimo salvataggio oppure no: dato che le possibilità sono solo due (il testo è cambiato/il testo non è cambiato) è opportuno utilizzare una variabile booleana che valga false se il testo non è cambiato e true se il testo è cambiato; per capire quando il testo cambia si può semplicemente sfruttare l'evento change (Modifica) del txtFile, che viene appunto generato quando il valore della proprietà txtFile.Text cambia.
Questa variabile però non può essere locale rispetto alla routine txtFile_Change (modifica), perché deve essere visibile anche nel menù Nuovo, quindi bisogna dichiararla a livello di modulo; allora scriviamo, nella sezione dichiarazioni del frmNotePad:

Dim blnTextChanged As Boolean

e nella routine txtFile_Change:

blnTextChanged = True

Non basta: non dobbiamo soltanto impostare a true blnTextChanged quando il testo cambia, ma dobbiamo anche impostare la variabile a False quando i cambiamenti vengono salvati su un file; perciò, nella routine mnuSalva_Click scriviamo alla fine:

blnTextChanged = False

Ora torniamo al menù Nuovo: innanzitutto bisogna inserirlo nel menù File, quindi apriamo il menù editor, selezioniamo dalla lista la voce "Apri", premiamo il pulsante "Inserisci" e infine scriviamo la caption "Nuovo" e il nome "mnuNuovo"; ora dall'editor del codice selezioniamo la routine mnuNuovo_Click: qui dobbiamo inserire il controllo per verificare se il testo è cambiato o no.
Trattandosi di un controllo, dobbiamo usare l'istruzione If...Then, secondo questo schema:

If blnTextChanged Then
' il testo è cambiato, quindi chiediamo
                                    ' all'utente se vuole salvare le modifiche
Else
' il testo non è cambiato, quindi
                                    ' basta svuotare il txtFile
End If

Per chiedere all'utente se vuole salvare le modifiche, in teoria potremmo utilizzare ancora la funzione inputbox: ma in questo caso è molto più conveniente usare un MessageBox, ovvero una di quelle normalissime finestre di Windows che visualizzano un messaggio chiedendoci cosa vogliamo fare: esattamente la stessa finestra che ci mostra proprio il Blocco Note quando non abbiamo salvato le modifiche a un file.
Per far apparire questa finestra dobbiamo usare l'istruzione MsgBox, la cui sintassi è questa:

msgbox prompt, buttons, title, helpfile, context 

Tralasciando gli ultimi due argomenti, il prompt è il messaggio vero e proprio visualizzato nella finestra; title è invece il titolo della finestra di messaggio; il parametro buttons è un numero che indica lo "stile" del MessageBox, ovvero indica quali pulsanti devono essere associati alla finestra: ad ogni pulsante o combinazione di pulsanti è associata una costante di Visual Basic, così che per visualizzare, ad es., i pulsanti "sì" e "no", basta scrivere vbYesNo al posto del parametro buttons, anche se è possibile scrivere direttamente il valore della costante vbYesNo, e cioè 4.
Analoghe costanti sono associate all'icona che è possibile visualizzare nel messaggio: per visualizzare i pulsanti e un'icona è perciò sufficiente fare la somma dei relativi valori; il principio è lo stesso dell'esempio sulle proprietà di un file che abbiamo visto nella lez. 9.
Per i nostri scopi potremmo utilizzare un'istruzione del genere:

MsgBox "Il testo è stato modificato. Vuoi salvare le modifiche?", _
                                    vbYesNo+vbQuestion, "Conferma salvataggio" 

ma questa istruzione non fa altro che visualizzare il messaggio: noi invece vogliamo anche sapere qual è la risposta dell'utente, cioè vogliamo sapere qual è il valore restituito dalla funzione MsgBox; ertanto dobbiamo usare msgbox con le parentesi, come se fosse una funzione e non una semplice istruzione, in modo che restituisca un valore. Tale valore è una costante che rappresenta il pulsante premuto dall'utente: ad es., la costante vbOK vale 1 e indica che l'utente ha premuto il pulsante ok; pertanto noi dovremmo fare un controllo di questo tipo:

Dim intRisposta As Integer
                intRisposta = MsgBox("Il testo è stato modificato." & _
                                     " Vuoi salvare le modifiche?", _
                                     vbYesNo+vbQuestion, "Conferma salvataggio")
    If intRisposta = vbYes Then
       'salviamo le modifiche
    Else
       'non salviamo le modifiche
    End If

Per salvare le modifiche dovremmo scrivere istruzioni analoghe a quelle utilizzate nella routine mnuSalva_Click: ma chi ce lo fa fare di ripetere le medesime istruzioni, quando abbiamo già una routine bell'e pronta? Basterà semplicemente richiamarla.
In definitiva, la routine mnuNuovo_click sarà così:

Dim intRisposta As Integer
If blnTextChanged Then
               intRisposta = MsgBox("Il testo è stato modificato." & _
                                    "Vuoi salvare le modifiche?", _
                                    vbYesNo+vbQuestion, "Conferma salvataggio")
   If intRisposta = vbYes Then
   'richiamo la routine che permette di salvare le modifiche
        mnuSalva_Click
   Else
        txtFile = ""
   End If
Else
   txtFile = ""
End If
            blnTextChanged =false

Nella prossima lezione apporteremo ulteriori miglioramenti al nostro Blocco Note personale

Undicesima lezione - Studiare un'applicazione professionale: il Blocco Note di Windows - Terza parte

Nella lezione precedente abbiamo visto come agire nel caso in cui l'utente abbia apportato modifiche al testo senza salvarle: questo controllo non va effettuato solo se l'utente sceglie di creare un nuovo file, ma anche se ne vuole aprire uno già esistente o se vuole chiudere il programma; quindi dobbiamo modificare anche le routine mnuApri e mnuEsci. In questi casi, però, non c'è bisogno di pulire il TextBox, perciò basterà scrivere:

Dim intRisposta As Integer
    If blnTextChanged Then
                   intRisposta = MsgBox("Il testo è stato modificato." & _
                                        " Vuoi salvare le modifiche?", vbYesNo + vbQuestion, "Conferma salvataggio")
                end if                       
           
                	 	 	
    	 	 	
            	if intRisposta = vbYes Then mnuSalva_click
       		 		 		 		
            		'richiamo la routine che permette
       'salvare le modifiche
    	 	 	
            	End If

La figura sotto mostra il risultato che si ottiene attraverso il codice appena descritto:

Per quanto riguarda l'uscita dal form possiamo usare il pulsante "X" in alto a destra nel form.


In questo secondo caso, infatti, non viene certo richiamata la routine mnuEsci_click, ma viene generato direttamente l'evento Unload del frmNotePad: pertanto saremo noi a richiamare la routine mnuEsci_click dall'evento Unload:

mnuEsci_Click 

A questo punto, se si cerca di aprire un file senza avere salvato le modifiche al file correntemente aperto, succederà questo: compare un messaggio con la richiesta di confermare il salvataggio delle modifiche; se si risponde sì, comparirà una finestra per chiedere il nome del file da salvare, dopodiché comparirà un'altra finestra per chiedere il nome del file da aprire.
Poiché queste due ultime finestre sono identiche, sarà opportuno differenziarle leggermente per evitare confusioni: basterà a questo scopo specificare il titolo della finestra in questo modo:

strNomeFile = InputBox("Inserisci il nome del file:", "Salva") 

nella routine mnuSalva_click,

e:

strNomeFile = InputBox("Inserisci il nome del file:", "Apri") 

nella routine mnuApri_click

C'è un altro problema: quando si apre un file, il suo contenuto viene caricato in txtFile, e di conseguenza viene generato l'evento Change che imposta blnTextChanged a True; il programma perciò penserà che il testo sia stato modificato dall'utente, anche se in realtà è stato semplicemente aperto.

Per correggere questo "bug" (i "bug", come dovreste sapere, sono gli errori commessi dai programmatori nello sviluppo di un'applicazione), basterà aggiornare blnTextChanged in mnuApri_Click aggiungendo in fondo, DOPO l'istruzione txtFile = strTesto:

blnTextChanged = False

Un altro bug da correggere è quello che si verifica quando il file che abbiamo inserito nell'inputbox non viene trovato: ciò può accadere sia perché abbiamo sbagliato a scrivere il nome del file, sia perché non abbiamo inserito il percorso completo del file; infatti, se manca il percorso, ACCESS cercherà il file nella directory corrente, che solitamente è quella del file MDB stesso.

Per ovviare a questo inconveniete useremo, al posto del banale InputBox, le finestre di dialogo standard utilizzate da Windows per scegliere i file da aprire o salvare: queste finestre si chiamano CommonDialog e sono contenute nel file comdlg32.ocx.
Il controllo CommonDialog di norma non è presente nella casella degli strumenti, perciò dobbiamo aggiungerla noi. cliccate sul pulsante come visibile in figura e selezionare Microsoft Commond Dialog Control


Una volta selezionato il cursore appare come un martellino. cliccando nel punto di destinazione sul form vedrete un oggetto con questo disegno.

Questo controllo dispone di diverse interessanti proprietà, che approfondiremo a suo tempo: le cose essenziali da sapere sono che per visualizzare la finestra bisogna utilizzare uno dei metodi Show (nel nostro caso serviranno ShowOpen e ShowSave), e che il nome del file scelto dall'utente è contenuto nella proprietà FileName, insieme al percorso completo; invece il semplice nome del file, senza il percorso, è contenuto nella proprietà FileTitle. Nominiamo tale controllo con il nome CommonDialog1
Ora, al posto dell'InputBox, possiamo usare:

CommonDialog1.ShowOpen 

in mnuApri_click, e:

CommonDialog1.ShowSave 

in mnuSalva_click.

Non abbiamo più bisogno della variabile strNomeFile, perché per aprire il file basterà fare:

Open CommonDialog1.FileName For ... 

Dato che solitamente col blocco note si aprono i file di testo, impostiamo i tipi di file che è possibile scegliere dalla finestra di dialogo nella casella di riepilogo posta in basso: per fare questo bisogna impostare correttamente la proprietà Filter, secondo questo schema:

descrizione1|filtro1|descrizione2|filtro2 

La descrizione è la voce che compare nella casella di riepilogo, mentre il filtro è una stringa che indica quali file visualizzare; il carattere che separa la descrizione dal rispettivo filtro è la pipe (simbolo "|"), che solitamente si trova sulla tastiera sopra "\"; nel nostro caso basterà fare:

CommonDialog1.Filter="File di testo|*.txt|Tutti i file|*.*" 

Questa riga si può mettere ad esempio nel Form_Load oppure appena prima di visualizzare la finestra di dialogo, o ancora direttamente nella finestra delle proprietà in fase di progettazione; selezionando, nella finestra di dialogo, il tipo "File di testo", saranno visualizzati solo i file con estensione *.txt; selezionando "Tutti i file" saranno visualizzati tutti i file. Ora il nostro blocco note ha un'aspetto un po' più professionale.
Restano ancora un paio di cose da fare (veramente ce ne sarebbero diverse, ma per ora ci accontentiamo così): se l'utente prova a ridimensionare la finestra, avrà la spiacevole sorpresa che il TextBox non si adegua alle dimensioni del form.
Per rimediare a quest'altro bug, dobbiamo sfruttare l'evento Resize del frmNotePad: questo evento è scatenato da qualunque operazione di ridimensionamento, e noi non dobbiamo fare altro che rendere uguali le dimensioni del txtFile a quelle del frmNotePad; basterà inserire queste due istruzioni:

Me.TxtFile.Width = Me.InsideWidth - 10
            Me.TxtFile.Height = Me.InsideHeight - Me.IntestazioneMaschera.Height  - 10

Il "-10" serve solo a lasciare un po' di spazio tra il margine inferiore del txtFile e quello del frmNotePad;
Il secondo problema irrisolvibile è che il txtFile non ha la ScrollBar orizzontale ma questo è uun limite che dipende dal controllo di access..

Ora il nostro blocco note è pronto: in un'oretta e con circa 40 righe di codice abbiamo replicato in buona parte il Blocco Note di Windows.

Riprenderemo in seguito l'applicativo per aggiungere la funzionalità di ricerca.


Dodicesima lezione - Nozioni avanzate sulle variabili

Completiamo il discorso sulle variabili e sulla loro dichiarazione: finora abbiamo visto i tipi Byte, Integer, Long, String e Boolean; naturalmente ce ne sono diversi altri, ad eempio: Date, utilizzato per memorizzare le date e le ore, che occupa 8 byte in memoria e che rappresenta una data compresa tra il 1/1/100 e il 31/12/9999; Single, utilizzato per memorizzare valori frazionari a precisione singola (4 byte) compresi tra circa -3,4*10^38 e circa 3,4*10^38; Double, che memorizza valori frazionari a precisione doppia (8 byte) compresi tra circa -1,8*10^308 e circa 1,8*10^308; Currency, che memorizza valori frazionari con quattro decimali (8 byte), solitamente utilizzato per calcoli con valori monetari; Object, che memorizza un riferimento a un oggetto qualunque, come un form, un textbox, un file, ecc.
I tipi Single e Double sono gestiti in modo diverso rispetto ai valori interi come Integer e Long: infatti i bit al loro interno sono divisi in due gruppi, la mantissa e l'esponente, secondo la notazione scientifica in base allo standard IEEE; per questo motivo i Single e i Double sono in grado di gestire numeri molto grandi o molto piccoli, ma talvolta hanno difficoltà a trattare le operazioni con numeri interi e con le approssimazioni.
Per quanto riguarda il tipo String, finora abbiamo considerato solo le stringhe a lunghezza variabile, ma in realtà ci sono anche le stringhe a lunghezza fissa, che si dichiarano in questo modo:

Dim Nome As String * lunghezza della stringa 

Mentre una stringa di lunghezza variabile occuperà in memoria 10 byte più il numero di byte necessari per la stringa vera e propria, una stringa di lunghezza fissa occuperà solo il numero di byte indicato nella dichiarazione della variabile: questo perché, se la stringa è di lunghezza variabile, Visual Basic dovrà utilizzare memoria aggiuntiva (10 byte, in questo caso) per conservare informazioni quali, appunto, la lunghezza effettiva della stringa.
Esistono poi i dati definiti dall'utente, o meglio dal programmatore, che consistono in un insieme di sottovariabili ciascuna col suo tipo specifico; ad esempio io posso definire un mio tipo di variabile in questo modo:

Type TipoPersonaggioDIWaltDisney
      Eta  As Integer
      Name As String * 25
End Type

Successivamente dichiarare una variabile di questo tipo:

Dim Personaggio As TipoPersonaggioDIWaltDisney

la variabile Personaggio occuperà in memoria tanto spazio quanto è necessario per l'insieme dei suoi membri, cioè 2+25=27 byte, e si potrà assegnare un valore a ciascuno dei suoi membri scrivendo, ad esempio:

Personaggio.Eta = 100
            Personaggio.Name = "Pluto" 

Nella prima lezione avevo detto che la sintassi per la dichiarazione di variabili è la seguente:

Dim Nome As Tipo 

In realtà la seconda parte ("As Tipo") si può omettere: in tal caso, per default, la variabile dichiarata sarà di tipo Variant.
Questo particolare tipo di dati è in grado di contenere qualunque altro tipo di dati, tranne che quelli definiti dall'utente e le stringhe di lunghezza fissa: in virtù di questa sua caratteristica, il tipo variant ha una grande flessibilità, che consente di utilizzarlo per memorizzare dati che a priori potrebbero assumere diversi tipi. Ad esempio, è possibile fare una cosa del genere:

Dim V As Variant 'oppure semplicemente: Dim V
                        V = 13.5
                        V = "Pippo"
                        V = 4000 

Se si utilizzassero variabili di tipo definito, bisognerebbe dichiararne tre diverse: una di tipo Double, una di tipo String e una di tipo Integer.
La flessibilità del tipo Variant ha ovviamente un rovescio della medaglia: poiché un Variant può assumere valori di vario tipo, VB deve memorizzare, oltre al dato in sé, anche altre informazioni sul tipo di dato; per questo un Variant impegna 16 byte aggiuntivi oltre a quelli necessari per memorizzare il dato, se si tratta di un dato numerico, e ben 22 byte aggiuntivi se il dato contiene caratteri.
Non solo, ma poiché non si sa a priori di che tipo è il dato memorizzato in un Variant, ogni volta che questo dato viene utilizzato Visual Basic deve perdere tempo a convertirlo opportunamente nel tipo più appropriato: infatti ogni variabile di tipo Variant assume di volta in volta un sottotipo che indica per l'appunto quale tipo di dati sarebbe più appropriato per il valore assegnato alla variabile Variant.
Così, nell'esempio precedente, la variabile V assumerà i sottotipi Double, String e Integer: per rendersene conto è sufficiente utilizzare la funzione

TypeName(NomeVar) 

che restituisce una stringa corrispondente al sottotipo della variabile NomeVar; oppure la funzione

VarType(nomevar) 

che invece restituisce un Integer corrispondente.
Da tutto ciò risulta che il tipo Variant deve essere utilizzato solo se è giustificato da un bisogno di flessibilità: se un programmatore sa che una variabile assumerà sempre e soltanto valori compresi, ad esempio tra 1000 e 100000, dovrebbe dichiarare questa variabile come Long, perché così facendo otterrà un vantaggio in termini di utilizzazione della memoria e di velocità di esecuzione.
E' vero che se le variabili sono poche questo vantaggio risulta impercettibile, ma se le variabili sono numerose dichiararle come variant è una scelta decisamente inefficiente.
E comunque è sempre una buona regola di programmazione assegnare un tipo definito a ogni variabile tutte le volte che è possibile farlo.
C'è un modo per evitare che le variabili dichiarate senza tipo assumano di default il tipo variant: bisogna usare l'istruzione DefTipo, la cui sintassi è:

DefTipo IntervalloCaratteri 

dove tipo indica il tipo di dati definito (ad esempio DefBool, DefInt, DefStr), e intervallocaratteri indica l'intervallo di lettere per le quali sarà impostato il tipo predefinito.
Ad esempio, scrivendo, PRIMA di ogni dichiarazione a livello di modulo (e quindi dopo Option Explicit):

DefLng A - G, X - Z
Dim Arg
Dim X
Dim Pippo 

Le variabili Arg e X saranno di tipo Long, perché il carattere iniziale del loro nome è compreso negli intervalli specificati nell'istruzione Deftipo, mentre la variabile Pippo sarà un Variant perché la "p" non rientra negli intervalli "a-g" e "x-z".
Naturalmente è possibile usare più istruzioni DefTipo, con intervalli diversi, ed è anche possibile ignorare questa istruzione scrivendo esplicitamente il tipo di dato da assegnare a una variabile: la dichiarazione esplicita completa ha sempre la prevalenza rispetto alle impostazioni di default.
L'istruzione DefTipo non vale soltanto per le variabili dichiarate, ma anche per i parametri passati come argomenti delle routine e per il tip restituito dalle funzioni.

ecco l'elenco

DefBool intervallolettere[, intervallolettere] . . .
DefByte
intervallolettere[, intervallolettere] . . .
DefInt
intervallolettere[, intervallolettere] . . .
DefLng
intervallolettere[, intervallolettere] . . .
DefCur
intervallolettere[, intervallolettere] . . .
DefSng
intervallolettere[, intervallolettere] . . .
DefDbl
intervallolettere[, intervallolettere] . . .
DefDec
intervallolettere[, intervallolettere] . . .
DefDate
intervallolettere[, intervallolettere] . . .
DefStr
intervallolettere[, intervallolettere] . . .
DefObj
intervallolettere[, intervallolettere] . . .
DefVar
intervallolettere[, intervallolettere] . . .

Un modo per abbreviare le dichiarazioni consiste nell'utilizzare i caratteri di dichiarazione del tipo: i tipi principali, infatti, sono identificati da un carattere speciale che può essere posposto al nome della variabile.
Ad esempio, le seguenti dichiarazioni sono equivalenti:

Dim X As Integer
Dim X% 

perché il carattere % è il carattere di dichiarazione del tipo Integer: notate che il nome della variabile è semplicemente "X", senza il carattere di dichiarazione del tipo.
I caratteri di dichiarazione supportati in Visual Basic sono questi

Carattere Tipo
% Integer
& Long
@ Currency
# Double
! Single
$ String

Spesso è necessario convertire un tipo di variabile in un altro: la conversione più frequente è forse quella da numero a stringa e viceversa (cioè da 34 a "34" e viceversa), ma talvolta capita anche di dover convertire un Integer in un Long, o un Single in un Integer, o un Long in Date, ecc.
Esistono perciò delle apposite funzioni di conversione del tipo, che iniziano con "c" seguito dall'abbreviazione del tipo restituito:
 

Funzione Tipo Restituito Intervallo
CBool Booleano Qualsiasi espressione numerica o stringa valida
CByte Byte Da 0 a 255
CCur Currency Da -922.337.203.685.477,5808 a 922.337.203.685.477,5807
CDate Date Qualsiasi espressione di data valida
CDbl Double Da -1,79769313486232E308 a -4,94065645841247E-324 per valori negativi; da 4,94065645841247E-324 a 1,79769313486232E308 per quelli positivi
CDec Decimal +/-79.228.162.514.264.337.593.543.950.335 per numeri con fattore di divisione zero, ovvero numeri senza decimali. Per i numeri con 28 decimali l'intervallo è +/-7,9228162514264337593543950335. Il numero minore possibile diverso da zero è 0,0000000000000000000000000001
CInt Integer Da -32.768 a 32.767; le frazioni vengono arrotondate
CLng Long Da -2.147.483.648 a 2.147.483.647; le frazioni vengono arrotondate
CSng Single Da -3,402823E38 a -1,401298E-45 per valori negativi; da 1,401298E-45 a 3,402823E38 per valori positivi
CStr String I valori restituiti da CStr dipendono dall'argomento della funzione
CVar Variant Lo stesso intervallo di Double per valori numerici. Lo stesso intervallo di String per valori non numerici

Naturalmente il parametro passato a queste funzioni deve poter assumere un valore che rientra nell'intervallo specifico del tipo restituito: ad esempio non è possibile scrivere:

CInt(100000.45) 

perché il valore 100000 è superiore al massimo consentito per un Integer.

Tredicesima lezione - Estensione della logica binaria

Nella lezione 8 abbiamo visto cosa sono i bit, i byte e come funziona la logica binaria che sta alla base dei moderni computer: seguendo questa logica abbiamo capito che un byte può contenere un valore compreso tra 0 e 255, due byte possono contenere un valore compreso tra 0 e 65535, e così via.
Tuttavia, questo è vero solo se si usa una logica strettamente binaria: nulla però impedisce di considerare una sequenza di bit in un modo leggermente diverso, secondo la nostra convenienza; potremmo cioè suddividere una sequenza di bit in uno o più gruppi aventi significati diversi.
In altre parole, i bit possono essere organizzati in modo tale che possano assumere un valore diverso da quello che avrebbero secondo il sistema binario puro e semplice: questo è proprio ciò che si fa per permettere al calcolatore di considerare i numeri negativi.
Una variabile di tipo Integer, ad esempio, consta di 16 bit (2 byte) e secondo il sistema binario dovrebbe poter assumere valori da 0 fino a 65535, mentre in realtà sappiamo che il suo intervallo di valori va da -32768 a +32767, ovvero da -2^15 a +2^15 - 1.
E' chiaro quindi che il valore assoluto del numero memorizzato in una variabile Integer è rappresentabile soltanto con 15 bit, e non con 16: che fine ha fatto il sedicesimo bit?
E' facile intuire che il sedicesimo bit è utilizzato per rappresentare il segno del numero; in fin dei conti non cambia nulla: l'intervallo -32768/+32767 ha lo stesso numero di elementi dell'intervallo 0/65535 (in entrambi i casi infatti il numero di valori possibili è 65536 = 2^16), però grazie al "trucco" di utilizzare un bit come un bit di segno abbiamo la possibilità di usare anche i numeri negativi.
Per la precisione, il bit di segno è il bit più significativo, ovvero quello più a sinistra (ricordate sempre che i bit si contano a cominciare da destra), e gli altri quindici sono usati per indicare il numero vero e proprio; quindi il modo in cui gli Integer vengono memorizzati nella memoria del computer è schematizzabile in questo modo:

 primo byte secondo byte ...
       x     xxxxxxx xxxxxxxx

dove la prima x a sinistra rappresenta il sedicesimo bit, ovvero il bit di segno: se questo vale 0, il valore della variabile sarà positivo, se vale 1 il valore sarà negativo; ad esempio, il numero 1453 è rappresentabile in questo modo:

00000101 10101101 

Contrariamente a quanto si potrebbe pensare, però, il numero -1453 NON è rappresentato così:

10000101 10101101 

bensì è rappresentato così:

11111010 01010011 

Il motivo di questo strano fenomeno è che per il computer il numero -1453 non è banalmente il numero 1453 con "un meno davanti", ma è quel numero che, sommato a 1453, restituisce 0: infatti se provate a fare la somma (nel sistema binario, quindi scrivendo 0 "col riporto di 1" quando la somma di due bit è uguale a 2)

                                    0000010110101101+ (+1453)
                                    1111101001010011= (-1453)
                                    -----------------
                                    10000000000000000 

troverete che il risultato della somma è un 1 seguito da 16 zeri; poiché noi stiamo considerando gli Integer, i bit che ci interessano sono solo gli ultimi 16, e quindi dell'1 iniziale possiamo fregarcene tranquillamente: quello che resta è 0, che è proprio il risultato della somma algebrica 1453 + (-1453).
Tutto ciò sembra molto macchinoso, ma c'è un modo molto semplice per trovare la rappresentazione binaria di un numero negativo, conoscendo quella del corrispondente numero positivo: la rappresentazione binaria di -x è uguale a (not x) +1: prendete la rappresentazione binaria di 1453, cambiate ogni bit (cioè applicate l'operatore not ad ogni singolo bit) e infine aggiungete 1 al risultato ("aggiungere" nel senso di fare la somma, non nel senso di scriverlo in fondo come se fosse il diciassettesimo bit!): troverete esattamente la rappresentazione binaria di -1453. Lo stesso discorso naturalmente può essere applicato anche agli altri tipi numerici (Long, Single, Double, ecc.): infatti in Visual Basic tutti i tipi di dati numerici, ad eccezione del tipo byte, sono "con segno", cioè utilizzano il bit più significativo come bit di segno. Una particolare organizzazione dei bit consente anche di rappresentare i numeri frazionari, oppure di rappresentare i numeri secondo la notazione scientifica (con una mantissa e un esponente) come avviene nei Single e nei Double.

Svelato l'arcano mistero dei numeri negativi, passiamo ai vettori di variabili: spesso si ha necessità di utilizzare più di una variabile dello stesso tipo per effettuare una stessa operazione; ad esempio, per calcolare la media di 10 valori ho bisogno di sommare questi 10 valori e dividere la somma per 10: a tale scopo potrei utilizzare 10 variabili diverse, che naturalmente vanno dichiarate una per una, più o meno in questo modo:

Dim Var1 As Integer
Dim Var2 As Integer
...
Dim Var10 As Integer
                                    Var1 = 1
                                    Var2 = 44
                                    Var = 13
                                    ...
                                    Var10 = 32
                                    Media = (Var1 + Var2 + Var3+ ... + Var10) / 10 

E se a un certo punto dovessi aver bisogno di fare la media non più di 10 valori, ma di 20 valori, dovrei dichiarare altre 10 variabili, impostare il valore opportuno per ciascuna di esse, sommarle alle 10 dichiarate in precedenza, e infine dividere la somma per 20! E' chiaro che questo modo di procedere è a dir poco laborioso: in questi casi è estremamente conveniente utilizzare i vettori, ovvero un insieme di variabili dello stesso tipo e con lo stesso nome, distinguibili le une dalle altre solo tramite un indice.
La dichiarazione di un vettore di variabili si fa scrivendo tra parentesi il numero di elementi che deve avere il vettore: per esempio nel nostro caso potremmo scrivere:

Dim Var(10) As Integer

Avremo così a disposizione 10 variabili (anzi, in realtà 11 come spiegherò tra poco) di tipo Integer di nome Var: la prima variabile che ci interessa sarà Var(1), la seconda Var(2), ecc. fino alla decima, Var(10).
Nella dichiarazione, il numero indicato tra parentesi indica il limite superiore del vettore, ovvero il valore massimo che può assumere l'indice: se provassimo a scrivere, ad esempio:

var(11) = 1000 

otterremmo un errore, perché il limite massimo dell'indice è 10.
Il limite superiore deve essere compreso nell'intervallo di valori valido per il tipo Long (-2147483648/+2147483647): da ciò segue che è possibile specificare come limite superiore anche un valore negativo, ma bisogna tenere presente un paio di cose.
Prima di tutto, così come c'è un limite superiore, c'è anche un limite inferiore per l'indice: se non è specificato nella dichiarazione, questo limite inferiore è per default 0; pertanto scrivendo:

Dim Var(10) As Integer

l'indice di questo vettore potrà andare da 0 a 10, e quindi il vettore avrà 11 elementi; se si desidera che l'indice parta sempre da 1 anziché da 0, è possibile utilizzare l'istruzione:

Option Base 1

Questa istruzione, come l'analoga Option Explicit va inserita nella sezione generale del form.
Però è anche possibile indicare entrambi i limiti, quello inferiore e quello superiore, nella dichiarazione stessa, usando la parola chiave To:

Dim Var(1 To 10) As Integer

indica un vettore con 10 elementi, il cui indice va da 1 a 10; analogamente si potrà scrivere:

Dim Var(10 To 20) As Long

oppure:

Dim Pippo(-5 To 32) As String

e così via. La seconda cosa da tenere presente è che, ovviamente, il limite inferiore deve sempre essere minore del limite superiore: perciò è sbagliato scrivere

Dim Var(10 To 0) As Integer

ed è anche sbagliato scrivere:

Dim Var(-5) As Integer

perché in questo caso il limite inferiore implicito è 0 o 1, che è maggiore di -5.

Quattordicesima lezione - Le matrici, il ciclo For...Next ed il campo minato passo per passo

Oltre ai vettori è possibile utilizzare anche matrici, che non sono altro che vettori a più dimensioni (o meglio, i vettori sono matrici a una sola dimensione).
Ad esempio, supponiamo di voler memorizzare le dimensioni di 100 scatole: non ci basterà un vettore con 100 elementi, perché per ogni scatola avremo bisogno di 3 variabili per le 3 dimensioni (altezza, larghezza, profondità), pertanto dovremo utilizzare una matrice:

Dim Scatole(1 To 100, 1 To 3) As Integer

o anche:

Dim Scatole(99,2) As Integer

In entrambi i casi (se non avete inserito l'istruzione Option Base 1) avremo una matrice con 100 * 3 = 300 elementi, ovvero con 100 "righe" e 3 "colonne".
Per indicare che si vogliono usare più dimensioni, quindi, bisogna separare i limiti di ogni dimensione con una virgola.
E' possibile inoltre dichiarare una matrice dinamica, il cui numero di elementi può cambiare nel corso dell'esecuzione del programma: a questo scopo è però necessario usare una parola chiave apposita, ReDim, e dichiarare la matrice senza indicarne le dimensioni:

Dim Var() As Long 'dichiarazione della matrice:
                                    'non è indicata la dimensione della matrice        

e successivamente, in un altro punto del codice:

ReDim Var(120) As Long 'ridimensionamento della matrice

In tal caso è anche possibile utilizzare una variabile, anziché un'espressione costante, per indicare il limite inferiore e/o superiore della matrice, ad esempio:

ReDim Var(x) As Long

L'importante è ricordare che il ridimensionamento di una matrice riguarda esclusivamente il numero di elementi, non il loro tipo, né tantomeno il nome della matrice.
A proposito del tipo degli elementi, è interessante notare che se essi sono Variant possono contenere a loro volta altre matrici o vettori:

Dim Vettore(3) As Long
Dim MatriceVariant(5,2)
    MatriceVariant(2,0) = Vettore
    MatriceVariant(2,1) = 10
    MatriceVariant(1,2) = "stringa" 

Le istruzioni precedenti definiscono un semplice vettore di quattro elementi e una matrice di 18 (6 righe per 3 colonne) elementi Variant, ad uno dei quali è stato assegnato il nostro vettore: perciò l'elemento 2,0 della matrice non è semplicemente un numero, o una stringa, o un qualunque altro tipo elementare di dato, ma è esso stesso un vettore di 4 elementi.
Per accedere a uno dei quattro elementi di MatriceVariant(2,0) bisogna usare questa sintassi:

MatriceVariant(2,0)(i) 'i varia da 0 a 3

E' come se la matrice bidimensionale MatriceVariant avesse, nel caso dell'elemento 2,0 (e SOLO per quell'elemento), una terza dimensione data dai 4 elementi del nostro vettore iniziale.
Naturalmente il vettore e la matrice restano variabili (o meglio, insiemi di variabili) distinte e indipendenti, e per rendersene conto basta mettere alla prova queste semplici istruzioni:

Dim vettore(3) As Long
Dim MatriceVariant(5,2)
                                    Vettore(2) = 10
                                    MatriceVariant(1,1) = Vettore
                                    Vettore(2) = 20
                                    Debug.Print Vettore(2), MatriceVariant(1,1)(2) 

Nella finestra di debug si leggeranno i valori 20 e 10, perché MatriceVariant(1,1) non risente delle modifiche successive all'assegnazione del vettore all'elemento della matrice.
Per utilizzare in modo efficiente i vettori non bisogna trattare gli elementi come se fossero variabili completamente distinte.
Supponiamo ad es. di voler inizializzare gli elementi di un vettore a un certo valore: non avrebbe molto senso scrivere:

Dim Vettore(2) As Long
                               Vettore(0) = 10
                               Vettore(1) = 10
                               Vettore(2) = 10 

perché in tal caso sarebbe come utilizzare tre variabili v1, v2, v3, distinte e indipendenti le une dalle altre: in altre parole, non si sfrutterebbe la loro appartenenza a un solo vettore che le comprende tutte.
Talvolta è necessario agire in questo modo, ma spesso è molto più conveniente sfruttare la possibilità di accedere ad ogni elemento tramite il suo indice, e questo si fa usando i cicli.
Un ciclo non è altro che un gruppo di istruzioni che vengono eseguite ripetutamente, ovvero ciclicamente, fino a che una certa condizione risulta verificata.
Di cicli ne esistono vari tipi in Visual Basic, ma quello in assoluto più utilizzato è il ciclo For...Next.
Questo ciclo è identificato da due istruzioni, poste rispettivamente all'inizio e alla fine del ciclo:

For contatore = inizio To fine
                                    ...
Next contatore 

dove contatore indica una variabile numerica che "conta" quante volte devono essere eseguite le istruzioni comprese nel ciclo: essa assume un valore iniziale (inizio) e viene incrementata dall'istruzione Next fino a raggiungere il valore finale (fine).
Approfondiremo più avanti il discorso del ciclo For...Next e degli altri cicli: per ora accontentatevi di sapere che il ciclo viene eseguito fintantoché il valore di contatore è minore o uguale a fine; il nostro esempio precedente risulterebbe trasformato così:

Dim Vettore(2) As Long
Dim i As Integer 'contatore per il ciclo
For i = 0 To 2
     Vettore(i) = 10
Next

Alla prima esecuzione del ciclo i assume valore 0 e quindi il primo elemento del vettore è inizializzato a 10.
Alla seconda esecuzione i è uguale a 1 ed è inizializzato il secondo elemento. Alla terza esecuzione i è uguale a 2 ed è inizializzato il terzo elemento; dopodiché i supera il valore 2 e quindi il ciclo termina. Torniamo alle variabili Variant: queste possono contenere, tra le altre cose, anche riferimenti ad oggetti, e in particolare riferimenti a controlli; è possibile quindi creare vettori o matrici di controlli.


Campo Minato (non ancora adattato al VBA di Access)

Per illustrare meglio questo concetto, realizziamo un altro progetto di esempio: questa volta proviamo a replicare uno dei giochi di Windows più famosi, il "Campo minato".
Cominciamo quindi con l'aprire un nuovo progetto "exe standard", e inseriamo nel form un pulsante: ridimensioniamolo fino a farlo diventare un quadrato abbastanza piccolo, cancelliamo la proprietà caption (in realtà questa proprietà non è vuota, ma contiene la stringa nulla) e assegniamo la stringa "Mina" alla proprietà name.

 



 

Ora selezioniamo il nostro pulsante, copiamolo andando sul menù Modifica o semplicemente premendo ctrl+c, e infine incolliamolo sullo stesso form andando ancora sul menù Modifica o premendo : apparirà un messaggio di questo tenore:

 



 

Noi diciamo di Sì, e com'era prevedibile un nuovo pulsante uguale al primo sarà inserito nel form.
In realtà i due pulsanti non sono proprio uguali: ciò che cambia è la proprietà Index.
Prima di copiare e incollare il nostro pulsante, la sua proprietà Index era vuota: è ovvio che sia così, perché questa proprietà, secondo la guida, "identifica un controllo in una matrice di controlli", e quando il pulsante era ancora da solo non c'era alcuna matrice di controlli.
Ora che abbiamo incollato un nuovo pulsante, invece, la matrice di controlli c'è, e pertanto è necessario distinguere i vari pulsanti per mezzo del loro indice: questo indice ha esattamente la stessa funzione dell'indice dei vettori di variabili, solo che ora al posto delle variabili ci sono dei controlli.
Continuiamo a incollare i pulsanti fino ad averne, ad esempio, 16 disponendoli in quadrato: non è indispensabile ma è molto meglio se li disponete in modo che l'indice segua un ordine logico, ad esempio procedendo da sinistra verso destra riga per riga.
Così nella prima riga ci saranno i pulsanti con indice 0,1,2,3, nella seconda quelli con indice 4,5,6,7, e così via.

 



 

Ora aggiungiamo due etichette, da mettere nella parte alta del form: lblMine e lblTempo (se volete, impostate la proprietà Alignment a "right"); infine aggiungiamo un menù "Partita" con le voci "Nuova" (il nome sarà "mnuNew") e "Esci" ("mnuExit"), separate da una barra orizzontale: per fare questa barra sarà sufficiente mettere come caption un solo trattino "-" e nient'altro; naturalmente le voci "Nuova", "Esci" e la linea di separazione devono essere gerarchicamente subordinati rispetto al menù "Partita" (v.anche la lez. 9). Resta ancora da inserire un timer, che trovate naturalmente sulla casella degli strumenti (è quello con un orologio come icona): potete metterlo dove volete, tanto risulta invisibile in fase di esecuzione; ora il nostro form è pronto, dobbiamo solo scrivere il codice per far funzionare il gioco, e lo faremo nelle prossime lezioni.

Quindicesima lezione - Campo Minato: il controllo Timer, le funzioni Rnd e Int(), le istruzioni Randomize e GoTo e la proprietà Tag

 

Cominciamo dall'etichetta lblTempo, che dovrà misurare il trascorrere del tempo di gioco: la cosa più ovvia è aggiornarla ogni secondo, quindi dobbiamo impostare correttamente le proprietà del Timer; questo controllo misura evidentemente il trascorrere del tempo generando un evento Timer ogni volta che passano tot millisecondi.
Il numero di millisecondi trascorsi i quali viene generato l'evento è indicato dalla proprietà Interval: dato che noi vogliamo contare i secondi, sarà bene impostare questa proprietà a 1000 ms, cioè 1 secondo; in parole povere, quando il timer viene attivato comincerà a contare i millisecondi: quando arriva a 1000 genera l'evento Timer, reimposta a 0 il contatore e ricomincia a contare fino ad arrivare nuovamente a 1000, generando un altro evento Timer; e così via.
Il controllo Timer non deve essere attivo subito, ma deve cominciare a contare solo quando l'utente darà il via alla partita: quindi nella finestra delle proprietà impostate Enabled a false.
Cliccando due volte sul Timer, comparirà l'editor del codice sulla routine dell'evento Timer: qui dobbiamo inserire le istruzioni per aggiornare l'etichetta; banalmente, sarà sufficiente dichiarare una variabile statica che funzioni da contatore e scriverne il valore nell'etichetta:

Static i As
                        Integer
                                    i = i + 1
                                   
                        lblTempo.Caption = CStr(i) 
            

La variabile i va dichiarata come Static perché deve tener conto dei secondi passati anche "al di fuori" della routine "timer1_timer": se non si usa la parola chiave Static, la variabile verrà creata ogni volta che l'evento timer viene generato, e di conseguenza assumerà sempre il valore zero.
Il timer, come ho detto, dovrà essere attivato quando comincia una nuova partita, quindi nella routine mnuNew_click bisognerà scrivere:

Timer1.Enabled = True
            

e corrispondentemente esso dovrà essere disattivato quando la partita finisce: ce ne occuperemo tra poco.
Ora viene la parte più difficile, scrivere il codice associato ai pulsanti, che costituiscono il gioco vero e proprio: innanzitutto bisogna decidere dove piazzare le mine; visto che le caselle sono 16, potremmo piazzare 3 mine.
Naturalmente non possiamo decidere noi direttamente dove metterle, ma dovremo affidarci al caso, usando quindi i numeri casuali: per generare i numeri casuali esiste una funzione, Rnd, che restituisce un numero (più o meno) casuale compreso tra 0 (incluso) e 1 (escluso), ovvero tra 0 e 0,99999.
Noi però dobbiamo decidere sotto quale pulsante mettere la mina, quindi dobbiamo avere un numero che ci dica qual è l'indice del pulsante in questione; e dal momento che i pulsanti sono 16, il loro indice andrà da 0 a 15, e pertanto abbiamo bisogno di un numero casuale compreso tra 0 e 15: ottenerlo è banalissimo, basta infatti moltiplicare il risultato della funzione Rnd per 16, e prenderne la parte intera:

Int(16 * Rnd) 
            

La funzione Int() non fa altro che troncare la parte decimale di un numero restituendo solo la parte intera: se ad esempio Rnd restituisce 0.1, 16 * 0.1 = 1.6 e quindi int(1.6) = 1; visto che Rnd restituisce sempre un numero minore di 1, il prodotto 16 * Rnd sarà sempre minore di 16, ovvero compreso tra 0 e 15: proprio ciò che ci serve.
Dato che abbiamo bisogno di 3 mine, ci conviene fare un vettore di tre elementi, anziché dichiarare tre variabili distinte; quindi, nella sezione delle dichiarazioni del form, scriviamo:

Dim PosMine(2) As
                        Integer
            

La posizione delle mine dobbiamo deciderla all'inizio di ogni partita: quindi in mnuNew_click, scriviamo:

Dim i As Integer
                        'contatore per il ciclo
                                    Randomize
                        Timer
            For i = 0 To 2
                                    PosMine(i) =
                        Int(Rnd * 16)
            Next i 
            

L'istruzione Randomize serve ad inizializzare il generatore di numeri casuali: come saprete, i numeri "casuali" generati da un qualunque computer non sono veramente casuali, poiché sono generati in modo deterministico, seguendo regole ben precise; solo che queste regole sono tali da garantire, entro certi limiti, una sorta di casualità nei numeri generati, che per questo vengono definiti più correttamente "pseudo-casuali".
Ora, la funzione rnd calcola i numeri pseudo-casuali a partire da un numero base, che viene modificato dall'istruzione Randomize tramite il suo argomento: nel nostro caso questo argomento è il valore restituito dalla funzione Timer, che restituisce il numero di secondi trascorsi dalla mezzanotte del giorno corrente (la funzione Timer non c'entra nulla con il controllo Timer, né tantomeno con l'evento Timer); dato che questo valore è altamente aleatorio, anche il numero base utilizzato dalla funzione rnd cambia in modo abbastanza casuale, e quindi tale funzione calcola numeri un po' più casuali di quanto accadrebbe se si utilizzasse rnd senza Randomize.
Ora che abbiamo le posizioni delle mine, potremmo controllare, ogni volta che il giocatore preme un pulsante, se l'indice di quel pulsante appartiene al vettore PosMine: questo è un metodo alquanto laborioso e inefficiente, e sarebbe molto meglio se ogni pulsante sapesse già se sotto di esso si nasconde una mina oppure no. Per fare questo è sufficiente sfruttare una proprietà comune a quasi tutti i controlli, la proprietà Tag, che consente di memorizzare dati aggiuntivi utili per la funzionalità del controllo: nel nostro caso, ad esempio, potremmo associare ai pulsanti che nascondono una mina la stringa "mina", assegnandola alla proprietà Tag; quando il giocatore preme il pulsante, basterà controllare se la proprietà contiene la stringa "mina" per sapere se il gioco è terminato o se può continuare.
Ancora meglio, visto che le possibilità sono solo due (la mina c'è o non c'è), potremmo assegnare alla proprietà Tag direttamente un valore booleano che indichi l'eventuale presenza della mina; in altre parole, potremmo scrivere, all'interno del ciclo descritto sopra:

Mina(PosMine(i)).Tag = True
            

C'è però da considerare un'altra cosa: sarebbe comodo che ogni pulsante sapesse non solo se nasconde o meno una delle mine, ma anche quante altre mine ci sono sotto i pulsanti che lo circondano; in tal caso, nella proprietà Tag dovremmo scrivere il numero di mine circostanti ogni pulsante e, se una mina si trova proprio sotto il pulsante considerato, dovremmo assegnare un valore particolare. Ad es., se intorno al pulsante x ci sono due mine, potremmo scrivere x.Tag = 2; se intorno ad esso ce n'è una sola, scriveremmo x.Tag = 1; ma se la mina si trova proprio sotto x, non possiamo scrivere di nuovo x.Tag = 1, altrimenti si farebbe confusione: possiamo però scrivere x.Tag = -1, perché il numero di mine circostanti non può essere negativo, e quindi se la proprietà Tag contiene il valore -1, il pulsante saprà che la mina è sotto di lui, e non sotto qualche altro pulsante che lo circonda.
Un pericolo di cui una persona inesperta si accorgerebbe difficilmente è che diversi elementi del vettore PosMine potrebbero assumere valori uguali: questo dipende banalmente dal fatto che noi prendiamo in considerazione non l'intero numero casuale (moltiplicato per 16), ma solo la sua parte intera; se ad esempio la funzione rnd restituisse consecutivamente i valori 0.19 e 0.24, risulterebbe che Int(16 * 0.19) = Int(16 * 0.24) = 3, con la conseguenza che le nostre mine sarebbero in realtà due e non tre, perché due dei tre valori calcolati coincidono.
Per evitare questo rischio, è sufficiente controllare che il numero casuale appena estratto sia diverso da quelli già memorizzati: il modo più naturale di eseguire questo controllo sarebbe utilizzare un altro ciclo (ma di tipo diverso rispetto al For...Next), e siccome non ne ho ancora parlato, preferisco usare un metodo meno ortodosso ma di più semplice comprensione; si tratta di riscrivere la routine mnuNew_click in questo modo:

Dim t As Integer
                        'variabile temporanea per eseguire i controlli
                                    Randomize
                        Timer
                                    PosMine(0)=
                        Int(Rnd * 16)
                                    Mina(PosMine(0)).Tag
                        = -1
            'Estrai:
                                    t = Int(Rnd *
                        16)
            If t = PosMine(0) Then
            GoTo Estrai
            Else
                                    PosMine(1) =
                        t
                                    Mina(PosMine(1)).Tag
                        = -1
            End If
            'EstraiDiNuovo:
                                    t = Int(Rnd *
                        16)
            If t = PosMine(0) Or t =
                        PosMine(1) Then
            Goto EstraiDiNuovo
            Else
                                    PosMine(2) =
                        t
                                    Mina(PosMine(2)).Tag = -1
            End if
            

Quello che succede è abbastanza intuitivo: si estrae un numero casuale e lo si memorizza in PosMine(0); se ne estrae un altro e si controlla che sia diverso da quello precedente: se è diverso, lo si memorizza nell'elemento successivo del vettore (PosMine(1)), altrimenti se ne estrae un altro; la riestrazione è ottenuta attraverso l'istruzione Goto, che ordina al computer di interrompere la normale esecuzione del programma saltando a un altro punto del programma stesso, e precisamente al punto indicato da un'apposita etichetta.
Nel nostro caso le etichette sono due: "Estrai" e "EstraiDiNuovo"; la sintassi vuole che il nome delle etichette non abbia alcuno spazio al suo interno e sia seguito dai due punti ":". La riestrazione continua finchè non si trova un numero diverso da quello memorizzato in PosMine(0).
Poi si passa alla terza e ultima estrazione, che segue lo stesso schema logico, controllando questa volta che il numero casuale sia diverso da entrambi i valori precedentemente memorizzati, ovvero PosMine(0) e PosMine(1), grazie all'operatore logico Or.

Sedicesima lezione - Campo Minato: algoritmi, diagrammi di flusso, l'operatore Mod, la funzione IIf, il parametro Index

Ricapitolando, ora conosciamo le posizioni delle mine, e ogni pulsante sa, grazie alla proprietà tag, se nasconde una mina oppure no; ora dobbiamo scrivere le istruzioni per il gioco vero e proprio: cosa succede quando il giocatore preme un pulsante ?
Quello che succede è molto semplice: se sotto il pulsante premuto si trova una mina, il gioco termina; altrimenti viene visualizzato il numero di mine circostanti quel pulsante, come mostra la figura sotto:

 



bisognerà quindi effettuare un controllo del genere:

If Mina(i).Tag = -1 Then
            	'il gioco è finito!
            Else
            	'il gioco continua
            End If
            

Prima di tutto, però, bisogna calcolare quante mine circondano ogni pulsante: questa operazione è opportuno farla all'inizio della partita, in modo che il calcolo sia effettuato una volta sola; quindi dobbiamo aggiornare la routine mnuNew_Click.
A questo punto dobbiamo escogitare un algoritmo semplice ed efficiente per fare questi calcoli: per chi non lo sapesse, un algoritmo è una sequenza di operazioni che permettono di ottenere un generico obiettivo; la sequenza con cui queste operazioni devono essere eseguite è determinata dal verificarsi di certe condizioni, e il controllo di queste condizioni è parte integrante dell'algoritmo.
Solitamente si usa un grafico (il cosiddetto diagramma di flusso) per descrivere un algoritmo (ovvero il flusso delle operazioni), ma qui preferisco usare un elenco numerato.
Un algoritmo per calcolare il numero di mine circostanti un pulsante potrebbe essere questo:

1) prendo in considerazione un pulsante (chiamiamolo "x");

2) prendo in considerazione tutti i pulsanti che lo circondano, uno alla volta (chiamiamo questo pulsante "y");

3) se y nasconde una mina, incremento un contatore c, altrimenti non faccio nulla;

4) esamino il pulsante successivo tra quelli che circondano x, e torno al punto 2); se ho esaminato tutti i pulsanti che circondano x, assegno x.Tag = c e reimposto c = 0 (altrimenti c si ricorderebbe dei valori relativi ad altri pulsanti);

5) esamino il pulsante successivo a x e torno al punto 1; se ho esaminato tutti i pulsanti, ho terminato l'algoritmo.

In parole povere, questo algoritmo descrive due cicli annidati tra loro: il primo esamina tutti i nostri 16 pulsanti, uno alla volta; il secondo, quello più interno, esamina tutti i pulsanti che circondano quello preso in esame dal primo ciclo; all'interno del secondo ciclo viene aggiornato il contatore delle mine.
C'è però un altro algoritmo, che mi sembra più semplice ed efficiente: visto che sappiamo che le mine sono in tutto 3, non c'è alcun bisogno di fare due cicli per andare a cercare i pulsanti che nascondono una mina; noi sappiamo dove sono le mine, e quindi ci basterà aggiornare le proprietà Tag dei pulsanti che circondano quelli che nascondono una mina.
L'algoritmo è questo:



1) inizializzo le proprietà tag di tutti i pulsanti a 0;

2) prendo in esame il pulsante con indice PosMine(i) (chiamiamolo "x"; inizialmente i = 0);

3) pongo x.tag = -1 e aggiorno le proprietà tag di tutti i pulsanti che circondano x;

4) se i = 2 termino l'algoritmo, altrimenti pongo i = i + 1 e torno al punto 2.

Tradotto in codice, questo algoritmo diventerebbe così (ovviamente va aggiunto alla routine mnuNew_Click):

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
            For i = 0 To 15
               Mina(i).Tag = 0
            Next i
            For i = 0 To 2
   Mina(PosMine(i)).Tag = -1
   x = Int(PosMine(i) / 4) 'riga in cui si trova la mina
   y = PosMine(i) Mod 4 'colonna in cui si trova la mina
   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 Mina(4 * x1 + y1).Tag > -1 Then
 	      Mina(4 * x1 + y1).Tag = Mina(4 * x1 + y1).Tag + 1
       	   End If
       Next y1
   Next x1
            Next i
            Timer1.Enabled = True
            

Il primo ciclo è quello di inizializzazione; il secondo appare più complicato di quanto sia in realtà: innanzitutto viene aggiornato il Tag del pulsante con la mina, poi vengono calcolate le coordinate di questo pulsante; questa operazione è opportuna perché i pulsanti sono disposti graficamente come una matrice bidimensionale 4 x 4, mentre nel codice sono "allineati" in un vettore unidimensionale.
Prendiamo ad esempio il decimo pulsante, quello con indice 9: dato che ogni riga è di quattro elementi, per sapere su quale riga e colonna si trova bisogna sottrarre dall'indice il multiplo di 4 immediatamente precedente all'indice stesso (in questo caso 8): la differenza (9 - 8 = 1) indica la colonna, mentre questo multiplo diviso per 4 indica la riga (8 / 4 = 2).
La colonna viene ottenuta tramite l'operatore Mod, che restituisce il resto intero di una divisione: ad esempio, 9 Mod 4 = 1 perché 9 / 4 è uguale a 2 col resto di 1; ovviamente 8 Mod 4 = 0, così come 10 Mod 5, 6 Mod 2, 3 Mod 3 ecc.
Per analogia con l'indice dei pulsanti, anche le righe e le colonne le contiamo a partire da 0, quindi il pulsante con indice 9 si troverà sulla riga 2 colonna 1, ovvero il decimo pulsante si trova sulla terza riga, seconda colonna.
Ottenute le coordinate, è più semplice controllare i pulsanti che circondano la mina.
Avrete notato una nuova funzione nei cicli più interni: la funzione IIf; questa funzione è simile ad una if "concentrata": i tre argomenti indicano rispettivamente la condizione da verificare, il risultato da restituire se la condizione è vera, il risultato da restituire se la condizione è falsa; in altri termini, IIf(x=0, 0, x-1) equivale a:

If x = 0 Then
                        	risultato = 0
            Else
                        	risultato = x - 1
            End If
            

Questo controllo è necessario perché, se è x = 0, cioè se la mina si trova sulla prima riga, i pulsanti circostanti partono anch'essi dalla prima riga e non dalla riga precedente come avverrebbe negli altri casi, semplicemente perché non esiste una riga precedente alla prima; lo stesso dicasi per l'ultima riga

IIf(x = 3, 3, x + 1)) 
            

e per la prima e ultima colonna.
Nel ciclo più interno ho introdotto un altro controllo per verificare che il pulsante controllato NON sia quello che nasconde la mina: infatti i due cicli analizzano tutti i pulsanti che circondano la mina, compreso quello che nasconde la mina stessa, e quest'ultimo non deve essere modificato.
Per non parlare della possibilità di avere due mine adiacenti. Infine, la proprietà Tag viene aggiornata incrementando di 1 il valore della stessa proprietà.
Questo per consentire un'indicazione corretta nel caso le mine circostanti un determinato pulsante siano 2, oppure 3.
L'ultima istruzione serve per abilitare il Timer, in modo da poter misurare i secondi di gioco. Ora possiamo finalmente scrivere il codice da eseguire quando il giocatore preme un pulsante: si tratta semplicemente di controllare il valore della proprietà Tag; nella routine Mina_Click noterete la presenza del parametro Index: questo è ovviamente l'indice del pulsante premuto dall'utente, ed è stato aggiunto automaticamente da Visual Basic quando abbiamo deciso di creare la matrice di pulsanti. In questa routine dunque scriviamo:

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 Mina(Index).Tag > 0 Then
   Mina(Index).Caption = Mina(Index).Tag
            ElseIf Mina(Index).Tag = -1 Then
   For x = 0 To 2
      Mina(PosMine(x)).Caption = "M"
   Next x
   Timer1.Enabled = False
            Else 'mina(index).tag=0
   x = Int(Index / 4) 'riga in cui si trova la mina
   y = Index Mod 4 'colonna in cui si trova la mina
   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)
             	 Mina(4 * x1 + y1).Caption = Mina(4 * x1 + y1).Tag
      Next y1
   Next x1
            End If      
            

Il significato di questo codice dovreste essere in grado di capirlo da soli, tenendo conto di come funziona il vero "Campo Minato". Comunque la prossima volta lo spiegherò in dettaglio

Diciassettesima lezione - Campo Minato: gli ultimi accorgimenti

Nell'ultima lezione vi avevo lasciato con la routine che gestisce il click sui pulsanti: quando viene premuto un pulsante, bisogna innanzitutto verificare se esso nasconde una mina oppure no, e per questo bisogna controllare la sua proprietà Tag. Se essa è maggiore di 0, significa che sotto di esso non ci sono mine e bisogna mostrare al giocatore quante mine circondano quel pulsante.

 

E' quello che fa la prima diramazione della If tramite l'aggiornamento della proprietà Caption. Se il Tag non è maggiore di 0, potrebbe essere uguale a -1: in tal caso, il pulsante nasconde una mina e lo facciamo capire all'utente visualizzando una "M" sul pulsante; il vero Campo Minato mostra l'icona della mina, ma noi per semplicità ci accontentiamo di modificare opportunamente la Caption dei pulsanti.
Trovata la mina, il gioco deve terminare e quindi interrompiamo il conteggio dei secondi disabilitando il Timer e mostrando al giocatore la posizione di tutte le mine: per questo motivo con un ciclo aggiorniamo la Caption di tutti i pulsanti con le mine, e non solo di quella trovata dal giocatore. La variabile contatore utilizzata nel ciclo è x, che di per sé dovrebbe essere usata per calcolare la riga in cui si trova il pulsante premuto: volendo essere diligenti, avremmo dovuto dichiarare un'altra variabile apposita, ma dato che l'utilizzo di x viene comodo e non crea problemi con le altre diramazioni della If, possiamo benissimo utilizzarla risparmiando così qualche byte. La terza diramazione della If serve per l'ultimo caso, quello in cui

mina(Index).Tag = 0 
            

Quando ciò si verifica, Campo Minato mostra tutta la "frontiera" dei pulsanti che si trovano nelle vicinanze di una mina.

Per ottenere lo stesso risultato, noi dovremmo usare una funzione ricorsiva (cioè una funzione che chiama se stessa un numero indefinito di volte) con opportuni controlli per evitare di bloccare l'applicazione, e siccome tutto ciò potrebbe rivelarsi complesso, per ora ci accontentiamo di mostrare il numero di mine vicine ai pulsanti che circondano quello premuto.
Ora il nostro Campo Minato è quasi pronto: possiamo già fare una prima partita, avviate il progetto, selezionate "Nuova" dal menù "Partita" e premete i pulsanti; ricordate di selezionare il menù, perché altrimenti i pulsanti non sono ancora inizializzati (o, se preferite, il campo non è ancora minato).
Ora che avete giocato la vostra prima partita, provate a farne un'altra: noterete che le Caption dei pulsanti rimangono invariate. Infatti ingenuamente ci siamo dimenticati di inizializzare anche quelle: per la prima partita non ce n'era bisogno, perché in fase di progettazione avevamo eliminato le proprietà Caption, ma per le partite successive dobbiamo ripristinare le condizioni iniziali. Pertanto aggiungiamo queste semplici righe nella routine mnuNew_click (all'inizio o anche alla fine, come preferite):

For i = 0 To 15
   Mina(i).Caption = ""
            Next i
            

Ora possiamo giocare quante partite vogliamo, ma non abbiamo ancora finito: innanzitutto dobbiamo scrivere un bel "Unload me" in mnuExit_click; e poi sarebbe bene congratularsi col giocatore quando vince la partita:

E' intuitivo che una partita a campo minato viene vinta quando il giocatore "scopre" tutti i pulsanti tranne quelli che nascondono le mine (nel nostro caso 13 pulsanti); avremo quindi bisogno di un contatore che tenga traccia di quanti pulsanti sono stati scoperti.
Il luogo più naturale in cui fare questi conti è la routine Mina_Click:

If Len(Mina(Index).Caption) = 0 Then
                        	ContaMine = ContaMine + 1
            End If
            If ContaMine = MaxContaMine Then
                        	Msgbox "HAI VINTO!"
            End If
            

La variabile ContaMine andrebbe logicamente dichiarata a livello di modulo, nella sezione delle dichiarazioni generali del Form; tuttavia si potrebbe anche dichiararla nella medesima routine Mina_Click usando la parola chiave Static.
La variabile MaxContaMine, invece, va necessariamente dichiarata a livello di modulo, perché non è utilizzata solo da Mina_Click: essa infatti deve essere inizializzata al valore 13 (il numero massimo di pulsanti scopribili) all'avvio dell'applicazione, cioè in Form_Load; dal momento che abbiamo un solo livello di gioco (e non tre come il vero Campo Minato), potremmo fare direttamente:

If ContaMine = 13 Then ... 
            

ma questo non è un buon metodo, perché limita la possibilità di ulteriori aggiornamenti ed espansioni dell'applicazione: se infatti un giorno volessimo aumentare il numero di pulsanti, dovremmo cambiare manualmente tutte le occorrenze del valore 13; per non parlare del caso in cui volessimo aggiungere un livello di gioco.
Invece utilizzando una variabile apposita, dovremmo preoccuparci soltanto di modificarla in fase di inizializzazione.
Ricordate inoltre che l'utilizzo di una variabile apposita rende più chiaro e leggibile il codice, anche a voi stessi. Ricordate anche di inserire l'istruzione:

ContaMine = 0 
            

nella routine mnuNew_Click, perché in ogni nuova partita il contatore deve partire da 0.
Per quanto riguarda la prima If che abbiamo inserito, c'è da dire che il contatore va aggiornato solo se il pulsante è stato premuto per la prima volta (altrimenti uno potrebbe "vincere" premendo per 13 volte lo stesso pulsante!), ovvero se la sua Caption è ancora una stringa nulla.
Il controllo non viene effettuato confrontando la Caption con "", ma esaminando la lunghezza della Caption stessa: questo perché le operazioni con i numeri (la funzione Len restituisce appunto la lunghezza della stringa, cioè un numero) sono più veloci di quelle con le stringhe.
La seconda If è così ovvia che se non l'avete capita dovreste ricominciare il corso daccapo...
L'aggiornamento del contatore però non deve essere fatto solo per il pulsante premuto, ma per qualunque pulsante sia stato "scoperto": in altri termini dobbiamo aggiornare il contatore anche quando si preme un pulsante che non è circondato da mine, perché in questo caso il gioco visualizzerà il numero di mine vicine ai pulsanti che circondano quello premuto.
Quindi le istruzioni:

If Len(Mina(Index).Caption) = 0 Then
   ContaMine = ContaMine + 1
            End If
            

vanno inserite nella prima e nella terza diramazione della If contenuta nella routine Mina_Click, che pertanto è diventata così:

If Mina(Index).Tag > 0 Then
   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(Index).Caption = "M"
   For x = 0 To 2
      Mina(PosMine(x)).Caption = "M"
   Next x
   Timer1.Enabled = False
            Else
   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
            

Non resta che aggiornare l'etichetta lblMine: a che serve? Serve a tenere il conto delle mine non ancora trovate; come saprete, cliccando col tasto destro su uno dei pulsanti del vero Campo Minato, su di esso apparirà una bandiera ad indicare che in quella posizione c'è (o pensiamo che ci sia) una mina, e quel pulsante diventerà insensibile al clic sinistro, almeno finchè su di esso resterà la bandierina.

Noi al posto della bandiera visualizzeremo una "x", ma non è questo il punto: la cosa importante è capire come intercettare il clic col tasto destro, ed è una cosa che impareremo più avanti. Per concludere questa lezione, vorrei tornare al punto da cui siamo partiti: la matrice di controlli. Dovreste esservi resi ormai conto dell'utilità dei vettori e delle matrici di variabili: con una sola dichiarazione avete a disposizione numerose variabili dello stesso tipo, collegate tra loro da un nesso logico, da una medesima funzione, da un medesimo scopo; potete modificarle tutte insieme con un semplice ciclo sfruttando la possibilità di accedere ad esse attraverso un indice. Per una matrice di controlli i vantaggi sono gli stessi: i pulsanti del nostro campo minato svolgono tutti la stessa funzione, cioè visualizzano quante mine ci sono nelle vicinanze o interrompono il gioco se nascondono una mina; grazie al fatto che appartengono a una matrice, possiamo accedere ad ognuno di essi tramite un indice che, nel caso dell'evento click, ci viene fornito direttamente da Visual Basic, con la conseguenza che possiamo scrivere il codice solo una volta, perché una sola è la routine Mina_Click; se non avessimo usato una matrice di pulsanti, avremmo dovuto crearne 16 diversi e indipendenti, e ripetere le istruzioni della routine per l'evento click di ognuno dei 16 pulsanti: immaginate se avessimo usato 480 pulsanti, come nel terzo livello del vero Campo Minato! Le matrici di controlli semplificano tutto questo.


Diciottesima lezione - Cicli For ... Next

Nella lezione 14 abbiamo visto un assaggio del ciclo For, di sicuro il più utilizzato nell'ambito dei cicli disponibili in Visual Basic: questo ciclo è identificato da due istruzioni, poste rispettivamente all'inizio e alla fine del ciclo :

For contatore = inizio To fine
   [istruzioni]
Next contatore
            

Contatore indica una variabile numerica che "conta" quante volte devono essere eseguite le istruzioni comprese nel ciclo: essa assume un valore iniziale (inizio) e viene incrementata dall'istruzione Next fino a raggiungere il valore finale (fine). Nella sintassi standard, le istruzioni contenute all'interno del ciclo sono eseguite fintantoché contatore risulta minore o uguale a fine: ciò significa che, quando il ciclo termina, la variabile contatore ha un valore pari a fine + 1, perché nell'ultima esecuzione del ciclo risulta contatore = fine e l'istruzione Next incrementa ulteriormente il contatore. Questa è una caratteristica tipica dei cicli "For", che si riscontra anche negli altri linguaggi di programmazione: e non potrebbe essere altrimenti, in quanto l'esecuzione del ciclo termina proprio quando contatore supera il valore fine.
Nella sintassi standard appena descritta, l'incremento del contatore è di una unità ad ogni esecuzione del ciclo, ma è possibile modificare il valore dell'incremento usando la clausola Step dopo il valore fine:

For contatore = inizio To fine Step incremento
                        	[istruzioni]
Next contatore

All'inizio del ciclo il valore del contatore è uguale a inizio, alla seconda esecuzione del ciclo è uguale a inizio+incremento, alla terza esecuzione è uguale a inizio+incremento+incremento e così via fino a che il contatore diventa maggiore del valore fine. Il valore di incremento viene sommato algebricamente al valore corrente del contatore: ciò significa che l'incremento può anche essere negativo, determinando così una diminuzione progressiva del valore del contatore: in questo caso il ciclo cesserà di essere eseguito quando il contatore diventa minore del valore finale specificato. Perché il ciclo funzioni correttamente, quindi, il valore iniziale dovrà essere maggiore del valore finale, ad es. il ciclo seguente viene eseguito tre volte, per i=10, i=8, i=6; all'uscita dal ciclo sarà i=4:

Dim i  as Integer 'contatore
For i = 10 To 5 Step -2
    [istruzioni]
Next i

Si è detto che il contatore è una variabile numerica, senza specificare il suo tipo: infatti, benché esso sia solitamente un Integer, è possibile anche utilizzare valori frazionari come Single o Double, e lo stesso vale per i valori inizio, fine e incremento. Tuttavia, è sempre preferibile utilizzare valori interi per motivi di precisione: le variabili di tipo Single o Double, infatti, commettono errori di precisione che, per quanto piccoli, non sono affatto trascurabili; basta un errore di un milionesimo per far credere a Visual Basic che il valore finale sia stato superato e determinare anzitempo la fine del ciclo. Si consideri ad esempio il ciclo seguente:

Dim i as Single
For i = 0.6 To 0.8 Step 0.1
  
  Debug.Print i
Next i
Debug.Print i

In teoria esso dovrebbe essere eseguito per tre volte, con il contatore i che vale 0.6, 0.7 e 0.8; invece esso è eseguito soltanto due volte, per i = 0.6 e i = 0.7: quando i viene ulteriormente incrementato non vale 0.8, bensì 0.8000001, che essendo maggiore di 0.8 determina l'interruzione del ciclo. Per questo motivo è sempre buona norma usare valori interi sia per il contatore che per i valori di inizio, fine e incremento, in quanto con valori interi il microprocessore non commette quegli errori di imprecisione che possono determinare comportamenti anomali.
Naturalmente i valori iniziale e finale del contatore, ovvero il numero di iterazioni, non devono necessariamente essere stabiliti esplicitamente durante la scrittura del codice: essi possono essere determinati durante l'esecuzione del programma semplicemente utilizzando delle variabili aggiuntive: ad esempio, il programma potrebbe chiedere all'utente quali debbano essere i valori di inizio e fine del contatore e memorizzare questi valori in due variabili da usare nell'istruzione For:

Dim i as Integer 'contatore
Dim intInizio as Integer, intFine as Integer 'valori iniziale e finale
                               
                    intInizio=Val(InputBox("Inserisci il valore iniziale del contatore:"))
                                intFine=Val(InputBox("Inserisci il valore finale del contatore:"))
        For i = intInizio To intFine
            Debug.Print i
        Next i

In questo esempio si è fatto uso della funzione InputBox, che richiede all'utente l'inserimento di un valore in una casella di testo: tale valore è quello restituito dalla funzione, e poiché è di tipo stringa esso deve essere convertito in numero attraverso la funzione Val. È chiaro che se l'utente inserisce nella casella un valore che non può essere convertito in numero (ad es. "a56bc"), sarà generato un errore causato dalla funzione Val.
Come per altri blocchi di istruzioni, anche per i blocchi For…Next è possibile usare la nidificazione, ovvero includere un blocco For all'interno di un altro blocco For:

For i = 1 To 10
                For k = 5 To 22
                    [istruzioni]
                Next k
Next i

La cosa importante è non intersecare mai due cicli distinti, ovvero non fare mai una cosa del genere:

For i = 1 To 10 'inizio PRIMO ciclo
                For k = 5 To 22 'inizio SECONDO ciclo
                    [istruzioni]
                Next i 'fine PRIMO ciclo
Next k 'fine SECONDO ciclo

Così facendo si genera un errore di sintassi, perché l'istruzione "Next i" non corrisponde all'istruzione "For k" che la precede: il secondo ciclo non è interamente contenuto nel primo, ma si sovrappone ad esso generando confusione. Volendo, è possibile anche omettere il nome del contatore nell'istruzione Next, e in tal caso Visual Basic assumerà automaticamente che esso si riferisce all'istruzione For immediatamente precedente

For i = 1 To 10
               For k = 5 To 22
                   [istruzioni]
               Next ' Visual Basic assume che l'istruzione sia: Next k
Next ' Visual Basic assume che l'istruzione sia: Next i

Spesso è necessario interrompere l'esecuzione di un ciclo se si verifica una determinata condizione: Visual Basic mette a disposizione del programmatore un'istruzione apposita: Exit For, che sposta il punto di esecuzione del programma all'istruzione immediatamente successiva all'istruzione Next relativa al ciclo interrotto. Solitamente questa istruzione si fa dipendere da un blocco If che controlla il verificarsi di una "condizione di uscita":

For i = 1 To 10
                If i = 8 Then Exit For
                   Debug.Print i
Next i
Debug.Print "ciclo interrotto"

Il ciclo precedente, che semplicemente visualizza il valore del contatore, teoricamente dovrebbe essere eseguito dieci volte ma in realtà è eseguito solo otto volte, perché all'ottava esecuzione la condizione i = 8 risulta verificata determinando l'uscita dal ciclo (senza stampare il valore del contatore) e la visualizzazione del messaggio "ciclo interrotto"; si noti che questo messaggio sarebbe visualizzato in ogni caso, al termine del ciclo. I "puristi" della programmazione non vedono di buon occhio l'utilizzo dell'istruzione Exit For, perché in questo caso il ciclo non termina solo con l'ultima istruzione "Next", come ci si potrebbe aspettare in condizioni normali, ma potrebbe terminare anche all'interno del ciclo stesso: ciò determina un'eccezione alla normale esecuzione del codice e pertanto potrebbe complicarne la comprensione e la modifica.
In effetti utilizzare l'istruzione Exit For non è l'unico modo per interrompere un ciclo: sapendo che esso termina quando il contatore supera il valore finale, la soluzione più intuitiva sarebbe quella di assegnare al contatore un valore maggiore del limite finale in modo che il ciclo non prosegua oltre:

For i =1 To 10
               If i = 8 then i =11
                  Debug.Print i
   end if
Next i
Debug.Print "ciclo interrotto"

Nell'esempio precedente, quando i=8 il programma assegna al contatore il valore 11, cioè un valore superiore al limite di 10 specificato nell'istruzione For; in questo modo il ciclo non è interrotto direttamente, e infatti l'istruzione Debug.Print i è eseguita normalmente anche quando i=8, però l'istruzione Next i verifica che il contatore ha superato il limite e quindi determina la fine del ciclo e la visualizzazione del messaggio "ciclo interrotto". Tuttavia, da un punto di vista stilistico questa soluzione è ancora peggiore della precedente, perché il valore del contatore non dovrebbe mai essere modificato direttamente all'interno del ciclo stesso: un metodo del genere può complicare molto il debug e l'eventuale modifica del codice perché può generare errori imprevisti e generalmente difficili da identificare.

Diciannovesima lezione - Cicli Do...Loop

C
ome accennato in altre lezioni, Visual Basic mette a disposizione del programmatore vari tipi di cicli iterativi; nell'ultima lezione abbiamo visto il ciclo principale (For… Next), ora vedremo un altro ciclo molto importante: il Do…Loop.
Come il ciclo For…Next, anche il ciclo Do…Loop esegue un gruppo di istruzioni finché risulta verificata (o falsificata, a seconda del punto di vista) una determinata condizione: questa condizione deve essere preceduta da una parola chiave che indica se il proseguimento del ciclo è subordinato alla verità o falsità della condizione. Le due parole chiave che possono essere utilizzate sono While e Until: la prima indica che la condizione specificata deve risultare vera affinché il ciclo prosegua, la seconda indica che la condizione deve risultare falsa affinché il ciclo prosegua. Ecco un esempio di ciclo Do…Loop:

Dim i as Integer
                i = 1
    Do While i<10
       i = i+1
                   Debug.Print i
    Loop
    Debug.Print "i vale " + cstr(i)+": il ciclo è terminato"

Questo ciclo incrementa e stampa il valore della variabile i fintantoché essa resta minore di 10: quando i assume il valore 10 la condizione i<10 risulterà falsa, e poiché nel ciclo è specificata la parola chiave While, il ciclo si interromperà e il controllo passerà all'istruzione successiva all'istruzione Loop.
Lo stesso identico ciclo può essere espresso anche utilizzando la parola chiave Until:

Dim i as Integer

    i = 1
    Do Until i>=10
                   i = i+1
                   Debug.Print i
    Loop
    Debug.Print "i vale " + cstr(i)+": il ciclo è terminato"

Avendo utilizzato Until, è necessario invertire la condizione di controllo affinché i due cicli siano equivalenti: nel primo esempio il ciclo era eseguito finché i<10 è vera, nel secondo esempio il ciclo è eseguito finché i>=10 è falsa, poiché il valore iniziale di i è sempre 1. In termini più generali, dato un ciclo Do While:

Do While condizione
               [istruzioni]
Loop
            

il ciclo Do Until equivalente sarà

Do Until Not condizione
               [istruzioni]
Loop
            

Da questi esempi appare con evidenza una forte analogia con il ciclo For visto sopra: in effetti le differenze sono ben poche, tant'è vero che qualunque ciclo For può essere espresso come un ciclo Do e viceversa. L'uso di un ciclo al posto di un altro va deciso in base a questioni di convenienza: si sceglie l'istruzione più semplice, più immediata, più facilmente comprensibile: negli esempi appena considerati il ciclo For è leggermente preferibile al ciclo Do, perché è più intuitivo e consente di evitare l'istruzione di incremento della variabile i, che in questi casi funge da "contatore": è proprio l'istruzione For…Next che si occupa di inizializzare e incrementare la variabile, e così il programmatore deve solo preoccuparsi di stamparne il valore.
In generale, il ciclo For è preferibile quando si conosce con esattezza il numero di iterazioni, ossia quando si sa per quante volte il ciclo deve essere eseguito; il ciclo Do è preferibile invece quando è necessaria più flessibilità, quando il numero di iterazioni non può essere conosciuto a priori. Ad esempio, si supponga di dover eseguire ciclicamente un gruppo di istruzioni finché l'utente non dice "basta!": non è possibile sapere in anticipo di quanta pazienza disponga l'utente, perciò è opportuno ricorrere a un ciclo Do che controlli il verificarsi di una condizione. In casi come questi, solitamente si fa ricorso all'utilizzo di una variabile flag di tipo booleano, che assuma il valore vero (o falso, a seconda della convenienza) quando l'utente decide di interrompere il ciclo:

Dim blnFlag as Boolean 'flag di controllo del ciclo
    Do While blnFlag
                   Debug.Print Rnd()
    Loop

Questo semplice ciclo continua a visualizzare numeri casuali finché la variabile blnFlag è vera: il ciclo si interromperà quando il flag, a seguito di un'azione dell'utente o al verificarsi di altre condizioni, assumerà il valore False. Per esempio, si inserisca in un Form un pulsante di comando e un'etichetta di testo: si imposti la Caption del CommandButton a "Avvia" e nella routine Click si scriva

Private Sub Command1_Click()
If blnFlag Then
               Command1.Caption = "Avvia"
               blnFlag = False
Else
               Command1.Caption = "Interrompi"
               blnFlag = True
End If
Do While blnFlag
               label1.Caption = CStr(Rnd())
               DoEvents ' Effettua il refresh del video
Loop
End Sub

La subroutine controlla innanzitutto il valore del flag (che naturalmente deve essere dichiarato a livello di modulo): se è falso (impostazione predefinita iniziale per tutte le variabili booleane), esso diventa vero e il ciclo Do successivo può essere eseguito dando il via alla visualizzazione dei numeri casuali; altrimenti diventa falso e quindi il ciclo Do si interrompe; inoltre viene modificata la Caption del pulsante per far capire all'utente l'effetto della pressione del pulsante stesso.
Nel ciclo Do compare l'istruzione DoEvents, che restituisce temporaneamente il controllo al sistema operativo per consentirgli di gestire altre operazioni accumulatesi durante l'esecuzione del nostro programma: senza questa istruzione, a causa del modo con cui Windows gestisce il multitasking, il programma si bloccherebbe nell'esecuzione del ciclo, perché la pressione del pulsante non avrebbe alcun effetto. Detto con parole poco precise, il programma è così impegnato ad eseguire il ciclo Do che non ha il tempo di gestire gli altri input forniti dall'utente (come la pressione del pulsante), né di aggiornare l'aspetto degli altri componenti del Form (l'etichetta non mostra nulla), a meno che non ci sia un'istruzione esplicita che obblighi il nostro programma a cedere momentaneamente il controllo al sistema operativo: questo è appunto il compito dell'istruzione DoEvents. Senza di essa l'unico modo per interrompere il programma è premere la combinazione di tasti ctrl+pausa.
L'eliminazione dell'istruzione DoEvents impedisce di modificare il flag blnFlag una volta che il ciclo Do è cominciato: la variabile booleana pertanto resta sempre True, e di conseguenza il ciclo non si interromperà mai; questo è un semplice esempio di ciclo infinito. La presenza di cicli infiniti è uno dei classici bug commessi da programmatori alle prime armi, ma anche da programmatori un po' più esperti: quando la condizione di controllo del ciclo dipende da diversi fattori può essere difficile rendersi conto dell'errore commesso, anche perché di norma l'errore sarà tanto più "nascosto" quanto più si presenta raramente nel corso dell'esecuzione. Per questo motivo è necessario stare bene attenti a fornire una condizione di uscita dal ciclo: se c'è il rischio che la condizione di controllo resti sempre vera (se si usa While) o sempre falsa (se si usa Until), può essere opportuno utilizzare ancora l'istruzione Exit; nel caso del ciclo For l'istruzione corretta era Exit For, nel caso del ciclo Do l'istruzione corretta è Exit Do. L'esempio precedente potrebbe quindi essere riscritto in questo modo:

Private Sub Command1_Click()
If blnFlag Then
               Command1.Caption = "Avvia"
               blnFlag = False
Else
               Command1.Caption = "Interrompi"
               blnFlag = True
End If
Do While True 'ciclo virtualmente infinito
               Label1.Caption = CStr(Rnd())
               DoEvents
   If Not blnFlag Then Exit Do 'condizione di uscita dal ciclo
Loop
End Sub

Il ciclo è potenzialmente infinito, perché la condizione di controllo non è una vera e propria condizione, ma è un valore letterale costante (True) che in quanto tale non potrà mai diventare False: la condizione di uscita è quindi ottenuta tramite un controllo interno al ciclo, che esamina il valore del flag: se questo è falso, significa che l'utente vuole interrompere il ciclo, pertanto Not blnFlag sarà True e sarà eseguita l'istruzione Exit Do che passa il controllo all'istruzione successiva al ciclo (in questo caso End Sub).
La clausola While o Until può trovarsi alternativamente dopo l'istruzione Do o dopo l'istruzione Loop:

Do While|Until condizione
               [istruzioni]
Loop
oppure
Do
               [istruzioni]
Loop While|Until condizione

La differenza consiste nel momento in cui è controllato il valore di condizione: nel primo caso il controllo avviene all'inizio di ogni iterazione, nel secondo caso alla fine di ogni iterazione. Ciò significa che se per ipotesi la condizione risulta subito falsa (o vera, se si usa Until), nel primo caso il ciclo non sarà eseguito mai, mentre nel secondo caso sarà eseguito solo una volta, perché la condizione è controllata dopo l'esecuzione delle istruzioni.
La condizione di controllo del ciclo può naturalmente essere rappresentata da un'espressione booleana anche complessa, come la seguente:

Do While blnFlag And  (intUsers > 1 Or intUsers = -1) And (lngCount Xor lngMax) = 20)
               [istruzioni]
Loop

In questi casi bisogna tenere presente che la condizione è sempre e soltanto una, che viene valutata tenendo conto complessivamente del valore delle singole parti e degli operatori logici utilizzati: quando l'intera espressione è vera, il ciclo prosegue, quando diventa falsa il ciclo si interrompe. Trasformare un ciclo del genere nel corrispondente Do Until…Loop può diventare molto complicato se si cerca di convertire ogni parte della condizione nel corrispondente opposto: in questo caso, l'equivalente sarebbe

Do Until Not blnFlag Or (intUsers <= 1 And intUsers <> -1) Or ((lngCount Xor lngMax) <> 20)
               [istruzioni]
Loop

Ma la cosa più semplice è premettere un Not all'intera condizione:

Do Until Not (blnFlag And (intUsers > 1 Or intUsers = -1) And ((lngCount Xor lngMax) = 20))
               [istruzioni]
Loop

Infine, come per i cicli For, anche i cicli Do possono essere nidificati a più livelli: ogni istruzione Loop corrisponderà all'istruzione Do immediatamente precedente, in modo che ogni ciclo sia interamente contenuto nel ciclo più esterno, come nell'esempio seguente:

Do While condizione1
               Do
                  [istruzioni2]
               Loop Until condizione2
               [istruzioni1]
Loop

 


Ora che conoscete anche il ciclo Do…Loop, potete riprendere in mano il progetto del Campo minato delle lezioni precedenti: ricordate lo stratagemma utilizzato per assicurarsi che le tre mine fossero disposte sotto tre differenti pulsanti? La soluzione escogitata consisteva nel simulare più estrazioni casuali tramite l'uso di etichette e dell'istruzione goto. In realtà una soluzione molto più efficiente e flessibile (oltre che più "bella" stilisticamente) è data dall'uso di un ciclo Do simile a questo:

Dim t as Integer 'variabile temporanea per eseguire i controlli
            Dim i as Integer, k as Integer 'contatori per i cicli
            Dim blnUguali as Boolean 'flag
            Randomize Timer
            For i = 0 To 2
               Do
                  PosMine(i) = Int(Rnd * 16)
                  blnUguali=False
      For k=0 To i-1
         If (PosMine(i)=PosMine(k)) Or blnUguali Then
                        blnUguali=True
         Else
                        blnUguali=False
         End If
      Next k
               Loop While blnUguali
               Mina(PosMine(i)).Tag = True
            Next i
            

L'algoritmo è solo apparentemente complicato: il ciclo For esterno conta le mine da piazzare, e contiene un ciclo Do che continua a estrarre valori casuali finché l'ultimo valore estratto risulta uguale a uno di quelli estratti nelle iterazioni precedenti. L'uguaglianza di due valori è segnalato dal flag blnUguali, il cui valore è impostato nel terzo ciclo, quello più interno che ha come contatore k: questo ciclo controlla se la posizione calcolata della mina considerata coincide con la posizione delle mine precedenti; se due mine coincidono, il flag è impostato a True, altrimenti è impostato a False: si noti che la condizione controllata non è semplicemente (PosMine(i)=PosMine(k)), ma (PosMine(i)=PosMine(k)) Or blnUguali, per evitare che blnUguali diventi False quando è già True, cioè quando è già stata trovata una coincidenza da eliminare. Per lo stesso motivo, blnUguali è reinizializzato a False prima di controllare la coincidenza di due mine col ciclo For interno. Una volta che si è calcolata una posizione della mina i-esima che è diversa dalle altre, il ciclo Do si interromperà e si potrà aggiornare la proprietà Tag di quella mina, e così via per tutte le tre mine.
Questo è un algoritmo semplice ma potente, perché consente di estrarre valori casuali certamente diversi l'uno dall'altro qualunque sia il numero di iterazioni da effettuare (ovvero il numero di mine da piazzare): se avessimo dovuto cavarcela con le etichette la situazione sarebbe diventata ingestibile anche solo con 10 mine.


Ventesima lezione - Cicli While...Wend

Tra le strutture iterative disponibili in Visual Basic, esiste anche il ciclo While…Wend, che è certamente il meno conosciuto e meno utilizzato; in effetti il funzionamento di questo tipo di ciclo è del tutto analogo a quello di un ciclo Do While…Loop: le istruzioni contenute nel ciclo sono eseguite finché la condizione espressa dopo l'istruzione While è vera

While condizione
               [istruzioni]
            Wend
            

Oltretutto, il normale ciclo Do While…Loop è più flessibile del ciclo While…Wend, e tanto basta a giustificare lo scarsissimo uso del ciclo While…Wend.

Tornando ai cicli in generale, quando si deve utilizzare una struttura iterativa bisogna fare attenzione in particolare a due cose: la prima, che abbiamo già visto, è fornire una condizione di uscita per evitare cicli infiniti; la seconda riguarda la corretta gestione della prima e dell'ultima iterazione. La questione non è così banale come sembra, e soprattutto all'inizio può portare a errori grossolani: prendiamo ad esempio il caso di una ricerca ricorsiva di una stringa all'interno di un testo. Spesso capita che sia necessario conoscere, oltre al nome di un file, anche il suo percorso (path in inglese), ovvero la posizione esatta all'interno dell'albero delle directory contenute nell'hard disk; e a volte è utile suddividere il percorso in tanti "pezzi" quante sono le cartelle che bisogna attraversare per giungere fino al file: per fare ciò occorre contare quanti sono i separatori di path presenti nella stringa che identifica il percorso, ovvero quanti backslash ("\") ci sono.
Per trovare una stringa all'interno di un'altra è sufficiente usare la comoda istruzione instr, la cui sintassi è:

Instr([start], string1, string2, [compare])

Il primo parametro è un numero che indica la posizione dalla quale cominciare a cercare la stringa da trovare: ad esempio, se stiamo cercando un backslash all'interno di un path completo, è inutile cominciare a cercare dal primo carattere del percorso, perché questo comincia con la lettera di un'unità seguita dai due punti (ad es. "C:"), quindi è possibile iniziare la ricerca direttamente dal terzo carattere. Il parametro è facoltativo, e se non viene specificato la ricerca partirà dal primo carattere. I parametri string1 e string2 identificano rispettivamente la stringa in cui bisogna cercare (nel nostro caso il percorso) e la stringa da cercare (nel nostro caso il backslash). L'ultimo parametro indica come va effettuata la ricerca: nel nostro caso i valori interessanti sono due: 0, espresso anche dalla costante vbBinaryCompare, e 1, espresso dalla costante vbTextCompare; nel primo caso il confronto tra le due stringhe è effettuato in modalità binaria, che tradotto un po' rozzamente significa che il confronto è "case sensitive", ovvero sensibile alla differenza tra maiuscole e minuscole; nel secondo caso invece la differenza tra maiuscole e minuscole è ignorata, perché il confronto è effettuato in semplice modalità testuale. Per chiarire la differenza, basta scrivere nella finestra immediata:

? instr(1, "Pippo", "p", vbBinaryCompare) 'oppure instr(1, "Pippo", "p", 0), è la stessa cosa

La funzione restituirà il valore 3.

Se invece si scrive:

? instr(1, "Pippo", "p", vbTextCompare) 'oppure instr(1, "Pippo", "p", 1)

la funzione restituirà 1.
 

Come avrete intuito, il valore restituito dalla funzione è la posizione della prima "p" trovata nella stringa "Pippo": ma mentre nel primo caso cerchiamo una p minuscola abilitando la ricerca "case sensitive", nel secondo caso la ricerca è solo in modalità testo, e quindi la funzione restituisce la posizione della prima "p" che trova, maiuscola o minuscola che sia. Se la stringa cercata non si trova in quella in cui è fatta la ricerca (se ad esempio cerchiamo una "z" in "pippo", o anche una "O" in "Pippo" abilitando la ricerca binaria), la funzione instr restituisce zero.
Tornando al nostro esempio, per contare i backslash in un path basta un semplice ciclo do…loop, utilizzando un contatore da incrementare ogni volta che instr trova un "\": dobbiamo però fare attenzione a spostare sempre in avanti il punto di inizio della ricerca (il parametro start), perché se cerchiamo sempre a partire dal primo carattere, il ciclo non si fermerà mai e restituirà sempre la posizione del primo "\". Una soluzione comoda è quella di utilizzare un'altra variabile che memorizzi la posizione dell'ultimo "\" trovato, ovvero che memorizzi il valore restituito da instr: la ricerca successiva dovrà partire dal carattere successivo:

Dim intPos as Integer
            Dim strPath as String
                strPath="C:\documenti\immagini\esempio.jpg"
                Do
                    intPos=Instr(intPos+1, strPath, "\")
                Loop

intPos memorizza la posizione dell'ultimo backslash trovato: poiché essa è inizializzata a 0, alla prima iterazione la ricerca partirà dal primo carattere, come avviene nella maggior parte dei casi (se vogliamo farla partire dal terzo carattere, perché i primi due contengono il nome dell'unità, basta inizializzare intPos=2 prima del Do); nelle iterazioni successive, partirà dal carattere successivo all'ultimo backslash trovato nel path. Il parametro "compare" è stato tralasciato perché non ha alcuna influenza: non esiste un "\" maiuscolo e un "\" minuscolo; comunque l'impostazione predefinita è vbBinaryCompare. La variabile strPath è inizializzata a un percorso qualunque (compreso il nome di un file) solo per far funzionare il codice: in realtà dovrebbe essere ignoto a priori, ad esempio potrebbe essere inserito dall'utente in fase di esecuzione. Nel ciclo appena descritto manca una cosa: la condizione di uscita. Questa dovrebbe verificare se la funzione instr ha restituito un valore positivo oppure no: nel primo caso significa che ha trovato un "\" e quindi possiamo continuare la ricerca; nel secondo caso vuol dire che non ha trovato nulla e quindi la ricerca deve essere fermata perché non ci sono ulteriori "\" nell'ultima parte del percorso. Pertanto la condizione potrebbe essere:

while intPos>0

oppure:

until intPos=0

Bisogna decidere dove mettere la condizione, se all'inizio o alla fine del ciclo: se intPos è inizializzata a 0, certamente la condizione non dovrà essere posta all'inizio, altrimenti il ciclo non eseguirebbe neppure un'istruzione:

Dim intPos as Integer
            Dim strPath as String
   strPath="C:\documenti\immagini\esempio.jpg"
   Do While intPos>0 'intPos è =0, quindi il ciclo non è eseguito
                   intPos=Instr(intPos+1,strPath,"\")
   Loop

D'altra parte, mettere la condizione alla fine comporta la perdita di una preziosa informazione: la posizione dell'ultimo backslash esistente nel path, che solitamente indica anche l'inizio del nome vero e proprio del file (oppure il nome della cartella in cui risiede il file, a seconda dei casi):

Dim intPos as Integer
            Dim strPath as String
                strPath="C:\documenti\immagini\esempio.jpg"
    Do
                    intPos=Instr(intPos+1,strPath,"\")
    Loop While intPos>0

Questo ciclo esegue almeno una volta la ricerca, e prosegue fino a trovare tutti i "\": ma all'uscita dal ciclo intPos vale 0, e noi non sappiamo più qual era la posizione dell'ultimo backslash; magari è un'informazione che non ci interessa, ma in caso contrario dobbiamo trovare una soluzione alternativa. Ad esempio si potrebbe utilizzare un'altra variabile per memorizzare il valore precedente di intPos:

Dim intPos as Integer
            Dim intPosPrec as Integer
            Dim strPath as String
                strPath="C:\documenti\immagini\esempio.jpg"
    Do
                    intPosPrec=intPos
                    intPos=Instr(intPos+1, strPath, "\")
    Loop While intPos>0

All'uscita dal ciclo, intPosPrec conterrà la posizione dell'ultimo "\". Oppure si può utilizzare come condizione di uscita non "intPos>0", bensì "Instr(…)>0", ma in questo modo la funzione Instr viene eseguita due volte per ogni ricerca: una volta per controllare se ci sono altri "\", e un'altra volta per sapere dove si trovano:

Dim intPos as Integer
            Dim strPath as String
   strPath="C:\documenti\immagini\esempio.jpg"
   Do While Instr(intPos+1, strPath, "\")
      intPos= Instr(intPos+1, strPath, "\")
   Loop

In questo caso la condizione può essere spostata all'inizio del ciclo: se questo non viene eseguito neppure una volta, significa che non ci sono "\" nel percorso; infatti instr() restituisce zero, che viene valutato come valore logico False, e quindi la condizione per eseguire il ciclo non è verificata. Una soluzione analoga può essere adottata inizializzando a un valore positivo la variabile intPos (ad esempio al valore 2, come suggerito prima):

Dim intPos as Integer
            Dim strPath as String
               strPath="C:\documenti\immagini\esempio.jpg"
               IntPos=2
               Do While intPos
                  IntPos = Instr(intPos+1, strPath, "\")
               Loop

Insomma, quando usate i cicli state attenti non solo a quando interromperli (condizione di uscita), ma anche a quale valore assumono le variabili coinvolte in essi (in particolare prima della prima iterazione e dopo l'ultima iterazione). Se non siete ancora convinti, proviamo ora a memorizzare le varie parti del percorso in un vettore: ogni elemento del vettore deve contenere una delle cartelle appartenenti al path, in ordine gerarchico; poiché non sappiamo a priori quante cartelle sono coinvolte, possiamo usare un vettore dinamico: ogni volta che troviamo un "\", dobbiamo aggiungere un elemento al vettore e assegnargli il nome della cartella che precede questo "\"

Dim strCartelle() as String
            Dim intPos as Integer
            Dim intPosPrec as Integer
            Dim strPath as String
                strPath="C:\documenti\immagini\esempio.jpg"
                Do
       IntPosPrec=intPos
                   IntPos=instr(intPos+1, strPath, "\")
                   Redim Preserve strCartelle(Ubound(strCartelle)+1)
                   StrCartelle(Ubound(strCartelle))=Mid$(strPath, intPosPrec+1, intPos-intPosPrec)
    Loop While intPos

Il vettore strCartelle contiene i nomi delle cartelle presenti nel path: è un vettore dinamico, che viene ridimensionato all'interno del ciclo ogni volta che viene trovato un "\". Il ridimensionamento, che avevamo brevemente già visto (lez. 14), avviene in base al valore della funzione Ubound, che restituisce il limite superiore attuale del vettore: se il vettore ha tre elementi (da 0 a 2), Ubound restituisce 2; attenzione: se Ubound restituisce zero non significa che il vettore non ha alcun elemento, ma che ne ha uno solo, con indice 0. Aggiungere un elemento al vettore significa quindi ridimensionarlo assegnandogli un elemento in più di quelli che già possiede (ubound()+1). La parola chiave Preserve serve invece a mantenere in memoria i valori già assegnati: infatti l'istruzione Redim usata da sola non fa altro che riallocare lo spazio di memoria disponibile per il vettore, reinizializzando tutti i suoi elementi e quindi di fatto cancellando i valori precedentemente assegnati. Il nome di ogni cartella è estratto attraverso la funzione Mid, che prende da una stringa (il primo parametro) un intervallo di caratteri definito da una posizione iniziale (il secondo parametro) e da una lunghezza (il terzo parametro). Ovviamente il secondo parametro dev'essere positivo, altrimenti si verifica un errore; il terzo invece può anche essere zero (in tal caso è restituita la stringa vuota ""), basta che sia non negativo. Nel nostro caso la posizione iniziale del nome della cartella è naturalmente il carattere successivo al penultimo backslash trovato, mentre la lunghezza del nome è calcolabile tramite la distanza tra il penultimo e l'ultimo backslash.
Ora, il ciclo appena descritto presenta alcuni inconvenienti; anzi, sono veri e propri errori:
1) prima di tutto, la funzione Ubound deve avere come argomento un vettore (o matrice) con dimensioni definite, altrimenti genera un errore: e siccome alla prima iterazione strCartelle non ha una dimensione definita, indovinate un po' che succede…
2) in secondo luogo, all'ultima iterazione intPos è uguale a 0, come si è visto prima; e in questo caso ad andare in errore è la funzione Mid, perché il terzo parametro risulterebbe negativo.

Ventunesima lezione - Ancora sui Cicli e Select Case

Nell'ultima lezione ci eravamo lasciati con un paio di errori da correggere; questo era il codice "sbagliato":

Dim strCartelle() as String
            Dim intPos as Integer
            Dim intPosPrec as Integer
            Dim strPath as String
    strPath="C:\documenti\immagini\esempio.jpg"
    Do
                   IntPosPrec=intPos
                   IntPos=instr(intPos+1, strPath, "\")
                   Redim Preserve strCartelle(Ubound(strCartelle)+1) as String
                   StrCartelle(Ubound(strCartelle))=Mid$(strPath, intPosPrec+1, intPos-intPosPrec)
    Loop While intPos

Se proviamo a eseguire questo codice (ad esempio inserendolo nella routine dell'evento Load di un form), otterremo questo errore:

"Indice non compreso nell'intervallo"

L'errore si verifica nella riga in cui si ridimensiona il vettore strCartelle, perché nella prima iterazione del ciclo esso non ha una dimensione definita, pertanto ad andare in errore è la funzione Ubound(strCartelle): risolvere questo errore è molto semplice, infatti basta inizializzare il vettore prima del ciclo Do. Poiché all'inizio il vettore deve essere vuoto, possiamo ridimensionarlo assegnandogli un solo elemento, in questo modo:

Redim strCartelle(0) as String

Così facendo, però, sprechiamo un elemento del vettore (l'elemento zero, appunto), perché nel ciclo il ridimensionamento avviene aumentando di 1 l'indice superiore del vettore: quindi il primo elemento del vettore riempito con il nome di una cartella sarebbe l'elemento 1, mentre l'elemento 0 resterebbe inutilizzato. Per rimediare a questo piccolo inconveniente (che a dire il vero potremmo anche trascurare), basta invertire le ultime due istruzioni del ciclo, in modo che prima si assegni un valore all'ultimo elemento del vettore, e poi si proceda al ridimensionamento di quest'ultimo:

Dim strCartelle() as String
            Dim intPos as Integer
            Dim intPosPrec as Integer
            Dim strPath as String
   strPath="C:\documenti\immagini\esempio.jpg"
   Redim strCartelle(0) as String
   Do
                  IntPosPrec=intPos
                  IntPos=instr(intPos+1, strPath, "\")
                  StrCartelle(Ubound(strCartelle))=Mid$(strPath, intPosPrec+1, intPos-intPosPrec)
                  Redim Preserve strCartelle(Ubound(strCartelle)+1) as String
   Loop While intPos

Il primo "bug" è risolto, ma ce n'è ancora un altro: se proviamo ad eseguire il ciclo appena modificato, otterremo il seguente messaggio:

"Chiamata di routine o argomento non valido"

Questa volta l'errore non si verifica all'inizio, ma alla fine del ciclo: infatti, quando sono finiti i "\" da cercare, la variabili intPos sarà uguale a zero, mentre la variabile intPosPrec è diversa da zero; pertanto il terzo parametro della funzione Mid$() è negativo, e ciò genera l'errore appena visto. Anche in questo caso la risoluzione è abbastanza semplice, basta usare il costrutto if…then:

Dim strCartelle() as String
            Dim intPos as Integer
            Dim intPosPrec as Integer
            Dim strPath as String
    strPath="C:\documenti\immagini\esempio.jpg"
    Redim strCartelle(0) as String
    Do
                   IntPosPrec=intPos
                   IntPos=instr(intPos+1, strPath, "\")
                   If intPos Then
                      StrCartelle(Ubound(strCartelle))=Mid$(strPath, intPosPrec+1, intPos-intPosPrec)
                   Else
                      StrCartelle(Ubound(strCartelle))=Mid$(strPath, intPosPrec+1)
                   End If
                   Redim Preserve strCartelle(Ubound(strCartelle)+1) as String
    Loop While intPos

Se intPos è diversa da zero (cioè se è stato trovato un ulteriore "\"), si prendono solo i caratteri tra il penultimo e l'ultimo backslash, altrimenti si prendono tutti i caratteri dall'ultimo backslash in poi. Questa soluzione, benché semplice, è però poco elegante perché costringe a effettuare un controllo che risulta sostanzialmente inutile in tutte le iterazioni tranne che nell'ultima; se il ciclo dovesse eseguire numerose iterazioni, il rallentamento derivante da questo controllo potrebbe farsi consistente. Una possibile alternativa è quella di spostare la ricerca del successivo backslash alla fine del ciclo, anziché porla all'inizio: così quando non ci sono più backslash il ciclo termina direttamente; in questo caso però occorre effettuare la ricerca del primo backslash prima dell'inizio del Do e assegnare l'ultimo elemento del vettore dopo la sua fine, in questo modo:

Dim strCartelle() as String
            Dim intPos as Integer
            Dim intPosPrec as Integer
            Dim strPath as String
   strPath="C:\documenti\immagini\esempio.jpg"
   redim strCartelle(0) as String
   IntPos=instr(intPos+1, strPath, "\")
   Do
                   StrCartelle(Ubound(strCartelle))=Mid$(strPath, intPosPrec+1, intPos-intPosPrec)
                   Redim Preserve strCartelle(Ubound(strCartelle)+1) as String
                   IntPosPrec=intPos
                   IntPos=instr(intPos+1, strPath, "\")
   Loop While intPos
   StrCartelle(Ubound(strCartelle))=Mid$(strPath, intPosPrec+1)
   For intPos=0 to Ubound(strCartelle)
                   Debug.Print strCartelle(intPos)
   Next intPos

Il piccolo ciclo For finale serve solo a visualizzare i nomi delle cartelle che abbiamo estrapolato dal percorso originale: come noterete, tutti i nomi delle cartelle (tranne il nome del file) terminano con "\", perché alla funzione Mid abbiamo chiesto di prendere tutti i caratteri compresi tra il penultimo e l'ultimo backslash, e in questi caratteri c'è anche l'ultimo backslash. Questo non è un vero e proprio errore, dipende più che altro dall'uso che vogliamo fare del vettore strCartelle; comunque, se volessimo solo il nome della cartella puro e semplice dovremmo chiamare la funzione Mid in questo modo:

StrCartelle(Ubound(strCartelle))=Mid$(strPath, intPosPrec+1, intPos-intPosPrec-1)

Il "-1" aggiunto al terzo parametro fa sì che l'ultimo backslash sia escluso dai caratteri da assegnare agli elementi del vettore strCartelle. Ci possono essere vari modi per risolvere problemi come quelli visti fin qui: l'importante è sapersi ingegnare e trovare la soluzione più adatta alle proprie esigenze.
Terminato il discorso sui cicli, passiamo a considerare un'altra istruzione utilizzata spesso per controllare il flusso del programma: l'istruzione Select case. Essa analizza un'espressione ed esegue istruzioni differenti in base al suo valore. Detto così sembra che l'istruzione Select case sia del tutto simile all'istruzione If, e in effetti è così: infatti è possibile esprimere una Select case come un blocco If con un adeguato numero di clausole ElseIf. La scelta tra le due di solito dipende da considerazioni di convenienza e di leggibilità del codice, perché spesso un blocco Select risulta strutturalmente più semplice di un blocco If. La sintassi dell'istruzione Select Case è la seguente:

Select Case espressione
      Case valore-1
                        	Istruzioni-1
                  Case valore-2
                        	Istruzioni-2
                       …
                  [Case Else
                        	istruzioni-else]
            End Select
            

Espressione è una variabile, una proprietà o una qualunque espressione che può assumere diversi valori e che va confrontata coi valori elencati nei vari "casi"; nel caso in cui il valore dell'espressione sia valore-1, saranno eseguite le istruzioni istruzioni-1 (che ovviamente possono essere più di una); se il valore è valore-2, saranno eseguite le istruzioni istruzioni-2, e così via, per tutti i casi elencati. Se espressione assume un valore che non rientra in quelli specificati, sarà eseguito il blocco di istruzioni istruzioni-else, se è presente: infatti, come per l'istruzione If, anche nella Select Case il blocco Else è facoltativo; inoltre, come si è visto più volte, anche per questa istruzione vale la regola di indicarne la fine con l'istruzione End Select. Ecco un esempio di istruzione Select case:

Select Case lblEtichetta.Caption
       Case "Testo1"
                        	lblEtichetta.FontSize = 12
                   Case Else
                        	lblEtichetta.FontName = "Arial"
                        	lblEtichetta.FontSize = 11
            End Select

Nel caso in cui fosse necessario eseguire lo stesso blocco di istruzioni in corrispondenza di più valori di espressione, l'elenco di questi valori va separato con la virgola; ad esempio, il blocco precedente può essere modificato in questo modo:

Select Case lblEtichetta.Caption
                   Case "Testo1", "Testo2", ""
                        	lblEtichetta.FontSize = 12
       Case Else
                        	lblEtichetta.FontName = "Arial"
                        	lblEtichetta.FontSize = 11
            End Select

L'elenco di valori con cui va confrontata l'espressione può assumere anche altre forme, che risultano particolarmente utili quando l'espressione da controllare è di tipo numerico: è possibile infatti indicare un intervallo di valori usando l'operatore To o usando l'operatore Is seguito da un operatore di confronto, come nell'esempio seguente

Select Case lblEtichetta.FontSize
                   Case Is < 8, 12 To 14, 18
                        	lblEtichetta.FontSize = 12
                   Case Else
                        	lblEtichetta.FontName = "Arial"
            End Select

In questo esempio la dimensione del carattere dell'etichetta sarà impostato a 12 punti se esso è minore di 8, compreso tra 12 e 14 (estremi inclusi), oppure uguale a 18; altrimenti la dimensione del font non sarà modificata ma il tipo di carattere diventerà Arial.
La clausola Else, pur non essendo obbligatoria, è comunque opportuna per gestire i casi "imprevisti", ad esempio generando un messaggio di errore per notificare all'utente che il valore dell'espressione non rientra in quelli previsti. Infine, come per l'istruzione If, anche le istruzioni Select Case possono essere nidificate: ovvero, dopo ogni clausola Case le istruzioni da eseguire possono includere un altro blocco Select Case, il quale a sua volta ne può contenere un altro, e così via.

Ventiduesima lezione - Debug del Codice

Oggi cominciamo un argomento al quale non sempre viene riservata la giusta importanza nei manuali di programmazione: il debug del codice. Spesso i manuali trattano il debug in uno degli ultimi capitoli, come se fosse una delle ultime cose che un programmatore deve sapere per svolgere bene il proprio lavoro; invece è vero proprio il contrario: avere difficoltà ad eseguire un corretto ed efficiente debug significa avere difficoltà a creare applicazioni valide ed affidabili, anche se un programmatore conoscesse a memoria tutte le istruzioni di un linguaggio. Infatti il debug è la procedura che permette a uno sviluppatore di scovare e correggere i "bug", ovvero gli errori e le imperfezioni presenti nel codice sorgente di un'applicazione: poiché anche in applicazioni semplici è quasi impossibile evitare del tutto errori o semplici sviste, è necessario sempre rivedere il funzionamento del programma sviluppato con l'aiuto degli strumenti di debug per verificare che tutto funzioni correttamente. Anzi, nella stragrande maggioranza dei casi le operazioni di debug sono molto più lunghe e complesse dello sviluppo vero e proprio dell'applicazione, e la complessità del debug è direttamente proporzionale (se non addirittura più che proporzionale) alla complessità dell'applicazione da testare.
Nell'IDE di Visual Basic for Application, un intero menù è dedicato al debug del codice: al suo interno ci sono varie voci che consentono allo sviluppatore di eseguire la propria applicazione istruzione dopo istruzione, di vedere in ogni momento il valore di tutte le variabili utilizzate dall'applicazione, di interrompere o modificare il flusso di esecuzione delle istruzioni. Altre funzionalità relative al debug si trovano nel menù Visualizza, ed è possibile anche utilizzare una barra degli strumenti dedicata al debug: basta cliccare col pulsante destro del mouse sulla barra degli strumenti standard e selezionare la voce "debug". Finora noi ci siamo limitati a una sorta di primitivo debug utilizzando la finestra Immediata per verificare il valore di certe variabili dopo l'esecuzione di alcune istruzioni: ora vedremo come sia possibile fare la stessa cosa in modo molto più potente ed efficiente.


Innanzitutto consideriamo le prime quattro voci del menù "Debug": la prima, "Esegui istruzione", indica la possibilità di eseguire l'applicazione un'istruzione alla volta, secondo la modalità comunemente definita "passo-passo" (single step), cioè appunto un passo alla volta; ad essa è associata il tasto F8, che premuto in sequenza permette di evidenziare man mano (in giallo, secondo l'impostazione standard modificabile nelle opzioni dell'IDE) la riga di istruzioni che sta per essere eseguita. Col tasto F8 si entra in modalità "interruzione" (visibile nella barra del titolo dell'IDE di Visual Basic) perché l'esecuzione del codice viene interrotta dopo ogni istruzione, fino a quando non si preme nuovamente F8.  Quando si è in modalità interruzione, è possibile conoscere il valore di ogni variabile posizionando semplicemente il puntatore del mouse sul nome della variabile che interessa: comparirà un tooltip col valore corrente della variabile. Poiché quando una riga di codice è evidenziata l'esecuzione è momentaneamente interrotta, è anche possibile modificare le istruzioni per vedere come cambia la situazione: in questo modo è molto semplice correggere errori banali come quelli di battitura dei valori da assegnare alle variabili. Errori che, per quanto facili da risolvere, talvolta possono essere molto difficili da individuare: gli strumenti di debug forniscono un valido aiuto a questo scopo.
Il debug passo-passo può risultare spesso molto noioso, perché costringe ad eseguire un'istruzione alla volta anche nelle porzioni di codice che abbiamo già controllato e che sappiamo funzionare bene, prima di arrivare al punto in cui pensiamo che si verifichi un errore e che quindi va controllato attentamente. Consideriamo ad esempio il seguente codice :

Private Sub Form_Load()
                              Dim i as Integer
                              Call Inizializza
                              Call Ridimensiona
                              i = Conta()
                              MsgBox i, "Contatore"
            End Sub

Nell'evento load del form sono richiamate due subroutine e una funzione, la quale restituisce un integer assegnato alla variabile i: il valore di tale variabile è poi visualizzato tramite un'istruzione MsgBox; a prescindere dalle subroutine, si vede subito che l'ultima istruzione contiene un errore, perché il secondo parametro dovrebbe essere una costante numerica mentre la variabile nel codice c'è una stringa: ci aspettiamo quindi un errore "Tipo non corrispondente", o "Type Mismatch". In apparenza, basta premere un certo numero di volte il tasto F8 per giungere all'istruzione incriminata; in realtà, quando viene evidenziata una riga che richiama una routine, premendo F8 si dà inizio all'esecuzione di quella routine e saranno man mano evidenziate le righe di codice di quella routine: è chiaro che se le routine "inizializza", "ridimensiona" e "conta" contengono migliaia di righe di codice oppure cicli con migliaia di iterazioni, passerà parecchio tempo prima di riuscire a giungere, premendo il tasto F8, all'ultima istruzione dell'evento load. Per evitare questo inconveniente è possibile usare la modalità step over (menù "Esegui istruzione/routine"), attivabile con la combinazione di tasti maiusc+F8: con questa modalità l'IDE di Visual Basic non esegue un'istruzione alla volta ma esegue un'istruzione o una routine; se la riga evidenziata in giallo è un'istruzione semplice, sarà eseguita quell'istruzione come nella modalità passo-passo; se invece è una chiamata a una funzione o subroutine, premendo F8 (single step) il debug passerà in rassegna le singole istruzioni della routine, mentre premendo maiusc+F8 (step over) sarà eseguita in colpo solo l'intera routine. Se ci troviamo col debug nel bel mezzo di una routine e vogliamo tornare subito all'istruzione successiva alla sua chiamata, possiamo premere ctrl+maiusc+F8 (menù "Esci da istruzione/routine"): ciò fa sì che tutte le istruzioni comprese tra quella che sta per essere eseguita (evidenziata in giallo) e l'uscita dalla routine siano eseguite in un colpo solo.
Spesso capita che si voglia esaminare con attenzione una piccola parte di una routine (solitamente, per la legge di Murphy, questa piccola parte si trova verso la fine della routine…), così che la modalità step over non possa essere utilizzata (altrimenti verrebbe eseguita tutta in una volta l'intera routine) e la modalità single step risulti decisamente noiosa. Per giungere subito al punto del codice che ci interessa esaminare senza consumarsi il dito premendo F8 esiste una soluzione molto semplice: impostare un punto di interruzione (breakpoint) sulla riga iniziale del blocco di codice di cui vogliamo fare il debug; in questo modo sarà sufficiente avviare il progetto e quando l'esecuzione arriverà al punto di interruzione l'IDE di Visual Basic passerà automaticamente in modalità interruzione evidenziando in giallo la riga su cui abbiamo impostato il breakpoint, consentendoci così di effettuare il debug senza ulteriori perdite di tempo. Il tasto per attivare un punto di interruzione sulla riga su cui si trova il cursore è F9, mentre una seconda pressione del tasto F9 rimuove il breakpoint appena inserito; c'è poi la combinazione ctrl+maiusc+F9 che rimuove in una volta sola tutti i punti di interruzione presenti nel progetto. Un altro modo ancora per ottenere lo stesso risultato consiste semplicemente nel posizionare il cursore sulla riga in cui l'esecuzione deve interrompersi e premere la combinazione ctrl+F8: ciò equivale alla voce di menù "Esegui fino al cursore", ed è come se su quella riga ci fosse un punto di interruzione. Mentre però il cursore può trovarsi solo su una riga, i punti di interruzione possono essere molteplici, anche su righe successive: essi rappresentano quindi uno strumento più flessibile.
Non tutte le righe di codice possono essere contrassegnate da un breakpoint: ad esempio non possono esserlo le etichette utilizzate in combinazione con l'istruzione GoTo, i commenti, oppure le istruzioni di dichiarazione di variabili. Il motivo è che queste righe identificano "codice non eseguibile": mentre per i commenti e le etichette di riga questo è abbastanza ovvio, lo è un po' meno per le dichiarazioni di variabili; le dichiarazioni rappresentano codice "non eseguibile" perché il loro unico scopo è quello di assegnare un nome e determinate caratteristiche alle variabili dichiarate: lo spazio di assegnazione però è già archiviato all'avvio dell'applicazione (o della routine se si tratta di variabili locali), infatti anche se una variabile è dichiarata in fondo a una routine, oppure all'interno di un costrutto if…then, essa è comunque creata all'inizio della routine (oppure anche se non si verifica la condizione della if). Del resto non avrebbe senso eseguire più volte una dichiarazione di variabile: la variabile dichiarata resterebbe sempre la stessa, non possono esserne create più copie distinte ma con lo stesso nome. L'unica istruzione di dichiarazione che può essere eseguita è l'istruzione Redim: ma in questo caso l'istruzione deve anche allocare lo spazio di memoria da riservare alle matrici dichiarate, quindi è ovvio che si tratta di codice eseguibile.

esempio di debugging


Tornando al nostro menù "Debug", il secondo gruppo di voci è quello relativo alle espressioni di controllo: queste sono espressioni che possono essere aggiunte o rimosse da una lista visualizzabile nella finestra omonima (vedi menù Visualizza) e consentono di tenere sempre sotto controllo il loro valore; le espressioni possono essere anche molto complesse e dipendere da più variabili. Supponiamo ad esempio di voler eseguire il debug di questo costrutto (tratto dalla routine mnuNew_Click del nostro Campo Minato):

If Mina(4 * x1 + y1).Tag > -1 Then
               Mina(4 * x1 + y1).Tag = Mina(4 * x1 + y1).Tag + 1
            End If

Per sapere se la condizione della If è verificata, occorre conoscere il valore di x1, di y1 e della proprietà Tag dell'elemento 4*x1+y1 del vettore Mina: per capire se è corretto che la condizione si verifichi oppure no, potremmo ogni volta controllare il valore di x1 e di y1, calcolare il risultato dell'espressione 4*x1+y1, e infine verificare che la proprietà Tag della mina corrispondente sia maggiore di -1. Un modo semplice per evitare tutto ciò è selezionare l'intera espressione "Mina(4 * x1 + y1).Tag > -1" e aggiungerla alle espressioni di controllo: nella finestra omonima vedremo in tempo reale il valore assunto da questa espressione, sapendo così subito se l'istruzione sottesa dalla If sarà eseguita oppure no. Ognuna di queste espressioni può essere modificata tramite l'apposito comando di menù, ed è inoltre possibile avere un riscontro immediato del valore di un'espressione utilizzando il comando "controllo immediato", che però non aggiunge l'espressione selezionata alla lista di espressioni di controllo esistente. Noi finora abbiamo usato la finestra immediata per raggiungere lo scopo a cui è destinata la finestra delle espressioni di controllo. Ogni espressione è caratterizzata da un "contesto", definito quando si aggiunge l'espressione alla lista, e che dipende sostanzialmente dall'area di validità delle variabili contenute nell'espressione: nei punti in cui tali variabili non sono definite, l'espressione diventa "fuori contesto" e quindi non è possibile conoscere il valore dell'espressione. Volendo, è possibile aggiungere come espressione di controllo anche una semplice variabile, ma è possibile tenere sotto controllo tutte le variabili definite ad esempio nel form corrente semplicemente usando la finestra "variabili locali", che permette agevolmente di navigare la struttura delle matrici, di copiare o modificare il valore di ogni singola variabile definita in un form o in un modulo. Nella parte alta della finestra è indicata la routine alla quale appartengono le variabili visualizzate nella finestra(le variabili locali, appunto) e c'è un pulsante con tre puntini tramite il quale è possibile accedere allo stack di chiamate. Lo stack delle chiamate è sostanzialmente l'elenco delle routine di cui l'esecuzione è cominciata ma non è ancora finita: quando in un punto del codice si verifica la chiamata a una routine, questa comincia ad essere eseguita ma la routine dalla quale è partita la chiamata non è ancora terminata, per cui si ritrovano ad essere entrambe sullo stack; se nella seconda routine c'è una chiamata ad una terza routine, anche quest'ultima sarà aggiunta allo stack, che funziona come un magazzino in cui l'ultimo lotto aggiunto è il primo ad essere prelevato (in gergo si dice che lo stack è di tipo LIFO: Last In, First Out): infatti prima di terminare l'esecuzione della prima routine è necessario che termini la seconda routine, ma prima che termini quest'ultima è necessario che termini la terza routine, ovvero l'ultima di cui è cominciata l'esecuzione. Visualizzando lo stack di chiamate, è possibile scegliere quale delle routine presenti nello stack deve essere "attivata" per poterne consultare le variabili locali. Lo stack di chiamate è visibile anche indipendentemente dalla finestra delle variabili locali, scegliendo l'apposita voce dal menù "Visualizza" o premente ctrl+L.

Ventitresima lezione - Debug e gestione degli errori

Le ultime due voci del menù Debug permettono di cambiare in fase di esecuzione (o meglio, in fase di interruzione) l'ordine delle istruzioni: è una opzione che risulta molto comoda quando ad esempio ci si accorge di un pezzo di codice difettoso, in cui il difetto consiste nell'aver eseguito troppo presto (o troppo tardi) una certa istruzione. L'ultima voce del menù, "Mostra istruzione successiva", permette di tornare subito alla routine in cui il codice è stato interrotto, mostrando l'istruzione evidenziata in giallo, che è appunto l'istruzione "successiva" ad essere eseguita; questo comando è utile quando, in modalità interruzione, si va a controllare altre porzioni di codice perdendo di vista il punto di interruzione. La penultima voce del menù, invece, serve a "impostare" l'istruzione successiva da eseguire, ottenendo il risultato descritto prima di modificare l'ordine delle istruzioni; prendiamo ad esempio in considerazione il codice seguente:

Dim nElementCount as Integer
            Redim sNomi(nElementCount) as String
   nElementCount=5

Supponiamo che l'istruzione successiva sia il ridimensionamento del vettore di stringhe sNomi: tale ridimensionamento avviene in base alla variabile nElementCount, che palesemente dovrebbe contenere il numero di elementi da assegnare al vettore. Peccato però che essa sia inizializzata DOPO il ridimensionamento, e non PRIMA come sarebbe logico. Ciò non genera alcun errore, perché all'esecuzione dell'istruzione Redim la variabile vale zero, e quindi il vettore conterrebbe un elemento (quello con indice zero, appunto): può anche darsi che in certe situazioni sia giusto procedere in questo modo, ma ragionando astrattamente verrebbe da pensare che l'ordine corretto delle istruzioni sia:

    nElementCount=5
                Redim sNomi(nElementCount) as String

Quando, procedendo col debug, arriviamo all'istruzione Redim e ci accorgiamo del nostro errore, possiamo posizionare il cursore sull'istruzione nElementCount=5 e premere ctrl+F9 (o l'equivalente comando "Imposta istruzione successiva"): automaticamente questa istruzione sarà evidenziata in giallo, ad indicare che essa è diventata l'istruzione successiva da eseguire. Dopo averla eseguita premendo F8, possiamo impostare come istruzione successiva quella di ridimensionamento: a questo punto la variabile nElementCount è già inizializzata a 5, e quindi il nostro vettore di stringhe sarà creato correttamente con sei elementi (da 0 a 5) premendo ancora F8. Naturalmente, dopo aver verificato che in questo modo il programma viene eseguito correttamente, potremo interrompere l'esecuzione e invertire l'ordine in cui le istruzioni sono scritte, in modo che ad una successiva esecuzione non dovremo più preoccuparci di impostare manualmente l'ordine delle istruzioni coi comandi del menù Debug. Un altro modo per ottenere lo stesso risultato è quello di trascinare la freccia gialla che compare nella barra a sinistra nella finestra del codice, rilasciandola all'altezza dell'istruzione da impostare come successiva; l'unica limitazione è che l'istruzione successiva da eseguire deve essere compresa nella stessa routine in cui il codice è stato interrotto.
E' possibile intercettuare l'errore via codice ed eventualmente provvedere, se possibile, alla sua correzione ( error handling, ovvero intercettazione degli errori). Visual Basic for Applications mette a disposizione una comoda istruzione per implementare l'error handling: l'istruzione On error goto. Questa istruzione ordina al programma di saltare a un certo punto del codice, identificato da un'etichetta, quando si verifica un errore; solitamente la struttura di una routine con l'error handling è simile alla seguente:

Sub Prova()
                            'dichiarazioni di variabili
                            '…
                            '…

                            On Error GoTo ErrorHandling

                            'istruzioni proprie della routine
                            '…
                            '…
                            '…

                            Exit Sub
ErrorHandling:
                            'codice destinato all'intercettazione dell'errore
                            '…
                            '…

            End Sub

Si possono notare diverse cose: innanzitutto, l'istruzione che permette l'intercettazione degli errori (On Error GoTo) è la prima istruzione eseguibile della routine, subito dopo le dichiarazioni di variabili; questo perché è importante intercettare un errore in qualunque punto del codice avvenga, infatti l'intercettazione viene abilitata solo dall'istruzione On Error GoTo in poi: se tale istruzione fosse posta, supponiamo, a metà routine, un eventuale errore che si verificasse prima di essa non sarebbe intercettato; invece ponendola all'inizio della routine si ha la sicurezza che qualunque errore si verifichi durante l'esecuzione della routine sarà intercettato. L'istruzione On Error GoTo avverte che quando si verifica un errore l'esecuzione del programma deve saltare al punto identificato dall'etichetta "ErrorHandling", che si trova in fondo alla routine, subito dopo l'istruzione Exit Sub: dopo l'etichetta ci sono le istruzioni che esaminano l'errore verificato (tipicamente con una Select Case, come vedremo in seguito) e tentano di risolverlo. L'etichetta è preceduta dall'istruzione Exit Sub per un motivo molto semplice: evitare che la parte di codice destinata a intercettare gli errori (ovvero le istruzioni successive all'etichetta) sia eseguita ad ogni esecuzione della routine. Senza Exit Sub, infatti, le istruzioni di error handling sarebbero eseguite in ogni caso, indipendentemente dal verificarsi o meno di un errore (si tratta pur sempre di istruzioni come le altre), e ciò può comportare a sua volta degli errori, oltre ad essere logicamente scorretto. Per assicurarsi quindi che quelle istruzioni siano eseguite solo quando effettivamente si verifica un errore, occorre farle precedere da un'istruzione che interrompa la routine nel caso in cui non ci sia stato alcun errore: l'istruzione Exit Sub, per l'appunto.
È bene precisare a questo punto che le istruzioni per l'intercettazione e la gestione degli errori riguardano soltanto gli errori di run-time, non quelli di  cosiddetti errori logici. Gli errori logici sono quelli più insidiosi perché non causano un errore vero e proprio ma fanno sì che il programma non si comporti nel modo previsto dallo sviluppatore. Un banale esempio di errore logico potrebbe essere quello in cui ci si dimentica di convertire in modo appropriato un'unità di misura generando risultati assurdi (ad esempio una performance del 567% anziché del 5,67%): il programma funziona normalmente, nel senso che non si verifica alcun errore che ne blocchi l'esecuzione, però non fa quello che dovrebbe, genera risultati sbagliati a causa di una o più istruzioni impostate in modo scorretto. Gli errori di run-time, invece, sono quelli che interrompono l'esecuzione del programma e, se non sono gestiti correttamente, possono mandarlo in crash senza troppi scrupoli. Quando si verifica un errore di run-time, Visual Basic imposta le proprietà dell'oggetto Err, che devono essere esaminate al fine di capire cosa è successo e come reagire: ogni errore standard definito in Visual Basic è identificato da un numero e da una descrizione (proprietà "Number" e "Description" dell'oggetto Err); vediamo ad esempio questo pezzo di codice:

Dim nRisultato as Integer
    nRisultato= CLng(txtNumeratore(mnIndex).Text) / CLng(txtDenominatore(mnIndex).Text)

dove la variabile mnIndex è un Integer dichiarata a livello di modulo che indica quali elementi delle due matrici di controlli TextBox "txtNumeratore" e "txtDenominatore" occorre considerare; per il momento non ci interessa sapere come viene calcolato mnIndex, né cosa indichino esattamente le variabili coinvolte; basta sapere che anche in un'istruzione semplice come questa i possibili errori sono almeno quattro:

1) mnIndex non è compreso tra i limiti inferiore e superiore delle matrici di controlli: "indice non compreso nell'intervallo" (errore 9);
2) il contenuto dei TextBox non è un numero, e di conseguenza la funzione CLng() fallisce: "Tipo non corrispondente" (errore 13);
3) txtDenominatore(mnIndex).Text è uguale a "0": "divisione per zero" (errore 11);
4) il rapporto tra i numeri contenuti nei due TextBox è esterno all'intervallo -32768/+32767 valido per gli integer: "Overflow" (errore 6)

Per intercettare un'eventuale errore scriviamo allora un'apposita routine:

Sub Pippo()
Dim nRisultato as Integer
    On Error GoTo Errore
    nRisultato= CLng(txtNumeratore(mnIndex).Text) / CLng(txtDenominatore(mnIndex).Text)
    Exit Sub 'oppure Exit Function, a seconda dei casi
Errore:
    On Error Goto 0
    Select Case Err.Number
       Case 6 'Overflow
               MsgBox "Il numeratore è troppo grande", vbOkOnly
       Case 9 ' indice non compreso nell'intervallo
                   MsgBox "Non esiste un TextBox con indice " & CStr(mnIndex), vbOkOnly
       Case 11 ' divisione per zero
                   MsgBox "Il valore inserito in txtDenominatore deve essere diverso da zero", vbOkOnly
       Case 13 ' Tipo non corrispondente
                   MsgBox "Non hai inserito un valore numerico", vbOkOnly
    End Select

End Sub 'termine della routine

L'istruzione On Error Goto 0 disattiva qualsiasi gestione dell'errore di tipo utente lasciando ad Access l'onere di gestirlo.

In questo semplice caso ci siamo limitati a visualizzare il tipo di errore riscontrato, senza tentare di risolverlo; d'altra parte sarebbe un compito abbastanza arduo da fare automaticamente, senza ricorrere all'aiuto dell'utente. In altri casi invece è più semplice risolvere in modo automatico l'errore; supponiamo ad esempio di voler aprire un file inesistente in sola lettura:

Open "c:\pippo.txt" For Input Access Read as #1

Poiché l'apertura del file è in sola lettura, se esso non esiste non sarà automaticamente creato (cosa che invece accadrebbe se il file fosse aperto anche in scrittura); per risolvere il problema possiamo intercettare l'errore:

   On Error GoTo Errore
   Open "c:\pippo.txt" For Input Access Read as #1
               Exit Sub
            Errore:
               If Err.Number=53 Then ' descrizione: impossibile trovare il file
                  Open "c:\pippo.txt" For Output Access Write as #1
                  Close 1
                  Resume
               End If

            In questo caso, se il file non esiste viene 
            creato tentando di aprirlo in modalità di scrittura, dopodiché il 
            controllo torna all'istruzione che ha generato l'errore grazie 
            all'istruzione "Resume", che vedremo meglio nella prossima lezione. 
            Per ora basti notare che esiste un modo più semplice ed elegante di 
            evitare questo errore banale, ovvero controllare in anticipo se il 
            file esiste, prima della sua apertura:
' Se non esiste il file lo creo
   If Len(Dir$("C:\pippo.txt"))=0 Then
      Open "c:\pippo.txt" For Output Access Write as #1
      Close 1
   End If
   Open "c:\pippo.txt" For Input Access Read as #1

La funzione Dir() restituisce il nome del file indicato come argomento: se restituisce la stringa nulla (di lunghezza zero), significa che il file non esiste: quindi viene aperto in scrittura (cioè creato) e subito chiuso; dopodiché viene normalmente aperto in lettura; se invece esiste già, viene aperto soltanto aperto in lettura.

Ventiquattresima lezione - Gestione degli errori

Nelle routine di gestione degli errori viste nella lezione precedente abbiamo considerato alcuni errori specifici, che sapevamo avrebbero potuto verificarsi durante l'esecuzione dell'applicazione; ovviamente non si può prevedere tutto, pertanto è necessario gestire anche gli errori non previsti. Un modo banale, ma che ha il vantaggio di impedire l'arresto inaspettato del programma, è quello di visualizzare un messaggio con la descrizione dell'errore: la clausola "else" dell'istruzione Select Case risulta particolarmente utile:

Sub Prova()
               On Error GoTo error_handler
            error_handler:
               Select Case Err.Number
                  Case 9
         …
                  Case 13
                     …
                  Case Else 'qualunque errore non gestito nei casi precedenti
                       MsgBox Err.Description, vbOkOnly, "Errore" & Str$(Err.Number)
   End Select
            End Sub

Un altro sistema per gestire gli errori di run-time è quello di utilizzare l'oggetto Debug, corrispondente alla finestra Immediata già utilizzata in varie occasioni durante le lezioni precedenti. L'oggetto Debug dispone di due metodi: il primo è il metodo Print, che visualizza nella finestra Immediata il valore del parametro passato alla funzione; il secondo, che forse non tutti conoscono, è il metodo Assert, che sospende l'esecuzione del codice se si verifica una determinata condizione. Col metodo Assert il programmatore fa un'asserzione che, nel caso in cui risulti non verificata, determina l'interruzione del programma in modo che si possa capire il motivo per cui la condizione espressa è risultata falsa. Ad es., supponiamo che alla centesima iterazione di un ciclo si verifichi un errore di cui non capiamo l'origine; potremmo impostare un punto di interruzione all'inizio del ciclo, e procedere passo-passo fino alla centesima iterazione: soluzione alquanto noiosa. Potremmo anche scrivere una routine di gestione degli errori e impostare un breakpoint su questa routine: ma così il codice sarebbe interrotto dopo che si è verificato l'errore, mentre potrebbe essere utile intercettare l'errore prima che si verifichi, sfruttando il fatto che sappiamo che l'errore si verifica alla centesima iterazione. Una soluzione conveniente è allora quella di usare l'istruzione Debug.Assert:

For i = 0 to 1000
                Debug.Assert i<99
        … 'istruzioni da eseguire ciclicamente
            Next i

Il metodo Assert dell'oggetto Debug controlla se il contatore del ciclo è minore di 99: in caso affermativo, l'esecuzione del codice prosegue normalmente, altrimenti l'IDE di Visual Basic interromperà l'esecuzione evidenziando in giallo la riga "Debug.Assert i<99"; infatti, la prima volta che la condizione espressa risulta falsa è quando i=99, ovvero quando siamo alla centesima iterazione del ciclo, e sappiamo che in questa iterazione si verifica il misterioso errore. Procedendo passo passo con F8 potremo finalmente capire cosa origina l'errore. L'istruzione Debug.Assert è equivalente all'istruzione Stop condizionata a un'istruzione If:

For i = 0 to 1000
    If i>=99 then
       Stop
    End If
                … 'istruzioni da eseguire ciclicamente
            Next i

L'oggetto Debug è utilizzabile esclusivamente dall'ambiente di progettazione.

Passiamo ora alla descrizione di alcune funzioni per la manipolazione dei tipi di dati, cominciando dalle stringhe: alcune funzioni le abbiamo già incontrate strada facendo, ma ora è il caso di descriverle con qualche dettaglio in più. Innanzitutto è bene chiarire una differenza che spesso non viene percepita da alcuni programmatori: per la manipolazione delle stringhe esistono funzioni che terminano con il simbolo del dollaro $ e funzioni con lo stesso nome ma senza questo simbolo; ad es. esiste la funzione Left() e la funzione Left$(). La differenza dovrebbe essere chiara per coloro che hanno avuto la possibilità di programmare con le vecchie versioni del basic: il simbolo del dollaro infatti era utilizzato (e può essere utilizzato ancora adesso, per compatibilità) come carattere di dichiarazione del tipo String; in altre parole, scrivere:

Dim sStringa$

è equivalente a scrivere:

Dim sStringa as String

Pertanto, le funzioni Left() e Left$() svolgono esattamente lo stesso compito, differenziandosi unicamente per il tipo di dati restituito e per il tipo di argomenti: Variant la prima, String la seconda. Ciò implica che la funzione Left$() sia più efficiente della corrispondente Left() perché evita l'implicita conversione delle variabili coinvolte da Variant a String: questo è il motivo per cui solitamente si consiglia di usare la versione "specializzata" della funzione al posto della versione più generica.
La funzione Left$(), dunque, restituisce i primi n caratteri di una determinata stringa, che viene passata alla funzione come argomento insieme al numero n di caratteri:

Left$("pippo", 3)

restituisce "pip". Se il numero di caratteri è 0, la funzione restituisce una stringa nulla; se invece è maggiore della lunghezza della stringa originale, la funzione restituisce tutta la stringa: Left$("pippo", 6) restituisce "pippo". Analogamente a Left$() c'è la funzione Right$() che restituisce invece gli ultimi n caratteri (o, se si preferisce, i primi n caratteri da destra):

                        Right$("pippo", 0) restituisce ""
                        Right$("pippo", 3) restituisce "ppo"
                        Right$("pippo", 6) restituisce "pippo"

E se uno vuole estrarre una parte intermedia di una stringa? Usa la funzione Mid$(), che a differenza di Left$() e Right$() necessita di un ulteriore parametro che rappresenta la posizione iniziale della stringa da estrarre:

Mid$(sStringa as String, lStart as Long, [Lunghezza])

Ad es.:

Mid$("pippo va a sciare", 7, 4) restituisce "va a"

Il secondo parametro indica la posizione del primo carattere della sottostringa da estrarre, mentre il terzo specifica la lunghezza della sottostringa; è chiaro che il secondo parametro deve essere maggiore di zero, ma non è necessario che sia minore della lunghezza della stringa originale: nel caso in cui il parametro lStart sia maggiore della lunghezza di sStringa, la funzione restituirà la stringa nulla. La lunghezza della sottostringa da estrarre non è un parametro obbligatorio, poiché ove mancasse la funzione Mid$() restituirebbe tutti i caratteri a partire da lStart; è un po' come usare la funzione Right$(), con la differenza che non occorre sapere quanti caratteri estrarre. Tuttavia le seguenti espressioni sono equivalenti:

                      Right$("pippo va a sciare", Len("pippo va a sciare") - 11)
                      Mid$("pippo va a sciare", 12)

Se sappiamo che la sottostringa che ci interessa (in questo caso "sciare") comincia al dodicesimo carattere, possiamo usare la funzione Right$() specificando come secondo parametro la lunghezza della stringa originale meno la posizione iniziale meno uno, poiché prima di sciare ci sono 12 - 1 = 11 caratteri da ignorare. Spesso non si conosce a priori la posizione iniziale della sottostringa da estrarre, né quella finale (ovvero la sua lunghezza): in molti casi si possono ottenere queste informazioni con la funzione InStr(), che ricerca una stringa all'interno di un'altra; la sintassi completa è:

Instr(lStart, sStringaOriginale, sStringaCercata, Confronto)

Il primo parametro determina la posizione da cui iniziare la ricerca, il secondo indica la stringa originale in cui cercare una particolare sequenza di caratteri specificata dal terzo parametro; l'ultimo parametro determina il tipo di ricerca: come già accennato nella lezione 20, è possibile specificare la costante vbTextCompare per una ricerca case insensitive, che cioè ignora la differenza tra minuscole e maiuscole; oppure la costante vbBinaryCompare per una ricerca case sensitive, che tiene conto della differenza tra maiuscole e minuscole perché considera il valore binario di ogni carattere delle stringhe confrontate, valore determinato dal corrispondente codice ANSI. Se questo parametro viene omesso, l'impostazione di default è quella di usare un confronto binario (quindi case sensitive), a meno che sia specificata l'istruzione Option Compare Text nella sezione delle dichiarazioni del form o del modulo in cui è utilizzata la funzione InStr(). L'istruzione Option Compare è analoga all'istruzione Option Explicit, ma determina a livello di modulo l'impostazione predefinita per confrontare le stringhe: come la funzione InStr(), anche Option Compare accetta alternativamente tre tipi di confronto:

Option Compare Text 'confronto testuale
            Option Compare Binary 'confronto binario
La funzione InStr() restituisce quindi la posizione iniziale della stringa cercata all'interno di quella originale: si tratta ovviamente di un numero intero positivo; infatti se la ricerca fallisce, la funzione restituisce 0, interpretabile come valore logico False nelle espressioni condizionali.
Un tipico esempio in cui si fa ricorso alla funzione InStr() è quello di separare il nome di un file dal suo percorso, come già visto nella lezione 20: talvolta però risulta più comodo usare la funzione InStrRev(), del tutto analoga a InStr() ma che ricerca la sottostringa a partire dalla fine della stringa iniziale anziché dall'inizio. Così, se voglio estrarre il nome di un file dal suo percorso, basterà scrivere:
sNomeFile = Mid$(sPath, InStrRev(sPath, "\")+1)

La funzione InStrRev() restituisce la posizione del primo backslash a partire dal fondo di sPath (quindi in effetti la posizione dell'ultimo backslash); ottenuta questa informazione, la funzione Mid$() senza il terzo argomento restituisce tutto ciò che segue l'ultimo backslash, ovvero il nome del file: notate che se non avessimo aggiunto 1 al valore restituito da InStrRev(), la funzione Mid$() avrebbe restituito anche l'ultimo backslash seguito dal nome del file.

Venticinquesima lezione - Option Compare

Le opzioni dell'istruzione Option Compare, viste nella lezione precedente, non influenzano solo il comportamento delle funzioni InStr e InStrRev, ma di qualunque confronto tra stringhe; solitamente, quando si vogliono confrontare due stringhe a prescindere dal carattere minuscolo o maiuscolo, si usava lo stratagemma di convertirle entrambe nella stessa forma (minuscola o maiuscola) usando le funzioni Lcase$() e Ucase$() (rispettivamente Lower e Upper Case); in questo modo l'utente avrebbe potuto evitare di scrivere correttamente la stringa da cercare, ad es. un cognome in un elenco telefonico:

Do Until Ucase$(sCognome)=Ucase$(sElenco(lCount))
               LCount=lCount+1
            Loop

Fare ricerche più complesse, ad es. utilizzando i caratteri jolly (* e ?), richiede un uso articolato delle funzioni viste fin qui, ovvero Left$, Right$, Mid$; ad es., per cercare un cognome del tipo Abr?i* si potrebbe implementare un ciclo di questo tipo:

Do Until ( Left$(sCognome, 3) = Left$(sElenco(lCount),3) and _
               Mid$(sCognome, 5,1) = Mid$(sElenco(lCount),5,1) )
               lCount=lCount+1
            Loop

Esiste però un metodo più semplice, che consiste nell'uso dell'operatore Like: questo operatore, in base all'impostazione definita con l'istruzione Option Compare (l'impostazione predefinita è sempre Binary), restituisce un valore booleano che indica la corrispondenza tra due stringhe, permettendo anche l'uso dei caratteri jolly; ad es.:

            "Abraini" like "Abr?i*" restituisce True
                        "Abraini" like "abr?i*" restituisce True
                        (se si è specificato Option Compare Text)

I caratteri jolly possono comparire solo nella stringa di destra dell'operatore like, ovvero nel "criterio" con cui si confronta la stringa:

sStringa Like sCriterio

Ovviamente caratteri come asterisco e punto di domanda possono comparire anche nell'espressione a sinistra, ma non vengono interpretati come caratteri jolly; per chi non lo sapesse, il preciso significato di questi particolari caratteri è il seguente:

            *	zero o più caratteri
                        ("" Like "*" restituisce True, come anche "abc" Like "*c")
                        ?	un qualunque carattere
                        (uno e uno solo però: "abc" Like "?abc" restituisce False)

Vediamo qualche altro esempio:

ESPRESSIONE	RISULTATO
                            "abc"  Like "*abc"	True
                            "abc*" Like "*abc"	False
                            (l'asterisco a sinistra di Like è un carattere come gli altri)
                            "abc*" Like "*abc?"	True
                            "*"    Like "?"         True
                            ""     Like "?"         False

Esistono poi un altro carattere "jolly" utilizzabile con l'operatore Like (l'asterisco e il punto di domanda hanno validità universale, questo no):

# una e una sola cifra da 0 a 9

Ad es. "abc0k" Like "*#?" restituisce True, come anche "abc0k" Like "*??" oppure anche "abc0k" Like "*": una cifra è pur sempre un carattere, pertanto è riconosciuto dai caratteri jolly * e ?, ma all'occorrenza è possibile identificarlo come cifra usando #.
Infine, è possibile specificare un determinato intervallo di caratteri tra parentesi quadre; ad es., al posto di usare # per le cifre è possibile usare l'intervallo [0-9]:

"abc0k" Like "*[0-9]?" restituisce true

C'è tutta una sintassi particolare, e anche un po' complessa, per usare compiutamente le opportunità offerte dall'operatore Like, ma anche un uso semplice con i tre caratteri jolly più utilizzati (*, ?, #) è già molto potente. Ad es., per verificare che un codice alfanumerico corrisponda (almeno nella forma) a un codice fiscale, basta scrivere:

sCodice Like "[A-Z][A-Z][A-Z][A-Z][A-Z][A-Z]##[A-Z]##[A-Z]###[A-Z]"

Effettuare la stessa verifica usando Left(), Mid(), e If sarebbe stato molto più complesso.


Clicca qui per ritornare all'inizio dell'applicazione Blocco Note

Per mettere in pratica le nozioni imparate in queste due lezioni, riprendiamo in mano il progetto sul nostro blocco note interrotto nell'undicesima lezione, e aggiungiamo un menù "Cerca" del tutto simile a quello del blocco note di windows, anzi con qualcosa in più: la ricerca con caratteri jolly. I nomi e le caption dei bottono di menù sono naturalmente a discrezione del programmatore; io seguirò questo standard:

CAPTION	NAME
                                    Trova…		mnuTrova
                                    Trova Successivo	mnuTrovaAncora
            

Per permettere all'utente di indicare la parola da cercare basterebbe un banale inputbox, ma vale la pena costruire una finestra un po' più sofisticata, con un textbox (txtTrova) per scrivere la stringa da cercare, un pulsante per avviare la ricerca (BtnTrova), uno per chiudere la finestra (BtnChiudi), tre optionbutton (optSu, optGiu, optTutto) per scegliere la direzione di ricerca, un checkbox per abilitare la ricerca case sensitive (chkCaseSens). Il form si può chiamare frmTrova.
Il codice di ricerca della stringa sarà tutto racchiuso nel nuovo form appena creato, cosicché il codice del menù "Cerca" sarà molto semplice: la voce mnuTrova dovrà soltanto richiamare la finestra frmTrova:

Private Sub mnuTrova_Click()
               DoCmd.OpenForm "FrmTrova", , , , , acDialog
            End Sub
            

La costante acDialog indica che il form frmTrova è modale (si dice anche "a scelta obbligatoria") rispetto al form che lo richiama (frmNotePad), cioè non è possibile tornare al form originale prima di aver compiuto qualche scelta (anche la sola chiusura) con la finestra in primo piano.
La voce mnuTrovaAncora dovrà invece richiamare la routine del pulsante cmdTrova, che scriveremo in seguito:

Private Sub mnuTrovaAncora_Click()
                 forms("frmTrova").cmdTrova_Click
            End Sub
            

Per fare ciò però è necessario che la routine cmdTrova_Click sia pubblica e quindi visibile anche da frmNotePad: perciò occorre sostituire "Private" con "Public" nella dichiarazione dell'evento Click:

Public Sub cmdTrova_Click()
               ...
            End Sub
            

Non è questa un'operazione molto raccomandabile, perché va a modificare delle dichiarazioni generate direttamente dall'IDE di Visual Basic; in realtà non ha molto senso che la routine di un evento sia "pubblica", perché la generazione dell'evento avviene privatamente rispetto al form che contiene il controllo a cui l'evento si riferisce. Richiamare la routine non corrisponde propriamente alla generazione dell'evento, anche se in buona sostanza le due cose sono uguali. In alternativa, è possibile non modificare la dichiarazione dell'evento Click e impostare a True il valore del pulsante cmdTrova:

Private Sub mnuTrovaAncora_Click()
    frmTrova.cmdTrova.Value=True
            End Sub
            

Possiamo ora dedicarci al codice del form frmTrova, cominciando dalla cosa più semplice: la chiusura del form:

Private Sub cmdChiudi_Click()
   docmd.close
            end sub

Il form viene solo nascosto, e non completamente scaricato, perché è sempre possibile continuare la ricerca della stringa specificata usando il menù mnuTrovaAncora, che deve poter accedere alla proprietà Text di txtTrova: se il form venisse scaricato, il contenuto di txtTrova andrebbe perso. In alternativa, si può memorizzare il contenuto del textbox in una variabile pubblica di frmNotePad, il che permetterebbe comodamente di scaricare frmTrova.
All'apertura del form, sarebbe bene che il pulsante cmdTrova sia disabilitato, perché il txtTrova è vuoto: se fosse abilitato e l'utente lo premesse subito, bisognerebbe cercare una stringa nulla, o visualizzare un messaggio di errore che avverta di indicare la stringa da cercare; è possibile evitare tutto ciò semplicemente disabilitando per default (ovvero in fase di progettazione) il pulsante, e abilitarlo quando il txtTrova contiene qualche carattere, sfruttando l'evento Change:

Private Sub txtTrova_Change()
   If Len(txtTrova.Text) Then
      cmdTrova.Enabled = True
   Else
      cmdTrova.Enabled = False
               End If
            End Sub
            

Prima di scrivere la routine di ricerca, è opportuno specificare l'istruzione Option Compare Binary nella sezione delle dichiarazioni del form: questo perché è semplice impedire un confronto case sensitive usando le funzioni Lcase o Ucase, ma è complicato tornare a un confronto case sensitive con l'operatore Like se l'impostazione di default è Option Compare Text.
Veniamo ora alla routine cmdTrova_Click: volendo scrivere una routine abilitata alla ricerca con caratteri jolly (per semplicità gli stessi usati dall'operatore like: *, ?, #), è opportuno procedere in questo modo: data l'impossibilità di cercare direttamente una stringa che contenga caratteri jolly, occorre suddividere il testo cercato in più parti, isolando il testo "puro" dai caratteri jolly. Ad es., se l'utente vuole cercare "abc*", non bisognerà trovare esattamente "abc*", bensì "abc": qualunque stringa cominci con "abc" soddisferà i requisiti indicati dall'utente. Più complesso è il caso di una stringa del tipo "##a*123??": in questo caso bisognerà cercare la lettera "a", oppure i numeri "123", e successivamente verificare che i caratteri circostanti corrispondano al criterio indicato. È ovvio che specificare soltanto "*" o "?" come testo da cercare non ha molto senso…
Come prima cosa, occorre eliminare gli asterischi iniziali e finali: cercare "*pippo*" è del tutto equivalente a cercare "pippo", ma il secondo caso è per noi molto più facile da trattare:

Public Sub cmdTrova_Click()
            Dim sTestoPuro(1) As String
            Dim lCount As Long
            Dim lInizio As Long
            Dim lFine As Long

               Do
                  lInizio = lInizio + 1
               Loop While Mid$(txtTrova.Text, lInizio, 1) = "*"

   lFine = Len(txtTrova.Text)
   Do While Mid$(txtTrova.Text, lFine, 1) = "*"
      lFine = lFine - 1
   Loop
   txtTrova.Text = Mid$(txtTrova.Text, lInizio, lFine - lInizio + 1)
            End Sub
            

Prima si cerca il primo carattere non-asterisco, poi si cerca l'ultimo carattere non-asterisco e infine si estrae il testo compreso tra i primi e gli ultimi asterischi (asterischi esclusi): questo sarà il vero testo da cercare.
A questo punto occorre isolare il testo "puro" dai caratteri jolly:

Public Sub cmdTrova_Click()
            Dim sTestoPuro(1) As String
            Dim lCount As Long
            Dim lInizio As Long
            Dim lFine As Long

   Do
                  lInizio = lInizio + 1
   Loop While Mid$(txtTrova.Text, lInizio, 1) = "*"

   lFine = Len(txtTrova.Text)
   Do While Mid$(txtTrova.Text, lFine, 1) = "*"
                  lFine = lFine - 1
   Loop
   txtTrova.Text = Mid$(txtTrova.Text, lInizio, lFine - lInizio + 1)

   'primo testo "puro"
   lInizio = 0
   Do
                  lInizio = lInizio + 1
   Loop While Mid$(txtTrova.Text, lInizio, 1) Like "[?#]"
   lFine = lInizio
   Do
                  lFine = lFine + 1
   Loop Until Mid$(txtTrova.Text, lFine, 1) Like "[*?#]" Or _
                                   
   lFine >= Len(txtTrova.Text)
   sTestoPuro(0) = Mid$(txtTrova.Text, lInizio, lFine - lInizio)

   'ultimo testo "puro"
   lInizio = lFine
   Do
                  lInizio = lInizio + 1
   Loop While Mid$(txtTrova.Text, lInizio, 1) Like "[*?#]"
   lFine = lInizio
   Do
                  lFine = lFine + 1
   Loop Until Mid$(txtTrova.Text, lFine, 1) Like "[*?#]" Or _
               lFine >= Len(txtTrova.Text)
   sTestoPuro(1) = Mid$(txtTrova.Text, lInizio, lFine - lInizio - 1)
            End Sub
            

Per cercare una sequenza di testo "puro", si cerca il primo carattere non-jolly, poi si va avanti fino a trovare un altro carattere jolly o ad arrivare alla fine della stringa; infine si estrae il testo trovato.
Le sequenze di testo "puro" da cercare sono due: la prima e l'ultima. Quello che sta in mezzo in fondo non ci interessa, perché una volta trovati gli estremi ci basterà confrontare il testo compreso tra questi estremi con il criterio indicato dall'utente: l'operatore Like ci renderà molto semplice questo confronto. Se ad es. l'utente cerca "pippo*abc*def*carlotta", a noi basta cercare "pippo" e "carlotta", dopodiché confronteremo l'intero testo compreso tra "pippo" e "carlotta" con la stringa "pippo*abc*def*carlotta" ricercata dall'utente. Se la sequenza di testo "puro" è solo una, la ricerca potrebbe complicarsi un po' nel caso in cui siano presenti degli asterischi (ad es. "#pippo*?#"). Complicazioni possono sorgere anche nel caso in cui non esiste alcun testo "puro" nella stringa specificata dall'utente: la stringa da cercare infatti conterrebbe solo caratteri jolly; in tali condizioni bisogna distinguere il caso in cui occorre cercare cifre dagli altri casi. Infatti, se può avere un senso cercare ad es. "#*#", non ha molto senso cercare "*?*", che si ridurrebbe banalmente a "*", ovvero tutto il testo del file e qualunque suo sottoinsieme!
Nella prossima lezione vedremo meglio queste eccezioni.

Ventiseiesima lezione - Stringhe

Cominciamo questa lezione correggendo quella precedente: nella routine cmdTrova_Click era infatti nascosto (anzi, era abbastanza evidente) un bug di cui avreste dovuto accorgervi: la parte di codice destinata a cercare l'ultimo testo puro in realtà non trova l'ultimo, ma quello successivo al primo. La correzione è molto semplice:

'ultimo testo "puro"
            lFine = Len(txtTrova.Text)
            Do While Mid$(txtTrova.Text, lFine, 1) Like "[*?#]" And lFine > 1
               lFine = lFine - 1
            Loop
            lInizio = IIf(lFine > 1, lFine, 2)
            Do
               lInizio = lInizio - 1
            Loop Until Mid$(txtTrova.Text, lInizio, 1) Like "[*?#]" Or lInizio <= 1
            If Mid$(txtTrova.Text, lInizio, 1) Like "[*?#]" Then
               sTestoPuro(1) = Mid$(txtTrova.Text, lInizio + 1, lFine - lInizio)
            Else
               sTestoPuro(1) = Mid$(txtTrova.Text, lInizio, lFine - lInizio + 1)
            End If
            

La soluzione consiste naturalmente nel partire "dal fondo" e risalire all'indietro la stringa alla ricerca di un carattere jolly che delimiti l'ultimo testo puro: la ricerca giunge sino all'inizio della stringa se occorre, e a tal fine è stata inserita la condizione "and lFine>1" nel primo loop; se infatti l'utente cerca solo caratteri jolly, lFine arriverebbe fino a zero generando un errore nella funzione Mid$(); per lo stesso motivo la variabile lInizio è inizializzata a 2 se lFine è uguale a 1. Quando lFine=1, alla fine del secondo loop anche lInizio è uguale a 1, così che sTestoPuro(1) diventa la stringa vuota. La If finale serve a distinguere il caso in cui c'è un solo testo puro: in tal caso la ricerca si ferma quando lInizio=1 e per estrarre correttamente il testo puro occorre capire se il primo carattere è un carattere jolly (quindi il testo puro parte dal secondo carattere) oppure no (quindi il testo puro parte dal primo carattere); se invece la ricerca si ferma prima (ovvero dopo il primo carattere, dato che si parte dal fondo) significa che essa è terminata perché abbiamo incontrato un carattere jolly: non ci sarebbe bisogno di eseguire la If, ma per evitarlo avremmo dovuto introdurre un ulteriore controllo sul valore di lInizio (If lInizio=1 then …).
La nostra routine trova sempre il primo e l'ultimo testo puro, anche nel caso in cui questi coincidono (ad es. quando l'utente vuole cercare "?pippo#" o semplicemente "pippo"): se ciò si verifica, per evitare problemi nella ricerca del testo è opportuno impostare l'ultimo testo puro alla stringa nulla, in modo da essere consapevoli che di testo puro effettivamente ce n'è uno solo:

If InStr(1, txtTrova.Text, sTestoPuro(0), vbBinaryCompare) = _
                                              InStrRev(txtTrova.Text, sTestoPuro(1), , vbBinaryCompare) Then
               'il primo e l'ultimo testo puro coincidono
               sTestoPuro(1) = ""
            End If
            

Per verificare la coincidenza dei due testi non basta che siano uguali (l'utente potrebbe cercare, ad es., "pippo*pippo"): condizione necessaria e sufficiente affinché i due testi siano coincidenti è che la loro posizione all'interno della stringa inserita dall'utente sia la stessa; il primo testo lo cerchiamo a partire dall'inizio della stringa con la funzione InStr, l'ultimo lo cerchiamo a partire dal fondo con la funzione InStrRev: se le due posizioni sono uguali, i due testi coincidono.
Fatto ciò, potremmo cominciare a cercare il testo, ma non abbiamo ancora considerato il caso in cui il primo e l'ultimo testo puro sia rispettivamente preceduto o seguito da caratteri jolly; in tal caso occorre infatti verificare se l'utente cerca anche una cifra oppure no. Gli asterischi, potendo rappresentare anche zero caratteri, non influiscono concretamente sulla ricerca; i punti di domanda non danno indicazioni su quale carattere cercare, pertanto basta verificare che ci siano tanti caratteri quanti sono i punti di domanda specificati nella stringa da cercare. Invece i cancelletti rappresentano una cifra, e non possono essere liquidati tanto facilmente: la cosa più conveniente è trattarli come se fossero "testo puro": quindi se l'utente specifica, ad es., "??*#pippo*?piero##", anziché limitarsi a trovare "pippo" e "piero" occorre trovare un "pippo" preceduto da una cifra e un "piero" seguito da due cifre. In realtà, le cose sono un po' più semplici: basta cercare due cifre all'interno del testo, assicurarsi che la prima cifra sia preceduta da almeno due caratteri, e confrontare l'intero testo contenuto tra questi limiti con la stringa cercata dall'utente: l'operatore like ci dirà se i due testi corrispondono oppure no.
Utilizziamo un vettore lCifra(1) per indicare quanti caratteri devono precedere la prima cifra e quanti devono seguire l'ultima: nell'esempio, lCifra(0) sarebbe uguale a 2 mentre lCifra(1)=0; si tratta del minimo numero di caratteri per soddisfare la sequenza cercata dall'utente, in realtà la prima cifra potrebbe essere preceduta anche da 10 caratteri o l'ultima seguita da 10 caratteri. Specifichiamo il valore -1 per indicare che non dobbiamo cercare cifre: ad es. nel caso "?pippo*piero*#" sarebbe lCifra(0)= -1, lCifra(1)=0.
Analogamente, utilizziamo un vettore lCarattere(1) per indicare quanti caratteri devono precedere il testo puro iniziale o seguire il testo puro finale: questa informazione ci servirà solo nel caso in cui la stringa da cercare non contenga numeri, ovvero caratteri "#". In un caso come "?pippo#", sarà lCifra(0)=-1, lCifra(1)=0, lCarattere(0)=1, lCarattere(0)=0; se non usassimo lCarattere, non potremmo sapere che prima di "pippo" ci deve essere almeno un carattere, anche se non è una cifra; lCarattere(1) non sarà utilizzato in quanto già lCifra(1) ci dà tutte le informazioni necessarie.
Ora possiamo cercare il testo. L'algoritmo è relativamente semplice: se il primo testo puro è preceduto da una cifra, cerchiamo una cifra, altrimenti cerchiamo il testo puro iniziale; una volta trovato, cerchiamo da quel punto in poi il testo puro finale o la cifra che lo deve seguire. A questo punto confrontiamo il testo contenuto tra gli estremi iniziale e finale e lo confrontiamo con la stringa definita dall'utente: se l'esito è negativo, continuiamo la ricerca del testo puro finale o della cifra finale, ricorsivamente, fino alla fine del file. Se invece l'esito è positivo, evidenziamo il testo trovato e ci prepariamo a un'ulteriore ricerca.

lInizio = 0: lFine = 0
            lTipoRicerca = IIf(chkCaseSens.Value = vbChecked, vbBinaryCompare, vbTextCompare)
            With frmNotePad.txtFile
                Do
       If lCifra(0) >= 0 Then
                      'cerca la prima cifra
                      Do
             lInizio = lInizio + 1
          Loop Until Mid$(.Text, lInizio, 1) Like "#" Or lInizio > Len(.Text)
                      If lInizio > Len(.Text) Then
             lInizio = 0
          Else
             lFine = lInizio
          End If
                   Else
                      If Len(sTestoPuro(0)) Then
                        'cerca il testo puro iniziale
                        lInizio = InStr(lInizio + 1, .Text, sTestoPuro(0), lTipoRicerca)
                      End If
                      lFine = lInizio + Len(sTestoPuro(0))-1
                   End If
                   If lInizio Then
                      Do
             If lCifra(1) >= 0 Then
                'cerca l'ultima cifra
             Do
                lFine = lFine + 1
             Loop Until Mid$(.Text, lFine, 1) Like "#" Or lFine > Len(.Text)
             If lFine > Len(.Text) Then
                lFine = 0
                         Else
                LFine = lFine+1
               'cerca il testo puro finale
               If Len(sTestoPuro(1)) Then
                  lFine = InStr(lInizio + 1, .Text, sTestoPuro(1), lTipoRicerca)
               End If
                        End If
                        If lFine = 0 Then
                Exit Do
                        Else
               lInizioTemp = lInizio - IIf(lCifra(0) < 0, IIf(lCarattere(0) < 0, 0,lCarattere(0)), lCifra(0))
                           If lCifra(1) >= 0 Then
                  lFineTemp = lFine + lCifra(1)
               Else
                  lFineTemp = lFine + Len(sTestoPuro(1)) + IIf(lCarattere(1) < 0, 0, lCarattere(1)) - 1
               End If
                        End If
                        DoEvents
                        sTestoTrovato = Mid$(.Text, lInizioTemp, lFineTemp - lInizioTemp+1)
                     Loop Until IIf(lTipoRicerca = vbBinaryCompare, (sTestoTrovato Like txtTrova.Text), _
                                                        UCase$(sTestoTrovato) Like UCase$(txtTrova.Text))
                     If lFine Then
                        .SelStart = lInizioTemp - 1
                        .SelLength = lFineTemp - lInizioTemp+1
                     End If
                  Else
                    'cifra o testo iniziale non trovati
                    MsgBox "Testo non trovato", vbOKOnly + vbExclamation, App.Title
                  End If
                         
               Loop While (lFine = 0) And (lInizio < Len(.Text))
            End With
            

Prima di tutto inizializziamo i due contatori lInizio e lFine, poi per comodità memorizziamo in lTipoRicerca se la ricerca deve essere case sensitive oppure no; dopo aver cercato la prima cifra o il primo testo puro, parte un ciclo per la ricerca dell'ultima cifra o dell'ultimo testo puro: se il testo non viene trovato, la variabile lFine è posta a 0 e il ciclo termina. Altrimenti gli estremi iniziali e finali del testo vengono aggiustati per tener conto dei caratteri identificati dai punti di domanda nella stringa specificata dall'utente; dopodiché, l'operatore like ci dice se il testo corrisponde a quello cercato oppure no: in quest'ultimo caso, la ricerca continua a partire dal punto in cui siamo arrivati. Se invece il testo corrisponde, esso viene evidenziato nella finestra frmNotePad. Se il testo non viene trovato per tutto il file, viene visualizzato un messaggio di errore: la proprietà App.Title indica il titolo dell'applicazione (si imposta tramite le proprietà del progetto, nella scheda "crea"). Nella routine ci sono due loop annidati, che idealmente hanno il compito di cercare il testo puro iniziale (il loop esterno) e quello finale (il loop interno); essi corrispondono a questa logica:

1) se c'è un testo puro iniziale, lo cerco: se lo trovo, passo al punto 2, altrimenti genero un errore;
2) se c'è un testo puro finale, lo cerco: se lo trovo, passo al punto 3, altrimenti torno al punto 1 cercando il successivo testo puro iniziale;
3) se il testo contenuto tra i due testi puri corrisponde a quello cercato dall'utente, lo seleziono; altrimenti torno al punto 2 e cerco il successivo testo puro finale

Detto in altri termini, prima cerco il testo puro iniziale: se lo trovo, procedo da quel punto in poi cercando il testo puro finale finché trovo un testo che globalmente corrisponda a quello cercato dall'utente (loop interno); se non lo trovo, torno all'inizio (loop esterno) e cerco il testo puro iniziale successivo a quello trovato nell'iterazione precedente, dopodiché ricomincio la ricerca del testo puro finale, a partire dalla posizione del testo puro iniziale appena trovato. L'errore "testo non trovato" viene perciò generato solo quando non trovo più un testo puro iniziale entro la fine del file, altrimenti devo continuare la ricerca del testo puro finale. Questa situazione è controllata dalla condizione del loop esterno: (lFine = 0) And (lInizio < Len(.Text)); se non ho trovato un testo puro finale (lFine=0) e se non ho ancora cercato per tutto il file (lInizio Per verificare la congruenza del testo trovato con quello cercato dall'utente, l'operatore Like viene utilizzato in due modalità diverse a seconda che la ricerca sia case sensitive oppure no: se vi ricordate, nella lezione scorsa avevo indicato l'opportunità di specificare l'istruzione Option Compare Binary nel form frmTrova; pertanto, se la ricerca è case sensitive è sufficiente usare l'operatore like, ma se la ricerca non è case sensitive, occorre ignorare la specifica Option Compare Binary usando la funzione Ucase$(); ecco perché la condizione del loop interno dipende dall'istruzione IIf(lTipoRicerca=vbBinaryCompare…).
L'istruzione With…End With indica che all'interno del blocco è "sottinteso" l'oggetto frmNotePad.txtFile: è un modo per rendere più chiaro e leggibile il codice e per rendere più veloce il riferimento alle proprietà di quell'oggetto.
La routine appena vista consente di cercare il testo dall'inizio del file, il che significa che premendo più volte il pulsante "trova", la stringa trovata sarà sempre la stessa, la prima dall'inizio del file; per fare in modo che si possano cercare anche le successive occorrenze del testo, basta fare una semplicissima modifica: anziché inizializzare lInizio a zero, bisogna inizializzarlo alla posizione corrente del cursore nel file. Questa posizione è data dalle proprietà SelStart e SelLength dell'oggetto TextBox, che indicano rispettivamente il numero del carattere iniziale del testo selezionato e il numero di caratteri selezionati: se ad es. nel testo "pippo e topolino" seleziono "po e to", risulterà SelStart=3 e SelLength=7; infatti SelStart inizia a contare da zero, nel senso che quando il cursore è proprio all'inizio del file aperto (prima della prima lettera), SelStart vale zero. Tornando al nostro algoritmo, basta modificare la prima riga in questo modo :

With frmNotePad.txtFile
                lInizio = .SelStart + .SelLength: lFine = 0
                          …
            

La riga di inizializzazione è stata portata all'interno del blocco With per comodità; lInizio è inizializzato alla posizione finale della selezione, se esiste un testo selezionato: questo perché l'algoritmo, quando trova il testo cercato dall'utente, lo seleziona. Se facessimo semplicemente lInizio=.SelStart, la ricerca partirebbe dal primo carattere della selezione, e quindi troverebbe ancora il testo già selezionato, e non quello successivo: in altre parole non avremmo risolto il nostro problema. Se fate qualche prova vi accorgerete che anche il vero blocco note e word funzionano allo stesso modo.
In conclusione, la nostra routine cmdTrova_Click, è la seguente:

Public Sub cmdTrova_Click()
                        Dim lTipoRicerca As Long
                        Dim lCifra(1) As Long
                        Dim lCarattere(1) As Long
                        Dim lInizio As Long
                        Dim lFine As Long
                        Dim lInizioTemp As Long
                        Dim lFineTemp As Long
                        Dim sTestoPuro(1) As String
                        Dim sTestoTrovato As String

                        lInizio = 0: lFine = 0
                        Erase sTestoPuro
                        Erase lCifra
                        cmdTrova.Enabled = False
                        cmdChiudi.Enabled = False
                        Me.MousePointer = vbHourglass

                        Do
               lInizio = lInizio + 1
                        Loop While Mid$(txtTrova.Text, lInizio, 1) = "*"
                                   
                        lFine = Len(txtTrova.Text)
                        Do While Mid$(txtTrova.Text, lFine, 1) = "*"
               lFine = lFine - 1
                        Loop
                                   
                        txtTrova.Text = Mid$(txtTrova.Text, lInizio, lFine - lInizio + 1)

                                   
                        'primo testo "puro"
                        lInizio = 0
                        Do
               lInizio = lInizio + 1
            Loop While Mid$(txtTrova.Text, lInizio, 1) Like "[*?#]"
                        lFine = lInizio
                                   
                        Do
               lFine = lFine + 1
                        Loop Until Mid$(txtTrova.Text, lFine, 1) Like "[*?#]" Or lFine > Len(txtTrova.Text)
                                   
                        sTestoPuro(0) = Mid$(txtTrova.Text, lInizio, lFine - lInizio)
                        lCifra(0) = InStr(1, Replace(Left$(txtTrova.Text, lInizio - 1), "*", ""), "#") - 1
                        lCarattere(0) = InStr(1, Replace(txtTrova.Text, "*", ""), sTestoPuro(0)) - 1

                                   
                        'ultimo testo "puro"
                        lFine = Len(txtTrova.Text)
                        Do While Mid$(txtTrova.Text, lFine, 1)  Like "[*?#]" And lFine > 1
               lFine = lFine - 1
                        Loop
                                   
                        lInizio = IIf(lFine > 1, lFine, 2)
                        Do
               lInizio = lInizio - 1
                        Loop Until Mid$(txtTrova.Text, lInizio, 1) Like "[*?#]" Or lInizio  <= 1
                                   
                        If Mid$(txtTrova.Text, lInizio, 1) Like "[*?#]" Then
               sTestoPuro(1) = Mid$(txtTrova.Text, lInizio + 1, lFine - lInizio)
                        Else
               sTestoPuro(1) = Mid$(txtTrova.Text, lInizio, lFine - lInizio + 1)
                        End If
                        lCifra(1) = Len(Replace(Mid$(txtTrova.Text, lFine + 1), "*", "")) - 
            _
                        InStrRev(Replace(Mid$(txtTrova.Text, lFine + 1), "*", ""), "#")
                        If lCifra(1) = Len(Replace(Mid$(txtTrova.Text, lFine + 1), "*", "")) Then
               lCifra(1) = -1
                        End If

                        lCarattere(1) = Len(Replace(txtTrova.Text, "*", "")) - _
                            InStrRev(Replace(txtTrova.Text, "*", ""), sTestoPuro(1)) - Len(sTestoPuro(1))
                        If InStr(1, txtTrova.Text, sTestoPuro(0), vbBinaryCompare) = 
            _                           
                          InStrRev(txtTrova.Text, sTestoPuro(1), , vbBinaryCompare) Then
                        'il primo e l'ultimo testo puro coincidono
                sTestoPuro(1) = ""
                        End If
                                   
            With frmNotePad.txtFile
                lInizio = .SelStart + .SelLength: lFine = 0
                lTipoRicerca = IIf(chkCaseSens.Value = vbChecked,  vbBinaryCompare, vbTextCompare)
                Do
                    If lCifra(0) >= 0 Then
                       'cerca la prima cifra
                       Do
                           lInizio = lInizio + 1
                       Loop Until Mid$(.Text, lInizio, 1) Like "#" Or lInizio > Len(.Text)
                       If lInizio > Len(.Text) Then
                          lInizio = 0
                       Else
                          lFine = lInizio
                       End If
                    Else
                       If Len(sTestoPuro(0)) Then
                         'cerca il testo puro iniziale
                          lInizio = InStr(lInizio + 1, .Text, 
            sTestoPuro(0), lTipoRicerca)
                       
            End If
                       
            lFine = lInizio + Len(sTestoPuro(0)) - 1
                    End If
                    If lInizio Then
                       Do
                          If lCifra(1) >= 0 Then
                             'cerca l'ultima cifra
                             Do
                                lFine = lFine + 1
                             Loop Until Mid$(.Text, lFine, 1) Like "#" Or   lFine > Len(.Text)
                             If lFine > Len(.Text) Then lFine = 0
                             Else
                                lFine = lFine + 1
                                'cerca il testo puro finale
                                If Len(sTestoPuro(1)) Then
                                   lFine = InStr(lInizio + 1, .Text,    sTestoPuro(1), lTipoRicerca)
                                End If
                             End If
                             If lFine = 0 Then
                                Exit Do
                             Else
                                lInizioTemp = lInizio - IIf(lCifra(0) < 0, 
            IIf(lCarattere(0) < 0, 0, lCarattere(0)), lCifra(0))
                                If lCifra(1) >= 0 Then
                                   lFineTemp = lFine + lCifra(1)
                                Else
                                   lFineTemp = lFine + Len(sTestoPuro(1)) + 
            iif(lCarattere(1) < 0, 0, lCarattere(1)) - 1
                                End If
                             End If
                             DoEvents
                             sTestoTrovato = Mid$(.Text, lInizioTemp, lFineTemp - lInizioTemp + 1)
                        
             end if
                       Loop Until IIf(lTipoRicerca = vbBinaryCompare, (sTestoTrovato Like txtTrova.Text), _
           			 UCase$(sTestoTrovato) Like UCase$(txtTrova.Text))
                       If lFine Then
                          .SelStart = lInizioTemp - 1
                          .SelLength = lFineTemp - lInizioTemp + 1
                       End If
                    Else
                       'cifra o testo iniziale non trovati
                       MsgBox "Testo non trovato", vbOKOnly + vbExclamation, App.Title
                    End If
                 Loop While (lFine = 0) And (lInizio < Len(.Text))
              End With
              cmdTrova.Enabled = True
              cmdChiudi.Enabled = True
              Me.MousePointer = vbDefault
              txtTrova.SetFocus
                                   
            End Sub
            

Restano ancora da definire un paio di dettagli: sfruttare gli optionbutton OptSu, OptGiu, OptTutto, e permettere la ricerca dei caratteri usati come jolly; intendo dire proprio i caratteri "#", "?" e "*", e non il testo che essi, in quanto caratteri jolly, rappresentano. Se l'utente, specificando ad es. "pippo*", volesse cercare proprio "pippo*" e non "pippo" seguito da un qualunque numero di caratteri? La nostra routine non glielo consentirebbe. Nella prossima lezione vedremo come risolvere il problema.

Ventisettesima lezione - Stringhe (2)

L'ultima lezione si era conclusa con alcuni dettagli da definire a proposito della ricerca di testo: ad es. occorre tener conto della direzione di ricerca scelta dall'utente. Per fare ciò dobbiamo introdurre un'altra variabile che ci indichi la direzione: risulta comodo che questa variabile sia di tipo numerico e che abbia valore +1/-1 a seconda che la direzione di ricerca sia verso il basso o verso l'alto. Questo perché per cercare un testo verso il basso ci è venuto naturale incrementare un contatore che indichi la posizione corrente all'interno del testo; analogamente, per cercare un testo verso il basso sarà sufficiente decrementare quel contatore, senza modificare pesantemente l'algoritmo di ricerca. Il flag di direzione può essere impostato in questo modo:

lDirezione = IIf(optSu.Value = True, -1, 1)
            

Se la direzione scelta dall'utente è verso l'alto, il flag assume valore -1, in modo che l'incremento del contatore può essere modificato semplicemente passando da un loop del tipo:

'cerca la prima cifra
            Do
    lInizio = lInizio + 1
            Loop Until (Mid$(.Text, lInizio, 1) Like "#") Or (lInizio > Len(.Text))
            
A uno del tipo:

            'cerca la prima cifra
            Do
   lInizio = lInizio + 1 * lDirezione
            Loop Until (Mid$(.Text, lInizio, 1) Like "#") Or (lInizio > Len(.Text)) Or (lInizio < 1)
            

Come si diceva, se la direzione scelta è verso l'alto il flag lDirezione sarà -1 e quindi il contatore (in questo caso lInizio) sarà decrementato anziché incrementato. Se la direzione scelta è "tutto", siamo in una situazione analoga alla direzione "giù", solo che bisognerà cercare dall'inizio anche se il cursore si trova a metà del testo:

If optTutto.Value Then
    lInizio = 0
            Else
    lInizio = .SelStart + .SelLength
            End If
            

Quando invece il testo è cercato non incrementando un contatore ma usando la funzione InStr, nel caso di ricerca verso l'alto basterà utilizzare la funzione InStrRev:

'cerca il testo puro iniziale
            If lDirezione = 1 Then
   lInizio = InStr(lInizio + 1, .Text, sTestoPuro(0), lTipoRicerca)
            Else
   lInizio = InStrRev(.Text, sTestoPuro(0), lInizio, lTipoRicerca)
            End If
            

Dobbiamo però ricordare che, se il testo va cercato verso l'alto, non va bene impostare il punto di partenza aggiungendo SelLength a SelStart, perché dovendo cercare "all'indietro" continueremmo a trovare sempre lo stesso testo: infatti il punto di partenza della ricerca è successivo al testo da cercare. Quando il testo andava cercato verso il basso, dovevamo impostare il punto di partenza a una posizione successiva a SelStart; se il testo va cercato verso l'alto, il punto di partenza deve precedere SelStart. Pertanto possiamo scrivere:

If optSu.Value Then
               lDirezione = -1
               lInizio = .SelStart
            Else
   lDirezione = 1
               If optGiu.Value Then
                  lInizio = .SelStart + .SelLength
               Else 'optTutto
                  lInizio = 0
               End If
            End If
            

In questa If abbiamo distinto i tre casi (direzione su, giù, tutto), e nel caso in cui la direzione scelta sia "tutto" abbiamo impostato a zero la variabile lInizio; questo perché, come si è detto prima, la ricerca deve coinvolgere tutto il testo del file, indipendentemente da dove si trova il cursore. Tuttavia, ciò è vero solo quando si procede alla prima ricerca del testo: quando si cerca l'occorrenza successiva sarebbe sbagliato impostare di nuovo a zero il punto di partenza, altrimenti la routine continuerebbe a trovare sempre lo stesso testo (la prima occorrenza dall'inizio del file). Si potrebbe usare un flag per indicare se la ricerca su "tutto" il file è all'inizio o sta continuando, tuttavia in questo caso potrebbe risultare comodo sfruttare l'evento Click dell'OptionButton in questo modo:

Private Sub optTutto_Click()
                frmNotePad.txtFile.SelStart = 0
            End Sub
            

Quando l'utente seleziona la direzione di ricerca "tutto", il cursore viene automaticamente impostato all'inizio del file: pertanto non c'è più bisogno dell'istruzione lInizio=0 nella routine di ricerca:

If optSu.Value Then
               lDirezione = -1
               lInizio = .SelStart
            Else
   lDirezione = 1
   lInizio = .SelStart + .SelLength
            End If
            

Così, con qualche piccola modifica, abbiamo implementato la ricerca in tutte le direzioni. Resta da trattare il caso della ricerca dei caratteri jolly.
Per permettere all'utente di cercare uno dei caratteri utilizzati come jolly (*, ?, #) occorre far capire alla routine di ricerca che il carattere inserito va preso come tale e non come simbolo che rappresenta un set di caratteri. La tecnica tradizionale per ottenere questo scopo è quella di abilitare una "sequenza di escape": le sequenze escape sono sequenze di caratteri che attribuiscono ai caratteri successivi un significato differente da quello normale. Ad es., un problema comune che si incontra in Visual Basic è quello di assegnare a una variabile stringa una stringa comprensiva di virgolette; se io scrivo:

sStringa="ciao"
            

la variabile sStringa conterrà le lettere "c", "i", "a", "o", ma non le virgolette che racchiudono queste lettere: questo perché le virgolette in Visual Basic (come in altri linguaggi) identificano proprio le stringhe; se non usassimo le virgolette, Visual Basic penserebbe che "ciao" è il nome di una variabile o una funzione o un'istruzione. Ma se io volessi assegnare a sStringa la parola "ciao" CON le virgolette, come potrei fare? Un modo è quello di concatenare le stringhe utilizzando il codice ascii delle virgolette:

sStringa=chr$(34) & "ciao" & "chr$(34)
            

Ma un altro modo è quello di usare le virgolette in un modo particolare, ovvero:

sStringa="""ciao"""
            

Ogni sequenza di tre virgolette rappresenta… le virgolette ripetute una sola volta. Ripeterle solo due volte non basterebbe, perché sarebbero confuse con la stringa nulla "", perciò occorre ripeterle tre volte: è come se le prime virgolette rappresentassero l'inizio della stringa, e le seconde virgolette fossero un modo per dire: "guarda che il carattere successivo (cioè le terze virgolette) vanno intese come testo, e non come carattere di chiusura della stringa"; lo stesso vale per le tre virgolette finali. In altre parole, in questo contesto le virgolette vengono usate come sequenza di escape, che alterano il normale significato attribuito alle virgolette. Un problema analogo si incontra nel linguaggio C quando occorre specificare il carattere "\" come testo: infatti questo carattere è solitamente utilizzato come carattere di escape per rappresentare particolari sequenze di caratteri.
Tornando a noi, dobbiamo inventarci un carattere di escape che inibisca l'uso dei caratteri jolly; supponiamo di usare proprio il backslash "\" come carattere di escape: il testo "pippo*" significa come al solito "pippo" seguito da qualunque cosa, ma "pippo\*" significa semplicemente "pippo*", dove l'asterisco questa volta non è un carattere jolly ma è un carattere come gli altri. Tenete presente che si potrebbe utilizzare il carattere di escape anche per altri scopi, ad es. per indicare il ritorno a capo: potremmo decidere cioè che la sequenza "\a" indichi il ritorno a capo, quindi: "pippo\apiero" non sarebbe altro che:

"pippo
                                    piero"
            

Una soluzione che risulta comoda data la difficoltà di rappresentare certi caratteri su una sola riga o perché il tasto corrispondente (invio nel caso del ritorno a capo) è associato ad altre funzioni. Ad ogni modo, per ora limitiamoci ai caratteri jolly. Ripercorriamo l'analisi della stringa immessa dall'utente nel campo "trova": dapprima si cercano (per eliminarli) gli asterischi iniziali e finali; prima di eliminare gli asterischi finali, però occorre controllare che non siano preceduti dal carattere escape "\":

lFine = Len(txtTrova.Text)
            Do While Mid$(txtTrova.Text, lFine, 1) = "*"
   lFine = lFine - 1
            Loop
            If Mid$(txtTrova.Text, lFine, 1) = "\" Then 'carattere escape
   lFine = lFine + 1
            End If
            txtTrova.Text = Mid$(txtTrova.Text, lInizio, lFine - lInizio + 1)
            

Il puntatore lFine viene spostato in avanti per non perdere l'ultimo asterisco. L'analisi prosegue poi cercando i testi puri e verificando se sono preceduti o seguiti da una cifra o da un carattere: nel caso in cui siano presenti caratteri escape, la routine confonderebbe il carattere "\" con un carattere qualunque, "distorcendo" la ricerca dei "veri" testi puri; ad es., se la stringa cercata dall'utente fosse: "*\#pippo*\?piero" i testi puri trovati sarebbero "\" e "piero" anziché "#pippo" e "?piero". In teoria dovremmo "aggiustare" la stringa da cercare in modo che la routine estragga correttamente i testi puri, ma in realtà sembra più conveniente lasciare l'aggiustamento a quando i testi puri sono già stati estratti: tutto quello che occorre fare è sostituire i caratteri escape con il carattere successivo:

If Right(sTestoPuro(0), 1) = "\" Then
               Mid$(sTestoPuro(0), Len(sTestoPuro(0)), 1) = Mid$(txtTrova.Text, _
                                      InStr(1, txtTrova.Text, sTestoPuro(0)) + Len(sTestoPuro(0)), 1)
            End If
            If Right(sTestoPuro(1), 1) = "\" Then
   Mid$(sTestoPuro(1), Len(sTestoPuro(1)), 1) = Mid$(txtTrova.Text, _
                                      InStr(1, txtTrova.Text, sTestoPuro(1)) + Len(sTestoPuro(1)), 1)
            End If
            

Inoltre, bisogna reimpostare l'elemento 1 dei vettori lCifra e lCarattere, perché se l'ultimo testo puro contiene un carattere escape significa che un "?" o un "#" sono stati erroneamente interpretati come caratteri jolly:

If Right(sTestoPuro(1), 1) = "\" Then
   Mid$(sTestoPuro(1), Len(sTestoPuro(1)), 1) = Mid$(txtTrova.Text, _
                                      InStr(1, txtTrova.Text, sTestoPuro(1)) + Len(sTestoPuro(1)), 1)
               If Right$(sTestoPuro(1), 1) = "#" Then
                  lCifra(1) = lCifra(1) - 1
               ElseIf Right$(sTestoPuro(1), 1) = "?" Then
                  lCarattere(1) = lCarattere(1) - 1
               End If
            End If
            

Prima di iniziare la ricerca occorre infine modificare opportunamente il contenuto del campo trova, poiché è quello che, tramite l'operatore Like, determina se il testo trovato corrisponde effettivamente al testo da cercare:

txtTrova.Text = Replace(txtTrova.Text, "\*", "[*]")
            txtTrova.Text = Replace(txtTrova.Text, "\#", "[#]")
            txtTrova.Text = Replace(txtTrova.Text, "\?", "[?]")
            

E occorre anche salvare la stringa di ricerca inserita dall'utente per evitare che nel campo "trova" restino le modifiche appena fatte: utilizziamo allo scopo una variabile sTestoCercato.

La nostra routine di ricerca è quindi diventata:

Dim lTipoRicerca As Long
            Dim lCifra(1) As Long
            Dim lCarattere(1) As Long
            Dim lInizio As Long
            Dim lFine As Long
            Dim lInizioTemp As Long
            Dim lFineTemp As Long
            Dim lDirezione As Long
            Dim sTestoPuro(1) As String
            Dim sTestoTrovato As String
            Dim sTestoCercato As String

                lInizio = 0: lFine = 0
                Erase sTestoPuro
                Erase lCifra

    cmdTrova.Enabled = False
    cmdChiudi.Enabled = False
    Me.MousePointer = vbHourglass

    sTestoCercato = txtTrova.Text

    Do
                   lInizio = lInizio + 1
    Loop While Mid$(txtTrova.Text, lInizio, 1) = "*"

    lFine = Len(txtTrova.Text)
    Do While Mid$(txtTrova.Text, lFine, 1) = "*"
                   lFine = lFine - 1
    Loop
    If Mid$(txtTrova.Text, lFine, 1) = "\" Then 'carattere escape
                   lFine = lFine + 1
    End If
    txtTrova.Text = Mid$(txtTrova.Text, lInizio, lFine - lInizio + 1)

    'primo testo "puro"
    lInizio = 0
    Do
                   lInizio = lInizio + 1
    Loop While Mid$(txtTrova.Text, lInizio, 1) Like "[*?#]"
    lFine = lInizio
    Do
                   lFine = lFine + 1
    Loop Until Mid$(txtTrova.Text, lFine, 1) Like "[*?#]" Or lFine > Len(txtTrova.Text)
                sTestoPuro(0) = Mid$(txtTrova.Text, lInizio, lFine - lInizio)
                lCifra(0) = InStr(1, Replace(Left$(txtTrova.Text, lInizio - 1), "*", ""), "#") - 1
    lCarattere(0) = InStr(1, Replace(txtTrova.Text, "*", ""), sTestoPuro(0)) - 1
            
    'ultimo testo "puro"
    lFine = Len(txtTrova.Text)
    Do While Mid$(txtTrova.Text, lFine, 1) Like "[*?#]" And lFine > 1
                   lFine = lFine - 1
    Loop
    lInizio = IIf(lFine > 1, lFine, 2)
    Do
                    lInizio = lInizio - 1
    Loop Until Mid$(txtTrova.Text, lInizio, 1) Like "[*?#]" Or lInizio <= 1
    If Mid$(txtTrova.Text, lInizio, 1) Like "[*?#]" Then
                   sTestoPuro(1) = Mid$(txtTrova.Text, lInizio + 1, lFine - lInizio)
    Else
                   sTestoPuro(1) = Mid$(txtTrova.Text, lInizio, lFine - lInizio + 1)
    End If

    lCifra(1) = Len(Replace(Mid$(txtTrova.Text, lFine + 1), "*", "")) - _
                InStrRev(Replace(Mid$(txtTrova.Text, lFine + 1), "*", ""), "#")
    If lCifra(1) = Len(Replace(Mid$(txtTrova.Text, lFine + 1), "*", "")) Then
                   lCifra(1) = -1
    End If

    lCarattere(1) = Len(Replace(txtTrova.Text, "*", "")) - _
                                    InStrRev(Replace(txtTrova.Text, "*", ""), sTestoPuro(1)) - Len(sTestoPuro(1))

    If Right(sTestoPuro(0), 1) = "\" Then
                   Mid$(sTestoPuro(0), Len(sTestoPuro(0)), 1) = Mid$(txtTrova.Text, _
                                        InStr(1, txtTrova.Text, sTestoPuro(0)) + Len(sTestoPuro(0)), 1)
    End If
    If Right(sTestoPuro(1), 1) = "\" Then
                   Mid$(sTestoPuro(1), Len(sTestoPuro(1)), 1) = Mid$(txtTrova.Text, _
                                        InStrRev(txtTrova.Text, sTestoPuro(1)) + Len(sTestoPuro(1)), 1)
                   If Right$(sTestoPuro(1), 1) = "#" Then
                      lCifra(1) = lCifra(1) - 1
                   ElseIf Right$(sTestoPuro(1), 1) = "?" Then
                      lCarattere(1) = lCarattere(1) - 1
                   End If
    End If

    If InStr(1, txtTrova.Text, sTestoPuro(0), vbBinaryCompare) = _
                                    InStrRev(txtTrova.Text, sTestoPuro(1), , vbBinaryCompare) Then
       'il primo e l'ultimo testo puro coincidono
                   sTestoPuro(1) = ""
    End If

    txtTrova.Text = Replace(txtTrova.Text, "\*", "[*]")
    txtTrova.Text = Replace(txtTrova.Text, "\#", "[#]")
    txtTrova.Text = Replace(txtTrova.Text, "\?", "[?]")

    With frmNotePad.txtFile
                   If optSu.Value Then
                      lDirezione = -1
                      lInizio = .SelStart
                    Else
                      lDirezione = 1
                      lInizio = .SelStart + .SelLength
                    End If
                    lFine = 0
                    lTipoRicerca = IIf(chkCaseSens.Value = vbChecked, vbBinaryCompare, vbTextCompare)
        Do
                       If lCifra(0) >= 0 Then
                          'cerca la prima cifra
                          Do
                             lInizio = lInizio + 1 * lDirezione
                          Loop Until (Mid$(.Text, lInizio, 1) Like "#") Or (lInizio > Len(.Text)) Or (lInizio < 1)
              If lInizio > Len(.Text) Then
                             lInizio = 0
                          Else
                             lFine = lInizio
                          End If
                       Else
                          If Len(sTestoPuro(0)) Then
                             'cerca il testo puro iniziale
                             If lDirezione = 1 Then
                                lInizio = InStr(lInizio + 1, .Text, sTestoPuro(0), lTipoRicerca)
                 Else
                    lInizio = InStrRev(.Text, sTestoPuro(0), lInizio, lTipoRicerca)
                 End If
                          End If
                          lFine = lInizio + Len(sTestoPuro(0)) - 1
                       End If
                                           
                       If lInizio Then
                          Do
                             If lCifra(1) >= 0 Then
                               'cerca l'ultima cifra
                                Do
                                   lFine = lFine + 1
                                Loop Until (Mid$(.Text, lFine, 1) Like "#") Or (lFine > Len(.Text)) Or (lFine < 1)
                                If lFine > Len(.Text) Then lFine = 0
                                Else
                                   lFine = lFine + 1
                                   'cerca il testo puro finale
                                   If Len(sTestoPuro(1)) Then
                                      lFine = InStr(lFine + 1, .Text, sTestoPuro(1), lTipoRicerca)
                                   End If
                                End If
                                If lFine = 0 Then
                                   Exit Do
                                Else
                                   lInizioTemp = lInizio - IIf(lCifra(0) < 0, _
                                                          IIf(lCarattere(0) < 0, 0, lCarattere(0)), lCifra(0))
                                   If lCifra(1) >= 0 Then
                                      lFineTemp = lFine + lCifra(1)
                                   Else
                                      lFineTemp = lFine + Len(sTestoPuro(1)) + _
                                                           IIf(lCarattere(1) < 0, 0, lCarattere(1)) - 1
                                   End If
                                End If
                    DoEvents
                                sTestoTrovato = Mid$(.Text, lInizioTemp, lFineTemp - lInizioTemp + 1)
                
            end if
                          Loop Until IIf(lTipoRicerca = vbBinaryCompare, _
                                              (sTestoTrovato Like txtTrova.Text), _
                                               UCase$(sTestoTrovato) Like UCase$(txtTrova.Text))
                          If lFine Then
                                       .SelStart = lInizioTemp - 1
                                       .SelLength = lFineTemp - lInizioTemp + 1
                          End If
                       Else
                    'cifra o testo iniziale non trovati
                                MsgBox "Testo non trovato", vbOKOnly + vbExclamation, App.Title
                       End If
                    Loop While (lFine = 0) And (lInizio < Len(.Text)) And (lInizio > 0)
                 End With

     cmdTrova.Enabled = True
     cmdChiudi.Enabled = True
     Me.MousePointer = vbDefault

     txtTrova.Text = sTestoCercato
     If frmTrova.Visible Then
                    txtTrova.SetFocus
     End If

            End Sub
            

Per le prove che mi è stato possibile fare, sembra che la routine funzioni a dovere. Naturalmente è possibile tutto un lavoro di ottimizzazione e di "ripulitura" del codice per renderlo più snello ed efficiente, ma per ora mi fermo qua.


Ventottesima lezione - Il sistema esadecimale

Dopo aver manipolato approfonditamente le stringhe, vediamo un po' più in dettaglio i numeri: in alcune lezioni passate avevo già spiegato la rappresentazione interna dei numeri interi in formato binario, che è quello utilizzato intrinsecamente da ogni processore. Oltre al formato binario, e al formato decimale usato da noi utenti, c'è almeno un altro sistema di numerazione degno di essere menzionato: il sistema esadecimale. Come il sistema binario è in base 2 (da 0 a 1) e il sistema decimale è in base 10 (da 0 a 9), così il sistema esadecimale è in base 16 (da 0 a F): per superare il limite delle 10 cifre insito nel sistema decimale da noi utilizzato, i numeri da 10 a 15 sono rappresentati dalle prime sei lettere dell'alfabeto occidentale, fino alla F per l'appunto. Per rappresentare numeri più grandi, il sistema è sempre lo stesso: si moltiplica ogni cifra del numero per la potenza della base indicata dalla posizione di quella cifra all'interno del numero, sempre partendo da destra; ad es.: 3d5c = 3*16^3 + d*16^2 + 5*16^1 + c*16^0 = 3*4096+13*256+5*16+12 = 15708 In Visual Basic i numeri esadecimali vanno sempre preceduti dall'apposito prefisso &h, per permettere al compilatore di riconoscerli come numeri esadecimali; senza il prefisso, sarebbero interpretati o come numeri decimali, o come nomi di variabili o routine se contengono lettere; ad es., possiamo scrivere:

Dim lVar as Long
                               lVar = &h12
                               Debug.Print lVar

Il risultato della stampa sarà naturalmente 18. A cosa serve utilizzare i numeri esadecimali? A nulla, se vi limitate ad usare i numeri per fare normali operazioni matematiche; l'utilizzo del formato esadecimale diventa conveniente quando c'è bisogno di ragionare con la rappresentazione interna dei numeri nella memoria del computer, ad es. quando occorre fare operazioni logiche con due numeri, perché bisogna tenere in considerazione l'impostazione dei singoli bit relativi ai due numeri. Poiché 16 è una potenza di 2, il formato esadecimale risulta essere un "parente stretto" del formato binario utilizzato dal processore, ma allo stesso tempo risulta più facilmente comprensibile (con un po' di allenamento) per noi abituati al sistema decimale; inoltre, 16 non è una qualunque potenza di 2, ma è 2^4, cioè rappresenta un gruppo di 4 bit: il che significa che 16^2=(2^4)^2=2^8=256, ovvero un byte è rappresentabile con 8 cifre binarie (bit) oppure con 2 cifre esadecimali. Ciò semplifica la lettura di una sequenza di byte, perché è molto più informativa rispetto alla stessa sequenza rappresentata in formato binario (troppo lunga) o decimale (troppo "estranea"): confrontiamo ad es. queste sequenze:

     7F  3F  4E  4F
                            127	 63  78  79

La relazione, a livello di singoli bit, tra i 4 byte della sequenza appare molto più evidente dalla lettura dei valori esadecimali che non dalla lettura dei valori decimali; solo leggendo la prima sequenza uno riesce a capire (ripeto, con un po' di allenamento) che tutti i byte sono minori di 128, che tutti hanno in comune almeno 3 bit (precisamente quelli di ordine 1,2,3 a partire da destra), che tutti tranne uno sono dispari: queste sono le caratteristiche immediatamente visibili a occhio. Leggendo la seconda sequenza, si riesce a capire subito soltanto che tutti i numeri sono minori di 128 e che tutti tranne uno sono dispari, ma non si riesce a intuire quali bit siano in comune e quali no. Perché tutti i byte sono minori di 128? Perché la prima cifra nel formato esadecimale è sempre minore di 8: &h80 è infatti pari a 8*16=128. "E allora?" direte voi: e allora, si dà il caso che la cifra 8 nel formato esadecimale riveste una particolare importanza (quando sta a sinistra come in &h80, e non quando sta a destra come in &h08) perché indica che il bit più significativo del byte è impostato; se vi ricordate le spiegazioni sull'organizzazione dei bit e sui numeri negativi, dovreste ricordarvi che il bit più significativo (ovvero quello più a sinistra) è solitamente utilizzato come bit di segno, perciò potete testare se il bit più significativo è impostato verificando la condizione

If x<0 then

Ma questo lo potete fare solo per i tipi di dati provvisti di segno: se avete a che fare con dati di tipo Byte sareste costretti a testare la condizione:

If x<128 then

Con l'operatore logico And e il formato esadecimale è possibile usare una sola espressione:

If x and &h80 then

Inoltre, la seconda cifra esadecimale nella sequenza di byte vista prima è sempre una E o una F: la F ha un altro significato particolare, perché indica che tutti i 4 bit che rappresenta sono impostati: F (esadecimale) = 15 (decimale) = 1111 (binario). Invece la E, siccome precede la F, ha solo tre bit impostati, tutti tranne l'ultimo: E (esadecimale) = 14 (decimale) = 1110 (binario): da ciò si deduce subito che tutti i quattro byte della sequenza hanno tre bit in comune, e solo uno è pari mentre gli altri sono dispari. Queste nozioni, ripeto, tornano utili principalmente quando c'è necessità di verificare quali bit di una certa variabile sono impostati e quali no, oppure se vi capita spesso di aprire un file con un editor esadecimale, ovvero un editor che mostra il contenuto del file tramite il valore numerico esadecimale dei singoli byte; in tal caso però, occorre fare attenzione al fatto che l'ordine dei byte può essere invertito. I processori Intel in particolare hanno la singolare caratteristica di invertire l'ordine dei byte quando li scrive sul disco fisso: ad es., il numero integer 18224 (che corrisponde a &h4730) sarà scritto su un file come &h30 &h47, invertendo l'ordine dei byte: prima quello meno significativo, poi quello più significativo; altri processori invece hanno un comportamento "normale". Il motivo di questa inversione non è affatto illogico come può sembrare a prima vista: il punto fondamentale è la modalità con cui il processore esegue le operazioni aritmetiche fondamentali sui byte; immaginate di scrivere su un foglio a quadretti la somma 54+82 in verticale, proprio come si fa alle elementari:

            54+
                        82=
                        ---

A questo punto occorre fare la somma: se avete cominciato a scrivere proprio all'inizio del foglio, ora vi troverete in difficoltà perché il risultato è di tre cifre, mentre gli addendi sono di due cifre; non solo manca lo spazio per una cifra, ma se si vuole mantenere l'incolonnamento manca lo spazio della cifra più significativa del risultato! Un modo semplice per ovviare a questo problema è invertire l'ordine delle cifre, esattamente come fanno i processori Intel, senza preoccuparsi preventivamente della lunghezza del risultato. Visual Basic fornisce l'apposita funzione Hex$ per convertire un numero in stringa esadecimale; per la conversione inversa basta semplicemente una qualunque funzione di conversione come CLng o Val, avendo cura di usare il prefisso "&h", ad es:

         Hex$(5423)="152F"
         Val("&h152F")=5423

Visual Basic è provvisto anche della funzione Oct$, che funziona come Hex$ ma converte un numero nel sistema ottale (in base 8): questo sistema è molto meno usato di quello esadecimale (che è già utilizzato limitatamente ad alcuni compiti); esso può tornare utile quando un parametro può assumere tre possibili valori o una loro combinazione: ad es., nei sistemi unix/linux un utente può avere tre tipi di permessi su un file: lettura, scrittura ed esecuzione (e ovviamente una loro combinazione); indicando con 1 la lettura, 2 la scrittura e 4 l'esecuzione, i diritti dell'utente sul file possono essere sintetizzati da un numero che va da 0 (nessun permesso) a 7 (tutti i permessi), secondo un sistema "ottale". Finora abbiamo trattato di numeri interi, ma naturalmente spesso si ha a che fare con numeri reali, in virgola mobile: per questo tipo di numeri si utilizzano solitamente i dati di tipo Single o Double; essi sono memorizzati in modo molto diverso dai numeri interi, seguendo gli standard dettati dall'IEEE (Institute of Electrical and Electronics Engineers). Un numero in virgola mobile è rappresentato da tre gruppi di bit: un bit di segno (il più significativo, come al solito), un "esponente" che indica l'ordine di grandezza del numero, e una "mantissa", ovvero un numero compreso tra 1 e 2: l'insieme della mantissa e dell'esponente forma il numero in formato decimale come lo rappresentiamo noi. Per il tipo di dati Single, l'esponente è rappresentato da 8 bit, che però NON formano un byte in quanto in realtà sono "a cavallo" di due byte (quello con il segno e quello con l'inizio della mantissa), mentre la mantissa è di 23 bit: in totale 4 byte; invece per il tipo di dati Double l'esponente è di 11 bit e la mantissa di 52 bit: in tutto 8 byte. Le operazioni in virgola mobile sono solitamente svolte dal coprocessore matematico, "specializzato" in quest'ambito. I numeri in virgola mobile permettono di gestire numeri molto grandi o molto piccoli, ma perdono in precisione (anche se il tipo Double, per definizione, è più preciso del tipo Single): ciò significa che i decimali successivi a una certa posizione sono ininfluenti e non permettono di distinguere un numero dall'altro; ad es. per Visual Basic sono indistinguibili i numeri (il punto esclamativo è il carattere di dichiarazione del tipo single):

        a!=1.123456789E+20
        b!=1.123456778E+20

anche se la loro differenza è di più di mille miliardi! Invece assegnando gli stessi valori a variabili di tipo double, i due numeri non sono indistinguibili. Quando ad una variabile si assegna, o direttamente o in seguito ad un'operazione, un valore superiore al massimo che può memorizzare, si ottiene un errore di "overflow"; analogamente può accadere, anche se molto più raramente, di ottenere un errore di "undeflow", quando il valore assegnato alla variabile va al di sotto del minimo consentito: ciò si verifica in particolare con i numeri in virgola mobile, quando la precisione richiesta è superiore a quella massima per il tipo di dati utilizzato. In realtà, per quanto mi consta, l'errore underflow non è previsto in Visual Basic, che in questo caso assegna alla variabile il valore 0 tout court; con altri linguaggi e altri compilatori invece può essere generato anche l'errore di underflow. Tra le operazioni matematiche utilizzabili in Visual Basic ci sono ovviamente quelle fondamentali più alcune altre; in particolare esiste un operatore di divisione intera ("\"), che esegue la divisione tra due numeri ma anziché restituire un numero reale restituisce un numero intero:

5/3=1.666666

5\3=1

Come si può intuire, la divisione intera semplicemente tronca la parte decimale, senza approssimazioni. Inoltre Visual Basic è provvisto di alcune funzioni matematiche e trigonometriche, in particolare:

Funzione - Descrizione
Abs(x) - Restituisce il valore assoluto di x
Sin(x), Cos(x) - Restituisce il seno o il coseno dell'angolo x, espresso in radianti
Log(x) - Restituisce il logaritmo naturale in base e) di x
Exp(x) - Restituisce e elevato alla x (funzione esponenziale: è la funzione inversa di log(x))
Round(x, i) - Restituisce x arrotondato alla i-esima cifra decimale
Sgn(x) - Restituisce -1, 0, 1 a seconda che x sia negativo, 0, positivo
Sqr(x) - Restituisce la radice quadrata di

La funzione Sgn può tornare utile ad es. quando si a che fare con un ciclo in cui il limite superiore non è conosciuto a priori e può anche essere negativo:

For x=0 to nLimite Step Sgn(nLimite)
                   '…
            Next x

Se nLimite è positivo, il ciclo viene eseguito alla solita maniera, se è negativo sarà eseguito al contrario, perché lo step è negativo; se è nullo, il ciclo eseguirà infinite iterazioni, perché il contatore non è incrementato (vige la clausola step 0): in tal caso bisognerà prevedere un'altra condizione di uscita dal ciclo. Le funzioni trigonometriche vogliono l'argomento espresso in radianti, in modo cioè che l'angolo giro (360°) corrisponda a 2*pi greco; si può quindi implementare una semplice funzione di conversione, ad es. per il seno:

Private Function SinG(dAngolo as Double) as Double
     SinG=Sin(dAngolo*3.14159/180)
            End Function

La funzione Log assume che la base sia il numero e (pari a circa 2,71828): per ottenere il logaritmo con una base diversa (ad esempio in base 10), basta semplicemente dividere per il logaritmo naturale della base, ovvero:

log10(x)=loge(x)/loge(10)

Con questa semplice formula si può implementare un'altra piccola funzione che accetti in ingresso il valore di cui calcolare il logaritmo e la base sul quale calcolarlo:

Private Function Logaritmo (dNumero as Double, nBase as Integer) as Double
                 Logaritmo=Log(dNumero)/Log(dBase)
            End Function

Note sul corso:
I diritti di ognuna delle lezioni presentate 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 contattare giorgio102@libero.it.
Fonte : VBItalia.it