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

发布于 2024-07-24 04:28:01 字数 973 浏览 11 评论 0原文

在此示例窗口中,按 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,你可以让代表。 感谢您的帮助。

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

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

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.

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

一片旧的回忆 2024-07-31 04:28:01

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

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

<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>
清风不识月 2024-07-31 04:28:01

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

通过 Mole 查看 Expander,或者查看 Blend 生成的 ControlTemplate,我们可以看到响应 Space/Enter/Click/etc 的标头部分实际上是一个 ToggleButton。 现在是坏消息,因为标题的 ToggleButton 对于 Expander 的扩展属性 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>

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

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.

alt text

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:

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

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

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

And the 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;
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文