6

在此示例窗口中,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="_abc">
            <TextBox TabIndex="30"></TextBox>
        </Expander>
        <TextBox TabIndex="40"></TextBox>
    </StackPanel>
</Window>

显然,我希望这个是第一个文本框,扩展标题,然后是最后一个文本框。有没有一种简单的方法可以将 TabIndex 分配给扩展器的标题?

我尝试使用 强制扩展器成为制表符KeyboardNavigation.IsTabStop="True",但这会使整个扩展器获得焦点,并且整个扩展器不会对空格键做出反应。在另外两个选项卡之后,标题再次被选中,我可以用空格键打开它。

编辑:我会为任何可以提出更清洁方法的人提供赏金 - 如果没有,那么 rmoore,你可以拥有代表。谢谢你的帮助。

4

2 回答 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>
于 2009-06-15T22:25:10.740 回答
3

我找到了一种方法,但必须有更好的方法。


通过 Mole 看 Expander,或者看 Blend 生成的 ControlTemplate,我们可以看到响应 Space/Enter/Click/etc 的 header 部分实际上是一个 ToggleButton。现在是个坏消息,因为 Header 的 ToggleButton 对 Expander 的 Expanded 属性 Up/Down/Left/Right 有不同的布局,它已经通过 Expander 的 ControlTemplate 分配了样式。这使我们无法做一些简单的事情,例如在 Expander 的资源中创建默认的 ToggleButton 样式。

替代文字

如果您可以访问后面的代码,或者不介意将 CodeBehind 添加到扩展器所在的资源字典中,那么您可以访问 ToggleButton 并在 Expander.Loaded 事件中设置 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;
    }
}

如果您需要它与多个扩展器一起使用,您也可以将发送器对象转换为扩展器。另一种选择是为扩展器创建自己的 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;
        }
    }
}
于 2009-06-12T22:48:31.897 回答