
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 3microsoft-ui-xaml 仓库中 WebView2 控件如何接入 Windows UI AutomationUIA无障碍体系。WebView2 基于 EdgeAnaheim浏览器内核其内容渲染在独立的浏览器进程中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 的创建链路仓库源码可以逐级印证创建 EnvironmentWebView2.cpp创建一个CoreWebView2EnvironmentEdge 侧称为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再通过.asCoreWebView2Controller()获得控制器、.CoreWebView2()获得 CoreWebView2见 WebView2.cpp。这里走的是**视觉托管模式CompositionController**而非窗口模式。创建 AutomationPeer当 UIA 尝试与 Xaml WebView2 控件交互时WebView2::OnCreateAutomationPeer()返回WebView2AutomationPeer见 WebView2.cppwinrt::AutomationPeer WebView2::OnCreateAutomationPeer() { return winrt::makeWebView2AutomationPeer(*this); }WebView2AutomationPeer继承自FrameworkElementAutomationPeer其 IDL 定义见 WebView2AutomationPeer.idl控制类型为Pane、类名为WebView2见 WebView2AutomationPeer.cpp。三、Core Xaml 侧CUIAWrapper 与 CUIAWindowXaml 运行时Core Xaml对IRawElementProviderSimple及系列接口的实现是CUIAWrapper。它由CUIAWindow创建——可以把 CUIAWindow 视为 CoreWindow 内容的 Provider同时它也负责托管 Popup 与 XamlIslandRoot 的 Xaml 内容此处 CUIAWindow 的名字具有误导性它并非仅限于 CoreWindow 场景。创建链路的两个关键点如下创建时机CUIAWindow 在GetOverrideProviderForHwndImpl()中调用CreateProviderForAP(CAutomationPeer* pAP, CUIAWrapper** ppRet)创建 CUIAWrapper。由于调用发生在该函数内UIA 只给出一个 HWNDXaml 必须据此反查对应的 AutomationPeer。CUIAWrapper 构造所需的两项关键参数元素的 AutomationPeer此处即WebView2AutomationPeer。Xaml 必须在拿到 HWND 后从GetAPForHwnd的树遍历中找到与之匹配的 Peer。WebView 的输入 HWND它被写入UIAHostEnvironmentInfo。IRawElementProviderSimple的get_HostRawElementProvider()方法总是调用 UIA 的UiaHostProviderFromHwnd(HWND)因此 CUIAWrapper 必须把这个输入 HWND 传进去。文档特别指出这两个问题理论上可以直接向 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 侧对应方法EmbeddedBrowserWebViewget_UIAProvider()该 WebView 的EmbeddedBrowserWebViewUIAProviderWebView2AutomationPeer::GetRawElementProviderSimple()EBWebViewEnvironmentGetProviderForHwnd(HWND)与输入 HWND 对应的 WebView 的 ProviderWebView2AutomationPeer::IsCorrectPeerForHwnd(HWND)其中IsCorrectPeerForHwnd(HWND)会同时调用get_UIAProvider()与GetProviderForHwnd()若二者返回的是同一个 Provider则判定该 HWND 属于当前这个 WebView2。源码级印证两个关键方法WinUI 侧的两个底层获取方法实现在 WebView2.cppwinrt::IUnknown WebView2::GetWebView2Provider() { winrt::com_ptrIUnknown provider; if (m_coreWebViewCompositionController) { CoreWebView2RunIgnoreInvalidStateSync( []() { auto coreWebView2CompositionControllerInterop m_coreWebViewCompositionController.asICoreWebView2CompositionControllerInterop(); winrt::check_hresult(coreWebView2CompositionControllerInterop-get_AutomationProvider(provider.put())); }); } return provider.aswinrt::IUnknown(); } winrt::IUnknown WebView2::GetProviderForHwnd(HWND hwnd) { winrt::com_ptrIUnknown provider; if (m_coreWebViewEnvironment) { CoreWebView2RunIgnoreInvalidStateSync( []() { auto coreWebView2EnvironmentInterop m_coreWebViewEnvironment.asICoreWebView2EnvironmentInterop(); winrt::hresult hr coreWebView2EnvironmentInterop-GetAutomationProviderForWindow(hwnd, provider.put()); if (hr ! UIA_E_ELEMENTNOTAVAILABLE) { winrt::check_hresult(hr); } }); } return provider.aswinrt::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.hGUID865F5B88-6506-4E64-A4C5-4B7723650731MIDL_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.cppHRESULT 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_asIRawElementProviderSimple(); if (hwndProvider hwndProvider m_provider) { *value true; } return S_OK; } bool WebView2AutomationPeer::InitProvider() { if (!m_provider) { m_provider GetImpl()-GetWebView2Provider().try_asIRawElementProviderSimple(); } 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.csWebView2_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通过IAutomationPeerHwndInteropGetRawElementProviderSimpleIsCorrectPeerForHwnd把 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),仅供参考