Questo sito utilizza cookies solo per scopi di autenticazione sul sito e nient'altro. Nessuna informazione personale viene tracciata. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - RichTextBox e Syntax Highlightning

Guida al Visual Basic .NET

Capitolo 69° - RichTextBox e Syntax Highlightning

<< Precedente Prossimo >>

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:

RichTextBox.jpg
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.


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.
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.

HtmlSyntaxHL.jpg

Esempio di Syntax Highlighting
<< Precedente Prossimo >>
A proposito dell'autore

C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...