一种新的获取 Windows 窗口的文本光标的方式

之前在 GitHub 的 Windows Terminal 仓库中看到一个 disscution,

https://github.com/microsoft/terminal/discussions/14664

这个讨论中提到的方法是在以前的比较经典的三种方法之外的获取 Windows 的窗口中的 caret 的坐标的另一种方法,以前的三种方法,可以在我的这个仓库中看到,

https://github.com/fanlumaster/FullIME

对于接下来要提到的这种新的方式,是使用到了 Windows 提供的 IUIAutomationTextPattern 这个组件。以前的方法是可以在 Chrome 和一些经典的比如记事本这样的传统的 Windows 应用中获取文本光标,对于我们经常用到的另外一个软件——Windows Terminal 则是无能为力,这个新的方法所解决问题,基本上就是 Windows Terminal 这个软件的获取文本光标的问题,以及开始菜单,和一些其他基于 WinUI 框架的窗口软件,今天偶然在 ahk 的论坛发现有人给出了实现,这里就记录一下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include <Windows.h>
#include <atlbase.h>
#include <atlsafe.h>
#include <UIAutomation.h>
#include <oleacc.h>
#include <iostream>

#pragma comment(lib, "Oleacc.lib")

HWND GetCaretPosEx(long *pX, long *pY, long *pW, long *pH);

int main()
{
if (FAILED(CoInitialize(nullptr)))
{
return 0;
}
int count = 0;
while (1)
{
long x = 0, y = 0, w = 0, h = 0;
HWND hwnd = GetCaretPosEx(&x, &y, &w, &h);
WCHAR className[255];
if (hwnd != nullptr)
{
GetClassNameW(hwnd, className, sizeof(className) / sizeof(*className));
std::wcout << count++ << "\t" << className << "\tx: " << x << "\ty: " << y << "\tw: " << w << "\th: " << h
<< std::endl;
}
Sleep(1000);
}
CoUninitialize();
return 0;
}

HWND GetCaretPosEx(long *pX, long *pY, long *pW, long *pH)
{
CComPtr<IUIAutomation> uia;
CComPtr<IUIAutomationElement> eleFocus;
CComPtr<IUIAutomationValuePattern> valuePattern;
if (S_OK != uia.CoCreateInstance(CLSID_CUIAutomation) || uia == nullptr)
{
return nullptr;
}
if (S_OK != uia->GetFocusedElement(&eleFocus) || eleFocus == nullptr)
{
goto useAccLocation;
}
if (S_OK == eleFocus->GetCurrentPatternAs(UIA_ValuePatternId, IID_PPV_ARGS(&valuePattern)) &&
valuePattern != nullptr)
{
BOOL isReadOnly;
if (S_OK == valuePattern->get_CurrentIsReadOnly(&isReadOnly) && isReadOnly)
{
#ifdef DEBUG
std::wcout << L"Read Only" << std::endl;
#endif // DEBUG
return nullptr;
}
}
useAccLocation:
// use IAccessible::accLocation
GUITHREADINFO guiThreadInfo = {sizeof(guiThreadInfo)};
HWND hwndFocus = GetForegroundWindow();
GetGUIThreadInfo(GetWindowThreadProcessId(hwndFocus, nullptr), &guiThreadInfo);
hwndFocus = guiThreadInfo.hwndFocus ? guiThreadInfo.hwndFocus : hwndFocus;
CComPtr<IAccessible> accCaret;
if (S_OK == AccessibleObjectFromWindow(hwndFocus, OBJID_CARET, IID_PPV_ARGS(&accCaret)) && accCaret != nullptr)
{
CComVariant varChild = CComVariant(0);
if (S_OK == accCaret->accLocation(pX, pY, pW, pH, varChild))
{
#ifdef DEBUG
std::wcout << L"IAccessible::accLocation Succeeded" << std::endl;
#endif // DEBUG
return hwndFocus;
}
}
if (eleFocus == nullptr)
{
return nullptr;
}
// use IUIAutomationTextPattern2::GetCaretRange
CComPtr<IUIAutomationTextPattern2> textPattern2;
CComPtr<IUIAutomationTextRange> caretTextRange;
CComSafeArray<double> rects;
void *pVal = nullptr;
BOOL IsActive = FALSE;
if (S_OK != eleFocus->GetCurrentPatternAs(UIA_TextPattern2Id, IID_PPV_ARGS(&textPattern2)) ||
textPattern2 == nullptr)
{
goto useGetSelection;
}
if (S_OK != textPattern2->GetCaretRange(&IsActive, &caretTextRange) || caretTextRange == nullptr || !IsActive)
{
goto useGetSelection;
}
if (S_OK == caretTextRange->GetBoundingRectangles(rects.GetSafeArrayPtr()) && rects != nullptr &&
SUCCEEDED(SafeArrayLock(rects)) && rects.GetCount() >= 4)
{
*pX = long(rects[0]);
*pY = long(rects[1]);
*pW = long(rects[2]);
*pH = long(rects[3]);
#ifdef DEBUG
std::wcout << L"IUIAutomationTextPattern2::GetCaretRange Succeeded" << std::endl;
#endif // DEBUG
return hwndFocus;
}
useGetSelection:
// use IUIAutomationTextPattern::GetSelection
CComPtr<IUIAutomationTextPattern> textPattern;
CComPtr<IUIAutomationTextRangeArray> selectionRangeArray;
CComPtr<IUIAutomationTextRange> selectionRange;
if (textPattern2 == nullptr)
{
if (S_OK != eleFocus->GetCurrentPatternAs(UIA_TextPatternId, IID_PPV_ARGS(&textPattern)) ||
textPattern == nullptr)
{
return nullptr;
}
}
else
{
textPattern = textPattern2;
}
if (S_OK != textPattern->GetSelection(&selectionRangeArray) || selectionRangeArray == nullptr)
{
return nullptr;
}
int length = 0;
if (S_OK != selectionRangeArray->get_Length(&length) || length <= 0)
{
return nullptr;
}
if (S_OK != selectionRangeArray->GetElement(0, &selectionRange) || selectionRange == nullptr)
{
return nullptr;
}
if (S_OK != selectionRange->GetBoundingRectangles(rects.GetSafeArrayPtr()) || rects == nullptr ||
FAILED(SafeArrayLock(rects)))
{
return nullptr;
}
if (rects.GetCount() < 4)
{
if (S_OK != selectionRange->ExpandToEnclosingUnit(TextUnit_Character))
{
return nullptr;
}
if (S_OK != selectionRange->GetBoundingRectangles(rects.GetSafeArrayPtr()) || rects == nullptr ||
FAILED(SafeArrayLock(rects)) || rects.GetCount() < 4)
{
return nullptr;
}
}
*pX = long(rects[0]);
*pY = long(rects[1]);
*pW = long(rects[2]);
*pH = long(rects[3]);
#ifdef DEBUG
std::wcout << L"IUIAutomationTextPattern::GetSelection Succeeded" << std::endl;
#endif // DEBUG
return hwndFocus;
}

这是一个简单的单文件,直接编译运行即可验证,

当然,对于一些自绘文本光标的窗口,比如 neovide、IDEA 这类,这个方法依然是不奏效的,对于自绘的窗口,目前应该只能通过 TSF 的接口来获取,但是 TSF 的一些接口目前应该只能在输入法中进行调用。

所以,这里只是对于之前的方法的一次补充。


参考:https://www.autoahk.com/archives/44158