Виджет Treeview / tkinter 22
В этом материале рассмотрим класс ttk.Treeview , с помощью которого можно выводить информацию в иерархической или форме таблицы.
Каждый элемент, добавленный к классу ttk.Treeview разделяется на одну или несколько колонок. Первая может содержать текст и иконку, которые показывают, может ли элемент быть раскрыт, чтобы показать вложенные элементы. Оставшиеся колонки показывают значения для каждой строки.
Первая строка класса ttk.Treeview состоит из заголовков, которые определяют каждую колонку с помощью имени. Их можно скрыть.
С помощью ttk.Treeview создадим таблицу из списка контактов, которые хранятся в CSV-файле:
Создадим виджет ttk.Treeview с тремя колонками, в каждой из которых будут поля каждого из контактов: имя, фамилия и адрес электронной почты.
Контакты загружаются из CSV-файла с помощью модуля csv , и после этого добавляется связывание для виртуального элемента , который генерируется при выборе одного или большего количества элементов:
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(">", 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(">", self.print_selection)— это виртуальное событие, которое генерируется при выборе одного или нескольких элементов из таблицы. В обработчике 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() на экземпляре виджета с последовательностью " " — здесь можно просто сослаться на реализацию функции-обработчика 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", "", 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(">", 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 нужно сделать автоматически изменяемым горизонтально и вертикально.
После этого выполняется связывание виртуального события " " , которое генерируется при открытии раскрываемого элемента в обработчике open_node() . populate_node() вызывается для загрузки записей конкретной директории: