背景
.net10.0 的 WinForms 控件在用户交互时(输入文本、切换选中项、展开折叠、改变值等)会主动向操作系统发出辅助功能事件,读屏软件会响应这些事件。这也就是读屏软件里的应用程序通知功能。 但是,这些通知基本上是无意义的,反而制造了干扰信息,所以需要屏蔽这批事件。WinForms 为此提供了官方 AppContext 开关,作用范围是整个进程。
原理:NoClientNotifications 开关
开关默认值为 false。当设为 true 后,基类 AccessibleObject 中四个面向 UIA 的播报方法全部被门控(dotnet/winforms main 分支 AccessibleObject.cs:3212-3288):
RaiseAutomationEventRaiseAutomationPropertyChangedEventRaiseStructureChangedEventRaiseAutomationNotification
每个方法的核心逻辑均为:
|
|
同时,ControlAccessibleObject.NotifyClients(负责 MSAA 事件)在开关开启时直接 return,不再广播。因此只要控件是通过这些基类方法播报的,无论具体是哪个控件,都会被静音。
影响范围
开关是基类级别的全局门控,判定某个控件是否受影响,只需看它是否调用上述 4 个 UIA 方法或 ControlAccessibleObject.NotifyClients。已确认的受影响控件(源码证据均为 main 分支):
| 控件 | 播报场景 | 源码证据 |
|---|---|---|
| TextBox / RichTextBox / MaskedTextBox | 文本、值、焦点、选区变化 | TextBoxBase.cs:1978-1980, 2172 |
| NumericUpDown / DomainUpDown | 值/文本变化(内嵌 UpDownEdit 即 TextBox) | UpDownBase.UpDownEdit Text setter |
| ComboBox | 选择项、文本、焦点、展开/折叠 | ComboBox.cs:1878, 2526, 2596, 2714, 2887, 2995, 3017 |
| Button / CheckBox / RadioButton | 名称变化走 UIA(被门控) | ButtonBase.cs:551-552 |
| ListBox / CheckedListBox | 选中项、焦点 | ListBox.cs:1714-1883 |
| ListView | 选中项、焦点、勾选 Toggle、Notification | ListView.cs:2212, 4428, 4783, 4825, 4931, 6013 |
| TreeView | 节点焦点、选中、展开/折叠 | TreeView.cs:2114-2236, 3100, 3459 |
| DataGridView | 单元格焦点(经 Cell AccessibleObject),编辑控件是 TextBox/ComboBox | DataGridViewCellAccessibleObject.cs:618 |
| DateTimePicker | 值变化、焦点 | DateTimePicker.cs:1053-1149 |
| TrackBar | 值变化 | TrackBar.cs:983-984 |
| ToolStrip / MenuStrip / StatusStrip | 项焦点走 MSAA 反逻辑路径(见下文怪癖) | ToolStrip.cs:1836、ToolStripItem.cs:2019-2024 |
不受影响:
- ProgressBar:不调用任何辅助功能事件方法(只发 Win32 消息),Value 变化本身不产生主动播报。
- 系统代理播报:Windows UIA/IAccessible 代理自动生成的焦点、名称、角色朗读不受此开关控制。
已知怪癖:MSAA 路径逻辑是反的
Control.AccessibilityNotifyClients 的实现(Control.cs:4440)与直觉相反:
|
|
开关开启时反而会发出 MSAA 事件。后果:
- 通过该方法播报的 MSAA 事件(ButtonBase 按下、CheckBox/RadioButton 状态切换、ToolStrip 项焦点、ComboBox DropDownList 焦点等)在老式 MSAA 客户端(如部分 NVDA 配置)下仍可能被朗读。
- 主流的 UIA 路径(争渡读屏、讲述人)已基本被静音,因此该开关对绝大多数场景有效。
实施方式
方案 A:代码中设置(推荐)
在 Program.cs 的 Main() 最顶部、任何控件创建及 ApplicationConfiguration.Initialize() 之前设置:
|
|
方案 B:配置文件设置(持久化)
编辑 PowerRecorder.runtimeconfig.json,在 configProperties 中加入条目:
|
|
此方式随应用配置永久生效,但不易在运行期临时关闭。
注意事项
- 进程级生效:开关是全局的,无法只对单个控件关闭;如需例外,应在控件内部自行控制播报逻辑。
- 排除系统代理:开关不阻止 Windows 系统代理为每个 UI 元素自动生成的焦点/名称朗读;若连这些也要屏蔽,需从系统无障碍设置层面处理。
- MSAA 残留:由于反逻辑怪癖,老式 MSAA 客户端仍可能收到部分事件,若目标阅读器走 MSAA 需另行验证。
- 版本:开关在 .NET 6+ 的 WinForms 中可用(含 .NET 10),代码路径在 main 与 release/10.0 分支一致。