Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki
В этом материале рассмотрим класс ttk.Treeview
, с помощью которого можно выводить информацию в иерархической или форме таблицы.
Каждый элемент, добавленный к классу ttk.Treeview
разделяется на одну или несколько колонок. Первая может содержать текст и иконку, которые показывают, может ли элемент быть раскрыт, чтобы показать вложенные элементы. Оставшиеся колонки показывают значения для каждой строки.
Первая строка класса ttk.Treeview
состоит из заголовков, которые определяют каждую колонку с помощью имени. Их можно скрыть.
С помощью ttk.Treeview
создадим таблицу из списка контактов, которые хранятся в CSV-файле:
Создадим виджет ttk.Treeview
с тремя колонками, в каждой из которых будут поля каждого из контактов: имя, фамилия и адрес электронной почты.
Контакты загружаются из CSV-файла с помощью модуля csv
, и после этого добавляется связывание для виртуального элемента <<TreeviewSelect>>
, который генерируется при выборе одного или большего количества элементов:
import csv
import tkinter as tk
import tkinter.ttk as ttk
class App(tk.Tk):
def __init__(self, path):
super().__init__()
self.title("Ttk Treeview")
columns = ("#1", "#2", "#3")
self.tree = ttk.Treeview(self, show="headings", columns=columns)
self.tree.heading("#1", text="Фамилия")
self.tree.heading("#2", text="Имя")
self.tree.heading("#3", text="Почта")
ysb = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscroll=ysb.set)
with open("../lesson_13/contacts.csv", newline="") as f:
for contact in csv.reader(f):
self.tree.insert("", tk.END, values=contact)
self.tree.bind("<<TreeviewSelect>>", self.print_selection)
self.tree.grid(row=0, column=0)
ysb.grid(row=0, column=1, sticky=tk.N + tk.S)
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
def print_selection(self, event):
for selection in self.tree.selection():
item = self.tree.item(selection)
last_name, first_name, email = item["values"][0:3]
text = "Выбор: {}, {} <{}>"
print(text.format(last_name, first_name, email))
if __name__ == "__main__":
app = App(path=".")
app.mainloop()
Если запустить эту программу, то каждый раз при выборе контакта данные о нем будут выводиться в стандартный вывод.
Как работает виджет Treeview
Для создания ttk.Treeview
с несколькими колонками нужно указать идентификатор каждой с помощью параметра columns
. После этого можно настроить текст заголовка с помощью метода heading()
.
Используем идентификаторы #1
, #2
и #3
, поскольку первая колонка, включающая иконку раскрытия и текст, всегда генерируется с идентификатором #0
.
Также параметру show
передается значение «headings», чтобы обозначить, что нужно скрыть колонку #0
, потому что вложенных элементов тут не будет.
Следующие значения являются валидными для параметра show
:
tree
— отображает колонку #0;headings
— отображает строку заголовка;tree headings
— отображает и колонку #0, и строку заголовка (является значением по умолчанию);""
— не отображает ни колонку #0, ни строку заголовка.
После этого к виджету ttk.Treeview
добавляется вертикальный скроллбар:
columns = ("#1", "#2", "#3")
self.tree = ttk.Treeview(self, show="headings", columns=columns)
self.tree.heading("#1", text="Фамилия")
self.tree.heading("#2", text="Имя")
self.tree.heading("#3", text="Почта")
ysb = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscroll=ysb.set)
Для загрузки контактов в таблицу файл нужно обработать с помощью функции render()
из модуля CSV. В процессе строка, прочтенная на каждой итерации, добавляется к ttk.Treeview
.
Это делается с помощью метода insert()
, который получает родительский узел и положение для размещения элемента.
Поскольку все контакты показываются как элементы верхнего уровня, передаем пустую строку в качестве первого параметра и константу END
, чтобы обозначить, что каждый элемент добавляется на последнюю позицию.
Также можно использовать другие аргументы-ключевые слова для метода insert()
. Здесь используется параметр values
, который принимает последовательность значений — они и отображаются в каждой колонке Treeview:
with open("../lesson_13/contacts.csv", newline="") as f:
for contact in csv.reader(f):
self.tree.insert("", tk.END, values=contact)
self.tree.bind("<<TreeviewSelect>>", self.print_selection)
<<TreeviewSelect>>
— это виртуальное событие, которое генерируется при выборе одного или нескольких элементов из таблицы. В обработчике print_selection()
получаем текущее выделение с помощью метода selection()
, и для каждого результата выполняем следующие шаги:
- С помощью метода
item()
получаем словарь параметров и значений выбранного элемента. - Получаем первые три значения словаря
item
, которые соответствуют фамилии, имени и адресу электронной почты контакта. - Значения форматируются и выводятся в стандартный вывод:
def print_selection(self, event):
for selection in self.tree.selection():
item = self.tree.item(selection)
last_name, first_name, email = item["values"][0:3]
text = "Выбор: {}, {} <{}>"
print(text.format(last_name, first_name, email))
Это были базовые особенности класса ttk.Treeview
, поскольку работа велась с обычной таблицей. Однако приложение можно расширить и с помощью более продвинутых особенностей этого класса.
Использование тегов в элементах Treeview
Тэги доступны для элементов ttk.Treeview
, благодаря чему существует возможность связать последовательности события с конкретными строками таблицы Contacts
.
Предположим, что есть необходимость открывать новое окно для добавления информации об электронной почте по двойному клику. Однако это должно работать только для записей, в которых поле email уже заполнено.
Это можно реализовать, добавляя тег с условием при вставке. После этого нужно вызывать tag_bind()
на экземпляре виджета с последовательностью "<Double-Button-1>"
— здесь можно просто сослаться на реализацию функции-обработчика send_email_to_contact()
по имени:
columns = ("Фамилия", "Имя", "Почта")
tree = ttk.Treeview(self, show="headings", columns=columns)
for contact in csv.reader(f):
email = contact[2]
tags = ("dbl-click",) if email else ()
self.tree.insert("", tk.END, values=contact, tags=tags)
tree.tag_bind("dbl-click", "<Double-Button-1>", send_email_to_contact)
По аналогии с тем, что происходит при связывании событий с элементами Canvas, важно не забывать добавлять элементы с тегами к ttk.Treeview
до вызова tag_bind()
, потому что связывания добавляются только к существующим совпадающим элементам.
Заполнение вложенных элементов в Treeview
ttk.Treeview
может использоваться и как обычная таблица, но также — содержать структуры с определенной иерархией. Визуально это напоминает дерево, у которого можно раскрывать определенные узлы.
Это удобно для отображения результатов рекурсивных вызовов и нескольких уровней вложенных элементов. В этом материале рассмотрим сценарий работы с такой структурой.
Для демонстрации рекурсивного добавления элементов в виджет ttk.Treeview
создадим базовый браузер файловой системы. Раскрываемые узлы будут представлять собой папки, а после раскрытия они будут показывать вложенные файлы и папки:
Дерево изначально будет заполняться с помощью метода populate_node()
, который содержит записи текущей директории. Если запись сама является директорией, то она добавляет дочерний раскрываемый узел.
Когда такой узел раскрывается, он «лениво» загружает содержимое с помощью еще одного вызова populate_node()
. В этот раз, вместо добавления элементов в качестве узлов верхнего уровня, они вкладываются внутрь открытого узла:
import os
import tkinter as tk
import tkinter.ttk as ttk
class App(tk.Tk):
def __init__(self, path):
super().__init__()
self.title("Ttk Treeview")
abspath = os.path.abspath(path)
self.nodes = {}
self.tree = ttk.Treeview(self)
self.tree.heading("#0", text=abspath, anchor=tk.W)
ysb = ttk.Scrollbar(self, orient=tk.VERTICAL,
command=self.tree.yview)
xsb = ttk.Scrollbar(self, orient=tk.HORIZONTAL,
command=self.tree.xview)
self.tree.configure(yscroll=ysb.set, xscroll=xsb.set)
self.tree.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W)
ysb.grid(row=0, column=1, sticky=tk.N + tk.S)
xsb.grid(row=1, column=0, sticky=tk.E + tk.W)
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.tree.bind("<<TreeviewOpen>>", self.open_node)
self.populate_node("", abspath)
def populate_node(self, parent, abspath):
for entry in os.listdir(abspath):
entry_path = os.path.join(abspath, entry)
node = self.tree.insert(parent, tk.END, text=entry, open=False)
if os.path.isdir(entry_path):
self.nodes[node] = entry_path
self.tree.insert(node, tk.END)
def open_node(self, event):
item = self.tree.focus()
abspath = self.nodes.pop(item, False)
if abspath:
children = self.tree.get_children(item)
self.tree.delete(children)
self.populate_node(item, abspath)
if __name__ == "__main__":
app = App(path="../")
app.mainloop()
Запуск предыдущего примера выведет иерархию файловой системы в зависимости от того, где запустить этот файл. Однако можно явно указать директорию с помощью аргумента path
конструктора App
.
Как работают выпадающие элементы
В этом примере будем использовать модуль os, который является частью стандартной библиотеки Python и предоставляет удобный способ для выполнения запросов к операционной системе.
Первый раз модуль используется для перевода начального пути в абсолютный, а также для инициализации словаря nodes
, который будет хранить соответствия между расширяемыми элементами и путями к директориям, которые те представляют:
import os
import tkinter as tk
import tkinter.ttk as ttk
class App(tk.Tk):
def __init__(self, path):
# ...
abspath = os.path.abspath(path)
self.nodes = {}
Например, os.path.abspath(".")
вернет абсолютную версию пути к папке, откуда был запущен скрипт. Этот подход лучше использования относительных путей, потому что он помогает не думать о возможных проблемах при работе с путями.
Дальше инициализируется экземпляр ttk.Treeview
с вертикальным и горизонтальным скроллбарами. Параметр text
иконки заголовка будет тем самым абсолютным путем:
self.tree = ttk.Treeview(self)
self.tree.heading("#0", text=abspath, anchor=tk.W)
ysb = ttk.Scrollbar(self, orient=tk.VERTICAL,
command=self.tree.yview)
xsb = ttk.Scrollbar(self, orient=tk.HORIZONTAL,
command=self.tree.xview)
self.tree.configure(yscroll=ysb.set, xscroll=xsb.set)
После этого виджеты размещаются с помощью geometry manager Grid. Экземпляр ttk.Treeview
нужно сделать автоматически изменяемым горизонтально и вертикально.
После этого выполняется связывание виртуального события "<<TreeviewOpen>>"
, которое генерируется при открытии раскрываемого элемента в обработчике open_node()
. populate_node()
вызывается для загрузки записей конкретной директории:
self.tree.bind("<<TreeviewOpen>>", self.open_node)
self.populate_node("", abspath)
Стоит обратить внимание на то, что первый вызов этого метода выполняется с пустой строкой для родительской директории, что значит, что у нее не будет родителей, и ее стоит отображать как элемент верхнего уровня.
В методе populate_node()
перечисляем названия записей директорий с помощью вызова os.listdir()
. Для каждого названия после этого выполняем следующие действия:
- Вычисляем абсолютный путь к записи. В UNIX-системах это делается за счет объединения строк слэшем, а в Windows используются обратные слэши. Благодаря
os.path.join()
с путями можно работать безопасно, не думая об особенностях платформ. - Вставляем строку
entry
как последний дочерний элемент конкретного узлаparent
. Всегда отмечаем их закрытыми, чтобы «лениво» загружать вложенные элементы только тогда, когда те потребуются. - Если абсолютный путь записи указывает на директорию, то добавляется связь для узла и пути в атрибут
nodes
, а также добавляется пустой дочерний элемент, позволяющий раскрывать его:
def populate_node(self, parent, abspath):
for entry in os.listdir(abspath):
entry_path = os.path.join(abspath, entry)
node = self.tree.insert(parent, tk.END, text=entry, open=False)
if os.path.isdir(entry_path):
self.nodes[node] = entry_path
self.tree.insert(node, tk.END)
При нажатии на такой элемент обработчик open_node()
получает выбранный элемент с помощью вызова метода focus()
экземпляра ttk.Treeview
.
Идентификатор элемента используется для получения абсолютного пути, который был добавлен до этого в атрибут nodes
. Чтобы ошибка KeyError
не появлялась, если узел не существует в словаре, используем метод pop()
, который возвращает второй параметр в качестве значения по умолчанию — False.
Если узел существует, очищаем «фейковый» элемент расширяемого узла. Вызов self.tree.get_children(item)
возвращает идентификаторы дочерних элементов item
. После этого они удаляются с помощью вызова self.tree.delete(children)
.
После очистки элемента добавляем реальные дочерние элементы с помощью метода populate_node()
и item
в качестве родителя:
def open_node(self, event):
item = self.tree.focus()
abspath = self.nodes.pop(item, False)
if abspath:
children = self.tree.get_children(item)
self.tree.delete(children)
self.populate_node(item, abspath)