Click here to Skip to main content
15,889,096 members
Articles / Desktop Programming / Windows Forms
Article

Code Editor (Part 1)

Rate me:
Please Sign up or sign in to vote.
1.61/5 (8 votes)
28 Jun 20072 min read 31K   225   11   5
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


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAn error? Pin
J Schewe15-Feb-09 19:49
J Schewe15-Feb-09 19:49 
Unsure | :~ I don't work much with VB, so I used my knowledge of that language and msdn help to convert it to C#. It appears to work just fine, so I don't know if this is a result of the conversion or something left out in the source.

It seems that if you press the delete key to remove a line, then the line numbers will not update, but they will update on enter.

It would be easy to just add the delete key to the txtEditor_KeyPress sub, however what if another exception comes along? And why draw on every delete and backspace key? Not all of them are new lines.

Here's my solution (I'm converting back to VB now):
-first I improved GetLogicalLineNumberFromIndex:
Private Function GetLogicalLineNumberFromIndex(ByVal index As Integer) As Integer
    Dim ln As Integer = 0
    Dim sindex As Integer = 0
    Do While (sindex < index)
        sindex = Me.txtEditor.Text.IndexOf(Chr(10), sindex) + 1
        If sindex = 0 OrElse sindex > index Then Exit Function
        ln += 1
    Loop
    Return ln
End Function

-next I deleted the sub txtEditor_KeyPress
-then I added the following:
Private last_line As Integer = -1
Private last_lines As Integer = -1
Private last_textLength As Integer = -1
Private Function GetLastCharInLogicalLine(ByVal charIndex As Integer) As Integer
    Dim endIndex As Integer = Me.txtEditor.Text.IndexOf(Chr(10), charIndex)
    If endIndex = -1 Then Return Me.txtEditor.TextLength
    Return endIndex
End Function
Private Sub txtEditor_ContentsResized(ByVal sender As Object, ByVal e As System.Windows.Forms.ContentsResizedEventArgs) Handles txtEditor.ContentsResized
    ' report that we've handled the text change
    last_textLength = this.txtEditor.TextLength
    ' get the last wrapped part on the current logical line
    Dim current_line As Integer = txtEditor.GetLineFromCharIndex(GetLastCharInLogicalLine(txtEditor.SelectionStart))
    ' if the amount of lines changed or the current logical line has wrapped again since last time then
    If Not this.txtEditor.Lines.Length = last_lines OrElse Not last_line = current_line Then
        ' redraw the numbers
        PaintLineNumberPanel()
        ' update the amount of wrapped parts on the current logical line
        last_line = current_line
        ' update the total amount of lines in the editor
        last_lines = this.txtEditor.Lines.Length
    End If
End Sub
Private Sub txtEditor_SelectionChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtEditor.SelectionChanged
    ' if the text changed then don't update here
    If this.txtEditor.TextLength = last_textLength Then
        ' update the selection point
        last_line = txtEditor.GetLineFromCharIndex(GetLastCharInLogicalLine(txtEditor.SelectionStart))
    End If
End Sub

-after that I realized that whenever the last line was a wrapped line then the width of the number panel decreased, so I modified this code in DrawLineNumbers:
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)

-and changed it so that the function GetNumStringToPrint was part of it:
Dim numtoprint As Integer = 1
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)
    numtoprint = Me.GetNumberToPrint(ln, i, numberoflines, ch)
End If
If Not PrintedLines.Contains(numtoprint) Then
    PrintedLines.Add(numtoprint)
    numstring = numtoprint + ":"
    g.DrawString(numstring, Me.txtEditor.Font, Brushes.DarkBlue, (Me.pnlLineNumbers.Width - g.MeasureString(numstring, Me.txtEditor.Font).Width) - 4, y)
End If

-that concludes my solution.


Also some unnecessary code is the DLL import, because Me.txtEditor already contains the function GetFirstCharIndexFromLine Wink | ;)

And now for a feature that I think would be awesome but I can't figure out how to do is have the horizontal scrollbar continue over top of the line numbers when not in word wrap.

Have a good day! Smile | :)
-J Schewe
Generallooking for the next release Pin
Moim Hossain28-Jun-07 22:38
Moim Hossain28-Jun-07 22:38 
GeneralRe: looking for the next release Pin
NormDroid29-Jun-07 1:59
professionalNormDroid29-Jun-07 1:59 
GeneralRe: looking for the next release Pin
Jason Barrera29-Jun-07 8:45
Jason Barrera29-Jun-07 8:45 
GeneralRe: looking for the next release Pin
Calvin111-Jun-09 3:03
Calvin111-Jun-09 3:03 

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.