Search code examples
c#winapiautomationmouseevent

How can I get the caret position from a textbox in another application? (Not the coordinates, but the actual index inside of the textbox)


I need to retrieve the index of the caret inside a textbox in the focused window, maybe using UI Automation, or maybe a Win32 API function, if there's any function that cat do that. And I emphasize, I don't mean the x,y coordinates, but the index of the caret inside the text of the textbox. How can I do that? Also see this similar question.


Solution

  • You can use UI Automation for that, and especially the IUIAutomationTextPattern2 interface that has a GetCaretRange method.

    Here are two sample Console app (C++ and C# code) that run continuously and display the caret position for the current element under the mouse:

    C++ version

    int main()
    {
        CoInitializeEx(NULL, COINIT_MULTITHREADED);
        {
            CComPtr<IUIAutomation> automation;
    
            // make sure you use CLSID_CUIAutomation8, *not* CLSID_CUIAutomation
            automation.CoCreateInstance(CLSID_CUIAutomation8);
            do
            {
                POINT pt;
                if (GetCursorPos(&pt))
                {
                    CComPtr<IUIAutomationElement> element;
                    automation->ElementFromPoint(pt, &element);
                    if (element)
                    {
                        CComBSTR name;
                        element->get_CurrentName(&name);
                        wprintf(L"Watched element %s\n", name);
    
                        CComPtr<IUIAutomationTextPattern2> text;
                        element->GetCurrentPatternAs(UIA_TextPattern2Id, IID_PPV_ARGS(&text));
                        if (text)
                        {
                            // get document range
                            CComPtr<IUIAutomationTextRange> documentRange;
                            text->get_DocumentRange(&documentRange);
    
                            // get caret range
                            BOOL active = FALSE;
                            CComPtr<IUIAutomationTextRange> range;
                            text->GetCaretRange(&active, &range);
                            if (range)
                            {
                                // compare caret start with document start
                                int caretPos = 0;
                                range->CompareEndpoints(TextPatternRangeEndpoint_Start, documentRange, TextPatternRangeEndpoint_Start, &caretPos);
                                wprintf(L" caret is at %i\n", caretPos);
                            }
                        }
                    }
                }
                Sleep(500);
            } while (TRUE);
        }
        CoUninitialize();
        return 0;
    }
    

    C# version

    static void Main(string[] args)
    {
        // needs 'using UIAutomationClient;'
        // to reference UIA, don't use the .NET assembly
        // but instead, reference the UIAutomationClient dll as a COM object
        // and set Embed Interop Types to False for the UIAutomationClient reference in the C# project
        var automation = new CUIAutomation8();
        do
        {
            var cursor = System.Windows.Forms.Cursor.Position;
            var element = automation.ElementFromPoint(new tagPOINT { x = cursor.X, y = cursor.Y });
            if (element != null)
            {
                Console.WriteLine("Watched element " + element.CurrentName);
                var guid = typeof(IUIAutomationTextPattern2).GUID;
                var ptr = element.GetCurrentPatternAs(UIA_PatternIds.UIA_TextPattern2Id, ref guid);
                if (ptr != IntPtr.Zero)
                {
                    var pattern = (IUIAutomationTextPattern2)Marshal.GetObjectForIUnknown(ptr);
                    if (pattern != null)
                    {
                        var documentRange = pattern.DocumentRange;
                        var caretRange = pattern.GetCaretRange(out _);
                        if (caretRange != null)
                        {
                            var caretPos = caretRange.CompareEndpoints(
                                TextPatternRangeEndpoint.TextPatternRangeEndpoint_Start,
                                documentRange,
                                TextPatternRangeEndpoint.TextPatternRangeEndpoint_Start);
                            Console.WriteLine(" caret is at " + caretPos);
                        }
                    }
                }
            }
            Thread.Sleep(500);
        }
        while (true);
    }
    

    The trick is to use the IUIAutomationTextRange::CompareEndpoints method that allows you to compare the caret range with another range, for example the whole document range.

    Note there are drawbacks:

    • some apps don't support any MSAA/UIA introspection at all, or don't support the text pattern. For these, there are simply no solution (even using Windows API I think)
    • some apps report the caret incorrectly, especially when you select text (so, with a moving caret). For example with Notepad, moving LEFT while pressing SHIFT will select backwards but for some reason UIA doesn't update caret pos. I think it's a problem with Notepad because it also has caret issue with IME (Input Method Editor, like the Emoji editor you can summon using Win+; keyboard combination) which also uses the global caret position. There's no problem with Wordpad for example.