WPF DataGridView键盘焦点从命令/视图模型

3

网络上充满了类似的问题,我也在SO上搜索过(最接近的:[1],[2],[3])。

到目前为止,我无法相信这个问题是如此丑陋/非平凡。

我有一个DataGridView。我还有其他控件(在这种情况下:在DataGridView的列标题中)。我想支持一个快速的“跳转到网格”命令。这个命令需要将键盘焦点设置到网格,以便用户可以使用箭头键在行之间导航。

在下面的简单测试用例中,您可以轻松选择数据网格中的元素,但似乎没有办法给它键盘焦点。除非在代码后台操作中,即使在那种情况下,似乎也必须绕过一些障碍(参见[2],通过调整单元格容器来设置键盘焦点,因为..从我所知道的所有情况来看,网格和行似乎都不起作用)。

看起来很简单吧?

一些简单的模型:

public class Item
{
    public int DayOfMonth { get; set; }
    public string Weekday { get; set; }
}

匹配微不足道的视图模型(假设您有一个RelayCommand实现,请参见JumpToGrid的主要部分):
public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private IList<Item> m_items;
    public IList<Item> SampleItems
    {
        get { return m_items; }
        set { SetField(ref m_items, value, () => SampleItems); }
    }

    private Item m_currentItem;
    public Item CurrentItem
    {
        get { return m_currentItem; }
        set { SetField(ref m_currentItem, value, () => CurrentItem); }
    }

    public ICommand JumpToGridCommand { get; private set; }

    public MainViewModel()
    {
        JumpToGridCommand = new RelayCommand(p => JumpToGrid());

        var items = new List<Item>();
        var today = DateTime.Now;
        for (int i = 1; i <= DateTime.DaysInMonth(today.Year, today.Month); i++ )
        {
            items.Add(new Item { DayOfMonth = i, Weekday = new DateTime(today.Year, today.Month, i).DayOfWeek.ToString() });
        }
        SampleItems = items;
    }

    private void JumpToGrid()
    {
        // I can change the selection just fine
        CurrentItem = SampleItems[0];

        // But the keyboard focus is broken, up/down doesn't work as expected
    }

    protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
    {
        if (selectorExpression == null) throw new ArgumentNullException("selectorExpression");
        MemberExpression body = selectorExpression.Body as MemberExpression;
        if (body == null) throw new ArgumentException("The body must be a member expression");
        var fieldName = body.Member.Name;

        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(fieldName);
        return true;
    }

    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

简单的视图(除了在加载事件中的“DataContext = new MainViewModel()”之外,代码后台为空):
<Window x:Class="DataGridKeyboardFocus.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataGridKeyboardFocus"
        Loaded="Window_Loaded"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid
                ColumnWidth="*" 
                AutoGenerateColumns="False"
                Margin="1"
                ItemsSource="{Binding SampleItems}" 
                SelectedItem="{Binding CurrentItem, Mode=TwoWay}"
                SelectionUnit="FullRow"
                IsReadOnly="True">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding DayOfMonth}">
                    <DataGridTextColumn.HeaderTemplate>
                        <DataTemplate>
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Some label" />
                            <TextBox>
                                <TextBox.InputBindings>
                                    <KeyBinding Modifiers="Control" Key="Tab" Command="{Binding DataContext.JumpToGridCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" />
                                </TextBox.InputBindings>
                            </TextBox>
                        </StackPanel>
                        </DataTemplate>
                    </DataGridTextColumn.HeaderTemplate>
                </DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Weekday}">
                    <DataGridTextColumn.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Vertical">
                                <TextBlock Text="Another label" />
                                <TextBox>
                                    <TextBox.InputBindings>
                                        <KeyBinding Modifiers="Control" Key="Tab" Command="{Binding DataContext.JumpToGridCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" />
                                    </TextBox.InputBindings>
                                </TextBox>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTextColumn.HeaderTemplate>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

1: WPF中的键盘焦点与逻辑焦点 2: 将键盘焦点设置到DataGrid 3: WPF:无法控制键盘焦点

将焦点放在DataGrid上似乎很困难。我使用了GotKeyboardFocus / LostKeyboardFocus事件(以及逻辑焦点变体)来记录焦点变化时的消息。如果我调用Keyboard.Focus(),我可以看到焦点被设置,但是当我按下箭头键时,焦点丢失并且显然移动到最右边的菜单项,因为该菜单会打开。 - fadden
1个回答

5

我已经为网格添加了选择更改事件,并尝试选择单元格。参考下面的代码。

 <DataGrid
            ColumnWidth="*" 
            AutoGenerateColumns="False"
            Margin="1"
            ItemsSource="{Binding SampleItems}" 
            SelectedItem="{Binding CurrentItem, Mode=TwoWay}"
            SelectionUnit="FullRow"
            IsReadOnly="True"
        SelectionChanged="DataGrid_SelectionChanged">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding DayOfMonth}">
                <DataGridTextColumn.HeaderTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Some label" />
                            <TextBox>
                                <TextBox.InputBindings>
                                    <KeyBinding Modifiers="Control" Key="T" Command="{Binding DataContext.JumpToGridCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
                                </TextBox.InputBindings>
                            </TextBox>
                        </StackPanel>
                    </DataTemplate>
                </DataGridTextColumn.HeaderTemplate>
            </DataGridTextColumn>
            <DataGridTextColumn Binding="{Binding Weekday}">
                <DataGridTextColumn.HeaderTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Another label" />
                            <TextBox>
                                <TextBox.InputBindings>
                                    <KeyBinding Modifiers="Control" Key="T" Command="{Binding DataContext.JumpToGridCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
                                </TextBox.InputBindings>
                            </TextBox>
                        </StackPanel>
                    </DataTemplate>
                </DataGridTextColumn.HeaderTemplate>
            </DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>

代码后台。
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        DataGrid dg = sender as DataGrid;
        SelectRowByIndex(dg, dg.SelectedIndex);
    }
    public static DataGridCell GetCell(DataGrid dataGrid, DataGridRow rowContainer, int column)
    {
        if (rowContainer != null)
        {
            DataGridCellsPresenter presenter = FindVisualChild<DataGridCellsPresenter>(rowContainer);
            if (presenter == null)
            {
                /* if the row has been virtualized away, call its ApplyTemplate() method
                 * to build its visual tree in order for the DataGridCellsPresenter
                 * and the DataGridCells to be created */
                rowContainer.ApplyTemplate();
                presenter = FindVisualChild<DataGridCellsPresenter>(rowContainer);
            }
            if (presenter != null)
            {
                DataGridCell cell = presenter.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
                if (cell == null)
                {
                    /* bring the column into view
                     * in case it has been virtualized away */
                    dataGrid.ScrollIntoView(rowContainer, dataGrid.Columns[column]);
                    cell = presenter.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
                }
                return cell;
            }
        }
        return null;
    }
    public static T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(obj, i);
            if (child != null && child is T)
                return (T)child;
            else
            {
                T childOfChild = FindVisualChild<T>(child);
                if (childOfChild != null)
                    return childOfChild;
            }
        }
        return null;
    }
    public static void SelectRowByIndex(DataGrid dataGrid, int rowIndex)
    {
        DataGridRow row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
        if (row != null)
        {
            DataGridCell cell = GetCell(dataGrid, row, 0);
            if (cell != null)
                cell.Focus();
        }
    }

请参考以下链接:http://social.technet.microsoft.com/wiki/contents/articles/21202.wpf-programmatically-selecting-and-focusing-a-row-or-cell-in-a-datagrid.aspx。该链接介绍了如何在 WPF 中编程选择和聚焦数据网格中的行或单元格。

这大致就是我在[2]中找到的,之前无法使其正常工作,但事实证明那是我的问题/在实际应用中的问题。你通过修复我的测试用例向我展示了这种方法可以很好地工作,而我则没有这么做。真对不起自己。谢谢你。 - Benjamin Podszun
这看起来很有前途,但在我的应用程序(.NET Core 6)中,dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) 在初始调用时返回 null。如果选择正在更改,因为我点击了 DataGrid 行,它会返回非空值,但在这种情况下,焦点已经正常工作了。 - fadden

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