禁用 .net10.0 的 WinForms 自身发出的应用程序通知

背景

.net10.0 的 WinForms 控件在用户交互时(输入文本、切换选中项、展开折叠、改变值等)会主动向操作系统发出辅助功能事件,读屏软件会响应这些事件。这也就是读屏软件里的应用程序通知功能。 但是,这些通知基本上是无意义的,反而制造了干扰信息,所以需要屏蔽这批事件。WinForms 为此提供了官方 AppContext 开关,作用范围是整个进程

原理:NoClientNotifications 开关

开关默认值为 false。当设为 true 后,基类 AccessibleObject 中四个面向 UIA 的播报方法全部被门控(dotnet/winforms main 分支 AccessibleObject.cs:3212-3288):

  • RaiseAutomationEvent
  • RaiseAutomationPropertyChangedEvent
  • RaiseStructureChangedEvent
  • RaiseAutomationNotification

每个方法的核心逻辑均为:

1
2
3
4
if (UiaClientsAreListening() && !AppContextSwitches.NoClientNotifications)
{
    // 实际发出 UIA 事件
}

同时,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:1836ToolStripItem.cs:2019-2024

不受影响:

  • ProgressBar:不调用任何辅助功能事件方法(只发 Win32 消息),Value 变化本身不产生主动播报。
  • 系统代理播报:Windows UIA/IAccessible 代理自动生成的焦点、名称、角色朗读不受此开关控制。

已知怪癖:MSAA 路径逻辑是反的

Control.AccessibilityNotifyClients 的实现(Control.cs:4440)与直觉相反:

1
2
3
4
if (IsHandleCreated && AppContextSwitches.NoClientNotifications)
{
    PInvoke.NotifyWinEvent((uint)accEvent, this, objectID, childID + 1);
}

开关开启时反而会发出 MSAA 事件。后果:

  • 通过该方法播报的 MSAA 事件(ButtonBase 按下、CheckBox/RadioButton 状态切换、ToolStrip 项焦点、ComboBox DropDownList 焦点等)在老式 MSAA 客户端(如部分 NVDA 配置)下仍可能被朗读
  • 主流的 UIA 路径(争渡读屏、讲述人)已基本被静音,因此该开关对绝大多数场景有效。

实施方式

方案 A:代码中设置(推荐)

Program.csMain() 最顶部、任何控件创建及 ApplicationConfiguration.Initialize() 之前设置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
namespace PowerRecorder
{
    internal static class Program
    {
        [STAThread]
        static void Main()
        {
            AppContext.SetSwitch("Switch.System.Windows.Forms.AccessibleObject.NoClientNotifications", true);

            // 其余初始化代码
            ApplicationConfiguration.Initialize();
            Application.Run(new MainForm());
        }
    }
}

方案 B:配置文件设置(持久化)

编辑 PowerRecorder.runtimeconfig.json,在 configProperties 中加入条目:

1
2
3
4
5
6
7
{
  "runtimeOptions": {
    "configProperties": {
      "Switch.System.Windows.Forms.AccessibleObject.NoClientNotifications": true
    }
  }
}

此方式随应用配置永久生效,但不易在运行期临时关闭。

注意事项

  1. 进程级生效:开关是全局的,无法只对单个控件关闭;如需例外,应在控件内部自行控制播报逻辑。
  2. 排除系统代理:开关不阻止 Windows 系统代理为每个 UI 元素自动生成的焦点/名称朗读;若连这些也要屏蔽,需从系统无障碍设置层面处理。
  3. MSAA 残留:由于反逻辑怪癖,老式 MSAA 客户端仍可能收到部分事件,若目标阅读器走 MSAA 需另行验证。
  4. 版本:开关在 .NET 6+ 的 WinForms 中可用(含 .NET 10),代码路径在 main 与 release/10.0 分支一致。
updatedupdated2026-08-182026-08-18