WPF数据网格和Tab键

12

另一个数据网格的按键绑定问题

我有一个数据网格。它的选择模式设置为 FullRow, KeyboardNavigation.TabNavigation="Once",我希望这样设置可以得到我想要的结果,但实际上并没有。

当焦点在数据网格上时按下 tab 键,它会逐个在表格中的每列之间切换。所以如果我进入具有 4 列的网格,我必须按下 4 次 tab 键才能到达下一个 tabindex。

我想要的是在第一次按下 tab 键时就可以将其从数据网格中移到右侧,并将焦点放在下一个 tabindex 上……如果这有意义的话。

我已经尝试在 keydown 事件处理程序中覆盖 tab 键。

class BetterDataGrid : DataGrid
{
  ..............
  protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
  {
    ..............
    if (e.Key == Key.Tab)
    {
        Console.WriteLine("TAB");
        MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
    }
    .........
  }

它确实会将"TAB"写入控制台,但Tab键仍然保持其默认行为。不确定这是否是下一个tabindex的正确方法,但这样应该使Tab键除了在控制台中写入内容或引发异常外不产生其他作用。
这让我觉得覆盖Tab键行为是不可能的。

希望能得到一些有帮助的输入。
感谢您的提前帮助。

5个回答

11

我想在我的企业软件中使用这个功能,而我找到的唯一解决方法是通过代码后台,使用DataGrid的PreviewKeyDown、GotKeyboardFocus和LostKeyboardFocus事件。我将这些事件处理程序放在了一个WPF修饰器中,以避免为每个单独的DataGrid重复编写它们。可能可以对DataGrid进行子类化,但我还没有尝试过。

以下是处理程序的代码(此示例代码中DataGrid的x:Name为"grid"):

        private IInputElement lastDataGridFocus = null;
    private int selectedcolumnindex = 0;

    void grid_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        if (grid.Items.Count > 0 && (e.NewFocus is DataGrid || (e.NewFocus is DataGridCell && !(e.OldFocus is DataGridCell))))
        {
            DataGridCell cell = null;

            if (lastDataGridFocus != null)
            {
                FocusManager.SetFocusedElement(grid, lastDataGridFocus);
                lastDataGridFocus = null;
                e.Handled = true;
                return;
            }

            if (grid.SelectedCells.Count == 0)
            {
                DataGridRow rowContainer = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(0);
                if (rowContainer != null)
                {
                    DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(rowContainer);
                    cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex((selectedcolumnindex < 0) ? 0 : selectedcolumnindex);
                }
            }
            else
            {
                DataGridCellInfo selectedDataGridCellInfo = (grid.SelectedCells[0] as DataGridCellInfo?).Value;
                DataGridRow rowContainer = (DataGridRow)grid.ItemContainerGenerator.ContainerFromItem(selectedDataGridCellInfo.Item);
                if (rowContainer != null)
                {
                    DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(rowContainer);
                    cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex((selectedcolumnindex < 0) ? 0 : selectedcolumnindex);
                }
            }
            if (null != cell)
            {
                FocusManager.SetFocusedElement(grid, cell as IInputElement);
                e.Handled = true;
            }
        }
    }

    void grid_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        if (!(e.NewFocus is DataGridCell))
        {
            if (grid.CurrentCell != null)
            {
                selectedcolumnindex = grid.Columns.IndexOf(grid.CurrentCell.Column);
            }
        }
    }

    void grid_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (Keyboard.Modifiers == ModifierKeys.Shift && e.Key == Key.Tab)
        {
            lastDataGridFocus = Keyboard.FocusedElement;
            grid.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
            e.Handled = true;
        }
        else if (Keyboard.Modifiers == ModifierKeys.None && e.Key == Key.Tab)
        {
            lastDataGridFocus = Keyboard.FocusedElement;
            grid.MoveFocus(new TraversalRequest(FocusNavigationDirection.Last));
            (Keyboard.FocusedElement as FrameworkElement).MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
            e.Handled = true;
        }
    }

使用此代码,您可以使用光标键在网格中导航,而tab键和shift-tab键让您退出数据网格。如果您从网格中切换,并返回到网格,则还可以到达您离开的同一单元格。这是我的用户和我想要的,也是我认为DataGrid控件应该提供默认行为。


1
抱歉晚了才接受,没有机会测试一下,但这很不错。谢谢。 - Steinthor.palsson
使用 FocusManager.SetFocusedElement 会导致两个问题:1. 它会将 '\t' 重新提交为所选单元格的输入;2. 它偶尔会无法设置键盘焦点和/或选择下一个单元格。使用 Dispatcher.BeginInvoke(new Action(() => cell.Focus())); 应该可以解决这两个问题。 - wondra

0

我也在寻找这种行为。虽然Guge提出的解决方案是一个好的开始,但我不喜欢它如何保存先前存储的元素,也不喜欢它的整体复杂性。最糟糕的是,无论我如何调整它,我都无法让它始终按预期工作。最终,我决定从头开始编写自己的解决方案。通过跳出常规思维(字面上),我想出了一个不同、更简单的解决方案。

在XAML文件中,在DataGrid之前和之后创建一个空控件,如下所示:

<DockPanel>
  <Control IsTabStop="False" x:Name="PreControl" />
  <DataGrid PreviewKeyDown="DataGrid_PreviewKeyDown">...</DataGrid>
  <Control IsTabStop="False" x:Name="PostControl" />
</DockPanel>

然后在代码后台,像这样为DataGrid的PreviewKeyDown事件添加一个函数:

private void DataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
    if (Keyboard.Modifiers == ModifierKeys.Shift && e.Key == Key.Tab)
    {
        PreControl.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
        e.Handled = true;
    }
    else if (Keyboard.Modifiers == ModifierKeys.None && e.Key == Key.Tab)
    {
        PostControl.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
        e.Handled = true;
    }
    else if (new[] { Key.Up, Key.Down, Key.Left, Key.Right }.Contains(e.Key))
    {
        var grid = (DataGrid)sender;
        grid.CurrentCell = new DataGridCellInfo(grid.SelectedItem, grid.CurrentColumn);
    }
}

第一个if和else-if通过从空控件而不是从数据网格中进行制表覆盖了默认的选项卡行为。下一个else-if语句在使用箭头键移动之前更新当前单元格。有时,在进出网格时切换焦点时,当前单元格会与所选单元格不同步。这是先前提出的解决方案以及此解决方案的问题,我还没有找到解决方法,但通过这样做,我可以确保在使用箭头键导航时,它相对于所选单元格而不是当前单元格进行导航。

这种方法的一些注意事项:

  • 当允许选择多行时,它将无法正常工作
  • 如前所述,有时当前单元格可能与所选单元格不同。这可能会导致视觉问题,但不会影响所选项目或导航。
  • 我只彻底测试了具有完整行选择的数据网格。所选列可能无法正确保存或可能存在导航问题。

0
这可能对某些人有所帮助... 我想使用 Tab 键移动到下一行,但同时也要选择或经过包含两个按钮的自定义单元格。解决方案是扩展 DataGrid 列的 CellStyle 并为每个列/单元格设置 FocusableFalse。以下是我的代码:
<DataGrid Grid.Row="1" Name="TutorialsDataGrid" AutoGenerateColumns="False" CanUserAddRows="False" IsReadOnly="True"
        EnableRowVirtualization="True" SelectionUnit="FullRow">
<DataGrid.Columns>
    <DataGridTextColumn Header="Title" Width="0.5*" MinWidth="380" Binding="{Binding Title}">
        <DataGridTextColumn.CellStyle>
            <Style TargetType="DataGridCell" BasedOn="{StaticResource MetroDataGridCell}">
                <Setter Property="Margin" Value="5,0"/>
                <!-- SOLUTION -->
                <Setter Property="Focusable" Value="False"/>
            </Style>
        </DataGridTextColumn.CellStyle>
    </DataGridTextColumn>
    <DataGridTemplateColumn Header="Format" Width="*" MinWidth="200">
        <DataGridTemplateColumn.CellStyle>
            <Style TargetType="DataGridCell" BasedOn="{StaticResource MetroDataGridCell}">
                <Setter Property="Margin" Value="5,0"/>
                <!-- SOLUTION -->
                <Setter Property="Focusable" Value="False"/>
            </Style>
        </DataGridTemplateColumn.CellStyle>
        <DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal" >
                    <Button Name="OpenVideo" ToolTip="Open Video" Margin="5,3" Width="80" Height="28" Tag="{Binding VideoPath}" Click="OpenVideo_Click">
                        <Path Height="16" Stretch="Uniform" Data="{StaticResource UniconsCirclePlay}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    </Button>
                    <Button Name="OpenPDF" ToolTip="Open PDF" Margin="5,3" Width="80" Height="28" Tag="{Binding PdfPath}" Click="OpenPDF_Click">
                        <Path Height="16" Stretch="Uniform" Data="{StaticResource FilePdf}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
                    </Button>
                </StackPanel>
            </DataTemplate>
        </DataGridTemplateColumn.CellTemplate>
    </DataGridTemplateColumn>
</DataGrid.Columns>

0
我终于找到了这个问题的最佳解答,我已经苦苦思索了很长时间。 在某个时刻,我确认在DataGrid中按下Ctrl+Tab会得到期望的行为。 基于此,我认为我可以在DataGrid中交换KeyboardNavigation.TabNavigation附加属性和KeyboardNavigation.ControlTabNavigation附加属性的值。 然而,它并没有起作用。 在使用ReferenceSource检查DataGrid的子元素代码后,我进一步认为我应该在DataGridCellsPanel中交换KeyboardNavigation.TabNavigation附加属性和KeyboardNavigation.ControlTabNavigation附加属性的值。 我运行了它,并得到了理想的行为。 更重要的是,当焦点重新进入时,当前单元格仍然是失去焦点时的当前单元格。
<DataGrid KeyboardNavigation.TabNavigation="Once"
          KeyboardNavigation.ControlTabNavigation="Continue">
    <DataGrid.RowStyle>
        <Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource {x:Type DataGridRow}}">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <DataGridCellsPanel KeyboardNavigation.TabNavigation="Continue"
                                            KeyboardNavigation.ControlTabNavigation="Local"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGrid.RowStyle>
</DataGrid>

它还可以通过以下附加属性实现,参考WinForms中DataGridView的StandardTab属性。
对于VB.net来说。
Public Class DataGridHelper

    Public Shared Function GetStandardTab(element As DataGrid) As Boolean
        If element Is Nothing Then Throw New ArgumentNullException(NameOf(element))
        Return CBool(element.GetValue(StandardTabProperty))
    End Function

    Public Shared Sub SetStandardTab(element As DataGrid, value As Boolean)
        If element Is Nothing Then Throw New ArgumentNullException(NameOf(element))
        element.SetValue(StandardTabProperty, value)
    End Sub

    Public Shared ReadOnly StandardTabProperty As DependencyProperty =
                           DependencyProperty.RegisterAttached("StandardTab",
                           GetType(Boolean), GetType(DataGridHelper),
                           New FrameworkPropertyMetadata(False, FrameworkPropertyMetadataOptions.Inherits, New PropertyChangedCallback(AddressOf OnStandardTabPropertyChanged)))

    Private Shared Sub OnStandardTabPropertyChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        If TypeOf d Is DataGrid OrElse TypeOf d Is DataGridCellsPanel Then
            If CBool(e.NewValue) Then
                If TypeOf d Is DataGridCellsPanel Then
                    d.SetValue(KeyboardNavigation.TabNavigationProperty, KeyboardNavigationMode.Continue)
                    d.SetValue(KeyboardNavigation.ControlTabNavigationProperty, KeyboardNavigationMode.Local)
                Else
                    d.SetValue(KeyboardNavigation.TabNavigationProperty, KeyboardNavigationMode.Once)
                    d.SetValue(KeyboardNavigation.ControlTabNavigationProperty, KeyboardNavigationMode.Continue)
                End If
            Else
                d.ClearValue(KeyboardNavigation.TabNavigationProperty)
                d.ClearValue(KeyboardNavigation.ControlTabNavigationProperty)
            End If
        End If
    End Sub

End Class

对于C#(未经测试)
public class DataGridHelper
{
    public static bool GetStandardTab(DataGrid element)
    {
        if (element == null)
        {
            throw new ArgumentNullException(nameof(element));
        }
        return (bool)element.GetValue(StandardTabProperty);
    }

    public static void SetStandardTab(DataGrid element, bool value)
    {
        if (element == null)
        {
            throw new ArgumentNullException(nameof(element));
        }
        element.SetValue(StandardTabProperty, value);
    }

    public static readonly DependencyProperty StandardTabProperty = 
                                              DependencyProperty.RegisterAttached("StandardTab", 
                                              typeof(bool), typeof(DataGridHelper), 
                                              new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits, new PropertyChangedCallback(OnStandardTabPropertyChanged)));

    private static void OnStandardTabPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is DataGrid || d is DataGridCellsPanel)
        {
            if ((bool) e.NewValue)
            {
                if (d is DataGridCellsPanel)
                {
                    d.SetValue(KeyboardNavigation.TabNavigationProperty, KeyboardNavigationMode.Continue);
                    d.SetValue(KeyboardNavigation.ControlTabNavigationProperty, KeyboardNavigationMode.Local);
                }
                else
                {
                    d.SetValue(KeyboardNavigation.TabNavigationProperty, KeyboardNavigationMode.Once);
                    d.SetValue(KeyboardNavigation.ControlTabNavigationProperty, KeyboardNavigationMode.Continue);
                }
            }
            else
            {
                d.ClearValue(KeyboardNavigation.TabNavigationProperty);
                d.ClearValue(KeyboardNavigation.ControlTabNavigationProperty);
            }
        }
    }
}

-2
你试图实现的不是正确的行为,每个人都期望在焦点在DataGrid上时按Tab键时具有类似Excel的导航。如果您不希望用户通过DataGrid导航,则最好通过在DataGrid上设置IsTabStop="False"来防止在DataGrid上进行制表符停止。

3
为什么要踩我?这只是我从用户体验角度给出的观点! - Mohammed A. Fadil
1
是的,我同意您的看法,Fadil先生。将IsTabStop设置为“False”会更好。 - user683957
3
抱歉给您点了“踩”但是您的回答并没有帮助到我。IsTabStop=false会阻止在网格中使用Tab键,而我想保留该功能,并且当网格被聚焦时,该方法也无法得到期望的行为。虽然在许多情况下,类似 Excel 的导航方式很实用,但在这种情况下它只是多余的,因为该表格是只读的,并且可以完全选择一整行。 - Steinthor.palsson
1
如果这是一个只读的“DataGrid”,我很难理解如何使用制表符。 - JohnB
5
@Hohinhime,你需要在DataGrid中使用Tab键进入,然后使用箭头键在DataGrid内部导航。当你想离开DataGrid时,再次使用Tab键即可。我认为这是最好的方法。而且非常重要的一点是,如果你从DataGrid中切换出去,然后再次切换回来,你应该回到上次停留的那个单元格。那个单元格会成为DataGrid的逻辑焦点。如果你有一个包含几百个单元格的DataGrid,你不想按下每个单元格的Tab键才能离开DataGrid。 - Guge
显示剩余3条评论

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