Finding the current line and column in a multi-line text box
There are times where you want to drop a multi-line text box into your form to allow your user to edit large ammounts of text. In my own particular case, I am writing a GUI to let programmers make changes in real time to a game engine using Python scripts. It works pretty good, but one of the main problems we have with it is that when there is an error in the Python scripts, it identifies the line where the error occured by its number. This can make it difficult to scroll through the text box manually counting the lines, especially as the script gets longer.
Our solution was to simply put a label on the form that would show the current position of the cursor. As far as I could find, there is no event that fires when the cursor moves. This isn’t a real problem though as you can simply catch two different events and cover most situations where the cursor might change postion. We caught the KeyDown and the MouseDown events. Every key (including arrow keys) fires the KeyDown event. We tried the KeyPress event at first and this only worked for valid printable keys since this event has a different purpose. The MouseDown event allows you to update the cursor position when a user click a specific spot in the text box.
In both events, you put the same code. Just one line will do it. This function is responsible for finding the current line and column from the supplied text box and setting the text property of the supplied label control to show the current line and postion.
The ShowCaretPos function is responsible for setting the label text. The other two functions which we will show later are responsible for actually calculating the position. The ShowCaretPos function just uses these to get the values and display them.
Public Sub ShowCaretPos(ByVal lbl As Label, ByVal txt As TextBox)
Dim l As Long
l = GetCurrLine(txt)
Dim c As Long
c = GetCurrColumn(txt)
lbl.Text = "ln " + l.ToString() + " col " + c.ToString()
ShowCaretPos itself really isn’t that special. Modify the display of the lines and columns to suit your tastes. The real work is done in GetCurrLine and GetCurrColumn. These functions manually figure out the current line and column of the cursor. A common way to find this information is to use the SendMessage function in the Win32 API. The problem with that method is that you get the exact line you are on in the text box and not the line in the file. This means that if you have word wrap turned on and you have 12 lines that each wrap to a second line, when your cursor is at the end it will show line 24 rather than line 12. In our case we are trying to jump to a line where the error occured. If the line number is calculated based on the text box rather than the file, it is basically useless.
This code actually starts with the current position and counts backwards until it quits finding line feeds (chr(10)). It is a little more processor intensive, but for our purposes it is more accurate and that is what counts. Also, our files don’t get too big (100’s of lines, not 1000’s or more) so the delay isn’t noticable. On a very large file, I could see this function running away with the CPU.
Public Function GetCurrLine(ByVal txt As TextBox) As Int32 Dim found As Int32 = -1 Dim linenum As Int32 = 1 Dim pos As Int32 pos = txt.SelectionStart If (pos = 0) Then Return 1 End If Do Until found = 0 found = InStrRev(txt.Text, Chr(10), pos, CompareMethod.Binary) If (found = 0) Then Exit Do linenum += 1 pos = found - 1 Loop Return linenum End Function
As mentioned before, it takes the text, looks at the current caret position (selectionstart) and rewinds counting the actual line feeds. Pretty simple.
Public Function GetCurrColumn(ByVal txt As TextBox) As Int32 Dim pos As Int32 Dim found As Int32 = -1 Dim cnt As Int32 = 0 pos = txt.SelectionStart If (pos = 0) Then Return 1 End If found = InStrRev(txt.Text, Chr(10), pos, CompareMethod.Binary) If (found = 0) Then ' We are on the line... cnt = txt.SelectionStart + 1 Else cnt = txt.SelectionStart - found + 1 End If Return cnt End Function
GetCurrColumn follows the same idea as GetCurrLine. It starts from the current position, but it only rewindws to the immediatly previous line feed and counts characters from there to determine the current column. One thing to note is that a tab character is considered one character according to this function. You could easily use the replace function to substitute tab characters for X number of spaces.
This solution is specifically designed for multi-line text boxes. It should work in any VB.NET and in VB 4,5,6 with a little modification (change int32 to long). It would probly also work on the Rich Text Box control, but that one already has current line properties and such. Eventually I think I will change my text boxes out for RTF boxes and add syntax highlighting, but since I have little time for extras on low priority projects, this will have to wait. Until then, this should save some time spent manually counting lines to find the error.
Computer Magic And Software Design