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 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
y = firstLineY + 2 + font_height * (i - firstLine - 1) LineNumberingString = Format(i, PrefixFormat) 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 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:
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)
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 IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown
IRTBForceONPaint = True
PBNumbering.Invalidate()
Case Keys.PageUp
IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown
Case Keys.PageDown 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) 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
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
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
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:
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" 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
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:
- By clicking on the line number.
- 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:
Once you have the collection of lines you can use the following method to jump to the line in the IRTB control:
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:
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.
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
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