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:

  1. Manipulating the user’s text as they enter it is fraught with peril. Be prepared to