Click here to Skip to main content
15,867,308 members
Articles / Multimedia / GDI+

LineNumbers for the RichTextBox

Rate me:
Please Sign up or sign in to vote.
4.94/5 (41 votes)
13 Apr 2019BSD9 min read 202.7K   3.5K   110   73
LineNumbers that dock to a RichTextBox or show as an overlay on top of it

Screenshot - linenumbers_for_rtb_examples.jpg

Introduction

Although there are already LineNumbering controls around, I decided to code one that gives the user a lot of freedom to create an individual look, whilst still handling the RichTextBox's dynamic content correctly. There is also a SeeThroughMode that allows the LineNumbers to be displayed as an overlay on top of the RichTextBox itself. Word wrapping and differences in line heights are all properly considered and, as the control only paints LineNumberItems for the text lines that are visible, the painting speed remains high even for large pieces of text with complex layout.

Screenshot - linenumbers_for_rtb_overlay.jpg

Using the Code

The download ZIP file contains the VB.NET solution folder. All of the code for the LineNumbers control is in the LineNumbers_For_RichTextBox class-code. Use the Solution Explorer to find it once you've opened the LineNumbers.sln project file. Make sure you build the project before opening the form or you'll get an error message. If that happens, close the Form's design tab and rebuild the solution. All of the code is provided "as is," with no rights nor liabilities attached. This means that you can use and change it as you please, at your own risk.

Copy the class-code into your project, and build or rebuild your application/solution. The LineNumbers control should then be available in your Toolbox. Once you've added the LineNumbers control to your form, you'll notice that it displays a vertical reminder message: you need to set the ParentRichTextBox property first so that it knows which RTB to show the LineNumbers for. Once it's set, the LineNumbers control will dock to the left side of the RTB -- controlled by the DockSide property -- and will either start showing the line numbers if there is already text in the RTB, or another reminder that shows which RTB it is connected to.

Available Properties

You can use the following elements to customize the look of the LineNumbers. All lines can have their Color, LineStyle (dot, dash, solid, etc.) and LineThickness changed. This LineNumbers control inherits from the basic Control class, so a BackgroundImage can also be set.

BorderLines

Element that defines the border around the whole control.

GridLines

Element that defines a horizontal divider line across the top of each LineNumber's item-area.

MarginLines

Element that defines a border line that can appear on the left, right, or both vertical sides of the control.

BackgroundGradient

Each LineNumber's item-area can have a gradient that softly blends two colors, named alpha and beta color in the public properties and Start/EndColor in the code. All colors can be transparent and you can also specify the gradient's direction, i.e., horizontal, vertical, forward/backward-diagonal. It's drawn via a Drawing2D.LinearGradientBrush. See the code snippet below:

VB
' --- BackgroundGradient
If zGradient_Show = True Then
   zLGB = New Drawing2D.LinearGradientBrush(zLNIs(zA).Rectangle, _
              zGradient_StartColor, zGradient_EndColor, zGradient_Direction)
   e.Graphics.FillRectangle(zLGB, zLNIs(zA).Rectangle)
End If

LineNumbers

The LineNumbers' color and font is set via the normal ForeColor and Font properties, but there are also extra properties available to change their look and behavior:

  • LineNrs_Alignment: by which you can set the alignment point (TopLeft, TopCenter, TopRight, ...) for the LineNumber so that the number is drawn relative to that corner/center-point of its item-area. This is the same as the TextAlign property on a regular Label.
  • LineNrs_LeadingZeroes: pads the LineNumber with leading zeroes, based on the total amount of text lines in the RichTextBox.
  • LineNrs_AsHexadecimal: shows the LineNumbers as hexadecimal values (i.e., no leading zeroes in that case).
  • LineNrs_Anti-Alias: some fonts look better when the edges of the text-characters are slightly blended with the background. However, other fonts may look crisper without that softening, especially small pixel fonts.
  • LineNrs_ClippedByItemRectangle: if the LineNumbers are using a large font, they may spill out of their own item-area. This can sometimes give cool effects in combination with a partially transparent BackgroundGradient. This option allows you to clip the LineNumbers so that they only appear inside their own area.
  • LineNrs_Offset: although the alignment will take care of the LineNumber placement, this property allows you to manually fine-tune the LineNumber's position. Use negative values for offsets towards the TopLeft, and positive values to shift the position of the LineNumbers towards the BottomRight.

LineNumbers_For_RichTextBox

The behavior of the LineNumbers_For_RichTextBox control is governed by these properties:

  • ParentRichTextBox: This needs to be set first, as it allows you to point to the RichTextBox control for which the LineNumbers will be displayed. In design mode, a vertical reminder message will show up when the parent RTB is not set, or when the RTB has no text in it yet.
  • _SeeThroughMode_: The LineNumbers control can either be displayed next to its parent RichTextBox, or it can be displayed as an overlay on top of the RTB. The empty parts of the LineNumbers are then both see-through and click-through, so you can still use the RTB underneath.
  • AutoSizing: When active, auto-sizing will automatically adjust the width and position of the LineNumbers control as needed in order to make sure that the LineNumbers remain visible.
  • DockSide: You can use this to dock the LineNumbers to the left or right side of the parent RTB, or to lock the height to that of the RTB. When set to none, you can position the LineNumbers control freely like any other. The standard Dock will override the DockSide behavior, though.

Points of Interest

Although this is a pretty straightforward control designed to do just one thing, there were a few problems that needed some attention to get the control working at a good speed. The central Sub, which is Update_VisibleLineNumberItems() takes care of several of them. The rest of the work is mostly being done by the overridden OnPaint sub.

Lining Up the LineNumbers and RTB Text Lines

The RichTextBox has an easy GetPositionFromCharIndex() method that computes the position of a given text character -- identified by its index within the full text -- but that position point is in client-coordinates. So, at the start of the Update_VisibleLineNumberItems(), you can see some conversions to screen coordinates and back, to determine where the RTB's (0,0) origin point is in the LineNumbers control. Also, there is an additional check to find the control's (0,0) origin point within the parent RTB because the LineNumber control's Top may be positioned lower on the form than the RTB. That would make a difference in the computation of which text lines should get a LineNumberItem drawn for them, as only visible LineNumberItems should matter, to keep things speedy. The Update_VisibleLineNumberItems() sub basically builds a list, named zLNIs, of only the visible LineNumberItems. Each LineNumberItem (which is a Structure Update B: this is now a nested class) holds a LineNumber and a rectangle that marks the LineNumber's item-area.

WordWrapping and LineHeight

The main problem was the fact that when word wrapping splits a text line into multiple lines, those new text lines spill into the RichTextBox's Lines collection -- this happens on a regular TextBox, as well -- without actually adding items to the collection. For example, an RTB with 5 real text lines and word wrapping disabled will have a correct Lines collection of 5 items where each item is a real text line. But when word wrapping is enabled and happens to wrap the first real text line into 2 lines, then the Lines collection will still have 5 items, but item2 will be the word-wrapped second half of the first real text line. To counter that peculiar behavior, the LineNumbers control needs to create its own Lines collection, one that isn't affected by the word wrapping and the real text lines. This is the zSplit list of strings in the Update_VisibleLineNumberItems() sub. The line-height (i.e., the height of the LineNumberItem's rectangle) will be computed by comparing the Y-coordinate of each real text line with that of the next real line. The GetPositionFromCharIndex() method will give us the Y-coordinates, but the char index of the first character of each visible text line needs to be known.

Computing which LineNumbers are Visible

The control needs to find out which text lines in the RTB need to have a LineNumberItem drawn for them. Only visible items should be drawn to keep the painting speed high. The initial value of the zStartIndex variable, which is the char index of the first (fully or partially) visible text character will be computed by the FindStartIndex() sub. It's a recursive sub (i.e., one that calls itself) that basically looks for a text character that has a Y-coordinate closest to 0 or closest to the target value. The code comments will explain how it's done exactly.

The Painting of the LineNumbers (Just the Numbers)

Here's a code-snippet that shows the painting of the LineNumbers in the overridden OnPaint sub. The large TextAlignment computations that determine zPoint are left out, though. You can see how the text clipping is done by using the Graphics.SetClip method to temporarily restrict the drawing area. Also notice that a rectangle, zItemClipRectangle, based on the LineNumber's text-dimensions (clipped or not) is added to the zGP_LineNumbers object. This is a GraphicsPath object that will be used in SeeThroughMode. More on that is to be found in the next article section.

VB
' --- LineNumbers
If zLineNumbers_Show = True Then
    '   TextFormatting
    If zLineNumbers_ShowLeadingZeroes = True Then
        zTextToShow = IIf(zLineNumbers_ShowAsHexadecimal, _
            zLNIs(zA).LineNumber.ToString("X"), _
            zLNIs(zA).LineNumber.ToString(zLineNumbers_Format))
    Else
        zTextToShow = IIf(zLineNumbers_ShowAsHexadecimal, _
            zLNIs(zA).LineNumber.ToString("X"), _
            zLNIs(zA).LineNumber.ToString)
    End If
    '   TextSizing
    zTextSize = e.Graphics.MeasureString(zTextToShow, Me.Font, zPoint, zSF)

    ' ==TextAlignment computation here (large Select Case to build zPoint)==
    
    '   TextClipping
    zItemClipRectangle = New Rectangle(zPoint, zTextSize.ToSize)
    If zLineNumbers_ClipByItemRectangle = True Then
        '   If selected, the text will be clipped so that it doesn't spill out
        '   of its own LineNumberItem-area. Only the part of the text inside 
        '   the LineNumberItem.Rectangle should be visible, so intersect with 
        '   the ItemRectangle.
        '   The SetClip method temporary restricts the drawing area of the 
        '   control for whatever is drawn next.
        zItemClipRectangle.Intersect(zLNIs(zA).Rectangle)
        e.Graphics.SetClip(zItemClipRectangle)
    End If
   
    '   TextDrawing
    e.Graphics.DrawString(zTextToShow, Me.Font, zBrush, zPoint, zSF)
    e.Graphics.ResetClip()   
   
    '   The GraphicsPath for the LineNumber is just a rectangle behind the 
    '   text, to keep the paintingspeed high and avoid ugly artifacts.
    zGP_LineNumbers.AddRectangle(zItemClipRectangle)
    zGP_LineNumbers.CloseFigure()
End If

SeeThroughMode

I can imagine people being interested in this, as it's a little more advanced than the simple painting of lines and rectangles. So, here's some information on how it's done: it works by using a Drawing2D.GraphicsPath object, which is similar to the more regularly used Graphics type. However, when you paint something on a GraphicsPath, you're basically painting which pixels will be see-through or not when that GraphicsPath -- or a combination of several GraphicsPaths, in this case -- is set as the Region of the control. In other words, you're creating a custom outline for the control so that you can make the control any shape you like, even with holes in it if needed.

I'm doing the painting on the GraphicsPaths at the same time as the regular painting in the overridden OnPaint sub. This is because the lines and rectangle figures are being computed anyway, so I might as well use them twice. The code-snippet below shows this clearly: the same border lines that are drawn on the regular Graphics (e.Graphics.DrawLines ...) are also drawn onto a GraphicsPath (zGP_BorderLines.AddLines...):

VB
Dim zGP_BorderLines As New Drawing2D.GraphicsPath(Drawing2D.FillMode.Winding)

Dim zP_Left As New Point(Math.Floor(zBorderLines_Thickness / 2), _
    Math.Floor(zBorderLines_Thickness / 2))
Dim zP_Right As New Point(
    Me.Width - Math.Ceiling(zBorderLines_Thickness / 2), _
    Me.Height - Math.Ceiling(zBorderLines_Thickness / 2))

' --- BorderLines 
Dim zBorderLines_Points() As Point = { _
    New Point(zP_Left.X, zP_Left.Y), _
    New Point(zP_Right.X, zP_Left.Y), _
    New Point(zP_Right.X, zP_Right.Y), _
    New Point(zP_Left.X, zP_Right.Y), _
    New Point(zP_Left.X, zP_Left.Y)}
If zBorderLines_Show = True Then
   zPen = New Pen(zBorderLines_Color, zBorderLines_Thickness)
   zPen.DashStyle = zBorderLines_Style
   e.Graphics.DrawLines(zPen, zBorderLines_Points)

   '   And the same shape is added to the border's GraphicsPath
   zGP_BorderLines.AddLines(zBorderLines_Points)
   zGP_BorderLines.CloseFigure()

   '   BorderThickness and Style for SeeThroughMode
   zPen.DashStyle = Drawing2D.DashStyle.Solid
   zGP_BorderLines.Widen(zPen)
End If

At the end of the OnPaint sub, the control simply checks whether zSeeThroughMode is active. If it is, then the different GraphicsPaths (named zGP_...) are combined and form the control's Region after an extra check is done, to make sure the control won't be empty:

VB
' --- SeeThroughMode
'   combine all the GraphicsPaths (= zGP_... ) and set them as the Region 
If zSeeThroughMode = True Then
    zRegion.MakeEmpty()
    zRegion.Union(zGP_BorderLines)
    zRegion.Union(zGP_MarginLines)
    zRegion.Union(zGP_GridLines)
    zRegion.Union(zGP_LineNumbers)
End If

' --- Region
If zRegion.GetBounds(e.Graphics).IsEmpty = True Then
    '   Note: If the control is in a condition that would show it as empty, 
    '   then a border-region is still drawn regardless of it's borders' 
    '   on/off state. This is added to make sure that the bounds of the 
    '   control are never lost (it would remain empty if this was not done).
    zGP_BorderLines.AddLines(zBorderLines_Points)
    zGP_BorderLines.CloseFigure()
    zPen = New Pen(zBorderLines_Color, 1)
    zPen.DashStyle = Drawing2D.DashStyle.Solid
    zGP_BorderLines.Widen(zPen)

    zRegion = New Region(zGP_BorderLines)
End If
Me.Region = zRegion

Updates

Fixed:

  • (A) When the first LineNumberItem had a negative Y-coordinate, the bottom line of the rectangle for the GridLines' GraphicsPath would show inside the control. Offsetting by -zLNIs(0).Rectangle.Y has fixed this.

Improved:

  • (B)Performance has been doubled by increasing the efficiency of the Update_VisibleLineNumberItems() method. This was achieved by halving the number of calls to the RTB's .GetPositionFromChar() method, which becomes slower as the number of text lines grows.
  • (B) Scrolling of large documents now has a time-based cutoff for computing LineNumberItems so that scrolling remains smooth.

The End

That's it, I hope you like this LineNumbers_For_RichTextBox control and find it useful in your own projects. Enjoy! 

History

  • 31st May, 2007: Article edited and moved to the main CodeProject.com article base
  • 12th April, 2007: Updated
  • 5th April, 2007: Original version posted

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Belgium Belgium
MSDN VB.NET Forums

Comments and Discussions

 
QuestionWhen the input Chinese, there are problems, Enter a word, there will be two Pin
sezooka27-Feb-20 19:13
sezooka27-Feb-20 19:13 
QuestionJust downloaded it about 40 mins ago and have some sugguestions... Pin
i0011-Feb-20 20:00
i0011-Feb-20 20:00 
QuestionUtterly SLOW when there are more then 50 lines Pin
SolarNigerija1-Apr-19 23:14
SolarNigerija1-Apr-19 23:14 
AnswerRe: Utterly SLOW when there are more then 50 lines Pin
nogChoco10-Apr-19 10:41
nogChoco10-Apr-19 10:41 
Praisevisual studio community 2015 compatibility? Pin
Member 1265263125-Jul-16 12:58
Member 1265263125-Jul-16 12:58 
QuestionThank you, for sharing Pin
Juan Del Cid13-Sep-15 9:34
Juan Del Cid13-Sep-15 9:34 
QuestionAwesome Pin
Yanick Lafontaine22-Feb-13 7:21
Yanick Lafontaine22-Feb-13 7:21 
AnswerRe: Awesome Pin
nogChoco4-Mar-13 7:25
nogChoco4-Mar-13 7:25 
SuggestionExcellent work, but PLEASE release under different licence Pin
Member 973382010-Jan-13 16:50
Member 973382010-Jan-13 16:50 
GeneralRe: Excellent work, but PLEASE release under different licence Pin
nogChoco13-Apr-19 3:36
nogChoco13-Apr-19 3:36 
GeneralMy vote of 5 Pin
ZangetsuZ30-Dec-12 8:55
ZangetsuZ30-Dec-12 8:55 
GeneralRe: My vote of 5 Pin
nogChoco8-Jan-13 5:04
nogChoco8-Jan-13 5:04 
QuestionVery good and practicable... Pin
dherrmann29-Oct-12 23:48
dherrmann29-Oct-12 23:48 
AnswerRe: Very good and practicable... Pin
nogChoco6-Nov-12 2:29
nogChoco6-Nov-12 2:29 
QuestionGreat work Pin
nguyenphuphi6-Dec-11 11:43
nguyenphuphi6-Dec-11 11:43 
AnswerRe: Great work Pin
nogChoco3-Jan-12 8:01
nogChoco3-Jan-12 8:01 
QuestionLooks great... Pin
Nosferatu The Prince of Darkness11-Apr-10 7:09
Nosferatu The Prince of Darkness11-Apr-10 7:09 
AnswerRe: Looks great... Pin
nogChoco15-Apr-10 15:06
nogChoco15-Apr-10 15:06 
GeneralRe: Looks great... Pin
Nosferatu The Prince of Darkness17-Apr-10 15:11
Nosferatu The Prince of Darkness17-Apr-10 15:11 
GeneralRe: Looks great... Pin
nogChoco18-Apr-10 4:25
nogChoco18-Apr-10 4:25 
Ah, yes, that looks like a copy/paste bug Blush | :O

I never worked with classes or graphics in VBA (as the output in AutoCAD was graphical anyway), so I don't think I can be much help to you, to be honest.

In VB.NET, the RTB gives the location for each line and each character's top-left corner. If the VB6 one does that too, then you should be able to do what I did, although probably with less fancy graphics as .NET uses GDI+, and perhaps the drawing and RTB's location-polling speeds may not be as fast.

The quirky .Lines array meant I couldn't just trust the position of the .Lines and had to find the first character of each real line myself. Looking back at the code, it's probably not that big a deal though it likely took me the most thought (counting linebreaks to find the right line instead of just using .Lines).

In essence, the RTB and LineNumbers are only linked by computing where the .Y location of the RTB's first character is, in the LineNumbering control. I compute that by converting the RTB's coordinates to screencoordinates and then to the LineNRs coordinates, but as I said: the coordinate system is different in VB6 so that will probably need rethinking - it's possible that you don't even have to convert and can just use the same .Y height in both RTB and LineNRs.

After you have that .Y offset, it's just a matter of getting the position of each visible RTB line's first character and drawing a LineNR on the same height.
GeneralGreat Work Pin
Anthony Daly10-Aug-09 8:22
Anthony Daly10-Aug-09 8:22 
GeneralRe: Great Work Pin
nogChoco13-Aug-09 11:07
nogChoco13-Aug-09 11:07 
GeneralAnother "great job" comment :o) Pin
RobW28-Jul-09 1:26
RobW28-Jul-09 1:26 
GeneralRe: Another "great job" comment :o) Pin
nogChoco13-Aug-09 11:06
nogChoco13-Aug-09 11:06 
QuestionC# Equivalent? Pin
Caglow12-Oct-08 6:49
Caglow12-Oct-08 6:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.