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

时间:2022-08-25 14:17:59

In this example window, tabbing through goes from the first textbox, to the last textbox and then to the expander header.

在此示例窗口中,Tabbing through从第一个文本框转到最后一个文本框,然后转到扩展器标题。

<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>

Obviously, I'd like this to go First text box, expander header, then last textbox. Is there an easy way to assign a TabIndex to the header of the expander?

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

I've tried forcing the expander to be a tabstop using KeyboardNavigation.IsTabStop="True", but that makes the whole expander get focus, and the whole expander doesn't react to the spacebar. After two more tabs, the header is again selected and I can open it with the spacebar.

我已经尝试使用KeyboardNavigation.IsTabStop =“True”强制扩展器成为一个tabstop,但这会使整个扩展器获得焦点,并且整个扩展器不会对空格键做出反应。再过两次选项卡后,再次选择标题,我可以用空格键打开它。

Edit: I'll throw a bounty out there for anyone who can come up with a cleaner way to do this - if not, then rmoore, you can have the rep. Thanks for your help.

编辑:我会为那些能够提出更清洁方式的人提供奖励 - 如果没有,那么rmoore,你可以拥有代表。谢谢你的帮助。

2 个解决方案

#1


10  

The following code will work even without the TabIndex properties, they are included for clarity about the expected tab order.

即使没有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>

#2


3  

I found a way, but there's got to be something better.

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


Looking at the Expander through Mole, or looking at it's ControlTemplate generated by Blend we can see that the header part that is responding to Space/Enter/Click/etc is really a ToggleButton. Now the bad news, Because the Header's ToggleButton has a diffrent layout for the Expander's Expanded properties Up/Down/Left/Right it's already has styles assigned to it through the Expander's ControlTemplate. That precludes us from doing something simple like creating a default ToggleButton style in the Expander's Resources.

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

alt text http://i44.tinypic.com/2dlq1pl.png

替代文字http://i44.tinypic.com/2dlq1pl.png

If you have access to the code behind, or don't mind adding a CodeBehind to the Resource Dictionary that the expander is in, then you can access the ToggleButton and set the TabIndex in the Expander.Loaded event, like this:

如果您可以访问后面的代码,或者不介意将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;
    }
}

You can also just cast the sender object to an Expander too, if you need it to work with multiple expanders. The other option, is to create your own ControlTemplate for the Expander(s) and set it up in there.

如果需要它可以使用多个扩展器,您也可以将发送方对象转换为扩展器。另一种选择是为扩展器创建自己的ControlTemplate并在那里进行设置。

EDIT We can also move the code portion to an AttachedProperty, making it much cleaner and easier to use:

编辑我们还可以将代码部分移动到AttachedProperty,使其更清晰,更易于使用:

<Expander local:ExpanderHelper.HeaderTabIndex="20">
    ...
</Expander>

And the AttachedProperty:

和AttachedProperty:

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;
        }
    }
}

#1


10  

The following code will work even without the TabIndex properties, they are included for clarity about the expected tab order.

即使没有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>

#2


3  

I found a way, but there's got to be something better.

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


Looking at the Expander through Mole, or looking at it's ControlTemplate generated by Blend we can see that the header part that is responding to Space/Enter/Click/etc is really a ToggleButton. Now the bad news, Because the Header's ToggleButton has a diffrent layout for the Expander's Expanded properties Up/Down/Left/Right it's already has styles assigned to it through the Expander's ControlTemplate. That precludes us from doing something simple like creating a default ToggleButton style in the Expander's Resources.

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

alt text http://i44.tinypic.com/2dlq1pl.png

替代文字http://i44.tinypic.com/2dlq1pl.png

If you have access to the code behind, or don't mind adding a CodeBehind to the Resource Dictionary that the expander is in, then you can access the ToggleButton and set the TabIndex in the Expander.Loaded event, like this:

如果您可以访问后面的代码,或者不介意将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;
    }
}

You can also just cast the sender object to an Expander too, if you need it to work with multiple expanders. The other option, is to create your own ControlTemplate for the Expander(s) and set it up in there.

如果需要它可以使用多个扩展器,您也可以将发送方对象转换为扩展器。另一种选择是为扩展器创建自己的ControlTemplate并在那里进行设置。

EDIT We can also move the code portion to an AttachedProperty, making it much cleaner and easier to use:

编辑我们还可以将代码部分移动到AttachedProperty,使其更清晰,更易于使用:

<Expander local:ExpanderHelper.HeaderTabIndex="20">
    ...
</Expander>

And the AttachedProperty:

和AttachedProperty:

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;
        }
    }
}