Extending the TextBox Control
As we all know the strength of VB is it's simplicity. Just draw some controls onto a form set some properties during design time, write some "glue code" and you're done (well, almost anyway). VB also provides us with ready to go controls that are the same as Windows own controls (i.e. TextBox, ListBox, CommandButton and so on) or are they?
Windows own controls have more properties then those available in VB. Properties that Microsoft was too lazy to include or just didn't think a VB developer needed.
Take an ordinary multiline textbox for example. It is almost the same as a Windows edit box except for the lack of several properties. An edit box has a property which the VB developer can use to retrieve the number of lines of text it contains. You are also able to get the index of the first visible line (almost like the TopIndex property of a list box) for example.
But is there a way to harness the power of these hidden properties? Luckily there is! With the help of the SendMessage API function you can extend an ordinary textbox so you can treat the lines of text as an array of strings. This is very useful when you want to parse the text. Let's say that you want to create an HTML editor and you want to add color coding (just like the editor in VB itself). Wouldn't it be very useful then if you could extract just the line of text the text caret is on and examines that text for the HTML tags you want to add colors to? Of course it would! But how do you add colors to a textbox you ask? Well, actually you can't. But the code I'm going to show you could as easily be used on a rich text box as well as a standard textbox.
Let's go to work!
Now, let's go to work, first you have to declare the SendMessage API function:
Public Declare Function SendMessage _
Lib "user32" Alias "SendMessageA" ( _
ByVal hwnd As Long, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
lParam As Any) As Long
You also need to declare the messages you want to send to the textbox. Let's say you want to write a wrapper function that can tell you the index of the first visible line in the textbox. Then you have to declare the following message:
Public Const EM_GETFIRSTVISIBLELINE = &HCE
Now you can go ahead and write the function:
Public Function TopLineIndex(txtBox As TextBox) As Long
TopLineIndex = SendMessage(txtBox.hWnd, _
EM_GETFIRSTVISIBLELINE, 0&, 0&)
The EM_GETFIRSTVISIBLELINE message doesn't take any arguments so wParam and lParam must be set to 0 (zero). If you want to use this function on a rich text box instead of the standard textbox then just change the argument type to RichTextBox instead of TextBox in the TopLineIndex function. Remember that the line index is zeroing based i.e. the function returns 0 for the first line, 1 for the second and so on.
A rich text box has a method called GetLineFromChar which returns the line index of the given character indexes. The standard textbox lack such a method though. Let's fix that! You need to use the EM_LINEFROMCHAR message witch is declared in the following manner:
Public Const EM_LINEFROMCHAR = &HC9
The EM_LINEFROMCHAR message takes the index of the character position in the wParam argument.
Public Function GetLineFromChar(txtBox As TextBox, CharPos As Long) As Long
GetLineFromChar = SendMessage( _
txtBox.hWnd, EM_LINEFROMCHAR, CharPos, 0&)
So if you want to know what line the text caret is on in a multiline textbox you could call the function in this manner:
Dim lngLineIndex As Long
lngLineIndex = GetLineFromChar(Text1, Text1.SelStart)
MsgBox "You are on line number " & lngLineIndex + 1
Again remember that the line index is zero based.
You can also do the opposite, finding the index of the first character of a line, with the EM_LINEINDEX message.
Public Const EM_LINEINDEX = &HBB
Public Function GetCharFromLine(txtBox As TextBox, LineIndex As Long) As Long
GetCharFromLine = SendMessage( _
txtBox.hWnd, EM_LINEINDEX, LineIndex, 0&)
Now that we know how to find out what line we currently standing on wouldn't it be nice to find out how many lined of text there is in the textbox? Well that's easily done if you know of the EM_GETLINECOUNT message. This message doesn't take any arguments so we must pass 0 to wParam and lParam.
Public Const EM_GETLINECOUNT = &HBA
Public Function LineCount(txtBox As TextBox) As Long
LineCount = SendMessage( _
TxtBox.hWnd, EM_GETLINECOUNT, 0&, 0&)
OK that was simple enough. Let us go on to the next message: EM_LINELENGTH. This message takes a character index in the wParam argument and returns the length of the line which contains that character:
Public Const EM_LINELENGTH = &HC1
Public Function LineLen(txtBox As TextBox, CharPos As Long) As Long
LineLen = SendMessage( _
TxtBox.hWnd, EM_LINELEENGTH, CharPos, 0&)
Easy wasn't it? What did you say? - Too easy? Well, let's do something a little more complicated then? Let us write a function that extracts a single line of text from the textbox. The message we need to use is called EM_GETLINE and it takes the line index in the wParam argument and a buffer string to save the text line in as the lParam argument.
Although this sounds faily easy, the problem is that the message requires that the max length of the string to return be saved in the first word of the buffer. A word is a 16-bit value. How can we save such a value in a VB unicode string? The answer is we can't. So what's the solution then? Well, we have to use a byte array and then convert it to a string afterward.
Public Const EM_GETLINE = &HC4
Public Function GetLine(txtBox As TextBox, _
LineIndex As Long) As String
Dim bBuffer( ) As Byte 'the byte array
Dim lngLength As Long 'the max length of the line
Dim sRetVal As String 'the text to return
'check to see if the LineIndex value is valid
If LineIndex >= LineCount(txtBox)
'call the LineCount function shown above
Exit Function 'bale out
'get the length of the line
lngLength = LineLen(txtBox, GetCharFromLine(txtBox, LineIndex))
'check that there is any text on the line
If lngLength < 1 Then
'ReDim the byte array
'Save the length in the first word of the array
bBuffer(0) = lngLength And 255
bBuffer(1) = lngLength 256
'Send the message
SendMessage txtBox.hWnd, EM_GETLINE, LineIndex, bBuffer(0)
'Finally convert the byte array into a string and return it
sRetVal = Left$(StrConv(bBuffer, vbUnicode), lngLength)
GetLine = sRetVal
As you can see there is a lot you can do to extend the functionality in the standard VB controls. And these are just a few of the messages you can use on a textbox. Some of the others are:
EM_CANUNDO - Determines if you can undo the last editing or not.
EM_UNDO - Undo the last editing.
EM_GETMODIFY - Determines if the text in the textbox has been modified. Great to check if you make an editor and want to know if the text have to be saved or not.
EM_SETMODIFY - Manually set the modify flag to true or false. This flag is automatically set to true when the text changes.
I have written a class called cTextBoxEx, with an accompanied demo application, which use all of the mentioned messages and also add a function to delete a single line from the textbox. Furthermore it adds a couple of shortcuts as well. These are CTRL+A to select all and CTRL+Y to cut the current line and put it on the clipboard.