WPF tab order with custom controls?

2019-02-18 13:46发布

I have a WPF page that contains several out of the box controls with the tab order set.

I have a custom control (NumericSpinner) that contains: border/grid/text box/2 Repeatbuttons (up/down).

Two issues:

1) when I am in the textbox for the custom selector control, I cannot tab out of it to other controls on the page. However after clicking on one of the up/down arrows, I am able to tab over to other controls.

2) I am unable to tab into the textbox of the custom control in order. It is only after I have tabbed through all of the controls, that the cursor lands in the text box (and cannot tab out).

Context:

<ComboBox Margin="97,315,21,0" Name="txtdweldatcdu" Style="{StaticResource fieldComboBoxStyle}" IsSynchronizedWithCurrentItem="True" HorizontalAlignment="Left" VerticalAlignment="Top" TabIndex="10" />
    <WpfControls:NumericSpinner Margin="97,338,21,0" Name="txtdweldatpctcomplete" HorizontalAlignment="Left" VerticalAlignment="Top" AllowNegativeValues="True" MaxValue="100" TabIndex="11" />
    <ComboBox Margin="97,363,21,0" Name="txtdweldatclass" Style="{StaticResource fieldComboBoxStyle}" IsSynchronizedWithCurrentItem="True" HorizontalAlignment="Left" VerticalAlignment="Top" TabIndex="12" />

Portion of the custom control:

 <Border BorderThickness="1" BorderBrush="Gray" Margin="0" HorizontalAlignment="Left" VerticalAlignment="Top" Height="20" Width="117">
        <Grid Margin="0">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="98"/>
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBox Name="valueText" 
                     BorderThickness="0" 
                     Grid.RowSpan="2"
                     Style="{StaticResource spinnerTextBoxStyle}"
                     PreviewKeyDown="valueText_PreviewKeyDown"
                     PreviewTextInput="valueText_PreviewTextInput"
                     TextChanged="valueText_TextChanged"
                     IsReadOnly="{Binding ElementName=Spinner, Path=IsReadOnly}"
                     Text="{Binding ElementName=Spinner, Path=Value, Mode=TwoWay}"
                     KeyboardNavigation.IsTabStop="True"
                     AcceptsTab="True"/>
            <RepeatButton Name="upButton" Style="{StaticResource spinnerRepeatButtonStyle}" Click="upButton_Click"  Grid.Column="1" Grid.Row="0" Height="10" Width="18" VerticalAlignment="Top" HorizontalAlignment="Right" HorizontalContentAlignment="Center">
                <Polygon  HorizontalAlignment="Center" Points="3,2 2,3 4,3"  Fill="Black"  Stretch="Uniform"  Stroke="Black"  StrokeThickness="0" />
            </RepeatButton>
            <RepeatButton Name="downButton" Style="{StaticResource spinnerRepeatButtonStyle}" Click="downButton_Click"  Grid.Column="1" Grid.Row="1" Height="10" Width="18" VerticalAlignment="Top" HorizontalAlignment="Right" HorizontalContentAlignment="Center">
                <Polygon  HorizontalAlignment="Center" Points="2,2 4,2 3,3"  Fill="Black"  Stretch="Uniform"  Stroke="Black"  StrokeThickness="0" />
            </RepeatButton>
        </Grid>
    </Border>

The custom control consists of the xaml and code-behind file.

Parent xaml page, containing all controls is loaded dynamically and contains no code-behind.

In the constructor for the custom control, I have set the following as a test:

    valueText.TabIndex = 3;
    this.TabIndex = 3;

The fourth time I tab, I actually get the cursor into the text field, however I cannot tab out of it.

With this in mind, the first step would be to create a control parameter that I can pass a tab order number that would get set in the control's codebehind.

I created a CustomTabIndex property:

/// <summary>
/// Custom tab index property
/// </summary>
public int CustomTabIndex
{
    get { return (int)GetValue(CustomTabIndexProperty); }
    set { SetValue(CustomTabIndexProperty, value); }
}

public static readonly DependencyProperty CustomTabIndexProperty = 
    DependencyProperty.Register("CustomTabIndex", typeof(int), typeof(NumericSpinner));

And in the xaml, when I try to set CustomTabIndex="3", I receive the error:

The property 'CustomTabIndex' was not found in type 'NumericSpinner'.

Some assistance would be appreciated.

5条回答
不美不萌又怎样
2楼-- · 2019-02-18 13:54

I've tried using the selected answer's GotFocus event, but it doesn't work for me, it seems to disable the tab navigation altogether, even though the first TabStop is indeed selected.
What worked for me was using the WindowActivated event and use the same command there:

MoveFocus(new TraversalRequest(FocusNavigationDirection.First));

I hope it might help someone.

查看更多
我命由我不由天
3楼-- · 2019-02-18 13:57

I have an answer to the first one... In your static constructor for your CustomControl add the following

KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(KeyboardNavigationMode.Local));

That should let you tab in and out of the control and allow you to set a tab index for each of the children of your custom control.

As for your second question, I'm working on figuring out the same thing. I think it has to due with the fact that your custom control has Focusable = False. But setting this to true will make the control get focus not the actual children. I think what might be required is an event handler on your CustomControl for the GotFocus event. When the control gets focus then do a

this.MoveFocus(new TraversalRequest(FocusNavigationDirection.First));

and that should move the focus to the child elements.

I'll post a follow up when I figure out the proper way of doing it.

Update

So for your second point.

Make sure you set focusable to false and the TabNavigationProperty like so in the static constructor for your control

FocusableProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(false));
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(KeyboardNavigationMode.Local));

This will allow the control to work as expected and respect the Tab Order on the control and also all the children. Make sure that in your style you don't set the Focusable or TabNavigation properties.

Raul

查看更多
Luminary・发光体
4楼-- · 2019-02-18 13:58

HaxElit's answer works unless we want to use labels to focus our control by Alt + hotkey like this:

<!-- Alt + C selects numCount -->
<Label Target="{Binding ElementName=numCount}">Elements _Count:</Label>
<local:NumericSpinner x:Name="numCount"/>

So my final solution is as follows:

static NumericSpinner()
{
    // Next line prevents focusing our wrapper parent control. The problem with that is that
    // it prevents Label controls to select our control by Alt+<hotkey>
    //FocusableProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(false));

    // Next line specifies that the children controls have their own tab subtree so a deep
    // traversal is performed when our control is focused
    KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(KeyboardNavigationMode.Local));
}

// our wrapper control is focused invisibly. We must relocate the focus.
protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);

    // Next line moves the focus either forward (to out first inner child)
    // or backward if Shift+TAB is pressed (to the previous control)
    MoveFocus(new TraversalRequest(Keyboard.IsKeyDown(Key.Tab) && (Keyboard.Modifiers & ModifierKeys.Shift) != 0 ? FocusNavigationDirection.Previous : FocusNavigationDirection.Next));
}
查看更多
放我归山
5楼-- · 2019-02-18 14:03

Raul answered the primary issue - being able to tab into the text box of a custom control. The secondary problem was not being able to tab out of the text box.

The issue with this control was that its textbox contains a keypress handler:

PreviewKeyDown="valueText_PreviewKeyDown"

The handler only allows certain keys to be pressed within the textbox:

/// <summary>
/// Since this event handler traps keystrokes within the control, in order to facilitate tabbing order, allowing the 
/// tab key press must be enabled
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void valueText_PreviewKeyDown(object sender, KeyEventArgs e)
{ 
    KeyConverter converter = new KeyConverter();
    string key = converter.ConvertToString(e.Key);
    int index = ((TextBox)sender).CaretIndex;
    if (key != null)
    {
        if (AllowNegativeValues && (e.Key == Key.Subtract || e.Key == Key.OemMinus))
        {
            e.Handled = (valueText.Text.Contains('-') || index > 0) == true;
        }
        else if (AllowDecimal && (e.Key == Key.OemPeriod || e.Key == Key.Decimal))
        {
            e.Handled = valueText.Text.Contains('.') == true;
        }
        else
            e.Handled = ((((e.Key >= Key.D0) && (e.Key <= Key.D9) && (e.KeyboardDevice.Modifiers != ModifierKeys.Shift))
                            || ((e.Key >= Key.NumPad0) && (e.Key <= Key.NumPad9) && (e.KeyboardDevice.Modifiers != ModifierKeys.Shift))
                            || e.Key == Key.Left || e.Key == Key.Right
                            || e.Key == Key.Back || e.Key == Key.Delete 
                            || e.Key == Key.Tab) == false);             
    }
    else
        e.Handled = true;
}

I simply had to add the allowable TAB keystroke and the custom control works fine:

e.Key == Key.Tab
查看更多
我欲成王,谁敢阻挡
6楼-- · 2019-02-18 14:06

I don't have a complete solution for you, but the following four things should be checked for tab-cycling:

  • TabIndex defines the tab-order
  • IsTabStop says if the control should be used for tab-cycling
  • FocusManager.IsFocusScope says, if an element builds its own focus area
  • Focusable says if an UIElement can get the focus.

I could imagine that the IsFocusScope would be interesting.

查看更多
登录 后发表回答