La RichTextBox è un controllo molto potente e dallo stile simile ai fogli di microsoft word, che mantiene, tuttavia, un layout windows
98. Costituisce un potenziamento della textbox normale poichè è in grado di visualizzare dei testi formattati, ossia contenenti tag che
ne definiscono lo stile: grassetto, sottolineato, barrato, corsivo, colore, grandezza, font ecc... Come suggerisce il nome, in questi
controlli il più delle volte viene caricato un file con estensione .rtf (rich text format). Un esempio grafico di come potrebbe apparire
un testo in una richtextbox:
La proprietà e i metodi più importanti di una richtextbox sono:
- AppendText(t): aggiunge la stringa t al testo della richtextbox
- CanRedo / CanUndo: proprietà che determinano qualora sia possibile rifare o annullare dei cambiamenti apportati al testo
- CaseSensitive: determina se la rixhtextbox faccia differenza tra le maiuscole o le minuscole o consideri solamente il testo (vedi Opzioni di Compilazione->Compare)
- Clear: cancella tutto il testo della richtextbox
- ClearUndo: cancella la lista che riporta tutti i cambiamenti effettuati, così che non sia più possibile richiamare la procedura Undo
- Copy / Cut / Paste: copia, taglia e incolla il testo selezionato dalla o nella clipboard
- DefaultFont / DefaultForeColor / DefaultBackColor: determinano rispettivamente il font, il colore del testo e il colore di sfondo preimpostati nella richtextbox
- DeselectAll: deseleziona tutto (equivale a porre SelectionLength = 0)
- DetectUrls: determina qualora tutti gli indirizzi url siano formattati secondo il calssico stile blu sottlineato dei collegamenti ipertestuali
- Find: importantissima funzione che permette di trovare qualsiasi stringa all'interno del testo. Ne esistono 4 versioni (in realtà 7, ma le altre non sono importanti per ora) modificate tramite overloading: la prima chiede di specificare solo la stringa, la seconda anche le opzioni di ricerca, la terza anche l'indice da cui iniziare la ricerca e la quarta anche l'indice a cui terminare la ricerca. Gli indici riferiscono una posizione nel testo basandosi sul numero di caratteri (ricordate, però, che gli indici in vb.net sono sempre a base 0, quindi il primo carattere avrà indice uguale a 0, il secondo a 1 e così via). Le opzioni di ricerca sono 5, determinate da un enumeratore: MatchCase indica se prendere in considerazione anche la maiuscole e le minuscole; NoHighlight indica di non evidenziare il testo trovato; None specifica di non far niente; Reverse specifica che bisogna trovare la stringa al contrario; WholeWord, invece, precisa che la stringa deve essere una parola a sè stante, quindi, nalla maggior parte dei casi, separata da spazi o da punteggiatura dalle altre
- GetCharFromPosition(p) / GetCharIndexFromPosition(p): funzioni che restituiscono il carattere (o il suo indice) che si trova in un punto preciso specificato come parametro p
- GetCharIndexFromLine(n) / GetCharIndexOfCurrentLine: funzioni che restituiscono rispettivamente l'indice del primo carattere della linea n e l'indice del primo della linea corrente, ossia quella su cui è fermo il cursore
- Lines: restituisce un array di stringhe rappresentanti il testo di ogni riga della richtextbox
- LoadFile(f): carica il file f nella rixhtextbox: f può essere anche un normale file di testo
- Rtf: restituisce il testo della richtextbox, includendo tutti i tag rtf
- SaveFile(f): salva il testo formattato in un file
- Select(i, l) / SelectAll: la prima procedura seleziona un testo lungo l a partire dall'indice i, mentre la seconda seleziona tutto
- SelectedRtf / SelectedText: imposta o restituisce il testo selezionato, sia in modo rtf (con i tag) che in modo normale (solo testo)
- Selection...: tutte le proprietà che iniziano con 'Selection' impostano o restituiscono le opzioni del testo selezionato, come il font, il colore, l'indentazione, l'allineamento ecc... SelectionStart indica l'indice a cui inizia la selezione, mentre SelectionLength la sua lunghezza: impostare questi due parametri equivale a richiamare la funzione Select
- Undo / Redo: annulla l'ultima azione o la ripete. Le proprietà UndoActionName e RedoActionName restituiscono il nome di quell'azione
- ZoomFactor: imposta o restituisce il fattore di ingrandimento della richtextbox
Si è visto che le operazioni che si possono eseguire su questo controllo sono numerosissime, una più utile dell'altra, ma non è finita qui. Oltre
a essere anche utilissima per contenere testo formattato, la richtextbox offre anche strumenti per modificarlo: uno di questi è il Syntax
Highlighting, ossia l'evidenziatore di sintassi, presente in quasi ogni IDE per linguaggi.
Questa tecnica consente di evidenziare determinate parole chiave nel testo del controllo con un colore o uno stile diverso dal resto. È il
caso delle parole riservate. Sia con Visual Basic Express che con SharpDevelop o Visual Studio, le keyword vengono evidenziate con un colore
differente, di solito in blu. È possibile riprodurre lo stesso comportamento nella RixhTextBox. Ho impiegato del tempo a trovare un codice
già fatto riguardo questo argomento e, dopo aver cercato molto, ci sono riuscito: sono giunto alla conclusione che
questo sia il migliore della rete, anche se si può sempre apportare qualche correzione.
Si apra un nuovo progetto Libreria di Classi, e s'incolli tutto il codice nella classe SyntaxRTB, dopodichè si clicchi Build->Build [Nome
progetto] per generare il controllo. Nonostante non si sia specificato che la classe rappresenti un controllo, il fatto che essa derivi da
RichTextBox l'ha implicitamente suggerito al compilatore. SyntaxRTB non è altro che una RichTextBox con dei metodi in più per il syntax
highlighting. Si trascini il controllo sul form normalmente come una textbox.
Ecco la classe commentata e riordinata:
Public Class SyntaxRTB Inherits System.Windows.Forms.RichTextBox 'La funzione SendMessage serve per inviare dati messaggi 'a una finestra o un dispositivo allo scopo di ottenere 'dati valori od eseguire dati compiti Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _ (ByVal hWnd As IntPtr, ByVal wMsg As Integer, _ ByVal wParam As Integer, ByVal lParam As Integer) As Integer 'Blocca il Refresh della finestra Private Declare Function LockWindowUpdate Lib "user32" _ (ByVal hWnd As Integer) As Integer 'Campo privato che specifica se il meccanismo di syntax 'highlighting è case sensitive oppure no Private _SyntaxHighlight_CaseSensitive As Boolean = False 'La tabella delle parole Private Words As New DataTable Public Property CaseSensitive() As Boolean Get Return _SyntaxHighlight_CaseSensitive End Get Set(ByVal Value As Boolean) _SyntaxHighlight_CaseSensitive = Value End Set End Property 'Contiene costanti usate nell'inviare messaggi all'API 'di windows Private Enum EditMessages LineIndex = 187 LineFromChar = 201 GetFirstVisibleLine = 206 CharFromPos = 215 PosFromChar = 1062 End Enum 'OnTextChanged è una procedura privata che ha il compito 'di generare l'evento TextChanged: prima di farlo, colora il 'testo, ma in questo caso l'evento non viene più generato Protected Overrides Sub OnTextChanged(ByVal e As EventArgs) ColorVisibleLines() End Sub 'Colora tutta la RichTextBox Public Sub ColorRtb() Dim FirstVisibleChar As Integer Dim i As Integer = 0 While i < Me.Lines.Length FirstVisibleChar = GetCharFromLineIndex(i) ColorLineNumber(i, FirstVisibleChar) i += 1 End While End Sub 'Colora solo le linee visibili Public Sub ColorVisibleLines() Dim FirstLine As Integer = FirstVisibleLine() Dim LastLine As Integer = LastVisibleLine() Dim FirstVisibleChar As Integer If (FirstLine = 0) And (LastLine = 0) Then 'Non c'è testo Exit Sub Else While FirstLine < LastLine FirstVisibleChar = GetCharFromLineIndex(FirstLine) ColorLineNumber(FirstLine, FirstVisibleChar) FirstLine += 1 End While End If End Sub 'Colora una linea all'indice LineIndex, a partire dal carattere 'lStart Public Sub ColorLineNumber(ByVal LineIndex As Integer, _ ByVal lStart As Integer) Dim i As Integer = 0 Dim SelectionAt As Integer = Me.SelectionStart Dim MyRow As DataRow Dim Line() As String, MyI As Integer, MyStr As String 'Blocca il refresh LockWindowUpdate(Me.Handle.ToInt32) MyI = lStart If CaseSensitive Then Line = Split(Me.Lines(LineIndex).ToString, " ") Else Line = Split(Me.Lines(LineIndex).ToLower, " ") End If For Each MyStr In Line 'Seleziona i primi MyStr.Length caratteri della linea, 'ossia la prima parola Me.SelectionStart = MyI Me.SelectionLength = MyStr.Length 'Se la parola è contenuta in una delle righe If Words.Rows.Contains(MyStr) Then 'Seleziona la riga MyRow = Words.Rows.Find(MyStr) 'Quindi colora la parola prelevando il colore da 'tale riga If (Not CaseSensitive) Or _ (CaseSensitive And MyRow("Word") = MyStr) Then Me.SelectionColor = Color.FromName(MyRow("Color")) End If Else 'Altrimenti lascia il testo in nero Me.SelectionColor = Color.Black End If 'Aumenta l'indice di un fattore pari alla lunghezza 'della parola più uno (uno spazio) MyI += MyStr.Length + 1 Next 'Ripristina la selezione Me.SelectionStart = SelectionAt Me.SelectionLength = 0 'E il colore Me.SelectionColor = Color.Black 'Riprende il refresh LockWindowUpdate(0) End Sub 'Ottiene il primo carattere della linea LineIndex Public Function GetCharFromLineIndex(ByVal LineIndex As Integer) _ As Integer Return SendMessage(Me.Handle, EditMessages.LineIndex, LineIndex, 0) End Function 'Ottiene la prima linea visibile Public Function FirstVisibleLine() As Integer Return SendMessage(Me.Handle, EditMessages.GetFirstVisibleLine, 0, 0) End Function 'Ottiene l'ultima linea visibile Public Function LastVisibleLine() As Integer Dim LastLine As Integer = FirstVisibleLine() + _ (Me.Height / Me.Font.Height) If LastLine > Me.Lines.Length Or LastLine = 0 Then LastLine = Me.Lines.Length End If Return LastLine End Function Public Sub New() Dim MyRow As DataRow Dim arrKeyWords() As String, strKW As String Me.AcceptsTab = True 'Carica la colonna Word e Color Words.Columns.Add("Word") Words.PrimaryKey = New DataColumn() {Words.Columns(0)} Words.Columns.Add("Color") 'Aggiunge le keywords del linguaggio SQL all'array arrKeyWords = New String() {"select", "INSERT IGNORE", "delete", _ "truncate", "from", "where", "into", "inner", "update", _ "outer", "on", "is", "declare", "set", "use", "values", "as", _ "order", "by", "drop", "view", "go", "trigger", "cube", _ "binary", "varbinary", "image", "char", "varchar", "text", _ "datetime", "smalldatetime", "decimal", "numeric", "float", _ "real", "bigint", "int", "smallint", "tinyint", "money", _ "smallmoney", "bit", "cursor", "timestamp", "uniqueidentifier", _ "sql_variant", "table", "nchar", "nvarchar", "ntext", "left", _ "right", "like", "and", "all", "in", "null", "join", "not", "or"} 'Quindi le aggiunge una alla volta alla tabella con 'colore rosso For Each strKW In arrKeyWords MyRow = Words.NewRow() MyRow("Word") = strKW MyRow("Color") = Color.LightCoral.Name Words.Rows.Add(MyRow) Next End Sub End Class
Il costruttore New ha il compito di inizializzare tutte le informazioni inerenti alle parole ed al loro colore. La struttura della classe
utilizza una DataTable in cui ci sono due colonne: Word, la parola da evidenziare, e Color, il colore da usare per l'evidenziazione. Ogni
riga contiene quindi queste due informazioni, e ci sono tante righe quante sono le keywords del linguaggio che si desidera. ColorLineNumber
è invece commentata nel sorgente.
Questi metodi, però, sebbene funzionino con il linguaggio di riferimento (SQL), perdono di ogni validità con l'HTML, dove le parola chiave
sono attaccate le une alle altre, ad esempio in:
<a href='http://totem.altervista.org'>Link</a>
a viene subito dopo la parentesi angolare, mentre href prima di un uguale. Nonostante il modo più preciso in assoluto per scovare le keywords sia usare le espressioni regolari, non ancora anlizzate, per ora si farà in altro modo. Ecco la classe riscritta da me, in modo da adeguare il funzionamento all'HTML e migliorando le prestazioni:
Public Class SHRichTextBox Inherits System.Windows.Forms.RichTextBox Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _ (ByVal hWnd As IntPtr, ByVal wMsg As Integer, _ ByVal wParam As Integer, ByVal lParam As Integer) As Integer Private Declare Function LockWindowUpdate Lib "user32" _ (ByVal hWnd As Integer) As Integer Private Enum EditMessages LineIndex = 187 LineFromChar = 201 GetFirstVisibleLine = 206 CharFromPos = 215 PosFromChar = 1062 End Enum Protected Overrides Sub OnTextChanged(ByVal e As EventArgs) 'Non colora tutte le linee visibili, bensì solo la riga 'dove si trova il cursorse: in questo modo l'applicazione 'risulta più veloce. L'unico caso in cui questo 'approccio non funzione è quando si copia un testo 'all'interno della richtextbox. In quel caso ci sarà 'un pulsante apposito Dim LineIndex As Int32 = Me.GetLineFromCharIndex(Me.SelectionStart) Me.ColorLineNumber(LineIndex) End Sub 'Colora tutta la RichTextBox Public Sub ColorRtb() For I As Int32 = 0 To Me.Lines.Length - 1 ColorLineNumber(I) Next End Sub 'Colora solo le linee visibili Public Sub ColorVisibleLines() Dim FirstLine As Integer = FirstVisibleLine() Dim LastLine As Integer = LastVisibleLine() If (FirstLine = 0) And (LastLine = 0) Then 'Non c'è testo Exit Sub Else While FirstLine < LastLine ColorLineNumber(FirstLine) FirstLine += 1 End While End If End Sub 'Questa è la nuova versione: nelle stesse condizioni sopra 'citate, impiega 50ms, quasi la metà! L'algoritmo vecchio 'per SQL ne impiegava 10, ma non era in grado di supportare tag 'vicini come quelli dell'HTML Public Sub ColorLineNumber(ByVal LineIndex As Int32) Try If Me.Lines(LineIndex).Length = 0 Then Exit Sub End If Catch Ex As Exception Exit Sub End Try 'Indice del primo carattere della linea Dim FirstCharIndex As Int32 = _ Me.GetFirstCharIndexFromLine(LineIndex) 'Tiene traccia del cursore Dim SelectionAt As Integer = Me.SelectionStart 'Blocca il refresh LockWindowUpdate(Me.Handle.ToInt32) 'Tiene traccia se ci siano tag aperti Dim TagOpened As Boolean = False 'Indica se il tag ha degli attributi Dim Attribute As Boolean = False 'Indica se un attributo è stato assegnato Dim Assigned As Boolean = False 'Indica, per gli attributi come [readonly], se le parentesi 'sono state aperte Dim AttributeOpened As Boolean = False 'Variabili locali che rappresentano Me.SelectionStart e 'Me.SelectionLength: usando la variable enregistration si 'guadagna qualche millisecondo Dim Start, Length As Int32 Dim Max As Int32 = _ (FirstCharIndex + Me.Lines(LineIndex).Length) - 1 Me.Select(FirstCharIndex, Max + 1) For Index As Int32 = FirstCharIndex To Max If Char.IsLetterOrDigit(Me.Text(Index)) Then Continue For End If 'Viene aperto un tag, inizia a selezionare 'Es.: <a If Me.Text(Index) = "<" Then Start = Index TagOpened = True Attribute = False Assigned = False ElseIf Me.Text(Index) = ">" Then 'Viene chiuso un tag: se sono stati definiti 'attributi, evidenzia solo la parentesi angolare, 'Es.: <a href='www.example.com'> 'altrimenti tutta la stringa da "<" a ">" 'Es.: <div> If Not Attribute Then Length = Index - Start Me.Select(Start, Length) Me.SelectionColor = Color.Blue End If Me.Select(Index, 1) Me.SelectionColor = Color.Blue Me.DeselectAll() TagOpened = False Attribute = False Assigned = False ElseIf TagOpened AndAlso Me.Text(Index) = " " Then 'Uno spazio: se un attributo è già stato impostato, 'si tratta di uno spazio che separa due attributi, 'quindi passa oltre, definendo solo 'Assigned = False; 'Es.: <div id='1' class='prova'> 'altrimenti è uno spazio che precede qualsiasi 'attributo, che quindi viene dopo la dichiarazione 'del tag, che viene colorato in blu 'Es.: <div id='1'> If Assigned Then Assigned = False Else Length = Index - Start Me.Select(Start, Length) Me.SelectionColor = Color.Blue End If Me.DeselectAll() Start = Index + 1 ElseIf TagOpened AndAlso Me.Text(Index) = "=" Then 'Un uguale: a un attributo viene assegnato un 'valore, perciò evidenzia l'attributo, 'dallo spazio precedente fino a = non compreso, 'e lo colore in rosso 'Es.: <table width='100'> Length = Index - Start Me.Select(Start, Length) Me.SelectionColor = Color.Red Me.DeselectAll() Attribute = True Assigned = True ElseIf Me.Text(Index) = "[" Then 'Apre un attributo Start = Index AttributeOpened = True ElseIf Me.Text(Index) = "]" And AttributeOpened Then 'Chiude un attributo 'Es.: <input type='text' [readonly]> Length = Index - Start Me.Select(Start, Length) Me.SelectionColor = Color.Red Me.DeselectAll() AttributeOpened = False End If Next 'Ripristina la selezione Me.SelectionStart = SelectionAt Me.SelectionLength = 0 'E il colore Me.SelectionColor = Color.Black 'Riprende il refresh LockWindowUpdate(0) End Sub 'Ottiene la prima linea visibile Public Function FirstVisibleLine() As Integer Return SendMessage(Me.Handle, EditMessages.GetFirstVisibleLine, 0, 0) End Function 'Ottiene l'ultima linea visibile Public Function LastVisibleLine() As Integer Dim LastLine As Integer = FirstVisibleLine() + _ (Me.Height / Me.Font.Height) If LastLine > Me.Lines.Length Or LastLine = 0 Then LastLine = Me.Lines.Length End If Return LastLine End Function End Class
In questa versione modificate ci sono parecchie divergenze:
- Non viene utilizzata una tabella dei colori: il motivo è semplice; viene eseguito un controllo un carattere alla volta e, quale che sia il nome del tag e dell'attributo specificato, viene comunque colorato. Questa caratteristica ha dei pregi e dei difetti. Non evidenzia gli errori, ma in questo caso si può sempre ripristinare la tabella perdendo un po' di velocità. Tuttavia evidenzia anche i tag nuovi che vengono usati dai css: ad esempio, questa pagina usava dei tag "<k>", che non esistono nell'HTML ma sono pur sempre tag, e vengono usati per definire le keywords e per colorare il listato. Se si considera la prima ipotesi, sarebbe meglio utilizzare una collezione a dizionario a tipizzazione forte, per sprecare meno memoria.
- Non divide la stringa: analizza semplicemente un carattere per volta dall'inizio alla fine. Questo procedimento è assai più rapido e ovviamente non funzionerebbe con uno split, dato che i tag sono attaccati l'uno all'altro
- Non utilizza ColorRtb su OnTextChanged: dato che il controllo è progettato per aiutare nella scrittura, si suppone che chi immetta il codice stia scrivendo, quindi colora soltanto la linea su cui si sta operando e non tutte le linee visibili. Questo contribuisce a velocizzare il meccanismo
Per chi avesse letto la versione precedente della guida, si sarà certamente notato il cambiamento radicale di algoritmo utilizzato, rispetto a quello più rudimentale:
For Each Word As String In Words I = FirstCharIndex Do I = Me.Find(Word, I, I + Me.Lines(LineIndex).Length, _ RichTextBoxFinds.None) If I >= 0 Then Me.SelectionStart = I Me.SelectionLength = Word.Length 'Qui utilizo un dictionary Me.SelectionColor = Words(Word) I += Word.Length End If Loop While I >= 0 Next
Quest'ultimo colora solo le parole indicate, ma esegue almeno (almeno!) un centinaio di controlli ogni volta, ossia uno per ogni parola
data: se poi queste appaiono nella riga, il conto raddoppia! Questo approccio, per fare un esempio, su una linea di 37 caratteri con cinque
o sei parole riservate, impiega circa 90ms per colorare, ed il tempo aumenta vertiginosamente di 10/20ms per ogni carattere in più. Nel
nuovo algoritmo, il tempo è ridotto a circa 50ms, con un aumento di 2/3ms per ogni carattere in più. L'algoritmo iniziale, invece, dovendo
analizzare solo il numero di parole della stringa, impiegava, sempre nelle stesse condizioni, circa 10ms, con un aumento di 1/2ms ogni
parola in più. (Bisogna però ricordare che il primo proposto colorava tutte le linee visibili ad ogni modifica).
Si può capire quindi come sia vantaggioso quello iniziale in termini di tempo, e quanto svantaggioso in termini di
prestazioni.