- 跨平台
- 前端
- 移动开发
- OpenHarmony
【免费下载链接】flutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
导读
本文以flutter_flutter(Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本)仓库中的 Windows 平台无障碍(Accessibility)设计文档为骨架,系统讲解 Flutter Windows 桌面端如何同时对接 Windows 两大无障碍接口体系:历史悠久的 Microsoft Active Accessibility(MSAA)与新一代的 UI Automation(UIA)。你将了解到AXPlatformNodeWin、AXFragmentRootPlatformNodeWin等核心类如何实现 COM 接口、WM_GETOBJECT消息如何驱动屏幕阅读器(Windows Narrator、NVDA)与 Flutter 语义树握手,以及引擎如何通过FLUTTER_ENGINE_USE_UIA宏控制 UIA 的启用时机,从而为无障碍功能开发与引擎二次开发提供源码级的参考。
关联文档:Accessibility-on-Windows.md
一、Windows 无障碍接口体系概览
在 Windows 平台上,无障碍工具(Assistive Technology,简称 AT)与被测应用之间的通信主要有两代接口协议,Flutter Windows 引擎两者都做了支持:
- Microsoft Active Accessibility(MSAA):较早出现的无障碍接口,基于 COM,可被 Windows Narrator、NVDA 等屏幕阅读器使用。它功能相对有限,可用的能力少于新一代接口。
- UI Automation(UIA):Windows 新一代无障碍框架,同样服务于 Narrator、NVDA 等工具。相比 MSAA,UIA 提供了更丰富的属性、模式(Pattern)与事件机制,是当前 Windows 无障碍体系的主流接口。
两者的核心差异可以概括为:MSAA 以IAccessible接口为根基,方法以acc_前缀命名;UIA 则以IRawElementProviderFragment、IRawElementProviderFragmentRoot等接口为骨架,配合各类可选的 Control Pattern(如IToggleProvider、ITextProvider)提供细粒度能力。
二、MSAA:基于 IAccessible 的经典无障碍路径
2.1 IAccessible 接口与 acc_ 系列方法
MSAA 的基础是IAccessible接口,所有提供无障碍能力的类都必须继承它,MSAA 的方法统一以acc_前缀命名(如accHitTest、accLocation、accNavigate、get_accName、get_accRole、get_accState等)。
在 Flutter 引擎中,AXPlatformNodeWin继承IAccessible并完整实现了这些方法。从 ax_platform_node_win.h 的 COM 接口映射(BEGIN_COM_MAP/END_COM_MAP)可以看到:
COM_INTERFACE_ENTRY(IAccessible)与COM_INTERFACE_ENTRY(IDispatch)声明了 MSAA 兼容所需的接口;accHitTest(命中测试)、accLocation(屏幕位置)、get_accChildCount(子节点计数)、get_accFocus(焦点对象)、get_accName/get_accRole/get_accState(名称/角色/状态)等均逐一实现。
2.2 COM 对象生命周期约束
由于所有IAccessible对象都是 COM 接口对象,必须按照 COM 规范进行初始化和引用计数管理。AXPlatformNodeWin通过继承CComObjectRootEx<CComMultiThreadModel>获得 ATL 提供的引用计数与QueryInterface基础能力,并在头文件中通过UIA_VALIDATE_CALL()系列宏保证每次调用前检查 delegate 是否仍然有效,失效时返回UIA_E_ELEMENTNOTAVAILABLE,防止屏幕阅读器访问已销毁的对象。
2.3 WM_GETOBJECT 消息与 LresultFromObject
当屏幕阅读器等程序向应用查询无障碍对象时,会向窗口发送WM_GETOBJECT窗口消息。窗口的响应方式是:把根节点的IAccessible传给系统 APILresultFromObject,并将结果返回给消息发送方。
在引擎实现中,该逻辑位于 flutter_window.cc 的FlutterWindow::OnGetObject:
- 取出
lparam的低 32 位作为对象 ID(因为该值有时会被错误地符号扩展),与OBJID_CLIENT比对判断是否为 MSAA 请求; - 当收到 MSAA 请求时,先调用
CreateAxFragmentRoot()确保无障碍根存在,再通过ax_fragment_root_->GetNativeViewAccessible()->QueryInterface(IID_PPV_ARGS(&root))取得根节点的IAccessible,最后以LresultFromObject(IID_IAccessible, wparam, root.Get())生成可返回给消息发送方的 COM 引用。
窗口消息分发的入口在FlutterWindow::HandleMessage中:case WM_GETOBJECT直接调用OnGetObject,若返回值非空则立即返回(见 flutter_window.cc)。
三、UI Automation:新一代无障碍框架
3.1 IRawElementProviderFragment:UIA 片段的最小单位
UIA 要求每一个代表屏幕上无障碍元素的“片段”(fragment)实现IRawElementProviderFragment接口。一个 fragment 就是一个真实存在于屏幕上的无障碍项,例如文本标签、面板、按钮等。
AXPlatformNodeWin同样继承IRawElementProviderFragment,其抽象方法提供了以下核心能力:
- 获取片段在屏幕上的边界:
get_BoundingRectangle,实现位于 ax_platform_node_win.cc。它通过delegate_->GetBoundsRect(kScreenPhysicalPixels, kUnclipped)拿到屏幕物理像素坐标系下的矩形,再填入UiaRect(left/top/width/height)。 - 判断屏幕点是否位于片段内:
Navigate与命中测试相关逻辑,用于遍历相邻元素并判断命中。 - 获取片段的属性(名称、角色等):
GetPropertyValue,内部转发到GetPropertyValueImpl,在 ax_platform_node_win.cc 中按UIA_AriaRolePropertyId、UIA_ControlTypePropertyId、UIA_ClassNamePropertyId、UIA_CulturePropertyId等属性 ID 逐一计算返回VARIANT。 - 返回片段所属的 fragment root:
get_FragmentRoot,通过AXFragmentRootWin::GetForAcceleratedWidget(widget)找到与窗口 HWND 对应的 fragment root,再QueryInterface出IRawElementProviderFragmentRoot返回。
AXPlatformNodeWin实现的全部IRawElementProviderFragment方法都列在 ax_platform_node_win.h,包括Navigate、GetRuntimeId、get_BoundingRectangle、GetEmbeddedFragmentRoots、SetFocus、get_FragmentRoot。
3.2 Fragment Root:片段树的根
IRawElementProviderFragment中有一个方法返回 fragment 的根节点。fragment root 代表一棵 fragment 树的根,需要继承IRawElementProviderFragmentRoot接口。一般情况下——在 Flutter 中也是如此——fragment root 与窗口一一对应(一个 HWND 对应一个 fragment root)。
引擎在 ax_fragment_root_win.cc 中通过AXFragmentRootPlatformNodeWin类实现该接口(声明见 ax_fragment_root_win.h)。该类提供两个关键方法:
- 按坐标取片段:
GetFragmentProviderForHitTest之类的命中测试逻辑,返回给定屏幕坐标处的 fragment。 - 获取树当前焦点:
GetFocus(ax_fragment_root_win.cc),遍历节点找到当前持有焦点的元素并返回其IRawElementProviderFragment。
设计上每个 HWND 暴露一个 fragment root,因为 UIA 会从底层 HWND 层级推断部分信息;持有 HWND 的类(即FlutterWindow)负责持有对应的AXFragmentRootWin。从代码注释看,之所以把IRawElementProviderFragmentRoot单独实现为独立的节点,是因为内部无障碍树可能暂时还不知道自己的根(例如 iframe 的文档反序列化晚于宿主文档),而 COM 规则要求QueryInterface返回的接口集合在对象生命周期内保持静态。
3.3 窗口侧创建 fragment root
FlutterWindow::CreateAxFragmentRoot(flutter_window.cc)负责创建ui::AXFragmentRootWin,并额外创建一个AlertPlatformNodeDelegate与AXPlatformNodeWinalert 节点,通过ax_fragment_root_->SetAlertNode(...)挂到 fragment root 上——该 alert 节点用于向屏幕阅读器播报应用主动发出的消息(alert)。
3.4 WM_GETOBJECT 与 UiaReturnRawElementProvider
窗口的OnGetObject同时处理 UIA 请求:当WM_GETOBJECT消息的lparam等于常量UiaRootObjectId时,说明对方要的是 UIA 根对象。引擎先将窗口的 fragment root 通过QueryInterface提升为IRawElementProviderSimple,再调用UiaReturnRawElementProvider(window_handle_, wparam, lparam, root.Get())创建一个 COM 引用返回给消息发送方(见 flutter_window.cc)。
需要注意的是,UIA 根对象查询逻辑整体包裹在#ifdef FLUTTER_ENGINE_USE_UIA条件编译块中;若未定义该宏,UIA 请求路径将直接跳过,此时引擎只响应 MSAA 请求。
四、Pattern:UIA 的附加能力接口
4.1 Pattern 的含义与查询入口
Pattern(控制模式)是 UIA 元素可选的附加接口,用于提供额外功能。例如:
- 提供
ITextProvider:允许把 fragment 当作格式化文本来处理(读取文档范围、选区、按坐标取文本范围等); - 提供
IToggleProvider:表示该 fragment 可以被切换状态(如复选框),并暴露当前切换状态(on / off / mixed)。
UIA 客户端通过GetPatternProvider方法请求指定类型的接口实例;如果该元素不提供对应模式,则返回nullptr。
在引擎中,GetPatternProvider转发到GetPatternProviderImpl(ax_platform_node_win.cc):它先通过GetPatternProviderFactoryMethod(pattern_id)查表拿到工厂函数,再调用工厂创建实例。工厂方法的分发逻辑中可以看到多种 Pattern 的判定条件,例如:
| Pattern ID | 判定条件 | 返回对象 |
|---|---|---|
UIA_TogglePatternId | SupportsToggle(data.role)成立 | 直接返回节点自身(PatternProvider<IToggleProvider>) |
UIA_TextPatternId/UIA_TextEditPatternId | 是文本、文本输入框或根 Web 区域 | 返回独立对象AXPlatformNodeTextProviderWin::CreateIUnknown |
UIA_ValuePatternId | IsValuePatternSupported成立 | 节点自身(PatternProvider<IValueProvider>) |
UIA_WindowPatternId | 节点带kModal属性 | 节点自身(PatternProvider<IWindowProvider>) |
UIA_TableItemPatternId | 是单元格/表头且表有行列头 | 节点自身(PatternProvider<ITableItemProvider>) |
此外,UIA_AnnotationPatternId、UIA_DragPatternId、UIA_TransformPatternId等一批模式在 ax_platform_node_win.cc 中被明确注释为“Not currently implemented”(当前未实现)。
4.2 Pattern 与 fragment 本体可以不是同一个对象
关键设计点是:返回的 Pattern 实现对象可能就是 fragment 自身,也可能是另一个独立对象。
- IToggleProvider 直接复用节点:当请求
IToggleProvider时,返回的是强转指针后的AXPlatformNodeWin自身,因为该类直接继承IToggleProvider。其实现(ax_platform_node_win.cc)把Toggle()映射为ax::mojom::Action::kDoDefault动作下发,而get_ToggleState则根据CheckedState映射为ToggleState_On/ToggleState_Indeterminate/ToggleState_Off。 - ITextProvider 需要独立对象:当请求
ITextProvider时,返回的是新建的AXPlatformNodeTextProviderWin对象(见 ax_platform_node_textprovider_win.h,该类继承ITextEditProvider并通过 COM map 同时暴露ITextProvider与ITextEditProvider),而不是AXPlatformNodeWin自身。
为何文本模式必须用独立对象?因为AXPlatformNodeWin从其他父接口继承的方法中,存在与ITextProvider同名但签名不同的方法(例如GetSelection、GetVisibleRanges等),导致它无法同时直接继承ITextProvider,否则会发生方法签名冲突。将文本模式拆到独立对象中,既规避了 C++ 多重继承冲突,也保持了 COM 接口实现的清晰边界。
五、UIA 的启用控制:FLUTTER_ENGINE_USE_UIA 宏
引擎是否实际启用 UIA,由Window类中的Window::OnGetObject(即 flutter_window.cc 的FlutterWindow::OnGetObject)控制:
- 收到 UIA 消息(
lparam == UiaRootObjectId)时,只有在编译期定义了FLUTTER_ENGINE_USE_UIA宏,才会走UiaReturnRawElementProvider分支返回 UIA 根对象; - 未定义该宏时,UIA 分支被条件编译剔除,
reference_result保持 0,UIA 查询得不到响应; - 源码中的注释与 TODO 也明确指出 UIA 默认是关闭的(
TODO(schectman): UIA is currently disabled by default.,关联 flutter/flutter#114547 的问题说明,此处仅作背景理解,不展开外部链接)。
因此,若要在自定义引擎构建中启用 UIA,需要在构建配置中定义FLUTTER_ENGINE_USE_UIA宏后重新编译引擎。
六、无障碍请求驱动的语义树启用链路
WM_GETOBJECT不只是返回一个 COM 对象,它还会驱动 Flutter 语义(Semantics)子系统的启动。在 flutter_window.cc 中,无论是 UIA 还是 MSAA 请求,OnGetObject都会先调用OnUpdateSemanticsEnabled(true)。
原因是 Windows 上引擎无法获知屏幕阅读器何时启用/禁用:虽然存在查询屏幕阅读器状态的 API,但该状态并非所有屏幕阅读器都会设置,包括 Windows 自带的 Narrator(该行为在代码注释中明确说明)。因此引擎采用的策略是:只要 Windows 发出针对 MSAA COM 对象的查询(意味着有无障碍工具在访问),就立即开启语义支持。
该调用沿以下链路传递:
FlutterWindow::OnUpdateSemanticsEnabled→binding_handler_delegate_->OnUpdateSemanticsEnabled(enabled);- 在 flutter_windows_view.cc 中转发到
engine_->UpdateSemanticsEnabled(enabled); - 引擎层据此创建/销毁
AccessibilityBridge,从而在 Flutter 侧构建语义树,并通过FlutterWindowsView::GetNativeViewAccessible(flutter_windows_view.cc)把accessibility_bridge_->GetChildOfAXFragmentRoot()暴露为 fragment root 的子节点,形成“UIA fragment root → Flutter 语义树根”的桥接结构。
七、从源码与测试印证实现事实
7.1 单测覆盖
仓库为上述机制提供了单元测试印证:
- window_unittests.cc 中的
DISABLED_GetObjectUia测试验证“根 UIA 对象由WM_GETOBJECT查询”:向窗口注入WM_GETOBJECT(lparam = UiaRootObjectId),断言OnGetObject被调用;测试体内通过FLUTTER_ENGINE_USE_UIA宏条件判断uia_called,再次印证该宏的编译期开关作用。 - ax_platform_node_win_unittest.cc 与 ax_fragment_root_win_unittest.cc、ax_platform_node_textprovider_win_unittest.cc 分别覆盖了
AXPlatformNodeWin、fragment root 与文本 Provider 的行为。
7.2 关键源码文件索引
| 主题 | 文件 |
|---|---|
AXPlatformNodeWin类声明与 COM 接口映射 | ax_platform_node_win.h |
AXPlatformNodeWin各接口实现 | ax_platform_node_win.cc |
| fragment root 实现 | ax_fragment_root_win.cc、ax_fragment_root_win.h |
| 文本 Provider(独立对象) | ax_platform_node_textprovider_win.cc、ax_platform_node_textprovider_win.h |
WM_GETOBJECT处理与 UIA 开关 | flutter_window.cc |
| 语义启用转发链路 | flutter_windows_view.cc |
八、给开发者的实践要点
- MSAA 是最小可用基线:即便不定义
FLUTTER_ENGINE_USE_UIA,引擎也能通过 MSAA 路径为 Narrator / NVDA 提供基础的无障碍访问(名称、角色、状态、命中测试等)。 - UIA 是增强路径,默认关闭:需要更丰富的属性与 Pattern(文本编辑、Toggle 状态、表格、滚动等)时,编译引擎须定义
FLUTTER_ENGINE_USE_UIA宏;启用后注意WM_GETOBJECT的 UIA 分支才会生效。 - COM 生命周期管理是硬性要求:
AXPlatformNodeWin所有实现 UIA COM 接口的方法都以UIA_VALIDATE_CALL()开头,保证 delegate 失效时返回UIA_E_ELEMENTNOTAVAILABLE,二次开发继承该类时务必保持这一约定。 - 模式查询走工厂表:新增自定义 Pattern 时,需要接入
GetPatternProviderFactoryMethod的工厂分发表,并注意“返回节点自身”与“返回独立对象”两种策略的取舍——当存在同名不同签名的方法冲突时(如文本接口),必须拆分独立对象。 - 语义树是惰性启动的:引擎只有在收到无障碍工具的实际查询后才开启语义支持,因此应用侧不应假设语义树默认存在;调试无障碍功能时,先启动屏幕阅读器再观察语义节点创建行为。
结语
Flutter Windows 引擎的无障碍设计是一条“双轨制”实现:以IAccessible/MSAA 保证兼容下限,以 UIA fragment 树与 Pattern 体系提供现代能力,并通过WM_GETOBJECT消息、AXFragmentRootWin与FLUTTER_ENGINE_USE_UIA宏将两条路径有机串联。理解这套机制,无论是排查 Windows 桌面端无障碍问题、为屏幕阅读器适配自定义控件,还是基于本仓库二次开发引擎的无障碍能力,都能获得直接可用的源码级依据。更多 Flutter 无障碍相关设计可进一步阅读 docs/platforms/desktop/windows/ 目录下的其他平台文档。
- 跨平台
- 前端
- 移动开发
- OpenHarmony
【免费下载链接】flutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
相关推荐
深入剖析 Flutter Windows 桌面端无障碍实现:MSAA 与 UI Automation 架构详解
深入剖析 Flutter Windows 桌面端无障碍实现:MSAA 与 UI Automation 架构详解 Windows 桌面端的辅助功能(Accessi
跨平台移动开发前端UI组件桌面应用WPF-Samples 无障碍样例精讲:UI Automation 的 InvokePattern、ExpandCollapsePattern 与 TogglePattern
WPF Samples 无障碍样例精讲:UI Automation 的 InvokePattern、ExpandCollapsePattern 与 Toggle
示例工程Windows Terminal 无障碍架构解析:UI Automation 演进、高对比度与屏幕阅读器交互设计
Windows Terminal 无障碍架构解析:UI Automation 演进、高对比度与屏幕阅读器交互设计 本文基于 Windows Terminal 仓
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考