【问题标题】:WPF treeview: how to implement keyboard navigation like in Explorer?WPF treeview:如何在资源管理器中实现键盘导航?
【发布时间】:2011-04-16 08:23:49
【问题描述】:

我是第一次使用 WPF 树视图,我对它所做的所有基本操作感到惊讶。其中之一是键盘导航,在任何自尊的树视图中实现,例如在 Windows Explorer 或 Regedit 中。

它应该是这样工作的:

如果树视图具有焦点并且我键入(字母/数字),则选择应移动到当前所选项目下方与我键入的字符串匹配的第一个可见(也称为扩展)项目,并将其显示在视图中。如果在当前项目下方未找到匹配项,则应从顶部继续搜索。如果未找到匹配项,则所选项目不应更改。

只要我继续输入,搜索字符串就会增长并且搜索会得到优化。如果我停止输入一段时间(2-5 秒),搜索字符串就会被清空。

我准备从头开始“手动”编程,但由于这是非常基本的,我想肯定有人已经这样做了。

【问题讨论】:

    标签: wpf treeview keyboard navigation


    【解决方案1】:

    有趣的是,这似乎不是一个热门话题。无论如何,与此同时,我已经开发了一个让我满意的问题的解决方案:

    我将行为附加到 TreeViewItems。在这种行为中,我处理 KeyUp 事件。在 KeyUp 事件处理程序中,我在可视化树显示时从上到下搜索它。如果我找到第一个匹配节点(其名称以按下的键上的字母开头),我会选择该节点。

    【讨论】:

    • @Helge Klein:正如你所说,你已经开发了一个解决方案——你能分享一下代码示例吗?
    【解决方案2】:

    我知道这是一个老话题,但我想它对某些人来说仍然相关。我做了这个解决方案。它附加到 WPF TreeView 上的 KeyUp 和 TextInput 事件。除了 KeyUp 之外,我还在使用 TextInput,因为我很难使用 KeyEventArgs 将“国家”字符转换为真实字符。使用 TextInput 变得更加顺利。

    // <TreeView Name="treeView1" KeyUp="treeView1_KeyUp" TextInput="treeView1_TextInput"/>
    
        private bool searchdeep = true;             // Searches in subitems
        private bool searchstartfound = false;      // true when current selected item is found. Ensures that you don't seach backwards and that you only search on the current level (if not searchdeep is true)
        private string searchterm = "";             // what to search for
        private DateTime LastSearch = DateTime.Now; // resets searchterm if last input is older than 1 second.
    
        private void treeView1_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
        {  
            // reset searchterm if any "special" key is pressed
            if (e.Key < Key.A)
                searchterm = "";
    
        }
    
        private void treeView1_TextInput(object sender, TextCompositionEventArgs e)
        {
            if ((DateTime.Now - LastSearch).Seconds > 1)
                searchterm = "";
    
            LastSearch = DateTime.Now;
            searchterm += e.Text;
            searchstartfound = treeView1.SelectedItem == null;
    
            foreach (var t in treeView1.Items)
                if (SearchTreeView((TreeViewItem) t, searchterm.ToLower()))
                    break;
        }
    
       private bool SearchTreeView(TreeViewItem node, string searchterm)
        {
            if (node.IsSelected)
                searchstartfound = true;
    
            // Search current level first
            foreach (TreeViewItem subnode in node.Items)
            {
                // Search subnodes to the current node first
                if (subnode.IsSelected)
                {
                    searchstartfound = true;
                    if (subnode.IsExpanded)
                        foreach (TreeViewItem subsubnode in subnode.Items)
                            if (searchstartfound && subsubnode.Header.ToString().ToLower().StartsWith(searchterm))
                            {
                                subsubnode.IsSelected = true;
                                subsubnode.IsExpanded = true;
                                subsubnode.BringIntoView();
                                return true;
                            }
                }
                // Then search nodes on the same level
                if (searchstartfound && subnode.Header.ToString().ToLower().StartsWith(searchterm))
                {
                    subnode.IsSelected = true;
                    subnode.BringIntoView();
                    return true;
                }
            }
    
            // If not found, search subnodes
            foreach (TreeViewItem subnode in node.Items)
            {
                if (!searchstartfound || searchdeep)
                    if (SearchTreeView(subnode, searchterm))
                    {
                        node.IsExpanded = true;
                        return true;
                    }
            }
    
            return false;
        }
    

    【讨论】:

    • 谢谢!这非常有效。我只需要将时间更改为 500 毫秒即可达到我的需要。
    【解决方案3】:

    我也在寻找键盘导航,令人惊讶的是,模板项目的解决方案并不明显。

    在 ListView 或 TreeView 中设置 SelectedValuePath 会产生这种行为。 如果项目是模板化的,那么将附加属性:TextSearch.TextPath 设置为要搜索的属性的路径也可以解决问题。

    希望这会有所帮助,它肯定对我有用。

    【讨论】:

    • 我无法让它工作。您能否为 TreeView 使用 HierarchicalDataTemplate 的情况提供代码示例?
    【解决方案4】:

    由于这个问题在搜索时最突出,我想发布一个答案。 当我使用带有 HierarchicalDataTemplate 的数据绑定 TreeView 时,lars 的上述帖子对我不起作用,因为 Items 集合返回实际的数据绑定项,而不是 TreeViewItem。

    我最终解决了这个问题,方法是使用 ItemContainerGenerator 处理单个数据项,并使用 VisualTreeHelper 搜索“向上”以查找父节点(如果有)。我将它实现为一个静态帮助器类,以便我可以轻松地重用它(对我来说基本上是每个 TreeView)。 这是我的助手类:

    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    
    namespace TreeViewHelpers
    {
        public static class TreeViewItemTextSearcher
        {
            private static bool checkIfMatchesText(TreeViewItem node, string searchterm, StringComparison comparison)
            {
                return node.Header.ToString().StartsWith(searchterm, comparison);
            }
    
            //https://stackoverflow.com/questions/26624982/get-parent-treeviewitem-of-a-selected-node-in-wpf
            public static TreeViewItem getParentItem(TreeViewItem item)
            {
                try
                {
                    var parent = VisualTreeHelper.GetParent(item as DependencyObject);
                    while ((parent as TreeViewItem) == null)
                    {
                        parent = VisualTreeHelper.GetParent(parent);
                    }
                    return parent as TreeViewItem;
                }
                catch (Exception e)
                {
                    //could not find a parent of type TreeViewItem
                    return null;
                }
            }
    
            private static bool tryFindChild(
                int startindex,
                TreeViewItem node,
                string searchterm,
                StringComparison comparison,
                out TreeViewItem foundnode
                )
            {
                foundnode = null;
                if (!node.IsExpanded) { return false; }
    
                for (int i = startindex; i < node.Items.Count; i++)
                {
                    object item = node.Items[i];
                    object tviobj = node.ItemContainerGenerator.ContainerFromItem(item);
                    if (tviobj is null)
                    {
                        return false;
                    }
    
                    TreeViewItem tvi = (TreeViewItem)tviobj;
                    if (checkIfMatchesText(tvi, searchterm, comparison))
                    {
                        foundnode = tvi;
                        return true;
                    }
    
                    //recurse:
                    if (tryFindChild(tvi, searchterm, comparison, out foundnode))
                    {
                        return true;
                    }
                }
    
                return false;
            }
            private static bool tryFindChild(TreeViewItem node, string searchterm, StringComparison comparison, out TreeViewItem foundnode)
            {
                return tryFindChild(0, node, searchterm, comparison, out foundnode);
            }
    
            public static bool SearchTreeView(TreeViewItem node, string searchterm, StringComparison comparison, out TreeViewItem found)
            {
                //search children:
                if (tryFindChild(node, searchterm, comparison, out found))
                {
                    return true;
                }
    
                //search nodes same level as this:
                TreeViewItem parent = getParentItem(node);
                object boundobj = node.DataContext;
                if (!(parent is null || boundobj is null))
                {
                    int startindex = parent.Items.IndexOf(boundobj);
                    if (tryFindChild(startindex + 1, parent, searchterm, comparison, out found))
                    {
                        return true;
                    }
                }
    
                found = null;
                return false;
            }
        }
    }
    

    我还保存了最后选择的节点,如this post中所述:

    <TreeView ... TreeViewItem.Selected="TreeViewItemSelected" ... />
    private TreeViewItem lastSelectedTreeViewItem;
    private void TreeViewItemSelected(object sender, RoutedEventArgs e)
    {
        TreeViewItem tvi = e.OriginalSource as TreeViewItem;
        this.lastSelectedTreeViewItem = tvi;
    }
    

    这是上面的TextInput,修改为使用这个类:

    private void treeView_TextInput(object sender, TextCompositionEventArgs e)
    {
        if ((DateTime.Now - LastSearch).Seconds > 1) { searchterm = ""; }
    
        LastSearch = DateTime.Now;
        searchterm += e.Text;
    
        if (lastSelectedTreeViewItem is null)
        {
            return;
        }
    
        TreeViewItem found;
        if (TreeViewHelpers.TreeViewItemTextSearcher.SearchTreeView(
                node: lastSelectedTreeViewItem,
                searchterm: searchterm,
                comparison: StringComparison.CurrentCultureIgnoreCase, 
                out found
            ))
        {
            found.IsSelected = true;
            found.BringIntoView();
        }
    }
    

    注意这个方案和上面的有点不同,我只搜索选中节点的子节点,和选中节点同级的节点。

    【讨论】:

      【解决方案5】:

      这并不像我们预期的那样简单。但我找到的最好的解决方案是在这里: http://www.codeproject.com/Articles/26288/Simplifying-the-WPF-TreeView-by-Using-the-ViewMode

      如果您需要更多详细信息,请告诉我。

      【讨论】:

      • 我当然知道那篇文章。它没有描述如何实现键盘导航。
      • 是的,我同意。虽然我们可以使用那篇文章中的方法来实现简单的搜索,但是增量搜索是非常困难的。正如您所说,您已经开发了一个解决方案 - 您可以分享代码示例吗?
      猜你喜欢
      • 2012-01-01
      • 2011-09-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-11
      • 2015-05-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多