Manipulating the caret (text cursor) using the Win32 API
published 19 jun 2011
I recently dealt with a case where I wanted to automatically format user’s text as they type it. For example, if the user is typing a phone number, they might type “9595551234”, and as they type, their text would be formatted as such (system-inserted characters emphasized):
(959) 555-1234.
This kind of user interface, when done very carefully 1, can make data entry faster and ensure better-formatted results.
Unfortunately using the WM_SETTEXT
message to set the text of the control
causes the text caret (aka cursor, aka text insertion point) to revert to the beginning of the text. We want to
make sure that the user’s cursor ends up where it belongs, so that they can continue to type.
Initial research led me to believe that setcursorpos
and its counterpart getcursorpos
would do what I wanted,
but after some frustrating trials, I found that while getcursorpos
would return something vaguely plausible,
setcursorpos
did nothing at all. I suspect that in fact these functions only work if you are manually managing
cursors you create yourself using createcursor
, but I’m not sure.
Fortunately, some vague whisperings pointed me to a pair of messages that would do
the job: EM_SETSEL
and EM_GETSEL
. From the EM_GETSEL
docs:
If there is no selection, the starting and ending values are both the position of the caret.
I made a trimmed down sample app for this post. Let’s say I’ve got users who love typing “hahahaha” ad infinitum; By inserting the “a” for them, they could type that string simply by holding down the “h” key, which cuts their keystrokes by 50%! The code is as follows:
Inside your handler for the text change notification (see full solution for more context):
// first get the text the user has entered
TCHAR buff[64];
SendMessage(hwndEdit, WM_GETTEXT, 32, (LPARAM)buff); // (32 in case these are wide chars)
size_t len = _tcslen(buff);
// if they've entered text, and the last character is an h
if(len > 0 && buff[len - 1] == 'h')
{
// also, if we have room in our buffer for the "a"
if ( len <= 62)
{
// add the a
_tcsncat(buff, L"a", 1);
// before manipulating the text, save their current selection
DWORD firstChar, lastChar;
SendMessage(hwndEdit, EM_GETSEL, (WPARAM) &firstChar, (LPARAM) &lastChar);
// now set the text -- guard is a simple mutex to prevent recursing into this
// change notification
gaurd = true;
SendMessage(hwndEdit, WM_SETTEXT, NULL, (LPARAM) buff);
gaurd = false;
if(firstChar == lastChar && firstChar == len)
{
firstChar++;
lastChar++;
}
// restore their cursor position or selection
SendMessage(hwndEdit, EM_SETSEL, (WPARAM) firstChar, (LPARAM) lastChar);
}
}
The key logic here (and this is probably not really sophisticated enough, to be honest—consider a user trying to delete a trailing “a”)
is that we check if they initially had nothing selected (firstChar == lastChar
), and if their cursor was at the end of the string
(firstChar == len
.) In this case, we want to move the cursor forward by one character to account for the character we just added.
It took me a while to figure this out, and for whatever reason, the documentation is poor, and there’s a lot of misleading information around the web, so I hope this helps someone!
I’ve made a simple sample project, which you can find on Github. Most of the code there is generated by VS2010’s project
template. The real action is in the message handling code in MoveCaret2.cpp
.
do a great deal of testing before you consider this feature ready for prime time. Some questions to ask:
- What happens if the user enters their own punctuation instead of accepting mine? Are users allowed to override default formatting rules?
- Am I assuming a certain length of string? What happens if the user enters a different length?
- What happens if the user goes back and edits their text somewhere in the middle?
- What happens if the user deletes their text, or a range of text?
-
Manipulating the user’s text as they enter it is fraught with peril. Be prepared to ↩