实现Server-Side UI 自动化提供程序

本主题介绍如何为用 C++ 编写的自定义控件实现服务器端 Microsoft UI 自动化提供程序。 它包含以下部分:

有关演示如何实现服务器端提供程序的代码示例,请参阅UI 自动化提供程序的帮助主题

提供程序树结构

必须为需要 UIA 客户端访问的每个 UI 元素实现 UIA 提供程序。

例如,每个元素都必须实现 IRawElementProviderFragment ,而应用程序的根元素必须实现 IRawElementProviderFragmentRoot。 此外,每个提供程序元素都应链接到:

  • 父级 (parent)
  • prior provider 元素
  • next provider 元素
  • 第一个提供程序子级
  • 上一个提供程序子级

提供程序接口

以下组件对象模型 (COM) 接口提供自定义控件的功能。 若要提供基本功能,每个UI 自动化提供程序必须至少实现 IRawElementProviderSimple 接口。 IRawElementProviderFragmentIRawElementProviderFragmentRoot 接口是可选的,但应该为复杂控件中的元素实现以提供其他功能。

接口 说明
IRawElementProviderSimple 为窗口中托管的控件提供基本功能,包括对控件模式和属性的支持。
IRawElementProviderFragment 为复杂控件中的元素添加功能,包括在片段中导航、设置焦点和返回元素的边框。
IRawElementProviderFragmentRoot 为复杂控件中的根元素添加功能,包括找到指定坐标处的子元素和设置整个控件的焦点状态。

 

注意

在托管代码的 UI 自动化 API 中,这些接口构成了继承层次结构。 C++ 中的情况并非如此,其中接口是完全独立的。

 

以下接口提供附加功能,但实现是可选的。

接口 说明
IRawElementProviderAdviseEvents 启用提供程序跟踪事件请求。
IRawElementProviderHwndOverride 允许在片段的UI 自动化树中重新定位基于窗口的元素。

 

UI 自动化提供程序所需的功能

若要与UI 自动化通信,控件必须实现下表中描述的main功能区域。

功能 实现
向UI 自动化公开提供程序。 响应发送到控件窗口 的WM_GETOBJECT 消息,返回实现 IRawElementProviderSimple 的对象。 对于片段,这必须是片段根的提供程序。
提供属性值。 实现 IRawElementProviderSimple::GetPropertyValue 以提供或替代值。
使客户端能够与 控件交互。 实现支持每个适当控件模式的接口,例如 IInvokeProvider。 在 IRawElementProviderSimple::GetPatternProvider 的实现中返回这些控件模式提供程序。
引发事件。 UiaRaiseAutomationEventIProxyProviderWinEventSink 的方法
在片段中启用导航和聚焦。 为片段中的每个元素实现 IRawElementProviderFragment 。 对于不属于片段的元素,不需要。
在片段中启用焦点和查找子元素。 实现 IRawElementProviderFragmentRoot。 对于不是片段根的元素不是必需的。

 

属性值

自定义控件UI 自动化提供程序必须支持可由UI 自动化和客户端应用程序使用的某些属性。 对于窗口中承载的元素,UI 自动化可以从默认窗口提供程序检索某些属性,但必须从自定义提供程序获取其他属性。

通常,基于窗口的控件的提供程序不需要提供由 PROPERTYID 标识的以下属性:

从窗口中获取托管在窗口中的简单元素或片段根的 RuntimeId 属性。 但是,根下的片段元素(例如列表框中的列表项)必须提供其自己的标识符。 有关详细信息,请参阅 IRawElementProviderFragment::GetRuntimeId

对于托管在 Windows 窗体 控件中的提供程序,应返回 IsKeyboardFocusable 属性。 在这种情况下,默认的窗口提供程序可能无法检索正确值。

Name 属性通常由主机提供程序提供。

来自提供程序的事件

UI 自动化提供程序应引发事件以通知客户端应用程序有关 UI 状态的变化。 以下函数用于引发事件。

函数 说明
UiaRaiseAutomationEvent 引发各种事件,包括由控件模式触发的事件。
UiaRaiseAutomationPropertyChangedEvent 当 UI 自动化属性更改时引发事件。
UiaRaiseStructureChangedEvent 当UI 自动化树的结构发生更改(例如,通过删除或添加元素)时引发事件。

 

事件的目的是通知客户端 UI 中发生某些事件。 无论更改是由用户输入触发还是由客户端应用程序使用UI 自动化触发,提供程序都应引发事件。 例如,每当通过直接用户输入或通过调用 IUIAutomationInvokePattern::Invoke 的客户端应用程序调用控件时,都应引发由 UIA_Invoke_InvokedEventId 标识的事件。

若要优化性能,提供程序可以有选择地引发事件,或者,如果没有注册任何接收事件的客户端应用程序,则不引发任何事件。 以下 API 元素用于优化。

API 元素 说明
UiaClientsAreListening 此函数确定任何客户端应用程序是否订阅了UI 自动化事件。
IRawElementProviderAdviseEvents 在片段根上实现此接口可使客户端在为片段上的事件注册和注销事件处理程序时收到通知。

 

注意

与在 COM 编程中实现引用计数类似,UI 自动化提供程序必须处理 IRawElementProviderAdviseEvents::AdviseEventAddedAdviseEventRemoved 方法,例如 IUnknown 接口的 IUnknown::AddRefRelease 方法。 只要针对特定事件或属性调用 AdviseEventRemoved 的次数多于 AdviseEventRemoved ,提供程序就应继续引发相应的事件,因为某些客户端仍在侦听。 或者,UI 自动化提供程序可以使用 UiaClientsAreListening 函数来确定是否至少有一个客户端正在侦听,如果是,则引发所有适当的事件。

 

提供程序导航

简单控件的提供程序(例如在窗口中托管的自定义按钮)不需要支持UI 自动化树中的导航。 与 元素的导航由主机窗口的默认提供程序处理,该提供程序在 IRawElementProviderSimple::HostRawElementProvider 的实现中指定。 但是,在实现复杂自定义控件的提供程序时,必须支持片段及其子代的根节点之间的导航,以及同级节点之间的导航。

备注

非根片段的元素必须从 HostRawElementProvider 返回 NULL,因为它们不直接托管在窗口中,并且任何默认提供程序都不能支持往返导航。

 

片段的结构由 IRawElementProviderFragment::Navigate 的实现确定。 对于自每个片段的每个可能的方向,此方法返回该方向的元素的提供程序对象。 如果该方向上没有元素,该方法将返回 NULL

片段根仅支持子元素的导航。 例如,当方向 NavigateDirection_FirstChild时,列表框返回列表中的第一项,当方向 NavigateDirection_LastChild时返回最后一项。 片段根不支持导航到父级或同级;这由主机窗口提供程序处理。

不是根的片段元素必须支持导航到父级、所有同级及其子级。

分配新父级

弹出窗口实际上是顶级窗口,默认情况下,在UI 自动化树中显示为桌面的子级。 但是,在许多情况下,弹出式窗口在逻辑上是一些其他控件的子级。 例如,组合框的下拉列表在逻辑上是组合框的子级。 同样,菜单弹出窗口在逻辑上是菜单的子级。 UI 自动化支持将新父级分配给弹出窗口,使其显示为关联控件的子级。

将新父级分配到弹出窗口:

  1. 为弹出窗口的创建一个提供程序。 这要求事先知道弹出窗口的 类。
  2. 像往常一样为该弹出窗口实现所有属性和控件模式,就好像它本身是一个控件一样。
  3. 实现 IRawElementProviderSimple::HostRawElementProvider 属性,以便返回从 UiaHostProviderFromHwnd 获取的值,其中 参数是弹出窗口的窗口句柄。
  4. 为弹出窗口及其父级实现 IRawElementProviderFragment::Navigate ,以便正确处理从逻辑父级到逻辑子级以及同级子级之间的导航。

当 UI 自动化遇到弹出窗口时,它可以识别导航正从默认值进行重写,并会跳过作为桌面子级出现的弹出窗口。 相反,只能通过片段访问节点。

分配新父级不适用于控件可以承载任何类的窗口的情况。 例如,rebar 控件可以在其带区中托管任何类型的窗口。 为了处理这些情况,UI 自动化支持另一种窗口重定位形式,如下一部分所述。

提供程序重新定位

UI 自动化片段可能包含两个或多个元素,每个元素都包含在一个窗口中。 由于每个窗口都有其自己的默认提供程序,该提供程序将窗口视为包含窗口的子窗口,因此默认情况下,UI 自动化树会将片段中的窗口显示为父窗口的子窗口。 在大多数情况下,这是所需的行为,但有时这可能会导致混淆,因为它不匹配 UI 的逻辑结构。

一个很好的示例则是 rebar 控件。 rebar 控件包含带区,每个带区又可以包含基于窗口的控件,例如工具栏、编辑框或组合框。 rebar 窗口的默认窗口提供程序将带控件窗口视为子窗口,rebar 提供程序将带区视为子级。 由于窗口提供程序和 rebar 提供程序协同工作并组合其子项,因此条带和基于窗口的控件都显示为 rebar 控件的子级。 但是,从逻辑上讲,只有带区应显示为 rebar 控件的子级,并且每个波段提供程序应与其包含的控件的默认窗口提供程序结合使用。

为此,rebar 控件的片段根提供程序公开一组表示带的子项。 每个波段都有一个提供程序,可以公开属性和控件模式。 在 IRawElementProviderSimple::HostRawElementProvider 的实现中,带区提供程序返回控件窗口的默认窗口提供程序,它通过调用 UiaHostProviderFromHwnd 将控件的窗口句柄 (HWND) 传递。 最后,rebar 的片段根提供程序实现 IRawElementProviderHwndOverride 接口,在其 IRawElementProviderHwndOverride::GetOverrideProviderForHwnd 的实现中,它将返回指定窗口中包含的控件的适当带区提供程序。

断开提供程序的连接

应用程序通常会根据需要创建控件,并在之后销毁它们。 销毁控件后,应通过调用 UiaDisconnectProvider 释放与控件关联的UI 自动化提供程序资源。

同样,应用程序应在关闭之前使用 UiaDisconnectAllProviders 函数释放应用程序中的所有提供程序保留的所有UI 自动化资源。

UI 自动化提供程序程序员指南