解决 WPF 分组的 ItemsControl 内部控件无法被 UI 自动化识别的问题

2023-10-23 17:00:59 浏览数 (1)

如果你试图给 WPF 的 ItemsControl 加入自动化识别,或者支持无障碍使用,会发现 ItemsControl 内的元素如果进行了分组,则只能识别到组而不能识别到元素本身。如果你正试图解决这个问题,那么本文正好能给你答案。

现象

现在,我们在 ItemsControl 的内部放几个按钮并进行分组。用自动化软件去捕获它,会发现整个 ItemsControl 会被视为一个控件(如下图上方),而我们期望的是像下图下方那样可识别到内部的每一个按钮。

这个例子的最简示例我已经开源到 GitHub 上了,感兴趣可以自行去看看:

  • https://github.com/walterlv/Walterlv.WpfIssues.ItemsControlAutomationIssue

官方推荐的解决方法(但有 bug,无效)

官方其实有一个开关 Switch.System.Windows.Controls.ItemsControlDoesNotSupportAutomation 解决这个问题。但是自 .NET Framework 4.7 开始直到 .NET 6 正式版,这个开关实际上一直都不会生效。

关于如何打开这个开关,可以查看林德熙的博客:https://blog.lindexi.com/post/WPF-Application-Compatibility-switches-list.html#switchsystemwindowscontrolsitemscontroldoesnotsupportautomation

关于这个 bug,我已经向微软官方 GitHub 仓库提了:

  • https://github.com/dotnet/wpf/issues/6861

后面我会解释原因。但是现在我们需要换一个新的方法来解决它。

临时解决方案(在官方 bug 修掉之前是最好方案)

在你的项目中增加一个自己实现的 ItemsControl,源码如下:

代码语言:javascript复制
namespace Walterlv.Windows.Controls;
// The fixed version of the ItemsControl.
public class FixedItemsControl : ItemsControl
{
    protected override AutomationPeer OnCreateAutomationPeer()
    {
        return new ItemsControlWrapperAutomationPeer(this);
    }

    private sealed class ItemsControlWrapperAutomationPeer : ItemsControlAutomationPeer
    {
        public ItemsControlWrapperAutomationPeer(ItemsControl owner) : base(owner)
        {
        }

        protected override ItemAutomationPeer CreateItemAutomationPeer(object item)
        {
            return new ItemsControlItemAutomationPeer(item, this);
        }

        protected override string GetClassNameCore()
        {
            return "ItemsControl";
        }

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

    private class ItemsControlItemAutomationPeer : ItemAutomationPeer
    {
        public ItemsControlItemAutomationPeer(object item, ItemsControlWrapperAutomationPeer parent)
            : base(item, parent)
        { }

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

        protected override string GetClassNameCore()
        {
            return "ItemsControlItem";
        }
    }
}

在你项目里原本需要使用到 ItemsControl 的地方,都换成以上这个修复版的 FixedItemsControl 就可以解决问题。

官方开关不生效的原因

会出现这个原因,是因为 ItemsControl 内部元素分组后,元素会在 GroupItem 中,GroupItem 重写了 OnCreateAutomationPeer 方法并返回了 GroupItemAutomationPeer 的实例。在其 GetChhildrenCore 方法中会试图从 ItemsControl 中获取它的 ItemsControlAutomationPeer 以返回子节点。然而在这段代码中,itemsControl.CreateAutomationPeer() 始终返回 null,所以永远没有子节点。

代码语言:javascript复制
// GroupItemAutomationPeer.cs
protected override List<AutomationPeer> GetChildrenCore()
{
    GroupItem owner = (GroupItem)Owner;
    ItemsControl itemsControl = ItemsControl.ItemsControlFromItemContainer(Owner);
    if (itemsControl != null)
    {
        ItemsControlAutomationPeer itemsControlAP = itemsControl.CreateAutomationPeer() as ItemsControlAutomationPeer;
        if (itemsControlAP != null)
        {
            List<AutomationPeer> children = new List<AutomationPeer>();
            // Ignore this code because in this case it will not be executed.
            return children;
        }
    }

    return null;
}

ItemsControlCreateAutomationPeer 是怎么实现的呢?直接靠 UIElement 基类来实现。可以发现,它单独对 ItemsControl 判断了我们本文一开始所说的开关。

按名称进行推测,ItemsControlDoesNotSupportAutomation 指“ItemsControl 不支持自动化”,也就是说我们需要将其设置为 false 才是让它支持自动化。但实际上这个值无论设置为 true 还是 false 都不会让自动化生效。

代码语言:javascript复制
// UIElement.cs
protected virtual AutomationPeer OnCreateAutomationPeer()
{
    if (!AccessibilitySwitches.ItemsControlDoesNotSupportAutomation)
    {
        AutomationNotSupportedByDefaultField.SetValue(this, true);
    }
    return null;
}

假设设置为 true,那么上述方法直接返回 null 即不会生成自动化节点。显然不能解决问题。

假设设置为 false,那么会设置一个标识位 AutomationNotSupportedByDefaultFieldtrue

现在我们继续看与之相关的代码,即 UIElementCreateAutomationPeer 方法。

代码语言:javascript复制
// UIElement.cs
// Method: CreateAutomationPeer
if (!AccessibilitySwitches.ItemsControlDoesNotSupportAutomation)
{
    // work around (ItemsControl.GroupStyle doesn't show items in groups in the UIAutomation tree)
    AutomationNotSupportedByDefaultField.ClearValue(this);
    ap = OnCreateAutomationPeer();

    // if this element returns an explicit peer (even null), use
    // it.  But if it returns null by reaching the default method
    // above, give it a second chance to create a peer.
    // [This whole dance, including the UncommonField, would be
    // unnecessary once ItemsControl implements its own override
    // of OnCreateAutomationPeer.]
    if (ap == null && !AutomationNotSupportedByDefaultField.GetValue(this))
    {
        ap = OnCreateAutomationPeerInternal();
    }
}
else
{
    ap = OnCreateAutomationPeer();
}

ItemsControlDoesNotSupportAutomation 标识设为 false 时,第一个 if 将进入,OnCreateAutomationPeer 将执行,然后将按前面的代码将 AutomationNotSupportedByDefaultField 标识设置为 true。这会导致第二个 if 不满足条件而退出,从而整个方法执行完毕——没有产生任何自动化节点。

而就算将 ItemsControlDoesNotSupportAutomation 标识设为 true,进入了 elseOnCreateAutomationPeer 内部也不会返回自动化节点。

于是,这个开关完全没有生效!

官方正在解决

在我查出以上原因之后,给官方提了此问题的修复方案,可以让这个开关正常工作。

  • https://github.com/dotnet/wpf/pull/6862

目前这个方案正在审查中。

但在官方合并之前,可以使用我在本文第二小节中提到的方案临时解决问题。

本文会经常更新,请阅读原文: https://cloud.tencent.com/developer/article/2350145 ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

0 人点赞