Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Improved RichTextBox - IRTB

0.00/5 (No votes)
3 Apr 2013 1  
This article describes a simple way to implement Line Numbering, HighLight Line and show current Line and Column
 

323059/IRTB.png

Introduction 

This article presents what I call an improved RichTextBox control. Why improved? Well, browsing the internet you can find a lot of comments pointing out to the different features that are missing on the control provided by Microsoft but three of the most obvious are:

  • Line numbering
  • Highlighting a line
  • Show current line and column

The first one has been addressed in several articles like the following ones:

Of course you can find more articles and code snippets related to line numbering, but while this issue has been solved the highlight line and showing current line/column were still a pending issues. For everyone using Notepad++ or PSPad it is obvious that one of the nice features is to highlight the line where currently the caret is and of course having an indication of line/column can be very helpful.

So what can you expect from this article? I will go through these three points showing how I solved them for my own project. Please notice that I am new to the .NET world so if anything can be improved or made in a different way please let me know. I hope the information here will be useful for anyone seeking for a small and simple solution for his/her own projects.

The code is divided in the following "Regions":

  • Region "ImprovedRTB Properties"
  • Region "General Subroutines"
  • Region "IRTB Events"
  • Region "Extra Features"

Implementation

The control is formed by two components, a RichTextBox and a PictureBox. The PictureBox will be used to draw the line numbers and the RichTextBox will provide all the features we need from a text editor.

IRTB features are:

  • Line Numbering. This can be enabled/disabled at run-time
  • HighLight Line. This can be enabled/disabled at run-time  
  • Event LineInformation which provides the information about the Current Line and Column
  • Event DragDropFileInformation to allow direct Drag and Drop files into the control. 
  • Event BMLInformation used to inform that a line has been added/removed to/from the bookmarks  
  • HighLight Line Color can be changed at run-time
  • Prefix for Line Numbering can be enabled/disabled at runtime.
  • Font can be changed at runtime.
  • Zoom 
  • Drag/Drop is enabled 
  • Bookmark Lines. New 2013.04.02 
  • XML verification. New 2013.04.02
  • Line/Column information without the need of LineInformation  event. New 2013.04.02 

Known issues:

  • Performance is not good with files bigger than 1500Kb and it is really bad with files bigger than 2500Kb when both options, line numbering and highlight, are enabled. This is mainly due the calculations needed to add the numbers and draw the highlight line in top of the RichTextBox
  • Some flickering can be noticed depending of the action or the size of the file
  • If wrap is set to true it won't give the correct line numbering

Line Numbering

So let's start with the first point, Line Numbering. You can find several solutions, as I have said before, some of them show really nice graphics but unfortunately this make the control unusable for large files, other ones are just not totally finished and other ones preferred the unmanaged code (sendmessage) approach which is totally valid (I have used it for some other projects), having this in mind I decide to get something in the middle, not too fancy that makes the control unusable and try not to use the "sendmessage" option; so since I have started already with a similar approach to the one proposed by Michael Elly I decided to continue on that line, the key of Michael's code is the way it is calculated the line height. Why? Because as everyone knows the RichTextBox scrolls using smooth scroll so instead of lines it uses pixels, this is due the nature of the control because it can support different formats, fonts, colors, etc. Therefore the only way to scroll is based on pixels rather than lines. So here is the core of the line numbering.

Dim font_height As Single = _
   IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(2)).Y - _
   IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(1)).Y
If font_height <= 0 Then font_height = IRTBTextContainer.Font.Height + 1 'Exit Sub
'Get the first line index and location
Dim firstIndex As Integer = IRTBTextContainer.GetCharIndexFromPosition(New Point(0, g.VisibleClipBounds.Y + font_height / 3))
Dim firstLine As Integer = IRTBTextContainer.GetLineFromCharIndex(firstIndex)
Dim firstLineY As Integer = IRTBTextContainer.GetPositionFromCharIndex(firstIndex).Y

In this way the real height of the line will be calculated giving the exact value needed to paint the numbers on the PictureBox. So now using this value we can calculate the Y position:

Dim y As Single
Do While y < g.VisibleClipBounds.Y + g.VisibleClipBounds.Height
    If i > (IRTBLineCount) Or (i > (IRTBLineNumber + 1) And (IRTBLineNumber + 1) = _
        IRTBLineCount) Then Exit Do
        '**This line will avoid to paint all possible numbers in
        ' the VisibleClipBounds even if the line number on the Richtextbox is 0
    y = firstLineY + 2 + font_height * (i - firstLine - 1) '
    LineNumberingString = Format(i, PrefixFormat) '** To Add some format to the line numbering
    Select Case PBAlign
        Case IRTBAlignPos.Left
            PBAlignNumbers = 0
        Case IRTBAlignPos.Right
            PBAlignNumbers = CInt(PBNumbering.Width - g.MeasureString(LineNumberingString, _
               IRTBTextContainer.Font).Width * IRTBTextContainer.ZoomFactor)
    End Select
    If i > 0 Then '**To avoid painting the number 0 so in case we are zooming it won’t be shown
        g.DrawString(LineNumberingString, PLNumberingFont, IRTBBrushes, PBAlignNumbers, y)
    End If
    i += 1
Loop
Me.TableLayoutPanel1.ColumnStyles.Item(0).Width = _
   CSng(Math.Ceiling(IRTBTextContainer.Font.Size) * 2) + _
   CInt(g.MeasureString(LineNumberingString, IRTBTextContainer.Font).Width * _
   IRTBTextContainer.ZoomFactor)

OK, now we have the routine to draw the numbers, great! (Thanks to Michael Elly), but how do we use it? When should we call it? Now is when we need to check the event Paint for the PictureBox. How can we trigger this event? And, under what conditions should we do it? The following events must be used for this:

  • Me.SizeChanged
  • IRTBTextContainer.VScroll
  • IRTBTextContainer.MouseWheel 
  • IRTBTextContainer.SelectionChanged
  • LoadFileAndNumbering 

When any of these events is raised the following sentence should be used:

PBNumbering.Invalidate()

By doing this we are forcing the PictureBox to re-paint so the event Paint will be fired and DrawIRTBLineNumbers will be called.

Here is the code used to do this:

Private Sub PLNumbering_Paint(ByVal sender As System.Object, _
          ByVal e As System.Windows.Forms.PaintEventArgs) Handles PBNumbering.Paint
    If EnableNumbering = True Then
        DrawIRTBLineNumbers(e.Graphics)
        If IRTBForceONPaint = True Then
            OnPaint(Nothing)
            IRTBForceONPaint = False
        End If
    End If
End Sub

And now numbers will be painted based on the lines shown on the VisibleClipBounds of the RichTextBox.

Highlight Line

OK, now that we have the numbers painted we can try to get a rectangle highlighting the current line, to do this we need to use the standard Windows GDI+ library (System.Drawing) and we need to override the OnPaint subroutine. It might be that you noticed that there is no Paint event exposed for the RichTextbox control so the only way to get some control on what we want to paint is using the OnPaint method provided by Control. We override the method and we add the needed code to paint the rectangle using the RichTextBox width and the line height based on the Font used.

Why are we not using WndProc to catch the paint event of the RichTextBox? Well I decided to go for the simpler solution but for those ones interested on this approach, here is a link showing how to catch the WM_PAINT message:

So going back to the code that will paint the rectangle:

Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
    Dim RTBPoint As Point
    IRTBTextContainer.Refresh()
    RTBPoint = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexOfCurrentLine)
    IRTBDrawRectangle(My.Settings.HighLightColor, 1, RTBPoint.Y, _
            IRTBTextContainer.Width - 1, CInt(IRTBTextContainer.Font.GetHeight))
    IRTBTextContainer.Focus()
End Sub

First we need to refresh the RichTextBox to clean any draw done before:

IRTBTextContainer.Refresh()

Then we need to get the Y position of the first character of the current line:

RTBPoint = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexOfCurrentLine)

And finally we can draw the rectangle:

IRTBDrawRectangle(My.Settings.HighLightColor, 1, RTBPoint.Y, _
        IRTBTextContainer.Width - 1, IRTBTextContainer.Font.GetHeight, True)

Here is the subroutine that will paint the rectangle:

Private Sub IRTBDrawRectangle(ByVal RTBDrawColor As Color, _
          ByVal RTBPointX As Integer, ByVal RTBPointY As Integer, _
          ByVal RTBWidth As Integer, ByVal RTBHeight As Integer)
    If EnableHighLight = True Then
        Dim MyPen As New System.Drawing.Pen(RTBDrawColor)
        Dim FormGraphics As System.Drawing.Graphics
        Dim MySolidBrush As SolidBrush
        RTBHeight = RTBHeight * IRTBTextContainer.ZoomFactor + 2
        If RTBDrawColor.A > 64 Then
            MySolidBrush = New SolidBrush(Color.FromArgb(64, RTBDrawColor.R, RTBDrawColor.G, RTBDrawColor.B))
        Else
            MySolidBrush = New SolidBrush(RTBDrawColor)
        End If
        FormGraphics = IRTBTextContainer.CreateGraphics()
        FormGraphics.DrawRectangle(MyPen, RTBPointX, RTBPointY, RTBWidth, RTBHeight)
        FormGraphics.FillRectangle(MySolidBrush, RTBPointX, RTBPointY, RTBWidth, RTBHeight)
        MyPen.Dispose()
        FormGraphics.Dispose()
    End If
End Sub

The result will look like this:

IRTB Example

Figure 1. HighLight Line

Great! Now we have a rectangle painted on top of the RichTextBox and semitransparent, so we can see what it is behind! But now there are some new scenarios to be verified like these ones:

  • What happen if the user types a new letter in the RichTextBox
  • What happen if the user press the arrows
  • What happen if the user decides to zoom (Scroll+Ctrl)
  • What happen if the user press Page Up/Down
  • What happen if the user press Back/Delete/Enter

All these actions will trigger the hidden Paint event for the RichTextBox making the control to redraw and deleting the rectangle highlighting the line. To handle this we need to use some of the events we have used to paint the numbers:

  • Me.SizeChanged 
  • IRTBTextContainer.MouseWheel
  • IRTBTextContainer.SelectionChanged

And we need to use few more events:

  • IRTBTextContainer.MouseClick 
  • IRTBTextContainer.HScroll
  • IRTBTextContainer.GotFocus
  • IRTBTextContainer.KeyDown
  • IRTBTextContainer.KeyUp

Now depending on the conditions we need to call the OnPaint method, by doing this the rectangle will be drawn after the Paint event for the RichTextBox has done its work.

OnPaint(Nothing)

Since we are looking to highlight the line where the caret is currently positioned, the main event we have to check is SelectionChanged. This event will be fired every time the selection changes inside the RichTextBox so here we can decide what to do based on the actions performed by the user (Key stroke, Mouse actions, etc.)

For that we declare an enumeration where we define these values:

Enum IRTBSelectionCase As Integer
    KeysPageUpDown = 1
    KeysLeftRight = 2
    KeysSpecial = 3
    KeysNormal = 4
    Mouse = 5
End Enum

We assign these values when the KeyDown/KeyUp events are fired:

    Private Sub RTBTextContainer_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles IRTBTextContainer.KeyDown
        Dim RTBText As RichTextBox
        RTBText = Nothing
        RTBText = CType(sender, RichTextBox)
        '***Here you can implement different actions for different combination of keys
        Select Case e.KeyCode
            Case Keys.Up
                IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown
            Case Keys.Down
                IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown
            Case Keys.Left
                IRTBKeysSelect = IRTBSelectionCase.KeysLeftRight
            Case Keys.Right
                IRTBKeysSelect = IRTBSelectionCase.KeysLeftRight
            Case Keys.Next 'handles Keys.PageDown
                IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown
                IRTBForceONPaint = True
                PBNumbering.Invalidate()
            Case Keys.PageUp
                IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown
            Case Keys.PageDown 'Handles in case of Windows Vista
                IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown
            Case Keys.Enter
                IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
            Case Keys.Back
                IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
            Case Keys.Delete
                IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
                RTBGetLineCol()
                IRTBTextContainer_SelectionChanged(Nothing, Nothing)
            Case CType(CInt(e.Control = True) And Keys.V, Keys) '*******Here is how to catch CTRL+KEYS
                IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
            Case CType(CInt(e.Alt = True) And Keys.B, Keys)
                Dim LineColArray As Array
                LineColArray = Split(GetLineCol, ",")
                If MarkedLines.Contains(CInt(LineColArray.GetValue(0).ToString)) = False Then
                    MarkedLines.Add(CInt(LineColArray.GetValue(0).ToString))
                ElseIf MarkedLines.Contains(CInt(LineColArray.GetValue(0).ToString)) = True Then
                    MarkedLines.Remove(CInt(LineColArray.GetValue(0).ToString))
                End If
                RaiseEvent BMLInformation(True)
                IRTBForceONPaint = True
                PBNumbering.Invalidate()
            Case CType(CInt(e.Control) And Keys.Home, Keys)
                IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
            Case CType(CInt(e.Control) And Keys.End, Keys)
                IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
            Case Keys.ControlKey
                IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
                If IRTBForceONPaint = True Then
                    IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
                    PBNumbering.Invalidate()
                End If
            Case Keys.Escape
                '********To set the ZOOM to 1 again
                If RTBText.ZoomFactor > 1 Or RTBText.ZoomFactor < 1 Then
                    RTBText.ZoomFactor = 1
                    IRTBForceONPaint = True
                    PBNumbering.Invalidate()
                End If
            Case Else
                IRTBKeysSelect = IRTBSelectionCase.KeysNormal
        End Select
    End Sub 

But there is a special case the "Delete" key, for this key We need to catch both events, KeyUp and KeyDown. Why? Because if We don't do this the line numbers won't be updated until the key is released, or they will be updated late so there will be numbers for lines that does not exist any more.

Private Sub IRTBTextContainer_KeyUp(ByVal sender As System.Object, _
             ByVal e As System.Windows.Forms.KeyEventArgs) Handles IRTBTextContainer.KeyUp
    '***Here you can implement different actions for different combination of keys
    Select Case e.KeyCode
        Case Keys.Delete
            IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
            RTBGetLineCol()
            IRTBTextContainer_SelectionChanged(Nothing, Nothing)
    End Select
End Sub

So far We have been able to catch all key strokes We need, now We can check what should be done when the SelectionChanged event is fired:

Private Sub IRTBTextContainer_SelectionChanged(ByVal sender As System.Object, _
           ByVal e As System.EventArgs) Handles IRTBTextContainer.SelectionChanged
    Dim IRTBCurrentLine As Integer
    IRTBLineCount = IRTBTextContainer.Lines.Length
    If IRTBTextContainer.Lines.Count = 0 Then
        IRTBLineCount = 1
    End If
    Select Case IRTBKeysSelect
        Case IRTBSelectionCase.KeysPageUpDown
            RTBGetLineCol()
            IRTBForceONPaint = True
            PBNumbering.Invalidate()
        Case IRTBSelectionCase.KeysLeftRight
            IRTBCurrentLine = IRTBLineNumber
            RTBGetLineCol()
            If IRTBLineNumber <> IRTBCurrentLine Then
                IRTBForceONPaint = True
            End If
            PBNumbering.Invalidate()
        Case IRTBSelectionCase.KeysSpecial
            RTBGetLineCol()
            IRTBForceONPaint = True
            PBNumbering.Invalidate()
        Case IRTBSelectionCase.KeysNormal
            RTBGetLineCol()
        Case IRTBSelectionCase.Mouse
            RTBGetLineCol()
            PBNumbering.Invalidate()
    End Select
End Sub

As you can see there are some conditions, please let me go through and explain one by one:

IRTBSelectionCase.KeysNormal

When this option is selected it means the user is typing normal characters so the only action needed is to get the Line and Column position.

RTBSelectionCase.KeysPageUpDown or IRTBSelectionCase.KeysSpecial

In this case the keys pressed could be BACK, ENTER, DELETE, PgDOWN, PgUP, so the Line/Column and the highlight line must be updated.

RTBSelectionCase.KeysLeftRight

When this option is selected is because the user hit the Left or Right arrows so this part of the code guarantee that the correct line is highlighted and it will update the Line/Col information:

Case IRTBSelectionCase.KeysLeftRight
    IRTBCurrentLine = IRTBLineNumber
    RTBGetLineCol()
    If IRTBLineNumber <> IRTBCurrentLine Then
        IRTBForceONPaint = True
    End If
    PBNumbering.Invalidate()

The subroutine that helps to do this is called RTBGetLineCol, this one is used to get the current Line and Column, but I will go through this later since it is the third point I want to show on this article.

Note: Please noticed that there is one scenario where the numbers and the line/col information won't be updated correctly. When the Arrows (Up/Down) are held down the action will be performed but due the calculations needed to paint the numbers and the highlight line depending on the size of the file (more than 350KB) the information will be updated only after the key is released. Unfortunately I have not been able to solve this issue so far. 

Correction: Now numbers and highlight line are being updated while the arrows (Up/Down) are being held down. To do this I have changed the following instruction:

PBNumbering.Invalidate()

For this one: 

PBNumbering.Refresh()  

By now we have managed to draw the rectangle based on key strokes, mouse click, but what would happen if We scroll and the caret goes beyond the visible bounds? How can We detect when it is inside the visible bounds again? The following lines of code will do the trick:

If EnableHighLight = True Then
    '**Here it will redraw the highlight line when the caret position is coming inside the VisibleClipBounds
    Dim CaretPos As Point = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexOfCurrentLine)
    If (CaretPos.Y > -10 And CaretPos.Y < 20) Or (CaretPos.Y < IRTBTextContainer.Height + _
                30 And CaretPos.Y > IRTBTextContainer.Height - 100) Then
        OnPaint(Nothing)
    End If
End If

This code is part of the subroutine that handles the RichTextBox event VScroll and it will be used only if highlight line is enabled

Line and Column Information

Now that We have Line Numbering and Highlight working the last point is to get the current line and column, these values are calculated on this subroutine:

Private Sub RTBGetLineCol()
    Dim GetFirstCharIndex As Integer = IRTBTextContainer.GetFirstCharIndexOfCurrentLine
    Dim GetRTBLine = IRTBTextContainer.GetLineFromCharIndex(GetFirstCharIndex)
    Dim GetPosition As Integer = IRTBTextContainer.SelectionStart - GetFirstCharIndex
    If GetPosition < 0 Then GetPosition = 0
    If GetRTBLine >= IRTBTextContainer.Lines.Count Then GetRTBLine = IRTBTextContainer.Lines.Count - 1
    If GetRTBLine = -1 Then GetRTBLine = 0
    IRTBLineNumber = GetRTBLine
    IRTBColumnNumber = GetPosition
    RaiseEvent LineInformation(GetRTBLine + 1 & "," & GetPosition + 1)
End Sub

After the calculations to get the line and column are done We need to "publish" them so We raise an event that has been declared.

Public Event LineInformation(ByVal LineStatus As String)

So that is what the last line on this subroutine does, it provides with the needed information to show the line and column.

RaiseEvent LineInformation(GetRTBLine + 1 & "," & GetPosition + 1)

Here is how it looks when the information provided by this event is used on the example attached:

IRTB Example

Figure 2. Line and Column Information.

And here is the code used on IRTB Test Form to get the information:

Private Sub ImprovedRTB1_LineInformation(ByVal LineStatus As System.String) Handles Irtb1.LineInformation
    Dim LineInfo As Array
    LineInfo = Split(LineStatus, ",")
    LineColInformation.Text = "Line:" & LineInfo(0) & " Col:" & LineInfo(1)
    TextBox7.Text = LineStatus
End Sub

New! Line and Column information can be read now using the property called IRTBLineCol. To get the information the method called GetLineCol is used. For example this property can be used when a form has several tabs and change between them won't fire the event LineInformation  so the only way to get the information without waiting for the event is through this new property. 

Extras 

As part of the project I decided to add few more features. Let's start with Drag and Drop. 

Drag and Drop

I decided to implement Drag and Drop files so it won't be needed to implement the option in the project. So the code to do this is under the region called "Extras" and it is quite simple:

#Region "Extra Features" 'Drop Files
Private Sub IRTBDragDrop(ByVal sender As Object, ByVal e As _
        System.Windows.Forms.DragEventArgs) Handles IRTBTextContainer.DragDrop
    Dim myFiles() As String
    myFiles = e.Data.GetData(DataFormats.FileDrop)
    For Each mc_file In myFiles
        Dim FileName As String = mc_file.ToString
        LoadFileAndNumbering(FileName)
        RaiseEvent DragDropFileInformation(FileName)
    Next
End Sub
Private Sub IRTBDragEnter(ByVal sender As Object, ByVal e As _
            System.Windows.Forms.DragEventArgs) Handles IRTBTextContainer.DragEnter
    If e.Data.GetDataPresent(DataFormats.FileDrop) Then
        e.Effect = DragDropEffects.All
    End If
End Sub
#End Region

As you can notice there is an event called DragDropFileInformation. By using this event on the windows form We can get the name of the file, so here is the code used for that:

Private Sub DragDropnInformation(ByVal FileName As System.String) Handles Irtb1.DragDropFileInformation
    If System.IO.File.Exists(FileName) Then
        Irtb1.FileToLoad = FileName
        Dim finfo As New FileInfo(FileName)
        TextBox10.Text = finfo.Name
        TextBox6.Text = Str(Math.Round(finfo.Length / 1024)) & "Kb"
        TextBox9.Text = Irtb1.IRTBContainer.Lines.Count.ToString
        TextBox8.Text = Str(Irtb1.ShowTotalChar)
    End If
End Sub

But remember, to be able to use Drag/Drop on a control you need to set this property IRTBTextContainer.AllowDrop to TRUE. This is already done when the control is loaded.

Zoom

One more thing that needs some explanation is how to handle the zoom option when Line Numbering and HighLight Line are enabled. As all we know a common shortcut for the zoom option on the RichTextBox is CTRL+SCROLL. But how to handle this?, how the highlight line and the line numbering will be affected?

First we need to detect the scroll action and the Ctrl Key. Here is the code to detect the Mouse Wheel:

Private Sub IRTBCheckScoll(ByVal sender As Object, ByVal e As _
        System.Windows.Forms.MouseEventArgs) Handles IRTBTextContainer.MouseWheel
    If IRTBKeysSelect = IRTBSelectionCase.KeysSpecial Then
        IRTBForceONPaint = True
    End If
End Sub

And we need to detect if CTRL has been pressed, we can use the following code to do it: 

Case Keys.ControlKey
    IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
    If IRTBForceONPaint = True Then
        IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
        RTBGetLineCol()
        PBNumbering.Invalidate()
        OnPaint(Nothing)
        IRTBForceONPaint = False
    End If

There are two keys detected here, CTRL and Escape, when CTRL is detected the IRTBKeysSelect is set to IRTBSelectionCase.KeysSpecial and when the wheel action is fired the IRTBForceONPaint is set to true, by doing this We are sure that while zooming the numbers and the highlight line are refreshed. Please noticed that the bookmark icons won't be zoomed nor repainted. 

When Escape is pressed the zoom will be set to 1 and the line numbers, highlight line and Bookmark icons will be painted again. Here is the code to detect the Escape key:   

Case Keys.Escape
    '********To set the ZOOM to 1 again
    If RTBText.ZoomFactor > 1 Or RTBText.ZoomFactor < 1 Then
        RTBText.ZoomFactor = 1
        PBNumbering.Invalidate()
        RTBGetLineCol()
        OnPaint(Nothing) 
End If 

Bookmark Lines - New Feature 2013.04.02 

 This feature was not part of the original development of the IRTB, however the time goes by and while using the control in some other projects I felt this part was missing, so I decided to add it and update the article.

This new feature can be used in two ways:

  1.  By clicking on the line number.
  2. By using Alt+B key combination. If the line is not part of the marked lines array will be added otherwise will be deleted.
Let's check the code:

    Private Sub MarkLines(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs)
        Dim J As Integer
        Dim PBNumberingTemp As New PictureBox
        Dim StartLine As Integer
        Dim EndLine As Integer
        Dim GetLines As Array
        Dim CountLine As Integer = 1
        Dim LineSelected As Integer = 0
        GetLines = Split(FromLineToLine, ",")
        StartLine = CInt(GetLines.GetValue(0))
        EndLine = CInt(GetLines.GetValue(1))
        Dim font_height As Single = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(2)).Y - IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(1)).Y
        If font_height <= 0 Then font_height = IRTBTextContainer.Font.Height + 1
        PBNumberingTemp = CType(sender, PictureBox)
        PBNumberingTemp.Image = New Bitmap(PBNumberingTemp.ClientSize.Width, PBNumberingTemp.ClientSize.Height)
        Dim g As Graphics = Graphics.FromImage(PBNumberingTemp.Image)
        Dim firstIndex As Integer = IRTBTextContainer.GetCharIndexFromPosition(New Point(0, CInt(g.VisibleClipBounds.Y + font_height / 3)))
        Dim firstLineY As Integer = IRTBTextContainer.GetPositionFromCharIndex(firstIndex).Y
        Dim ClickPosition As Point
        ClickPosition.X = e.X
        ClickPosition.Y = e.Y
        Dim LineNumber As Integer = CInt(ClickPosition.Y / font_height)
        For J = StartLine To EndLine
            If CountLine = LineNumber Then
                If firstLineY < 0 Then
                    LineSelected = J + 1
                Else
                    LineSelected = J
                End If
                If MarkedLines.Contains(LineSelected) = False Then
                    MarkedLines.Add(LineSelected)
                    RaiseEvent BMLInformation(True)
                    Exit For
                ElseIf MarkedLines.Contains(LineSelected) = True Then
                    MarkedLines.Remove(LineSelected)
                    RaiseEvent BMLInformation(True)
                    Exit For
                End If
            End If
            CountLine += 1
        Next
    End Sub  

One of the main issues here was to find the way to get the line number by clicking the PictureBox holding the line numbering. It took me a little bit of time but finally I figure out (If anyone has a better way please let me know), first we need to know the font height: 

        Dim font_height As Single = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(1)).Y - IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(0)).Y
        If font_height <= 0 Then font_height = IRTBTextContainer.Font.Height + 1 

 The purpose of the first line is to find the font height value corresponding to the font used by the line numbering at the moment. The second line will correct the value if for any reason the first calculation is less or equal to 0.

Once this is done we need to calculate the  calculate where is the Y position of the first line number inside the visible clip bounds and the point where we are clicking. Once we have this we can calculate the line number but keep in mind this line number will be a value between 0 to the max line number that is shown in the visible clip Bounds. 

        PBNumberingTemp = CType(sender, PictureBox)
        PBNumberingTemp.Image = New Bitmap(PBNumberingTemp.ClientSize.Width, PBNumberingTemp.ClientSize.Height)
        Dim g As Graphics = Graphics.FromImage(PBNumberingTemp.Image)
        Dim firstIndex As Integer = IRTBTextContainer.GetCharIndexFromPosition(New Point(0, CInt(g.VisibleClipBounds.Y + font_height / 3)))
        Dim firstLineY As Integer = IRTBTextContainer.GetPositionFromCharIndex(firstIndex).Y
        Dim ClickPosition As Point
        ClickPosition.X = e.X
        ClickPosition.Y = e.Y
        Dim LineNumber As Integer = CInt(ClickPosition.Y / font_height) 

 So how can we get the correct line number? Using these lines:

        GetLines = Split(FromLineToLine, ",")
        StartLine = CInt(GetLines.GetValue(0))
        EndLine = CInt(GetLines.GetValue(1)) 

 These lines give me the Start and End line number shown on the picture box, so now we can calculate the real line we have clicked:

For J = StartLine To EndLine
            If CountLine = LineNumber Then
                If firstLineY < 0 Then
                    LineSelected = J + 1
                Else
                    LineSelected = J
                End If
                If MarkedLines.Contains(LineSelected) = False Then
                    MarkedLines.Add(LineSelected)
                    RaiseEvent BMLInformation(True)
                    Exit For
                ElseIf MarkedLines.Contains(LineSelected) = True Then
                    MarkedLines.Remove(LineSelected)
                    RaiseEvent BMLInformation(True)
                    Exit For
                End If
            End If
            CountLine += 1
        Next 

 MarkedLines is an ArrayList to save the lines that are bookmarked, if the line number is not in the collection then it will be added otherwise is removed, this Collection can be accessed using the property called IRTBMarkedLines. 

 The last part of this procedure is the new event BMLInformation that will be raised to inform that new lines are added or removed. 

 So now how can we bookmark the line using ALT+B? Adding the following lines to the method RTBTextContainer 

            Case CType(CInt(e.Alt = True) And Keys.B, Keys)
                Dim LineColArray As Array
                LineColArray = Split(GetLineCol, ",")
                If MarkedLines.Contains(CInt(LineColArray.GetValue(0).ToString)) = False Then
                    MarkedLines.Add(CInt(LineColArray.GetValue(0).ToString))
                ElseIf MarkedLines.Contains(CInt(LineColArray.GetValue(0).ToString)) = True Then
                    MarkedLines.Remove(CInt(LineColArray.GetValue(0).ToString))
                End If
                RaiseEvent BMLInformation(True)
                IRTBForceONPaint = True
                PBNumbering.Invalidate() 

Using the GetLineCol function we can get the line where the caret is at the moment, so just pressing ALT+B will add or remove the line from the MarkedLines collection and the new event BMLInformation will be raised to inform that new lines are added. 

I have added one more option for this feature, a list of the bookmarked lines can be retrieved using the following property: 

  • IRTBMarkedLines 

 Once you have the collection of lines you can use the following method to jump to the line in the IRTB control:  

  •  GoToBookmark(linenumber) 
Here is the code of this method:

    Public Sub GoToBookmark(ByVal LineNumber As Integer)
        Dim FirstCHRInLine As Integer = IRTBTextContainer.GetFirstCharIndexFromLine(LineNumber - 1)
        IRTBContainer.SelectionStart = FirstCHRInLine
        IRTBContainer.SelectionLength = 0
        IRTBContainer.ScrollToCaret()
        IRTBKeysSelect = 3
        IRTBForceONPaint = True
        PBNumbering.Refresh()
    End Sub   

 In the IRTB_Demo.zip you can find how to use this option. 

XML Parser 

This option was not written by me, I just added as an extra feature. All code comes from MSDN and it can be found here:

http://code.msdn.microsoft.com/windowsdesktop/VBRichTextBoxSyntaxHighligh-2d18b6cc

Please noticed that this portion of the code is subject to the Microsoft Public License. Check here:

http://www.microsoft.com/opensource/licenses.mspx#Ms-PL  

It works quite fine as far I have seen. So if you want further explanations about How an XML parser works google for "XML Parser" and you will get a lot of information, including some links to articles here in CodeProject like this one: 

http://www.codeproject.com/Articles/176236/Parsing-an-XML-file-in-a-C-C-program  

How to use it? Just drop a button on  your form and then some lines of code like these ones in your project:

     Private Sub IRTBXMLParser_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBXMLParser.Click
        Irtb1.Process(True)
    End Sub  

 Here Irtb1  is the name of IRTB control on FORM1. 

Conclusion

The three points mentioned at the beginning of the article have been presented and I hope the information is good enough to give you some ideas. Unfortunately, and you will noticed this, the performance of the RichTextBox is being impacted when Line Numbering and/or High Light Line is enabled. Why is happening this? The principal reason lies in the use of these lines of code:

Dim CaretPos As Point = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexOfCurrentLine)
                                    ...
                                    ...
Dim firstIndex As Integer = IRTBTextContainer.GetCharIndexFromPosition(New Point(0, g.VisibleClipBounds.Y + font_height / 3))
Dim firstLine As Integer = IRTBTextContainer.GetLineFromCharIndex(firstIndex)
Dim firstLineY As Integer = IRTBTextContainer.GetPositionFromCharIndex(firstIndex).Y 

These calculations are really CPU intensive when the size of the file increases, therefore the performance of the control is affected by this. As I have read in other articles, and I do agree, if you need a high performance control you have two options, built your own control from scratch (check this project in Code Project http://www.codeproject.com/Articles/161871/Fast-Colored-TextBox-for-syntax-highlighting)  or try scintilla

Regarding the actions where the numbers and the highlight line need to be painted again handling the keys is not always easy but all combinations can be detected so we can select what actions should be performed.

Now let's take a look on How to use the control 

Using the Control

There are two zip files on this article:

  • IRTB_CodeProject.zip 
  • IRTB_Demo_new.zip 

To check what can be done with the IRTB control, download IRTB_Demo_new.zip, unzipped any place you want and then double click on IRTB_Test.exe.

The IRTB_CodeProject.zip contains the solution, unzip the file and open the solution. I have added two projects so when the file is open you can see the following:

IRTB Example

Figure 3. Solution opened on Visual Studio

The one called IRTB is the control and the one called IRTB_Test is a Windows Form to check the basic functionality of the control.

The properties exposed by the IRTB control are:

  • IRTBFileToLoad
  • IRTBFont
  • IRTBLNFontColor 
  • IRTBHighLightColor
  • ShowTotalChar  New Name IRTBShowTotalChar 
  • IRTBEnableNumbering
  • IRTBEnableHighLight 
  • RTBContainer
  • RTBnumbering
  • IRTBPrefix 
  • IRTBAlignNumbers 
There are 2 new properties (2013.04.02): 
  • IRTBMarkedLines >> It contains the Bookmarked lines, so the information can be read and used by the application to jump to the marked lines. See GoToBookmark method 
  • IRTBLineCol >>  Line and Column information are provided by an event called LineInformation, but if the application needs to get this information without wait for the event to be raised it can be done through this property. 

IRTB Example

Figure 4. IRTB Properties

The purpose adding the Windows Form was to show what can be done with the control, please check the code and I hope is clear enough and shows what can be done with the IRTB control.

If you are going to use the IRTB control on your personal project don't forget to add the DLL on the reference tab, otherwise you will no be able to use it. Noticed that the new version is 2.5.0.0 

IRTB Example

Figure 5. Add IRTB to the Reference tab

Here is the code used on the IRTB_Test Windows Form, I have added the new options (2013.04.02) to the form. 

Imports System.IO
Public Class Form1
    Private Sub ImprovedRTB1_LineInformation(ByVal LineStatus As System.String) Handles Irtb1.LineInformation
        Dim LineInfo As Array
        LineInfo = Split(LineStatus, ",")
        LineColInformation.Text = "Line:" & LineInfo(0) & " Col:" & LineInfo(1)
        TextBox7.Text = LineStatus
    End Sub
    Private Sub DragDropnInformation(ByVal FileName As System.String) Handles Irtb1.DragDropFileInformation
        If System.IO.File.Exists(FileName) Then
            Irtb1.IRTBMarkedLines.Clear()
            BMLinesTool.Items.Clear()
            Irtb1.IRTBFileToLoad = FileName
            Dim finfo As New FileInfo(FileName)
            TextBox10.Text = finfo.Name
            TextBox6.Text = Str(Math.Round(finfo.Length / 1024)) & "Kb"
            TextBox9.Text = Irtb1.IRTBContainer.Lines.Count.ToString
            TextBox8.Text = Str(Irtb1.IRTBShowTotalChar)
        End If
    End Sub
    Private Sub IRTBCheckLN_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBCheckLN.CheckedChanged
        Irtb1.IRTBEnableNumbering = IRTBCheckLN.Checked
    End Sub

    Private Sub IRTBCheckHL_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBCheckHL.CheckedChanged
        Irtb1.IRTBEnableHighLight = IRTBCheckHL.Checked
    End Sub

    Private Sub IRTBFont_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBFont.Click
        FontDialog1.ShowDialog()
        Irtb1.IRTBFont = FontDialog1.Font
    End Sub

    Private Sub IRTBHLColor_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBHLColor.Click
        SelectColor.Color = Irtb1.IRTBHighLightColor
        SelectColor.ShowDialog()
        Irtb1.IRTBHighLightColor = SelectColor.Color
    End Sub

    Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click
        TestLoadFile.ShowDialog()
        If System.IO.File.Exists(TestLoadFile.FileName) Then
            Irtb1.IRTBFileToLoad = TestLoadFile.FileName
            Irtb1.IRTBMarkedLines.Clear()
            BMLinesTool.Items.Clear()
            Dim finfo As New FileInfo(TestLoadFile.FileName)
            TextBox10.Text = finfo.Name
            TextBox6.Text = Str(Math.Round(finfo.Length / 1024)) & "Kb"
            TextBox9.Text = Irtb1.IRTBContainer.Lines.Count.ToString
            TextBox8.Text = Str(Irtb1.IRTBShowTotalChar)
        End If
    End Sub

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        IRTBCheckLN.Checked = Irtb1.IRTBEnableNumbering
        IRTBCheckHL.Checked = Irtb1.IRTBEnableHighLight
        BMLinesTool.Items.Clear()
        Irtb1.IRTBPrefix = False
        RadioButton1.Checked = True
    End Sub

    Private Sub RadioButton1_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton1.CheckedChanged
        Irtb1.IRTBAlignNumbers = IRTB.IRTB.IRTBAlignPos.Left
    End Sub

    Private Sub RadioButton2_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton2.CheckedChanged
        Irtb1.IRTBAlignNumbers = IRTB.IRTB.IRTBAlignPos.Right
    End Sub

    Private Sub CheckBox1_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CheckBox1.CheckedChanged
        Irtb1.IRTBPrefix = CheckBox1.Checked
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        SelectColor.Color = Irtb1.IRTBLNFontColor
        SelectColor.ShowDialog()
        Irtb1.IRTBLNFontColor = SelectColor.Color
    End Sub

    Private Sub IRTBXMLParser_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBXMLParser.Click
        Irtb1.Process(True)
    End Sub

    Private Sub Irtb1_BMLInformation(ByVal NewEvent As System.Boolean) Handles Irtb1.BMLInformation
        BMLinesTool.Items.Clear()
        For Each BMLineTemp In Irtb1.IRTBMarkedLines
            BMLinesTool.Items.Add("Line " & BMLineTemp)
        Next
    End Sub

    Private Sub BMLinesTool_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BMLinesTool.SelectedIndexChanged
        If Not BMLinesTool.SelectedItem Is Nothing Then
            Dim LineNumber As Integer = CInt(Replace(BMLinesTool.SelectedItem.ToString, "Line ", ""))
            Irtb1.GoToBookmark(LineNumber)
        End If
    End Sub
End Class

 To show the new features I have added two components to the original demo, the first one is a button called "Check XML" to do the XML parsing. Please notice that if the file is not XML formatted the process will throw an error, this can be caught to show a Messagebox with the error information. To do this here is the coded used:  

    Private Sub IRTBXMLParser_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBXMLParser.Click
        Try
            Irtb1.Process(True)
        Catch appException As ApplicationException
            MessageBox.Show(appException.Message, "ApplicationException")
        Catch ex As Exception
            MessageBox.Show(ex.Message, "Exception")
        End Try
    End Sub  

 The second one is a ListBox called BMLinesTool. This listbox will show the bookmarked lines, by clicking on any of the items in the list the method GoToBookmark will be used and the line will be highlighted. 

    Private Sub BMLinesTool_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BMLinesTool.SelectedIndexChanged
        If Not BMLinesTool.SelectedItem Is Nothing Then
            Dim LineNumber As Integer = CInt(Replace(BMLinesTool.SelectedItem.ToString, "Line ", ""))
            Irtb1.GoToBookmark(LineNumber)
        End If
    End Sub 

 Please review the project, check the code and if you have any questions let me know. I will try to answer them in the best possible way.

History

  • February 1st 2012  
  • February 21st 2012
    • Small corrections based on the comments received. 
  • April 3rd 2013    
    • New features added. Bookmark Line and XML Parser. 
    • ShowTotalChar  property name is changed for IRTBShowTotalChar  

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here