如果你需要获取某个浏览器中所有打开的标签页的列表,该怎么做呢?在(非常)久远的时代,你可能会假设每个标签页都是自己的一个窗口,因此你可以使用例如 FindWindow 这样的函数来找到一个主浏览器窗口,然后通过EnumChildWindows 枚举子窗口来定位标签页。然而,这种方法注定会失败。下面是使用 WinSpy 查看 Microsoft Edge 主窗口的一个截图:
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#示例):
- CComPtr<IUIAutomation> spUI;
- auto hr = spUI.CoCreateInstance(__uuidof(CUIAutomation));
- if (FAILED(hr))
- 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中的“查看自动化树”菜单项会显示完整的自动化树,你可以深入到任何层级,同时右侧会显示所选元素的许多属性:
WinSpy自动化树视图
如果你深入挖掘,你会发现MS Edge的标签页有一个UI自动化类名“EdgeTab”。这是定位浏览器标签页的关键。(其他浏览器可能有不同的类名)。为了找到标签页,我们可以手动枚举整个树,但幸运的是,有更好的方法。IUIAutomationElement有一个FindAll方法,它基于一组条件搜索元素。可用的条件非常灵活——基于元素的一些属性或属性组合,可以使用And、Or等来获得更复杂的条件。在我们的情况下,我们只需要一个条件——一个名为“EdgeTab”的类名。 - int main() {
- //首先,我们将创建根对象,并设置条件(为了简洁起见,省略了错误处理):
- ::CoInitialize(nullptr);
- CComPtr<IUIAutomation> spUI;
- auto hr = spUI.CoCreateInstance(__uuidof(CUIAutomation));
-
- CComPtr<IUIAutomationCondition> spCond;
- CComVariant edgeTab(L"EdgeTab");
- spUI->CreatePropertyCondition(UIA_ClassNamePropertyId, edgeTab, &spCond);
-
- //我们有一个针对类名属性的条件,该条件在自动化头文件中有一个定义的ID。接下来,我们将从根元素(桌面)开始搜索:
- CComPtr<IUIAutomationElementArray> spTabs;
- CComPtr<IUIAutomationElement> spRoot;
- spUI->GetRootElement(&spRoot);
- hr = spRoot->FindAll(TreeScope_Descendants, spCond, &spTabs);
-
- //接下来要做的就是收集结果:
- int count = 0;
- spTabs->get_Length(&count);
- for (int i = 0; i < count; i++) {
- CComPtr<IUIAutomationElement> spTab;
- spTabs->GetElement(i, &spTab);
- CComBSTR name;
- spTab->get_CurrentName(&name);
- int pid;
- spTab->get_CurrentProcessId(&pid);
- printf("%2d PID %6d: %ws\n", i + 1, pid, name.m_str);
- }
- }
复制代码
.NET代码: 一个方便的NuGet包名为Interop.UIAutomationClient.Signed,它为.NET客户端提供了自动化API的包装。在添加NuGet包引用后,以下是使用C#完成的相同搜索: - static void Main(string[] args) {
- const int ClassPropertyId = 30012;
- var ui = new CUIAutomationClass();
- var cond = ui.CreatePropertyCondition(ClassPropertyId, "EdgeTab");
- var tabs = ui.GetRootElement().FindAll(TreeScope.TreeScope_Descendants, cond);
- for (int i = 0; i < tabs.Length; i++) {
- var tab = tabs.GetElement(i);
- Console.WriteLine($"{i + 1,2} PID {tab.CurrentProcessId,6}: {tab.CurrentName}");
- }
- }
复制代码
更多自动化功能: UI自动化包含许多内容——“自动化”一词意味着更多的控制。API的一个功能是当元素的某些方面发生变化时提供各种通知。例如,IUIAutomation方法包括AddAutomationEventHandler、AddFocusChangedEventHandler、AddPropertyChangedEventHandler和AddStructureChangedEventHandler。 与控件相关的更具体接口也提供了有关元素(和一些控件)的更具体信息,例如IUIAutomationTextPattern、IUIAutomationTextRange等,还有许多其他接口。
原文链接 |