Get Row and Column positions Fast in a Textbox or Rich Textbox (clean-up)
This short article will show you how you can EASILY obtain an accurate logical text row count, plus the logical row and column number the cursor is located at in a Textbox or Rich Textbox. Line wraps that will force several additional displayed lines are no problem at all, and the returned indexes will account for them. Plus, afterward, I will show you how you can quickly dress up a selection in a textbox so that it is not always at the bottom of the textbox. Because these textbox controls have to manage these row and column counts and indexes anyway, why not let THEM do ALL the work for you.
Original Author: David Ross Goben
Code
Get Row and Column Positions Fast in a Textbox or Rich Textbox
By David Ross Goben
You are free to use this information, plus the included snippets in your own
code without even acknowledging my work or me.
A number of people have asked me over the years how to grab the line count,
line number, and column position the cursor is located at in a Textbox or Rich
Textbox. Other queries regarded positioning the cursor for display.
I have seen a number of samples published here, but they involved VB-only solutions,
usually using the Split() function. This is fine for short lines,
but if you are writing a text editor (every programmer should write at least
one text editor during their career - I have written 8 descent ones, 2 multi-document
editors, and VERY profitable professional word-processing and spell-checker packages that used
to sell for the Tandy Model I/III/4 TRS-80s).
Most examples for obtaining the row count, row number, and column usually employ
a lot of math, because it is a rather complex thing to do. This is due to the reason
that multi-line text boxes more often than not have lots of line wraps and such,
which forces most VB-only methods to fail miserably.
However, by utilizing just a single API function and 4 constants, we can obtain
everything we need in just a very few lines of code by letting the Textbox or
Rich Textbox do all the work for us.
We will require the SendMessage() API, but we want to use it without
the As Any frivolity that the VB6 API Viewer provides (the API
Viewer is activated under the Add-Ins menu via the Add-In Manager;
select the VB 6 API Viewer and ensure that the Loaded/Unloaded
and Load on Startup options are checked). As such, we will rename it
SendMessageByNum(), and change its lParam from As Any
to As Long. We will also grab 4 constants from the API Viewer: EM_LINEFROMCHAR, EM_LINEINDEX, EM_GETLINECOUNT,
and EM_LINESCROLL.
So far, we have the following code in our module or form or class (or wherever
you need to use these things in):
Private Declare Function SendMessageByNum
Lib "user32"
Alias "SendMessageA"
_
(ByVal hwnd
As Long,
_
ByVal wMsg
As Long,
_
ByVal wParam
As Long,
_
ByVal lParam
As Long)
As Long
Private Const EM_LINEFROMCHAR
As Long
= &HC9
Private Const EM_LINEINDEX
As Long
= &HBB
Private Const EM_GETLINECOUNT
As Long
= &HBA
Private Const EM_LINESCROLL
As Long
= &HB6
We have just set up for using the API. The actual Code is little longer than this,
though we will make it a bit longer for reasons of clarity.
Note that I declared the API and all the constants Private. Some people prefer
to declare them as Public and use them willy-nilly throughout their application,
but this is messy. Keep them local and private to one file, and simply write
public server subroutines and functions to be invoked from the outside. By wrapping
these system services inside controllable environments (wrapping is just a fancy
way of saying we are placing a stronger layer of control between the user and
the system services), we are less likely to make a goof, and hence our code
is more robust (a fancy term we developers like to use to say something
is bullet-proof or goof-proof), and it is also easier to debug if we
do discover an error.
The following code is designed to work with either a standard Textbox or Rich Textbox
(the RTB is enabled via the Project/Components menu and checking the
Microsoft Rich Textbox Control 6.0 item).
All you need to do is add the following 3 functions:
'*******************
' GetLineCount
' Get the line count
'*******************
Public Function
GetLineCount
(tBox
As Object)
As Long
GetLineCount
= SendMessageByNum(
tBox.hwnd,
EM_GETLINECOUNT, 0&, 0&)
End Function
'*******************
' GetLineNum
' Get current line number
'*******************
Public Function
GetLineNum(
tBox
As Object)
As Long
GetLineNum =
SendMessageByNum(
tBox.hwnd, EM_LINEFROMCHAR
, tBox.SelStart,
0&)
End Function
'*******************
' GetColPos
' Get current Column
'*******************
Public Function
GetColPos(
tBox
As Object)
As Long
GetColPos =
tBox.SelStart -
SendMessageByNum(
tBox.hwnd, EM_LINEINDEX
,
-1&,
0&)
End Function
By declaring tBox as Object
instead of a specific TextBox or RichTextbox, we can now use these
functions for either, though you can surely define them specifically for your
object, which will result in slightly faster-running code.
Some people prefer line numbers and column positions to start with 1, not 0.
In this case, all you need to do is add 1 to the results of GetLineNum()
and GetColPos().
How easy are they to invoke? Almost too easy. Suppose we have 3 labels on a form
named lblLines, which will report the line count in the textbox, a label
named lblRow to report the current row, and a label named lblCol
to report the column position in the row. These are in place to service a Rich
Textbox control named rtbText (although I am using labels in this
example, you can just as easily designate panels in a Statusbar control as the
targets). Suppose further that we want to report
line and column numbers from 1, not zero. We could write a subroutine to gather
all three pieces of information, or just the items we require. Suppose we wanted
all three. We would need only the following 3 lines of code:
Me.lblLines.Caption
= "Lines: " & CStr(GetLineCount(Me.rtbText))
Me.lblRow.Caption = "Row: " & CStr(GetLineNum(Me.rtbText) + 1)
Me.lblCol.Caption = "Col: " & CStr(GetColPos(Me.rtbText) + 1)
The final thing we may want to do is pretty up
the display. Suppose you have written a routine that searches through the text for
a character sequence, and then you want to display it on the screen. Normally when you
use the SelStart properties of the textbox, it tends to display at the bottom of
the textbox and looks kind of goofy. It looks a whole lot better if it is displayed
near the top of the Textbox, except when the selection is forced down due to
being near the bottom of the text anyway.
The fast trick around this is so simple that a lot of people never think of
it. Simply first set the SelStart property of the text box to the very bottom of the
text (tBox.SelStart = Len(tBox.Text))
and then point SelStart to the text you actually want pointed to. But it would be even nicer
if it were possible to display the target text a couple of line down from the top, so the user could
see what preceded it. All this can be accomplished with the following subroutine:
'*******************
' AdjustTextDisplay
' place current position
' at top of display, and
' scroll display up 2 lines
'*******************
Public Sub
AdjustTextDisplay(
tBox
As Object)
Dim cPos
As Long
Dim
cLen
As Long
With
tBox
cPos
= .SelStart 'Save selection
cLen
= .SelLength
'Save anything highlighted
.SelStart = Len(.Text) 'bottom
of text
.SelStart = cPos
'force top of display
.SelLength = cLen
'reselecting any selection
Call SendMessageByNum(
tBox.hwnd,
EM_LINESCROLL,
0&,
-2&)
End With
End Sub
This subroutine will even work if the text box contains only one line of text,
because if the API sub-system finds that the text box cannot actually scroll, it
will not try to force it.
If you know how many lines will typically be displayed in the text box, you could actually display
the selected text in the middle of the text box by replacing the -2& parameter in the
SendMessageByNum() invocation, which tells the control to scroll up 2 lines without
changing the selection point, to the number of lines displayable in the textbox divided by two.
And that is all there is to it. It is very simple, very fast, and very efficient.
- David
Loading Comments ...
Comments
No comments have been added for this post.
You must be logged in to make a comment.