简单的WPF ItemsControl存在奇怪的焦点行为

11

在焦点和键盘导航方面,我发现了一些奇怪的行为。在下面的示例中,我有一个简单的ItemsControl,它已经被模板化以呈现一个与ItemsSource绑定的CheckBox列表。

<ItemsControl FocusManager.IsFocusScope="True"
              ItemsSource="{Binding ElementName=TheWindow, Path=ListOStrings}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

由于某种奇怪的原因,FocusManager.IsFocusScope="True" 的赋值导致使用鼠标单击复选框时无法设置键盘焦点,并且在使用空格键选中复选框时焦点跳出ItemsControl。这两个症状似乎都指向当复选框被选中时发生了一些奇怪的导航,但我很难找到问题的根源。

如果我使用此方法将视觉树中的任何父级元素设置为焦点范围,则会出现此问题。如果我删除FocusManager.IsFocusScope="True",那么问题就消失了。不幸的是,在一个更大的项目中,我看到这个问题,我不能只删除这些焦点范围而不担心其他与焦点相关的后果。

有人能解释一下我看到的奇怪行为吗?这是一个 bug 还是我完全没有理解它?

2个回答

18

这篇文章解释得非常好:http://www.codeproject.com/KB/WPF/EnhancedFocusScope.aspx

FocusScope 的设计用途是什么?

Microsoft 在 WPF 中使用 FocusScope 创建一个临时的次级焦点。WPF 中的每个 ToolBar 和 Menu 都有自己的焦点范围。

有了这个知识,我们就可以清楚地看到为什么会出现这些问题:

工具栏按钮不应该在其自身上执行命令,而应该在工具栏被单击之前具有焦点的对象上执行。为了实现这一点,路由命令忽略焦点范围的焦点,而是使用“主”逻辑焦点。这就解释了为什么路由命令在焦点范围内不起作用。

测试应用程序截图中的大文本框为什么仍然显示光标?我不知道答案 - 但为什么不能呢?尽管文本框没有键盘焦点(WPF 焦点范围中的小文本框拥有它),但它仍然拥有活动窗口中的主逻辑焦点,并且是所有路由命令的接收者。

这一部分涵盖了您所看到的行为

当您在 WPF 焦点范围中按 Tab 键并按空格键切换 CheckBox 时,为什么键盘焦点会移动到大文本框中?

嗯,这正是您单击菜单项或工具栏时所期望的:键盘焦点应该返回到主焦点。所有 ButtonBase 派生控件都会执行此操作。


我刚刚添加了IsEnhancedFocusScope附加行为的实现。+1解释问题。 - Pavlo Glazkov
5
在这个解释中,我唯一还不明白的是,如果我拿另一个例子,设置网格为焦点作用域,并有一堆按钮和复选框作为子项,当我点击其中一个复选框时,我没有看到在我的ItemsControl中看到的奇怪失焦行为。为什么只有在ItemsControl内的复选框受到影响?这与ScrollViewer、ItemsPresenter有关吗? - jpierson

11

@Meleak很好地解释了问题。请阅读文章http://www.codeproject.com/KB/WPF/EnhancedFocusScope.aspx,以完全理解问题以及如何解决它。我将添加在文章中提到的IsEnhancedFocusScope附加行为的完整实现:

public static class FocusExtensions
{
    private static bool SettingKeyboardFocus { get; set; }

    public static bool GetIsEnhancedFocusScope(DependencyObject element) {
        return (bool)element.GetValue(IsEnhancedFocusScopeProperty);
    }

    public static void SetIsEnhancedFocusScope(DependencyObject element, bool value) {
        element.SetValue(IsEnhancedFocusScopeProperty, value);
    }

    public static readonly DependencyProperty IsEnhancedFocusScopeProperty =
        DependencyProperty.RegisterAttached(
            "IsEnhancedFocusScope",
            typeof(bool),
            typeof(FocusExtensions),
            new UIPropertyMetadata(false, OnIsEnhancedFocusScopeChanged));

    private static void OnIsEnhancedFocusScopeChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e) {
        var item = depObj as UIElement;
        if (item == null)
            return;

        if ((bool)e.NewValue) {
            FocusManager.SetIsFocusScope(item, true);
            item.GotKeyboardFocus += OnGotKeyboardFocus;
        }
        else {
            FocusManager.SetIsFocusScope(item, false);
            item.GotKeyboardFocus -= OnGotKeyboardFocus;
        }
    }

    private static void OnGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) {
        if (SettingKeyboardFocus) {
            return;
        }

        var focusedElement = e.NewFocus as Visual;

        for (var d = focusedElement; d != null; d = VisualTreeHelper.GetParent(d) as Visual) {
            if (FocusManager.GetIsFocusScope(d)) {
                SettingKeyboardFocus = true;

                try {
                    d.SetValue(FocusManager.FocusedElementProperty, focusedElement);
                }
                finally {
                    SettingKeyboardFocus = false;
                }

                if (!(bool)d.GetValue(IsEnhancedFocusScopeProperty)) {
                    break;
                }
            }
        }
    }
}

在你的XAML中,你只需要设置这个附加属性而不是标准的IsFocusScope属性:

<ItemsControl my:FocusExtensions.IsEnhancedFocusScope="True"
              ItemsSource="{Binding ElementName=TheWindow, Path=ListOStrings}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

它将按照您期望的焦点范围运作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接