Turbo Turtle
Turbo Turtle

Reputation: 305

Python: Tkinter Treeview Searchable

Fairly straight forward question, and despite my best Google-Fu I can't find anything on this.

I have a Python app that uses a Tkinter Treeview widget as a table. This works fine for what I need to use it for, but there are going to end up being a couple hundred items in a few of the trees.

Is there anyway to make a Treeview searchable, insofar as when the Tree has focus, the user can just type a few characters and have the first alphabetical match highlighted (rather than a separate entity in the window to type the search pattern into)?

Upvotes: 4

Views: 8942

Answers (3)

Kevin Walzer
Kevin Walzer

Reputation: 556

And, for those who want a Tcl-based solution, here is one:

 proc searchtree {} {

    set children [.tree children {}]
    foreach id $children {
    set text [.tree item $id -text]
    if {$text eq [.e get]} {
        .tree selection set $id

    } else  {
        set children [.tree children $id]
        foreach id $children {
        set text [.tree item $id -text]
        if {$text eq [.e get]} {
            .tree selection set $id
        }
        }
    }
    .tree see $id
    }
}


pack [ttk::treeview .tree]

# Inserted at the root, program chooses id:
.tree insert {} end -id widgets -text "Widget Tour"

# Same thing, but inserted as first child:
.tree insert {} 0 -id gallery -text "Applications"

# Treeview chooses the id:
set id [.tree insert {} end -text "Tutorial"]

# Inserted underneath an existing node:
.tree insert widgets end -text "Canvas"
.tree insert $id end -text "Tree"


pack [button .b -text "Search" -command searchtree]
pack [entry .e ]

Upvotes: 1

FabienAndre
FabienAndre

Reputation: 4604

The feature you are looking for does not exists out of the box, but you can easily code it.

In short: subclass Treeview widget, when a key is pressed on your widget, display an entry in the top right corner (place allow you to superimpose widgets), when you are done, remove the entry.

Here are a few snippets:

1) subclass the widget you want to extend

Instantiate your additionals widgets and your bindings.

class SearchableTreeview(ttk.Treeview):
    def __init__(self, *args, **kwargs):
        ttk.Treeview.__init__(self, *args, **kwargs)
        #create the entry on init but does no show it
        self._toSearch = tk.StringVar()
        self.entry = tk.Entry(self, textvariable=self._toSearch)

        self.bind("<KeyPress>", self._keyOnTree)
        self._toSearch.trace_variable("w", self._search)
        self.entry.bind("<Return>", self._hideEntry)
        self.entry.bind("<Escape>", self._hideEntry)

2) temporary entry

When a key is hit, show the entry with place. entry.place(relx=1, anchor=tk.NE) will show the entry above the tree in the top right corner. If the key pressed is a letter, copy this letter in the entry. Set the focus on the entry so that following key press land in it.

Symmetrically, when Escape or Return is hit while on the entry, flush the content, hide the entry (place_forget), and set the focus to the tree (otherwise, the entry keep the focus, even if not visible).

    def _keyOnTree(self, event):
        self.entry.place(relx=1, anchor=tk.NE)
        if event.char.isalpha():
            self.entry.insert(tk.END, event.char)
        self.entry.focus_set()

    def _hideEntry(self, event):
        self.entry.delete(0, tk.END)
        self.entry.place_forget()
        self.focus_set()

3) actual search code

You are free to search your items the way you want (depth or breadth first, match start or or full string...). Here is an example reusing A Rodas's answer and ignoring case.

    def _search(self, *args):
        pattern = self._toSearch.get()
        #avoid search on empty string
        if len(pattern) > 0:
            self.search(pattern)

    def search(self, pattern, item=''):
        children = self.get_children(item)
        for child in children:
            text = self.item(child, 'text')
            if text.lower().startswith(pattern.lower()):
                self.selection_set(child)
                self.see(child)
                return True
            else:
                res = self.search(pattern, child)
                if res:
                    return True

Upvotes: 6

A. Rodas
A. Rodas

Reputation: 20679

You can define your own recursive method to search in the Treeview widget, and call selection_set on the appropiate child if its text starts with the content of the entry:

import Tkinter as tk
import ttk

class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.entry = tk.Entry(self)
        self.button = tk.Button(self, text='Search', command=self.search)
        self.tree = ttk.Treeview(self)
        # ...
    def search(self, item=''):
        children = self.tree.get_children(item)
        for child in children:
            text = self.tree.item(child, 'text')
            if text.startswith(self.entry.get()):
                self.tree.selection_set(child)
                return True
            else:
                res = self.search(child)
                if res:
                    return True

Upvotes: 5

Related Questions