Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Code Editor (Part 1)

1.61/5 (8 votes)
28 Jun 20072 min read 1   226  
Create a Code/Text editor. Line numbering with or without wordwrap enabled.

Introduction

This article should help some figure out how to, or atleast get a starting point for building a Code/Text Editor. In this first part of my article I will start with "Line Numbering". I looked everywhere trying to figure this task out. The closest I got was an article posted here by Michael Elly, which is located here. So some of this code will look familiar if you read that article.

There was a few problems for me though with that article.

First it uses a particular function that is only in the .Net 2.0 version and above. Second, it added line numbers where there were no lines. Third, I dont like having to add the vbCrLf's in order to safely measure the font height/line spacing. Fourthly, It does not handle wordwrapping.

All problems I had with Michael's code were easily fixed, except the wordwrapping, but after several hours, I figured it out. "I think"

Using the code

For myself I made a single control out of this, but for this article, all the code is within a single form.

Ok, problem #1, the function only in .Net >=2.0. The fix for this was to write the same function, but had to use a bit of interop for this.

<System.Runtime.InteropServices.DllImport("user32.dll", CharSet:=System.Runtime.InteropServices.CharSet.Auto)> _

Private Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As IntPtr

End Function

Public Function GetFirstCharIndexFromLine(ByVal lineNumber As Integer) As Integer

If (lineNumber < 0) Then

Throw New ArgumentOutOfRangeException("lineNumber")

End If

Return SendMessage(Me.txtEditor.Handle, &HBB, lineNumber, 0).ToInt32

End Function

The second problem being that it added line numbers where there were no lines was fixed by this line "If i = numberoflines Then Exit Do"

The third problem that added the unwanted vbCrLf's was resolved by measuring the height with this function. If there is no text in the control, it simply returns the controls Font.Height. The static variable LastGoodHeight is used when the textbox is scrolling and for some reason it likes to return a number something like 65536 for a height. So, I used a value of 100 to check against.

VB.NET
Private Function GetFontHeight(ByVal ctl As RichTextBox) As Single

Static LastGoodHeight As Single = 0

Dim height As Single = ctl.GetPositionFromCharIndex(GetFirstCharIndexFromLine(2)).Y - ctl.GetPositionFromCharIndex(GetFirstCharIndexFromLine(1)).Y

height = Math.Abs(height)

If height = 0 Then

height = ctl.Font.GetHeight() + 1

LastGoodHeight = height

Else

If height < 100 Then

LastGoodHeight = height

Else

height = LastGoodHeight

End If

End If

Return height

End Function

Now, the 4th problem, and the worst of all.. First off I had to write a function to determine the Real/Logical line number a Char Index was found in. The function is as follows: Note that there is 2 so you can use the one that you think is best but the first one seems to be faster.

VB.NET
Private Function GetLogicalLineNumberFromIndex(ByVal index As Integer) As Integer

If Me.txtEditor.TextLength > 0 Then

Dim sindex As Integer = 0

If sindex = index Then Return 0

Dim lines As String() = Me.txtEditor.Text.Split(New Char() {ChrW(10)})

For ln As Integer = 0 To lines.Length - 1

sindex += (lines(ln).Length + 1)

If index < sindex Then Return ln

Next

Else

Return -1

End If

End Function

Private Function GetLogicalLineNumberFromCharIndex(ByVal index As Integer) As Integer

Dim ret As Integer = -1

Const NEWLINE As Char = Chr(10)

If Me.txtEditor.TextLength > 0 Then

Dim curline As Integer = 0

Dim txtlen As Integer = Me.txtEditor.TextLength

For i As Integer = 0 To txtlen - 1

Dim ch As Char = Me.txtEditor.Text.Chars(i)

If i = index Then Return curline

If ch = NEWLINE Then

curline += 1

End If

Next

End If

Return ret

End Function

Now a couple helper functions...

VB.NET
Private Function GetNumStringToPrint(ByVal numbertoprint As Integer, ByVal printedlines As ArrayList) As String

Dim numstring As String

If Not printedlines.Contains(numbertoprint) Then

printedlines.Add(numbertoprint)

numstring = numbertoprint & ":"

Else

numstring = " "

End If

Return numstring

End Function

Private Function GetNumberToPrint(ByVal logiclinenum As Integer, ByVal currentloopnum As Integer, ByVal numberofvisiblelines As Integer, ByVal currentchar As Char) As Integer

Dim ret As Integer = 1

If currentloopnum = numberofvisiblelines AndAlso currentchar = ChrW(10) Then

ret = logiclinenum + 2

Else

ret = logiclinenum + 1

End If

Return ret

End Function

and now the main function that puts it all together...

VB.NET
Private Sub DrawLineNumbers(ByVal g As Graphics)

Dim fontheight As Single = Me.GetFontHeight(Me.txtEditor)

If fontheight = 0 Then Exit Sub

Dim firstindex As Integer = Me.txtEditor.GetCharIndexFromPosition(New Point(0, CInt(g.VisibleClipBounds.Y + fontheight / 3)))

Dim firstline As Integer = Me.txtEditor.GetLineFromCharIndex(firstindex)

Dim firstliney As Integer = Me.txtEditor.GetPositionFromCharIndex(firstindex).Y

'' Paint the background color of the linenumbers panel

g.Clear(Me.txtEditor.BackColor)

'' Paint the line numbers

Dim i As Integer = firstline

Dim y As Single

Dim numberoflines As Integer = Me.txtEditor.GetLineFromCharIndex(Int32.MaxValue) + 1

Dim numstring As String = ""

Dim PrintedLines As New ArrayList

Do While y < g.VisibleClipBounds.Y + g.VisibleClipBounds.Height

y = firstliney + fontheight * (i - firstline - 1)

If i > 0 Then

If Me.txtEditor.TextLength > 0 Then

Dim cindex As Integer = Me.txtEditor.GetCharIndexFromPosition(New Point(0, CInt(y)))

Dim ch As Char = Me.txtEditor.GetCharFromPosition(New Point(0, CInt(y)))

Dim ln As Integer = Me.GetLogicalLineNumberFromIndex(cindex)

Dim numtoprint As Integer = Me.GetNumberToPrint(ln, i, numberoflines, ch)

numstring = Me.GetNumStringToPrint(numtoprint, PrintedLines)

Else

numstring = "1:"

End If

g.DrawString(numstring, Me.txtEditor.Font, Brushes.DarkBlue, (Me.pnlLineNumbers.Width - g.MeasureString(numstring, Me.txtEditor.Font).Width) - 4, y)

End If

If i = numberoflines Then Exit Do

i += 1

Loop

'resize the Linenumbers panel according to the width of the highest number,

' and tack on some padding so the numbers look centered

Me.pnlLineNumbers.Width = CInt(g.MeasureString(numstring, Me.txtEditor.Font).Width) + 6

End Sub

Thats pretty much it, this is my first article so sorry if I didnt explain well enough, but go through all the code, try it out to get a better understanding of it all.

In the zip file that you can download youll find a full working project ready to go, I added a menu to the control so you turn on,off the line numbering, or wordwrapping to see it all working better.

One more thing, I dont mind comments, but if your just trying to be a smartass, then piss off.

History

7-28-07: Got wordwrapping working.

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