如何在WPF Expander控件上设置TabIndex?

6
在这个例子中,通过制表键可以从第一个文本框到最后一个文本框,然后到展开头部。
<Window x:Class="ExpanderTab.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"
    FocusManager.FocusedElement="{Binding ElementName=FirstField}">
    <StackPanel>
        <TextBox TabIndex="10" Name="FirstField"></TextBox>
        <Expander TabIndex="20" Header="_abc">
            <TextBox TabIndex="30"></TextBox>
        </Expander>
        <TextBox TabIndex="40"></TextBox>
    </StackPanel>
</Window>

显然,我希望这个展开框的标题能够排在第一个文本框和最后一个文本框之前。有没有一种简单的方法来为展开框的标题分配 TabIndex?
我尝试使用 KeyboardNavigation.IsTabStop="True" 强制展开框成为一个 TabStop,但这会使整个展开框获得焦点,而整个展开框不会响应空格键。再按两次 Tab 键后,标题再次被选中,我可以用空格键打开它。
编辑: 如果有人能想出更简洁的方法来解决这个问题,我会放一个赏金,如果没有,那么 rmoore,你可以获得声望。谢谢你的帮助。

我更新了我的答案,以满足你的所有需求。 - jjxtra
2个回答

10

即使没有TabIndex属性,以下代码也能工作,它们只是为了清楚表明期望的Tab顺序而包含在内。

<Window x:Class="ExpanderTab.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300" FocusManager.FocusedElement="{Binding ElementName=FirstField}">
    <StackPanel>
        <TextBox TabIndex="10" Name="FirstField"></TextBox>
        <Expander TabIndex="20" Header="Section1" KeyboardNavigation.TabNavigation="Local">
            <StackPanel KeyboardNavigation.TabNavigation="Local">
                <TextBox TabIndex="30"></TextBox>
                <TextBox TabIndex="40"></TextBox>
            </StackPanel>
        </Expander>
        <Expander TabIndex="50" Header="Section2" KeyboardNavigation.TabNavigation="Local">
            <StackPanel KeyboardNavigation.TabNavigation="Local">
                <TextBox TabIndex="60"></TextBox>
                <TextBox TabIndex="70"></TextBox>
            </StackPanel>
        </Expander>
        <TextBox TabIndex="80"></TextBox>
    </StackPanel>
</Window>

非常酷,展开时是否有办法让它也尊重 TabIndex? - rmoore
完全解决了,现在所有情况下选项卡都能按预期工作了。 - jjxtra
只有在TabIndex按顺序设置时才能正常工作,就像上面一样。我认为这对于99%的情况都是好的,但它并不完整。 - rmoore
@rmoore - 你在哪些选项卡索引中遇到了问题? - jjxtra
看起来很不错——即使您放弃了明确的TabIndex值,它也能正常工作。 - micahtan
@Jeff Johnson 如果扩展器的TabIndex大于某个项的TabIndex,和/或者如果有一个在扩展器之外的项的TabIndex值在扩展器中的项之间或更低。StackPanel上的Local设置强制它在逃脱之前通过整个StackPanel进行制表符。虽然,正如我所提到的,我并不认为这是一个问题,我甚至无法想出任何我想要反向行为的例子。 - rmoore

3
我找到了一种方法,但肯定有更好的解决方案。
通过Mole查看Expander或者通过Blend查看它的ControlTemplate,我们可以看到响应空格/回车/单击等操作的头部实际上是一个ToggleButton。现在是坏消息,因为头部的ToggleButton对于Expander的展开属性Up/Down/Left/Right有不同的布局,所以它已经通过Expander的ControlTemplate分配了样式。这就排除了我们像在Expander的资源中创建默认的ToggleButton样式这样简单的做法。

alt text

如果您可以访问代码背后,或者不介意将CodeBehind添加到资源字典中的扩展器中,则可以在Expander.Loaded事件中访问ToggleButton并设置TabIndex,如下所示:
<Expander x:Name="uiExpander"
          Header="_abc"
          Loaded="uiExpander_Loaded"
          TabIndex="20"
          IsTabStop="False">
    <TextBox TabIndex="30">

    </TextBox>
</Expander>


private void uiExpander_Loaded(object sender, RoutedEventArgs e)
{
    //Gets the HeaderSite part of the default ControlTemplate for an Expander.
    var header = uiExpander.Template.FindName("HeaderSite", uiExpander) as Control;
    if (header != null)
    {
        header.TabIndex = uiExpander.TabIndex;
    }
}

如果您需要与多个可展开项一起使用,还可以将发送方对象直接转换为Expander。另一个选项是为可展开项创建自己的ControlTemplate,并在其中设置它。
编辑 我们还可以将代码部分移动到AttachedProperty中,使其更加清晰易用:
<Expander local:ExpanderHelper.HeaderTabIndex="20">
    ...
</Expander>

还有附加属性:

public class ExpanderHelper
{
    public static int GetHeaderTabIndex(DependencyObject obj)
    {
        return (int)obj.GetValue(HeaderTabIndexProperty);
    }

    public static void SetHeaderTabIndex(DependencyObject obj, int value)
    {
        obj.SetValue(HeaderTabIndexProperty, value);
    }

    // Using a DependencyProperty as the backing store for HeaderTabIndex.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty HeaderTabIndexProperty =
        DependencyProperty.RegisterAttached(
        "HeaderTabIndex",
        typeof(int),
        typeof(ExpanderHelper),
        new FrameworkPropertyMetadata(
            int.MaxValue,
            FrameworkPropertyMetadataOptions.None,
            new PropertyChangedCallback(OnHeaderTabIndexChanged)));

    private static void OnHeaderTabIndexChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var expander = o as Expander;
        int index;

        if (expander != null && int.TryParse(e.NewValue.ToString(), out index))
        {
            if (expander.IsLoaded)
            {
                SetTabIndex(expander, (int)e.NewValue);
            }
            else
            {
                // If the Expander is not yet loaded, then the Header will not be costructed
                // To avoid getting a null refrence to the HeaderSite control part we
                // can delay the setting of the HeaderTabIndex untill after the Expander is loaded.
                expander.Loaded += new RoutedEventHandler((i, j) => SetTabIndex(expander, (int)e.NewValue));
            }
        }
        else
        {
            throw new InvalidCastException();
        }
    }

    private static void SetTabIndex(Expander expander, int index)
    {
        //Gets the HeaderSite part of the default ControlTemplate for an Expander.
        var header = expander.Template.FindName("HeaderSite", expander) as Control;
        if (header != null)
        {
            header.TabIndex = index;
        }
    }
}

是的,我一直在玩自己的ControlTemplate,但它非常复杂。就像你说的,肯定有更好的方法。 - Eclipse

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