如何制作具有辅助功能支持的 UWP 自定义(即模板化)按钮控件

How do I make a UWP custom (i.e. templated) button control with accessibility support

我创建了一个 UWP 自定义(即模板化)控件。我如何使其易于访问?

现在,当我尝试将它与 Windows 讲述人工具一起使用时,它的表现很差。有时“讲述人”根本看不到它,有时它会在我不希望它看到时深入到我控制的 UI 元素树中。

最初我以为我只需要设置一些自动化附加属性,但它们没有明显的效果。

因此,当我发现有关 AutomationPeer 对象时,我对此有所了解。基本上每个控件 class 都需要一个关联的 AutomationPeer class,它将您的特定控件的行为映射到一组标准,以供辅助工具使用。

为了简单起见,我做了一个简单的 AccessibleButton class,它直接派生自 Control。 (如果你真的在制作一个按钮,你可能希望从 Button 或 ButtonBase 派生,但它们已经有一个关联的 AutomationPeer class。我只是为了解释的目的而用困难的方式来做。)

这是 Generic.xaml 代码:

<Style TargetType="local:AccessibleButton">
    <Setter Property="UseSystemFocusVisuals" Value="True"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:AccessibleButton">
                <Grid Background="{TemplateBinding Background}">
                    <Border BorderThickness="10">
                        <TextBlock x:Name="Name" Text="{TemplateBinding Label}"/>
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

下面是代码:

public sealed class AccessibleButton : Control
{
    public AccessibleButton()
    {
        this.DefaultStyleKey = typeof(AccessibleButton);
    }

    public static DependencyProperty LabelProperty = DependencyProperty.Register(
        "Label", typeof(string), typeof(AccessibleButton),
        PropertyMetadata.Create(string.Empty));

    public string Label
    {
        set { SetValue(LabelProperty, value); }
        get { return (string)GetValue(LabelProperty); }
    }

    protected override void OnPointerPressed(PointerRoutedEventArgs e)
    {
        Click?.Invoke(this, EventArgs.Empty);
    }

    public event EventHandler Click;
}

以及 MainPage.xaml 中的一些示例用法:

<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <local:AccessibleButton Background="Red" Label="Click Me!" Click="AccessibleButton_Click"/>
    <local:AccessibleButton Background="Green" Label="No, Click Me!" Click="AccessibleButton_Click"/>
    <local:AccessibleButton Background="Blue" Label="Ignore them, Click Me!" Click="AccessibleButton_Click"/>
</StackPanel>

如果您 运行 讲述人并将鼠标悬停在每个按钮上,您会发现它忽略了按钮的边框(您只会听到一点点敲击声)。如果将鼠标悬停在文本上,它有时会阅读整个文本,但通常会阅读单个单词以及文本末尾神秘的 "empty line"。我猜 TextBlock 控件将其输入分解为每个单词的单个对象...

很糟糕。

修复是一个 AutomationPeer。这是基本的 class:

public class AccessibleButtonAutomationPeer : FrameworkElementAutomationPeer
{
    public AccessibleButtonAutomationPeer(FrameworkElement owner): base(owner)
    {
    }
}

(FrameworkElementAutomationPeer 在 Windows.UI.Xaml.Automation.Peers 命名空间中。)

并在 AccessibleButton class 中添加此覆盖以创建它:

protected override AutomationPeer OnCreateAutomationPeer()
{
    return new AccessibleButtonAutomationPeer(this);
}

这还没有任何用处。我们需要添加一些方法。首先让屏幕 reader 无法通过实现 GetChildrenCore() 方法看到我们按钮的内容:

protected override IList<AutomationPeer> GetChildrenCore()
{
    return null;
}

如果你 运行 只用这个,你会发现它现在什么也做不了。如果控件确实获得焦点,它只会说 "Custom"。我们可以通过实现 GetNameCore() 方法让它说出控件标签中的文本:

protected override string GetNameCore()
{
    return ((AccessibleButton)Owner).Label;
}

这有帮助。现在,只要我们 select 一个控件,它就会说出按钮标签文本。但它最后仍然说 "Custom" 。为了解决这个问题,我们必须通过实现 GetAutomationControlTypeCore() 方法来告诉系统它是什么类型的控件,以指示该控件是一个按钮:

protected override AutomationControlType GetAutomationControlTypeCore()
{
    return AutomationControlType.Button;
}

现在它会说按钮标签后跟 "Button"。

这个还真有用!

视力不佳但能够通过鼠标或触摸在屏幕上导航的用户至少知道这些按钮上的标签说的是什么。


AutomationPeers 还允许您通过实施模式提供程序接口来支持 "interaction patterns"。对于这个按钮示例,Invoke 模式是合适的。我们需要实现 IInvokeProvider,因此将其添加到 class 声明中:

public class AccessibleButtonAutomationPeer :
    FrameworkElementAutomationPeer,
    IInvokeProvider

(IInvokeProvider 正在使用 Windows.UI.Xaml.Automation.Provider 命名空间。)

然后覆盖 GetPatternCore 以表明支持:

protected override object GetPatternCore(PatternInterface patternInterface)
{
    if (patternInterface == PatternInterface.Invoke)
    {
        return this;
    }

    return base.GetPatternCore(patternInterface);
}

并实现IInvokeProvider.Invoke方法:

public void Invoke()
{
    ((AccessibleButton)Owner).DoClick();
}

(为了支持这一点,我将 AccessibleButton.OnPointerPressed 方法的主体移到它自己的 DoClick() 方法中,以便我可以从这里调用它。)

要测试这个 select 使用讲述人的按钮,然后按 Caps Lock + Space 调用控件的默认功能。它将使用 Invoke 方法调用 DoClick()。


AutomationPeer class 支持比这更多的功能。如果我着手实施它,我会更新此 post 并提供更多详细信息。