之前写的通过hook资源管理器获取文本插入点坐标 – AutoAHK 这篇文章用了hook方式获取光标坐标,这是一种高成本高风险的方式,老实说不太推荐使用。经过一段时间的研究发现,除了过去已有的ACC(MSAA)途径,UIA也提供了相应的一种获取光标坐标的方法:IUIAutomationTextRange::GetBoundingRectangles 它一直被我忽略了。Explorer应该也是调用了这个方法。
下面提供封装好的函数,它结合了ACC和UIA,目前测试下来适用于包括单纯靠Win32的GetCaretPos和ACC无法获取坐标的Windows Terminal窗口,以及UWP这种Metro风格窗口在内的大部分窗口。对部分游戏和工业软件的自绘窗口可能依然无能为力,毕竟实现MSAA和UIA接口的主动权在软件开发者手里。
V1:
/*
f1::
CoordMode, ToolTip, Screen
if (hwnd := GetCaretPosEx(x, y, w, h)) {
WinGetClass, classname, ahk_id %hwnd%
ToolTip, %classname%, x, y + h
}
else {
ToolTip
}
return
*/
GetCaretPosEx(byref x = 0, byref y = 0, byref w = 0, byref h = 0) {
x := y := w := h := hwnd := 0
static iUIAutomation, hOleacc, IID_IAccessible, guiThreadInfo, init
if !init {
init := true
try
iUIAutomation := ComObjCreate("{E22AD333-B25F-460C-83D0-0581107395C9}", "{30CBE57D-D9D0-452A-AB13-7AC5AC4825EE}")
hOleacc := DllCall("LoadLibrary", "str", "Oleacc.dll", "ptr")
VarSetCapacity(IID_IAccessible, 16), NumPut(0x11CF3C3D618736E0, IID_IAccessible, "int64"), NumPut(0x719B3800AA000C81, IID_IAccessible, 8, "int64")
VarSetCapacity(guiThreadInfo, size := (A_PtrSize == 8 ? 72 : 48)), NumPut(size, guiThreadInfo, "uint")
}
if !iUIAutomation || DllCall(NumGet(NumGet(iUIAutomation + 0), 8 * A_PtrSize), "ptr", iUIAutomation, "ptr*", eleFocus) || !eleFocus
goto useAccLocation
; Check read only property
if !DllCall(NumGet(NumGet(eleFocus + 0), 16 * A_PtrSize), "ptr", eleFocus, "int", 10002, "ptr*", valuePattern) && valuePattern
if !DllCall(NumGet(NumGet(valuePattern + 0), 5 * A_PtrSize), "ptr", valuePattern, "int*", isReadOnly) && isReadOnly
goto cleanUp
; Plan A applies to windows that implement IAccessible, such as chrome
useAccLocation:
if DllCall("GetGUIThreadInfo", "uint", DllCall("GetWindowThreadProcessId", "ptr", WinExist("A"), "ptr", 0, "uint"), "ptr", &guiThreadInfo)
hwndFocus := NumGet(guiThreadInfo, A_PtrSize == 8 ? 16 : 12, "ptr")
if !hwndFocus
hwndFocus := WinExist()
if hOleacc && !DllCall("Oleacc\AccessibleObjectFromWindow", "ptr", hwndFocus, "uint", 0xFFFFFFF8, "ptr", &IID_IAccessible, "ptr*", accCaret) && accCaret {
VarSetCapacity(id, 24, 0), NumPut(3, id, "ushort")
if !DllCall(NumGet(NumGet(accCaret + 0), 22 * A_PtrSize), "ptr", accCaret, "int*", x, "int*", y, "int*", w, "int*", h, "ptr", &id) {
hwnd := hwndFocus
goto cleanUp
}
}
if iUIAutomation && eleFocus {
; use IUIAutomationTextPattern2::GetCaretRange
if DllCall(NumGet(NumGet(eleFocus + 0), 16 * A_PtrSize), "ptr", eleFocus, "int", 10024, "ptr*", textPattern2, "int") || !textPattern2
|| DllCall(NumGet(NumGet(textPattern2 + 0), 10 * A_PtrSize), "ptr", textPattern2, "int*", isActive, "ptr*", caretTextRange) || !caretTextRange || !isActive
|| DllCall(NumGet(NumGet(caretTextRange + 0), 10 * A_PtrSize), "ptr", caretTextRange, "ptr*", rects) || !rects || (rects := ComObject(0x2005, rects, 1)).MaxIndex() < 3
goto useGetSelection
x := rects[0], y := rects[1], w := rects[2], h := rects[3], hwnd := hwndFocus
goto cleanUp
useGetSelection:
; use IUIAutomationTextPattern::GetSelection
if DllCall(NumGet(NumGet(eleFocus + 0), 16 * A_PtrSize), "ptr", eleFocus, "int", 10014, "ptr*", textPattern) || !textPattern
|| DllCall(NumGet(NumGet(textPattern + 0), 5 * A_PtrSize), "ptr", textPattern, "ptr*", selectionRangeArray) || !selectionRangeArray
|| DllCall(NumGet(NumGet(selectionRangeArray + 0), 3 * A_PtrSize), "ptr", selectionRangeArray, "int*", length) || !length
|| DllCall(NumGet(NumGet(selectionRangeArray + 0), 4 * A_PtrSize), "ptr", selectionRangeArray, "int", 0, "ptr*", selectionRange) || !selectionRange
|| DllCall(NumGet(NumGet(selectionRange + 0), 10 * A_PtrSize), "ptr", selectionRange, "ptr*", rects) || !rects
goto useGUITHREADINFO
rects := ComObject(0x2005, rects, 1)
if rects.MaxIndex() < 3 && DllCall(NumGet(NumGet(selectionRange + 0), 6 * A_PtrSize), "ptr", selectionRange, "int", 0)
|| DllCall(NumGet(NumGet(selectionRange + 0), 10 * A_PtrSize), "ptr", selectionRange, "ptr*", rects) || !rects || (rects := ComObject(0x2005, rects, 1)).MaxIndex() < 3
goto useGUITHREADINFO
x := rects[0], y := rects[1], w := rects[2], h := rects[3], hwnd := hwndFocus
goto cleanUp
}
useGUITHREADINFO:
if hwndCaret := NumGet(guiThreadInfo, A_PtrSize == 8 ? 48 : 28, "ptr") {
VarSetCapacity(clientRect, 16)
if DllCall("GetWindowRect", "ptr", hwndCaret, "ptr", &clientRect) {
offset := A_PtrSize == 8 ? 56 : 32
w := NumGet(guiThreadInfo, offset + 8, "int") - NumGet(guiThreadInfo, offset, "int")
h := NumGet(guiThreadInfo, offset + 12, "int") - NumGet(guiThreadInfo, offset + 4, "int")
DllCall("ClientToScreen", "ptr", hwndCaret, "ptr", &guiThreadInfo + offset)
x := NumGet(guiThreadInfo, offset, "int")
y := NumGet(guiThreadInfo, offset + 4, "int")
hwnd := hwndCaret
}
}
cleanUp:
for _, p in [eleFocus, valuePattern, textPattern2, caretTextRange, textPattern, selectionRangeArray, selectionRange, accCaret]
(p && ObjRelease(p))
return hwnd
}
V2:
/*
f1::{
CoordMode("ToolTip", "Screen")
if hwnd := GetCaretPosEx(&x, &y, &w, &h)
ToolTip(WinGetClass(hwnd), x, y + h)
else
ToolTip()
}
*/
GetCaretPosEx(&x?, &y?, &w?, &h?) {
x := h := w := h := 0
static iUIAutomation := 0, hOleacc := 0, IID_IAccessible, guiThreadInfo, _ := init()
if !iUIAutomation || ComCall(8, iUIAutomation, "ptr*", eleFocus := ComValue(13, 0), "int") || !eleFocus.Ptr
goto useAccLocation
if !ComCall(16, eleFocus, "int", 10002, "ptr*", valuePattern := ComValue(13, 0), "int") && valuePattern.Ptr
if !ComCall(5, valuePattern, "int*", &isReadOnly := 0) && isReadOnly
return 0
useAccLocation:
; use IAccessible::accLocation
hwndFocus := DllCall("GetGUIThreadInfo", "uint", DllCall("GetWindowThreadProcessId", "ptr", WinExist("A"), "ptr", 0, "uint"), "ptr", guiThreadInfo) && NumGet(guiThreadInfo, A_PtrSize == 8 ? 16 : 12, "ptr") || WinExist()
if hOleacc && !DllCall("Oleacc\AccessibleObjectFromWindow", "ptr", hwndFocus, "uint", 0xFFFFFFF8, "ptr", IID_IAccessible, "ptr*", accCaret := ComValue(13, 0), "int") && accCaret.Ptr {
NumPut("ushort", 3, varChild := Buffer(24, 0))
if !ComCall(22, accCaret, "int*", &x := 0, "int*", &y := 0, "int*", &w := 0, "int*", &h := 0, "ptr", varChild, "int")
return hwndFocus
}
if iUIAutomation && eleFocus {
; use IUIAutomationTextPattern2::GetCaretRange
if ComCall(16, eleFocus, "int", 10024, "ptr*", textPattern2 := ComValue(13, 0), "int") || !textPattern2.Ptr
goto useGetSelection
if ComCall(10, textPattern2, "int*", &isActive := 0, "ptr*", caretTextRange := ComValue(13, 0), "int") || !caretTextRange.Ptr || !isActive
goto useGetSelection
if !ComCall(10, caretTextRange, "ptr*", &rects := 0, "int") && rects && (rects := ComValue(0x2005, rects, 1)).MaxIndex() >= 3 {
x := rects[0], y := rects[1], w := rects[2], h := rects[3]
return hwndFocus
}
useGetSelection:
; use IUIAutomationTextPattern::GetSelection
if textPattern2.Ptr
textPattern := textPattern2
else if ComCall(16, eleFocus, "int", 10014, "ptr*", textPattern := ComValue(13, 0), "int") || !textPattern.Ptr
goto useGUITHREADINFO
if ComCall(5, textPattern, "ptr*", selectionRangeArray := ComValue(13, 0), "int") || !selectionRangeArray.Ptr
goto useGUITHREADINFO
if ComCall(3, selectionRangeArray, "int*", &length := 0, "int") || length <= 0
goto useGUITHREADINFO
if ComCall(4, selectionRangeArray, "int", 0, "ptr*", selectionRange := ComValue(13, 0), "int") || !selectionRange.Ptr
goto useGUITHREADINFO
if ComCall(10, selectionRange, "ptr*", &rects := 0, "int") || !rects
goto useGUITHREADINFO
rects := ComValue(0x2005, rects, 1)
if rects.MaxIndex() < 3 {
if ComCall(6, selectionRange, "int", 0, "int") || ComCall(10, selectionRange, "ptr*", &rects := 0, "int") || !rects
goto useGUITHREADINFO
rects := ComValue(0x2005, rects, 1)
if rects.MaxIndex() < 3
goto useGUITHREADINFO
}
x := rects[0], y := rects[1], w := rects[2], h := rects[3]
return hwndFocus
}
useGUITHREADINFO:
if hwndCaret := NumGet(guiThreadInfo, A_PtrSize == 8 ? 48 : 28, "ptr") {
if DllCall("GetWindowRect", "ptr", hwndCaret, "ptr", clientRect := Buffer(16)) {
w := NumGet(guiThreadInfo, 64, "int") - NumGet(guiThreadInfo, 56, "int")
h := NumGet(guiThreadInfo, 68, "int") - NumGet(guiThreadInfo, 60, "int")
DllCall("ClientToScreen", "ptr", hwndCaret, "ptr", guiThreadInfo.Ptr + 56)
x := NumGet(guiThreadInfo, 56, "int")
y := NumGet(guiThreadInfo, 60, "int")
return hwndCaret
}
}
return 0
static init() {
try
iUIAutomation := ComObject("{E22AD333-B25F-460C-83D0-0581107395C9}", "{30CBE57D-D9D0-452A-AB13-7AC5AC4825EE}")
hOleacc := DllCall("LoadLibraryW", "str", "Oleacc.dll", "ptr")
NumPut("int64", 0x11CF3C3D618736E0, "int64", 0x719B3800AA000C81, IID_IAccessible := Buffer(16))
guiThreadInfo := Buffer(A_PtrSize == 8 ? 72 : 48), NumPut("uint", guiThreadInfo.Size, guiThreadInfo)
}
}
C++源码:
#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(500);
}
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;
}
可否分享下c++源码
我觉得uia获取部分是不是应该作一个系统判断,IUIAutomationTextPattern2只有在win8+才有效
不支持接口的话就直接返回了,没啥必要多做一个判断
iUIAutomation := ComObjCreate(“{E22AD333-B25F-460C-83D0-0581107395C9}”, “{30CBE57D-D9D0-452A-AB13-7AC5AC4825EE}”) win7上可能会弹窗报错没有注册类
大佬有空可否研究下 TSF,好像输入法是用的这个 ITfContextView::GetTextExt https://learn.microsoft.com/en-us/windows/win32/api/msctf/nf-msctf-itfcontextview-gettextext
研究过,结论是不行。这个接口只能给注册的输入法使用,在目标窗口进程之外的第三方进程无能为力
想问下UIA里有获取光标所在文本中的位置的方法吗?
类似这个文章里实现的效果:
https://www.autohotkey.com/boards/viewtopic.php?t=9787
但这里只支持标准edit控件,适用范围比较窄。