Click here to Skip to main content
12,816,775 members (35,178 online)
Click here to Skip to main content
Add your own
alternative version


103 bookmarked
Posted 5 Apr 2007

LineNumbers for the RichTextBox

, 31 May 2007 CPOL
Rate this:
Please Sign up or sign in to vote.
LineNumbers that dock to a RichTextBox or show as an overlay on top of it.
Screenshot - linenumbers_for_rtb_examples.jpg


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.


Element that defiones the border around the whole control.


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


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


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 code snippet below:

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


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.


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

' --- LineNumbers
If zLineNumbers_Show = True Then
    '   TextFormatting
    If zLineNumbers_ShowLeadingZeroes = True Then
        zTextToShow = IIf(zLineNumbers_ShowAsHexadecimal, _
            zLNIs(zA).LineNumber.ToString("X"), _
        zTextToShow = IIf(zLineNumbers_ShowAsHexadecimal, _
            zLNIs(zA).LineNumber.ToString("X"), _
    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.
    End If
    '   TextDrawing
    e.Graphics.DrawString(zTextToShow, Me.Font, zBrush, zPoint, zSF)
    '   The GraphicsPath for the LineNumber is just a rectangle behind the 
    '   text, to keep the paintingspeed high and avoid ugly artifacts.
End If


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

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

   '   BorderThickness and Style for SeeThroughMode
   zPen.DashStyle = Drawing2D.DashStyle.Solid
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:

' --- SeeThroughMode
'   combine all the GraphicsPaths (= zGP_... ) and set them as the Region 
If zSeeThroughMode = True Then
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).
    zPen = New Pen(zBorderLines_Color, 1)
    zPen.DashStyle = Drawing2D.DashStyle.Solid

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



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


  • (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! --- nogChoco


  • 31 May, 2007 -- Article edited and moved to the main article base
  • 12 April, 2007 -- Updated
  • 5 April, 2007 -- Original version posted


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


About the Author

Belgium Belgium

You may also be interested in...

Comments and Discussions

GeneralRe: C# Equivalent? Pin
Member 441891016-Nov-10 4:57
memberMember 441891016-Nov-10 4:57 
GeneralRe: C# Equivalent? Pin
antgraf30-Oct-11 10:18
memberantgraf30-Oct-11 10:18 
AnswerRe: C# Equivalent? Pin
Damian J. Suess3-Jun-13 12:40
memberDamian J. Suess3-Jun-13 12:40 
AnswerRe: C# Equivalent? Pin
Heriberto Lugo14-Aug-15 7:32
memberHeriberto Lugo14-Aug-15 7:32 
GeneralI don't understand the Part beginning "Using the Code" [modified] Pin
RogerBerglen20-Aug-08 8:08
memberRogerBerglen20-Aug-08 8:08 
GeneralRe: I don't understand the Part beginning "Using the Code" Pin
nogChoco21-Aug-08 19:00
membernogChoco21-Aug-08 19:00 
GeneralRe: I don't understand the Part beginning "Using the Code" Pin
RogerBerglen22-Aug-08 2:00
memberRogerBerglen22-Aug-08 2:00 
GeneralRe: I don't understand the Part beginning "Using the Code" Pin
nogChoco22-Aug-08 10:43
membernogChoco22-Aug-08 10:43 
It seems to be a combination of 2 problems with my control: the width wasn't being set properly (so width=0 and that made it lose its region, I think), and there is also an initial sizing that was forgotten - my control wasn't resizing until there was a contentresize on the parent RichTextBox.

For the width problem, simply put a "Me.Width = 1" line in the New() constructor so that it looks like this:
Public Sub New()
        With Me
            .SetStyle(ControlStyles.OptimizedDoubleBuffer, True)
            .SetStyle(ControlStyles.ResizeRedraw, True)
            .SetStyle(ControlStyles.SupportsTransparentBackColor, True)
            .SetStyle(ControlStyles.UserPaint, True)
            .SetStyle(ControlStyles.AllPaintingInWmPaint, True)
            .Margin = New Padding(0)
            .Padding = New Padding(0, 0, 2, 0)
        End With
        With zTimer
            .Enabled = True
            .Interval = 200
        End With
        Me.Width = 1
    End Sub

And for the intial sizing problem, add a "zContentRectangle = zParent.ClientRectangle" line to the ParentRichTextBox property setter, so that it looks like this:
Public Property ParentRichTextBox() As RichTextBox
            Return zParent
        End Get
        Set(ByVal value As RichTextBox)
            zParent = value
            If zParent IsNot Nothing Then
                Me.Parent = zParent.Parent
                zContentRectangle = zParent.ClientRectangle
            End If
            Me.Text = ""
        End Set
    End Property

(and don't forget to leave some room next to your richtextbox so that the LineNumbers control can show up there)

That should solve the problem, I think Smile | :) Sorry for the inconvenience Blush | :O
GeneralRe: I don't understand the Part beginning "Using the Code" Pin
RogerBerglen22-Aug-08 12:27
memberRogerBerglen22-Aug-08 12:27 
GeneralRe: I don't understand the Part beginning "Using the Code" Pin
RogerBerglen22-Aug-08 12:57
memberRogerBerglen22-Aug-08 12:57 
GeneralRe: I don't understand the Part beginning "Using the Code" Pin
nogChoco24-Aug-08 12:21
membernogChoco24-Aug-08 12:21 
GeneralRe: I don't understand the Part beginning "Using the Code" Pin
Heriberto Lugo14-Aug-15 7:33
memberHeriberto Lugo14-Aug-15 7:33 
GeneralThis is very cool! Pin
Rens Duijsens7-Apr-08 5:38
memberRens Duijsens7-Apr-08 5:38 
GeneralRe: This is very cool! Pin
nogChoco16-Apr-08 12:49
membernogChoco16-Apr-08 12:49 
GeneralConversion in C# Pin
naveenrhl24-Dec-07 22:45
membernaveenrhl24-Dec-07 22:45 
QuestionDelay while working with large file Pin
memberPRAVEEN KAUSHIK27-Sep-07 1:05 
AnswerRe: Delay while working with large file Pin
nogChoco27-Sep-07 22:38
membernogChoco27-Sep-07 22:38 
QuestionAll Right! Pin
computergeek10119-Sep-07 10:13
membercomputergeek10119-Sep-07 10:13 
AnswerRe: All Right! Pin
nogChoco22-Sep-07 22:26
membernogChoco22-Sep-07 22:26 
Questionone question Pin
Cecil Cheah29-Jun-07 0:01
memberCecil Cheah29-Jun-07 0:01 
AnswerRe: one question Pin
nogChoco3-Jul-07 12:47
membernogChoco3-Jul-07 12:47 
GeneralGreat.. Pin
American_Eagle5-Jun-07 12:46
memberAmerican_Eagle5-Jun-07 12:46 
GeneralRe: Great.. Pin
nogChoco6-Jun-07 10:48
membernogChoco6-Jun-07 10:48 
Generalresize / scroll bug Pin
frumbert4-Jun-07 14:24
memberfrumbert4-Jun-07 14:24 
GeneralRe: resize / scroll bug Pin
nogChoco5-Jun-07 5:47
membernogChoco5-Jun-07 5:47 

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.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.170308.1 | Last Updated 31 May 2007
Article Copyright 2007 by nogChoco
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid