WinUI 3 WebView2 无障碍实现解析:UIA Provider 与 CUIAWrapper 的深度协作机制
【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml
导读
本文深入剖析 WinUI 3(microsoft-ui-xaml 仓库)中 WebView2 控件如何接入 Windows UI Automation(UIA)无障碍体系。WebView2 基于 Edge(Anaheim)浏览器内核,其内容渲染在独立的浏览器进程中,Xaml 侧的自动化对等类(Automation Peer)无法直接访问 WebView 内部的 DOM 结构。WinUI 通过与 Edge 侧的EmbeddedBrowserWebViewUIAProvider协作,将浏览器内容的 UIA Provider 桥接进 Xaml 的CUIAWrapper,从而让屏幕阅读器等辅助工具可以完整感知并操作 Web 页面。读完本文,你将掌握这一桥接机制的整体架构、两条关键 API 调用链,以及仓库中对应的源码实现细节。
一、问题背景:为什么 WebView2 的无障碍接入如此特殊
在 WinUI 3 中,WebView2(源码位于 controls/dev/WebView2/WebView2.cpp)本质上是 Xaml 宿主窗口内的一块由 Edge 渲染的内容。与普通 Xaml 控件不同:
- WebView2 的页面内容由独立的浏览器进程(Anaheim / Edge)渲染,Xaml 的 UI 自动化(UIA)树无法直接枚举到这些内容;
- WebView2 拥有自己的输入窗口句柄(input HWND),浏览器进程内部维护着完整的 DOM 无障碍树;
- Xaml 的
AutomationPeer只是自动化对等对象,并不是 UIA Provider,二者互不持有,但通常彼此知晓。
因此,让 WebView2 的内容“可见”于 UIA,就必须打通两条树之间的桥梁:Xaml 树(CUIAWrapper)与Edge 浏览器树(EmbeddedBrowserWebViewUIAProvider)。
二、MUXC 侧 WebView2 的创建流程
文档明确描述了 Xaml 侧 WebView2 的创建链路,仓库源码可以逐级印证:
创建 Environment:
WebView2.cpp创建一个CoreWebView2Environment(Edge 侧称为EBWebViewEnvironment)。当前实现中,每个 WebView2 实例都会新建一个 Environment,这属于已知的临时方案,未来会改为复用(详见第六节)。- 源码印证:
WebView2::CreateCoreObjects中调用CreateDefaultCoreEnvironment(),其内部通过CoreWebView2Environment::CreateWithOptionsAsync(browserInstall, userDataFolder, environmentOptions)创建环境,见 WebView2.cpp。创建失败时(如未安装 Edge Runtime)会置位m_shouldShowMissingAnaheimWarning并触发CoreWebView2Initialized事件携带错误 HRESULT。
- 源码印证:
创建 Controller:由 Environment 创建
CoreWebView2Controller,它拥有CoreWebView2对象。Edge 侧则由EBWebViewEnvironment创建对应的 Controller。- 源码印证:
CreateCoreWebViewFromEnvironment调用m_coreWebViewEnvironment.CreateCoreWebView2CompositionControllerAsync(windowRef)创建 CompositionController,再通过.as<CoreWebView2Controller>()获得控制器、.CoreWebView2()获得 CoreWebView2,见 WebView2.cpp。这里走的是**视觉托管模式(CompositionController)**而非窗口模式。
- 源码印证:
创建 AutomationPeer:当 UIA 尝试与 Xaml WebView2 控件交互时,
WebView2::OnCreateAutomationPeer()返回WebView2AutomationPeer,见 WebView2.cpp:winrt::AutomationPeer WebView2::OnCreateAutomationPeer() { return winrt::make<WebView2AutomationPeer>(*this); }WebView2AutomationPeer继承自FrameworkElementAutomationPeer,其 IDL 定义见 WebView2AutomationPeer.idl,控制类型为Pane、类名为WebView2,见 WebView2AutomationPeer.cpp。
三、Core Xaml 侧:CUIAWrapper 与 CUIAWindow
Xaml 运行时(Core Xaml)对IRawElementProviderSimple(及系列接口)的实现是CUIAWrapper。它由CUIAWindow创建——可以把 CUIAWindow 视为 CoreWindow 内容的 Provider,同时它也负责托管 Popup 与 XamlIslandRoot 的 Xaml 内容(此处 CUIAWindow 的名字具有误导性,它并非仅限于 CoreWindow 场景)。
创建链路的两个关键点如下:
- 创建时机:CUIAWindow 在
GetOverrideProviderForHwndImpl()中调用CreateProviderForAP(CAutomationPeer* pAP, CUIAWrapper** ppRet)创建 CUIAWrapper。由于调用发生在该函数内,UIA 只给出一个 HWND,Xaml 必须据此反查对应的 AutomationPeer。 - CUIAWrapper 构造所需的两项关键参数:
- 元素的 AutomationPeer(此处即
WebView2AutomationPeer)。Xaml 必须在拿到 HWND 后从GetAPForHwnd的树遍历中找到与之匹配的 Peer。 - WebView 的输入 HWND,它被写入
UIAHostEnvironmentInfo。IRawElementProviderSimple的get_HostRawElementProvider()方法总是调用 UIA 的UiaHostProviderFromHwnd(HWND),因此 CUIAWrapper 必须把这个输入 HWND 传进去。
- 元素的 AutomationPeer(此处即
文档特别指出:这两个问题理论上可以"直接向 CoreWebView2 索要输入 HWND"来解决,但在新的开源代码中引入 HWND 并不被推崇(Edge 团队强烈反对),于是选择了另一种方案——由 Edge 侧主动提供 Provider 信息给 Xaml。
四、Edge 侧如何传递 UIA Provider 信息给 Xaml
设计核心:在 Edge 侧实现IRawElementProviderSimple,并把它交给 Xaml,这样 Xaml 无需直接接触 HWND 即可获得所需信息。
EmbeddedBrowserWebViewWindow会创建并拥有一个EmbeddedBrowserWebViewUIAProvider,它实现了IRawElementProviderSimple。当前实现中它在任何 UIA 调用之前就创建(无论是否被查询),未来可改为仅在 UIA 查询时惰性创建。- 该 Provider 未来可以扩展实现更多 RawElementProvider 接口,从而把更多逻辑从 CUIAWrapper 中迁移出来。
从 Edge WebView2 获取IRawElementProviderSimple有两条路径,每条都是 Edge 侧的一个 API,并对应 Xaml WebView2 元素上一个把 Provider 传给 Core Xaml 的方法:
| 提供方 | API | 返回内容 | Xaml 侧对应方法 |
|---|---|---|---|
EmbeddedBrowserWebView | get_UIAProvider() | 该 WebView 的EmbeddedBrowserWebViewUIAProvider | WebView2AutomationPeer::GetRawElementProviderSimple() |
EBWebViewEnvironment | GetProviderForHwnd(HWND) | 与输入 HWND 对应的 WebView 的 Provider | WebView2AutomationPeer::IsCorrectPeerForHwnd(HWND) |
其中IsCorrectPeerForHwnd(HWND)会同时调用get_UIAProvider()与GetProviderForHwnd(),若二者返回的是同一个 Provider,则判定该 HWND 属于当前这个 WebView2。
源码级印证:两个关键方法
WinUI 侧的两个底层获取方法实现在 WebView2.cpp:
winrt::IUnknown WebView2::GetWebView2Provider() { winrt::com_ptr<IUnknown> provider; if (m_coreWebViewCompositionController) { CoreWebView2RunIgnoreInvalidStateSync( [&]() { auto coreWebView2CompositionControllerInterop = m_coreWebViewCompositionController.as<ICoreWebView2CompositionControllerInterop>(); winrt::check_hresult(coreWebView2CompositionControllerInterop->get_AutomationProvider(provider.put())); }); } return provider.as<winrt::IUnknown>(); } winrt::IUnknown WebView2::GetProviderForHwnd(HWND hwnd) { winrt::com_ptr<IUnknown> provider; if (m_coreWebViewEnvironment) { CoreWebView2RunIgnoreInvalidStateSync( [&]() { auto coreWebView2EnvironmentInterop = m_coreWebViewEnvironment.as<ICoreWebView2EnvironmentInterop>(); winrt::hresult hr = coreWebView2EnvironmentInterop->GetAutomationProviderForWindow(hwnd, provider.put()); if (hr != UIA_E_ELEMENTNOTAVAILABLE) { winrt::check_hresult(hr); } }); } return provider.as<winrt::IUnknown>(); }GetWebView2Provider()通过ICoreWebView2CompositionControllerInterop::get_AutomationProvider拿到本 WebView 的 Provider,对应 Edge 侧EmbeddedBrowserWebView::get_UIAProvider();GetProviderForHwnd(HWND)通过ICoreWebView2EnvironmentInterop::GetAutomationProviderForWindow按输入 HWND 查 Provider,对应 Edge 侧EBWebViewEnvironment::GetProviderForHwnd(HWND);当该 HWND 无对应 Provider 时返回UIA_E_ELEMENTNOTAVAILABLE,此时静默返回空 Provider 而不抛错。
源码级印证:WebView2AutomationPeer 的桥接实现
WebView2AutomationPeer通过自定义 COM 接口IAutomationPeerHwndInterop暴露两个方法,接口定义见 WebView2AutomationPeer.h(GUID865F5B88-6506-4E64-A4C5-4B7723650731):
MIDL_INTERFACE("865F5B88-6506-4E64-A4C5-4B7723650731") IAutomationPeerHwndInterop : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetRawElementProviderSimple( _Outptr_opt_ IRawElementProviderSimple** value) = 0; virtual HRESULT STDMETHODCALLTYPE IsCorrectPeerForHwnd( HWND hwnd, _Out_ bool* value) = 0; };实现位于 WebView2AutomationPeer.cpp:
HRESULT WebView2AutomationPeer::GetRawElementProviderSimple(_Outptr_opt_ IRawElementProviderSimple** value) { InitProvider(); m_provider.copy_to(value); return S_OK; } HRESULT WebView2AutomationPeer::IsCorrectPeerForHwnd(HWND hwnd, _Out_ bool* value) { *value = false; if (!InitProvider()) { return S_OK; } auto hwndProvider = GetImpl()->GetProviderForHwnd(hwnd).try_as<IRawElementProviderSimple>(); if (hwndProvider && hwndProvider == m_provider) { *value = true; } return S_OK; } bool WebView2AutomationPeer::InitProvider() { if (!m_provider) { m_provider = GetImpl()->GetWebView2Provider().try_as<IRawElementProviderSimple>(); } return !!m_provider; }m_provider缓存 Edge 侧 Provider 的IRawElementProviderSimple指针,InitProvider()惰性初始化;IsCorrectPeerForHwnd的比较逻辑与文档描述完全一致:用GetProviderForHwnd(hwnd)的结果与自身缓存 Provider 做 COM 指针相等性比较,相等即说明该 HWND 属于当前 WebView2。
五、WinUI 消费 Edge Provider 的两种方式
1. 创建 WebView2 的 CUIAWrapper 时(HWND → Peer 反查)
常规路径下,UIA 给出 HWND 后,Xaml 通过GetAPForHwnd遍历 AutomationPeer 树,找到InteropHwnd与给定 HWND 匹配的那个 Peer,再据此创建 Wrapper。
对 WebView2,遍历时改为对每个WebView2AutomationPeer调用IsCorrectPeerForHwnd()直至找到匹配者。这不会带来性能问题:每个 WebView2 只需做一次树遍历,而一个应用内 WebView2 的数量通常很少,因此该调用不会频繁发生。
仓库中的典型调用场景是WebView2::GetComponentHwnd()(见 WebView2.cpp):它遍历父窗口下类名为Chrome_WidgetWin_0的子窗口,对每个子 HWND 调用peer->IsCorrectPeerForHwnd(childWindow, &foundHwnd)验证归属——因为当应用中有多个 WebView2 时会存在多个子 HWND,必须用 Provider 匹配来区分。
2. CUIAWrapper 的 IRawElementProviderSimple 方法实现内
创建 CUIAWrapper 时,Xaml 会保存EmbeddedBrowserWebViewUIAProvider;此后 CUIAWrapper 实现每一个IRawElementProviderSimple方法时,都先询问 Edge Provider 的答案,必要时才回退到 CUIAWrapper 自身的实现。
其中最重要的是get_HostRawElementProvider():它需要调用UiaHostProviderFromHwnd(HWND)并传入输入 HWND——而 CUIAWrapper 手上没有这个 HWND。Edge 侧的EmbeddedBrowserWebViewUIAProvider则没有这个障碍,它可以直接访问输入 HWND。
该方案的优势
EmbeddedBrowserWebViewUIAProvider可被其他第三方消费者复用于各自的无障碍解决方案;- 可扩展实现其他 Provider 接口,把更多逻辑从 CUIAWrapper 迁出,最终甚至可以让 WebView2 完全摆脱 Xaml Wrapper。
六、未来规划:Environment 复用
文档明确记录了 Edge 侧与 MUXC 侧的演进方向:目前每个 WebView2 实例都会新建一个 Environment,未来将改为复用同一个 Environment:
- 在 MUXC 中新增
WebViewElementEnvironment类; - 提供公共静态方法
GetOrCreateEnvironment():被调用时,要么用CreateWebView2EnvironmentWithDetails创建新环境,要么返回已创建的环境; - 待决问题:该类是否需要维护正在使用该环境的 WebView 列表,并在它们全部销毁后自我删除;
- 其中 "details" 指
browserExecutableFolder、userDataFolder与additionalBrowserArguments三项;只有userDataFolder不是硬编码的(测试应用中的值为%LOCALAPPDATA%\Packages\MUXControlsTestApp_6f07fta6qpts2\AC)。由于同一应用内这些参数在多次 WebView 创建之间不应变化,复用环境被认为是安全的。
七、参考附录:IRawElementProvider* 方法清单(CUIAWrapper 实现)
以下是 CUIAWrapper 需要实现(或委托给 Edge Provider)的完整方法签名,供无障碍功能开发与调试参考:
// IRawElementProviderSimple 方法 HRESULT get_ProviderOptions(_Out_ ProviderOptions * pRetVal); HRESULT GetPatternProvider(_In_ PATTERNID patternId, _Out_ IUnknown ** pRetVal); HRESULT GetPropertyValue(_In_ PROPERTYID propertyId, _Out_ VARIANT * pRetVal); HRESULT get_HostRawElementProvider(_Out_ IRawElementProviderSimple ** pRetVal); // IRawElementProviderSimple2 方法 HRESULT ShowContextMenu(); // IRawElementProviderFragment 方法 HRESULT get_BoundingRectangle(_Out_ UiaRect * pRetVal); HRESULT get_FragmentRoot(_Out_ IRawElementProviderFragmentRoot** pRetVal); HRESULT GetEmbeddedFragmentRoots(_Out_ SAFEARRAY **pRetVal); HRESULT GetRuntimeId(_Out_ SAFEARRAY ** pRetVal); HRESULT Navigate(NavigateDirection direction, _Out_ IRawElementProviderFragment ** pRetVal); HRESULT SetFocus(); // IRawElementProviderAdviseEvents 方法 HRESULT AdviseEventAdded(_In_ EVENTID eventId, _Out_ SAFEARRAY *propertyIDs); HRESULT AdviseEventRemoved(_In_ EVENTID eventId, _Out_ SAFEARRAY *propertyIDs);其中get_HostRawElementProvider()正是前文所述需要 Edge Provider 提供输入 HWND 的关键方法;GetEmbeddedFragmentRoots()与Navigate()则负责在 Xaml 树与浏览器树之间衔接片段与导航。
八、测试与验证:仓库中的 UIA 相关实践
仓库在 controls/dev/WebView2/TestUI 下提供了交互测试页面与自动化测试工程:
WebView2BasicPage.xaml中为前后导航按钮设置了AutomationProperties.AutomationId(如GoBackButton、GoForwardButton),并绑定CanGoBack/CanGoForward驱动IsEnabled,供 UIA 测试驱动与断言;- WebView2BasicPage.xaml.cs 与
WebView2CoreObjectsPage.xaml.cs中有明确的注释说明:浏览器 HWND 的 UIA 树在 WebView2 元素销毁时不会同步断开,其残留会影响测试运行器通过 UIA 激活后续测试,因此测试页面在离开前必须确保所有 WebView 元素被销毁并从 UIA 树移除——这是理解本桥接机制生命周期特性的重要实践注脚; - 交互测试工程位于
InteractionTests/WebView2Tests.cs(WebView2_InteractionTests.projitems),可通过 docs/testing/testing-FAQ.md 中的说明构建运行。
总结
WinUI 3 的 WebView2 无障碍方案,本质上是一套"Provider 桥接"架构:Xaml 侧保留CUIAWrapper作为 UIA 对 Core Xaml 的统一出口,但其关键数据(AutomationPeer 对应关系、输入 HWND)由 Edge 侧的EmbeddedBrowserWebViewUIAProvider通过get_UIAProvider()/GetProviderForHwnd(HWND)两条通道补齐。WebView2AutomationPeer通过IAutomationPeerHwndInterop(GetRawElementProviderSimple+IsCorrectPeerForHwnd)把 Edge 侧能力接入 Xaml 的 HWND 反查流程与 Provider 方法实现,从而在不把 HWND 直接引入开源代码的前提下,实现了 Web 内容无障碍树的完整可见。未来通过 Environment 复用与 Provider 接口扩展,这一桥接还会进一步简化,最终目标是让 WebView2 摆脱对 Xaml Wrapper 的依赖。
【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考