WPF树视图:如何像在资源管理器中一样实现键盘导航?

发布于 2024-09-24 19:33:07 字数 380 浏览 1 评论 0原文

我是第一次使用 WPF 树视图,并对它做的所有基本事情感到惊讶。其中之一是键盘导航,在任何独立的树视图中实现,例如在 Windows 资源管理器或 Regedit 中。

这应该是这样工作的:

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

只要我继续输入,搜索字符串就会不断增长,搜索也会变得更加精细。如果我停止输入一段时间(2-5 秒),搜索字符串就会被清空。

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

I am using the WPF treeview for the first time and am astonished of all the basic things it does not do. One of those is keyboard navigation, implemented in any self-respecting treeview, e.g. in Windows Explorer or Regedit.

This is how it should work:

If the treeview has the focus and I type (letters/numbers) the selection should move to the first visible (aka expanded) item below the currently selected item that matches the string I typed and bring that into view. If not match is found below the current item the search should continue from the top. If no match is found, the selected item should not change.

As long as I continue typing, the search string grows and the search is refined. If I stop typing for a certain time (2-5 seconds), the search string is emptied.

I am prepared to program this "by hand" from scratch, but since this is so very basic I thought surely someone has already done exactly this.

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

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

发布评论

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

评论(5

默嘫て 2024-10-01 19:33:07

有趣的是,这似乎不是一个热门话题。不管怎样,与此同时,我已经开发了一个令我满意的问题解决方案:

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

Funny, this does not seem to be a popular topic. Anyway, in the meantime I have developed a solution to the problem that satisfies me:

I attach a behavior to the TreeViewItems. In that behavior, I handle KeyUp events. In the KeyUp event handler, I search the visual tree top to bottom as it is displayed. If I find a first matching node (whose name starts with the letter on the key pressed) I select that node.

胡大本事 2024-10-01 19:33:07

我知道这是一个老话题,但我想它对某些人来说仍然相关。我做了这个解决方案。它附加到 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;
    }

I know that is an old topic, but I guess it is still relevant for some people. I made this solution. It is attached to the KeyUp and the TextInput event on a WPF TreeView. I'm using TextInput in addition to KeyUp as I had difficulty translating "national" chars to real chars with KeyEventArgs. That went much more smooth with 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;
    }
白衬杉格子梦 2024-10-01 19:33:07

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

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

希望这有帮助,它绝对对我有用。

I was also looking for keyboard navigation, amazing how not obvious the solution was for templated items.

Setting SelectedValuePath in ListView or TreeView gives this behavior.
If the items are templated then setting the attached property: TextSearch.TextPath to the path of the property to search on will also do the trick.

Hope this helps, it definitely worked for me.

§普罗旺斯的薰衣草 2024-10-01 19:33:07

由于这个问题在搜索时出现得最多,所以我想发布一个答案。
当我使用带有 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;
        }
    }
}

我还保存了最后选择的节点,如这篇文章中所述:

<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();
    }
}

请注意,此解决方案与上面的解决方案略有不同,因为我只搜索所选节点的子节点以及与所选节点处于同一级别的节点。

Since this question comes up most prominently when searching, I wanted to post an answer to it.
The above post by lars doesn't work for me when I'm using a databound TreeView with a HierarchicalDataTemplate, because the Items collection returns the actual databound items, not the TreeViewItem.

I ended up solving this by using the ItemContainerGenerator for individual data items, and the VisualTreeHelper to search "up" to find the parent node (if any). I implemented this as a static helper class so that I can easily reuse it (which for me is basically every TreeView).
Here's my helper class:

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

I also save the last selected node, as described in 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;
}

And here's the above TextInput, modified to use this class:

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();
    }
}

Note that this solution is a little bit different from the above, in that I only search the children of the selected node, and the nodes at the same level as the selected node.

巷子口的你 2024-10-01 19:33:07

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

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

It is not very straightforward as we expect it to be. But the best solution I have found is here:
http://www.codeproject.com/Articles/26288/Simplifying-the-WPF-TreeView-by-Using-the-ViewMode

Let me know if you need more details.

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