找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 489|回复: 0

[翻译] UI自动化的威力

[复制链接]

149

主题

0

回帖

943

积分

管理员

积分
943
发表于 2025-3-10 14:53:58 | 显示全部楼层 |阅读模式
    如果你需要获取某个浏览器中所有打开的标签页的列表,该怎么做呢?在(非常)久远的时代,你可能会假设每个标签页都是自己的一个窗口,因此你可以使用例如 FindWindow 这样的函数来找到一个主浏览器窗口,然后通过EnumChildWindows 枚举子窗口来定位标签页。然而,这种方法注定会失败。下面是使用 WinSpy 查看 Microsoft Edge 主窗口的一个截图:

image.png
MS Edge 仅显示两个子窗口

    主窗口的标题暗示了有26个标签页存在,但实际上只有两个子窗口,而且它们并不是标签页。不可避免的结论是,标签页根本不是窗口。它们是通过某种Win32窗口基础设施既不知道也不关心的技术“绘制”出来的。
    我们如何获取这些浏览标签页的信息呢?这时就需要用到UI自动化(UI Automation)技术了。
    UI自动化(UI Automation)技术已经存在多年了,它起源于更早的技术——“Active Accessibility”。Active Accessibility技术主要是为了提升无障碍性而设计的,同时它还能为无障碍性客户端提供丰富的信息。尽管出于兼容性的原因,Active Accessibility仍然受到支持,但一项名为UI自动化的新技术已经取代了它。
    UI自动化提供了一个UI自动化元素树,该树代表了用户界面的各个方面。一些元素代表“真正的”Win32窗口(具有HWND),一些元素代表内部控件,如按钮和编辑框(无论使用何种技术创建),还有一些元素是虚拟的(没有任何图形方面),但它们提供了与其他项目相关的“元数据”。
    UI自动化客户端API使用COM,其中根对象实现了IUIAutomation接口(它还实现了扩展接口)。要获取自动化对象,可以使用以下C++代码(稍后我们将看到一个C#示例):
  1. CComPtr<IUIAutomation> spUI;
  2. auto hr = spUI.CoCreateInstance(__uuidof(CUIAutomation));
  3. if (FAILED(hr))
  4.     return Error("Failed to create Automation root", hr);
复制代码
    客户端自动化接口在<UIAutomationClient.h>中声明。代码使用了ATL的CComPtr<>智能指针,但任何COM智能指针或原始指针都可以。
    有了UI自动化对象指针后,有几个选项可供选择。其中之一是枚举UI元素树的全部或部分。首先,我们可以通过调用IUIAutomation::get_RawViewWalker来获取一个“遍历器”对象。从那里开始,我们可以通过调用IUIAutomationTreeWalker接口方法(如GetFirstChildElement和GetNextSiblingElement)来开始枚举。
    每个由IUIAutomationElement接口表示的元素都提供了一组属性,其中一些属性可以直接在接口上获取(例如get_CurrentName、get_CurrentClassName、get_CurrentProcessId),而其他属性则隐藏在一个通用方法get_CurrentPropertyValue之后,其中每个属性都有一个整数ID,结果是一个VARIANT,以允许各种类型的值。
    使用这种方法,WinSpy中的“查看自动化树”菜单项会显示完整的自动化树,你可以深入到任何层级,同时右侧会显示所选元素的许多属性:

image-1.png
WinSpy自动化树视图

    如果你深入挖掘,你会发现MS Edge的标签页有一个UI自动化类名“EdgeTab”。这是定位浏览器标签页的关键。(其他浏览器可能有不同的类名)。为了找到标签页,我们可以手动枚举整个树,但幸运的是,有更好的方法。IUIAutomationElement有一个FindAll方法,它基于一组条件搜索元素。可用的条件非常灵活——基于元素的一些属性或属性组合,可以使用And、Or等来获得更复杂的条件。在我们的情况下,我们只需要一个条件——一个名为“EdgeTab”的类名。
  1. int main() {
  2.     //首先,我们将创建根对象,并设置条件(为了简洁起见,省略了错误处理):
  3.     ::CoInitialize(nullptr);
  4.     CComPtr<IUIAutomation> spUI;
  5.     auto hr = spUI.CoCreateInstance(__uuidof(CUIAutomation));
  6.     CComPtr<IUIAutomationCondition> spCond;
  7.     CComVariant edgeTab(L"EdgeTab");
  8.     spUI->CreatePropertyCondition(UIA_ClassNamePropertyId, edgeTab, &spCond);
  9.     //我们有一个针对类名属性的条件,该条件在自动化头文件中有一个定义的ID。接下来,我们将从根元素(桌面)开始搜索:
  10.     CComPtr<IUIAutomationElementArray> spTabs;
  11.     CComPtr<IUIAutomationElement> spRoot;
  12.     spUI->GetRootElement(&spRoot);
  13.     hr = spRoot->FindAll(TreeScope_Descendants, spCond, &spTabs);
  14.     //接下来要做的就是收集结果:
  15.     int count = 0;
  16.     spTabs->get_Length(&count);
  17.     for (int i = 0; i < count; i++) {
  18.         CComPtr<IUIAutomationElement> spTab;
  19.         spTabs->GetElement(i, &spTab);
  20.         CComBSTR name;
  21.         spTab->get_CurrentName(&name);
  22.         int pid;
  23.         spTab->get_CurrentProcessId(&pid);
  24.         printf("%2d PID %6d: %ws\n", i + 1, pid, name.m_str);
  25.     }
  26. }
复制代码
.NET代码:
    一个方便的NuGet包名为Interop.UIAutomationClient.Signed,它为.NET客户端提供了自动化API的包装。在添加NuGet包引用后,以下是使用C#完成的相同搜索:
  1. static void Main(string[] args) {
  2.     const int ClassPropertyId = 30012;
  3.     var ui = new CUIAutomationClass();
  4.     var cond = ui.CreatePropertyCondition(ClassPropertyId, "EdgeTab");
  5.     var tabs = ui.GetRootElement().FindAll(TreeScope.TreeScope_Descendants, cond);
  6.     for (int i = 0; i < tabs.Length; i++) {
  7.         var tab = tabs.GetElement(i);
  8.         Console.WriteLine($"{i + 1,2} PID {tab.CurrentProcessId,6}: {tab.CurrentName}");
  9.     }
  10. }
复制代码
更多自动化功能:
    UI自动化包含许多内容——“自动化”一词意味着更多的控制。API的一个功能是当元素的某些方面发生变化时提供各种通知。例如,IUIAutomation方法包括AddAutomationEventHandler、AddFocusChangedEventHandler、AddPropertyChangedEventHandler和AddStructureChangedEventHandler。
    与控件相关的更具体接口也提供了有关元素(和一些控件)的更具体信息,例如IUIAutomationTextPattern、IUIAutomationTextRange等,还有许多其他接口。

原文链接
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表