Guida al Visual Basic .NET
Capitolo 69° - RichTextBox e Syntax Highlightning
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:
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. Syntax Highlighting
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. 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. <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:
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.
Esempio di Syntax Highlighting
C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...
|