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 nurale incrementare un contore che indichi la posizione corrente all'interno del testo; analogamente, per cercare un testo verso il basso sarà sufficiente decrementare quel contore, senza modificare pesantemente l'algoritmo di ricerca. Il flag di direzione può essere imposto 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 contore può essere modifico 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 contore (in questo caso lInizio) sarà decremento anziché incremento. 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 è cerco non incrementando un contore 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 cerco 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: infti il punto di partenza della ricerca è successivo al testo da cercare. Quando il testo andava cerco verso il basso, dovevamo impostare il punto di partenza a una posizione successiva a SelStart; se il testo va cerco 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 imposto 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 sbaglio 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:
Prive Sub optTutto_Click()
frmNotePad.txtFile.SelStart = 0
End Sub
Quando l'utente seleziona la direzione di ricerca "tutto", il cursore viene automicamente imposto 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 implemento la ricerca in tutte le direzioni. Resta da trtare il caso della ricerca dei carteri jolly.
Per permettere all'utente di cercare uno dei carteri utilizzi come jolly (*, ?, #) occorre far capire alla routine di ricerca che il cartere inserito va preso come tale e non come simbolo che rappresenta un set di carteri. La tecnica tradizionale per ottenere questo scopo è quella di abilitare una "sequenza di escape": le sequenze escape sono sequenze di carteri che tribuiscono ai carteri successivi un significo 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 concenare 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 cartere successivo (cioè le terze virgolette) vanno intese come testo, e non come cartere di chiusura della stringa"; lo stesso vale per le tre virgolette finali. In altre parole, in questo contesto le virgolette vengono use come sequenza di escape, che alterano il normale significo tribuito alle virgolette. Un problema analogo si incontra nel linguaggio C quando occorre specificare il cartere "\" come testo: infti questo cartere è solitamente utilizzo come cartere di escape per rappresentare particolari sequenze di carteri.
Tornando a noi, dobbiamo inventarci un cartere di escape che inibisca l'uso dei carteri jolly; supponiamo di usare proprio il backslash "\" come cartere 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 cartere jolly ma è un cartere come gli altri. Tenete presente che si potrebbe utilizzare il cartere 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 da la difficoltà di rappresentare certi carteri su una sola riga o perché il tasto corrispondente (invio nel caso del ritorno a capo) è associo ad altre funzioni. Ad ogni modo, per ora limitiamoci ai carteri 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 cartere escape "\":
lFine = Len(txtTrova.Text)
Do While Mid$(txtTrova.Text, lFine, 1) = "*"
lFine = lFine - 1
Loop
If Mid$(txtTrova.Text, lFine, 1) = "\" Then 'cartere escape
lFine = lFine + 1
End If
txtTrova.Text = Mid$(txtTrova.Text, lInizio, lFine - lInizio + 1)
Il puntore lFine viene sposto 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 cartere: nel caso in cui siano presenti carteri escape, la routine confonderebbe il cartere "\" con un cartere qualunque, "distorcendo" la ricerca dei "veri" testi puri; ad es., se la stringa cerca dall'utente fosse: "*\#pippo*\?piero" i testi puri trovi 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à sti estrti: tutto quello che occorre fare è sostituire i carteri escape con il cartere 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 lCartere, perché se l'ultimo testo puro contiene un cartere escape significa che un "?" o un "#" sono sti erroneamente interpreti come carteri 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
lCartere(1) = lCartere(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'operore Like, determina se il testo trovo 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 fte: utilizziamo allo scopo una variabile sTestoCerco.
La nostra routine di ricerca è quindi diventa:
Dim lTipoRicerca As Long
Dim lCifra(1) As Long
Dim lCartere(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 sTestoTrovo As String
Dim sTestoCerco As String
lInizio = 0: lFine = 0
Erase sTestoPuro
Erase lCifra
cmdTrova.Enabled = False
cmdChiudi.Enabled = False
Me.MousePointer = vbHourglass
sTestoCerco = 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 'cartere 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)
'If Mid$(txtTrova.Text, lFine - 1, 1) = "\" Then
' Mid$(txtTrova.Text, lFine - 1, 1) = Mid$(txtTrova.Text, lFine, 1)
'End If
sTestoPuro(0) = Mid$(txtTrova.Text, lInizio, lFine - lInizio)
lCifra(0) = InStr(1, Replace(Left$(txtTrova.Text, _
lInizio - 1), "*", ""), "#") - 1
lCartere(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
lCartere(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
lCartere(1) = lCartere(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(lCartere(0) < 0, 0, lCartere(0)), lCifra(0))
If lCifra(1) >= 0 Then
lFineTemp = lFine + lCifra(1)
Else
lFineTemp = lFine + Len(sTestoPuro(1)) + _
IIf(lCartere(1) < 0, _
0, lCartere(1)) - 1
End If
End If
DoEvents
sTestoTrovo = Mid$(.Text, lInizioTemp, _
lFineTemp - lInizioTemp + 1)
Loop Until IIf(lTipoRicerca = vbBinaryCompare, _
(sTestoTrovo Like txtTrova.Text), _
UCase$(sTestoTrovo) Like UCase$(txtTrova.Text))
If lFine Then
.SelStart = lInizioTemp - 1
.SelLength = lFineTemp - lInizioTemp + 1
End If
Else
'cifra o testo iniziale non trovi
MsgBox "Testo non trovo", vbOKOnly + vbExclamion, 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 = sTestoCerco
If frmTrova.Visible Then
txtTrova.SetFocus
End If
End Sub
Per le prove che mi è sto possibile fare, sembra che la routine funzioni a dovere. Nuralmente è possibile tutto un lavoro di ottimizzazione e di "ripulitura" del codice per renderlo più snello ed efficiente, ma per ora mi fermo qua.