Виджет Treeview / tkinter 22

Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

В этом материале рассмотрим класс ttk.Treeview, с помощью которого можно выводить информацию в иерархической или форме таблицы.

Каждый элемент, добавленный к классу ttk.Treeview разделяется на одну или несколько колонок. Первая может содержать текст и иконку, которые показывают, может ли элемент быть раскрыт, чтобы показать вложенные элементы. Оставшиеся колонки показывают значения для каждой строки.

Первая строка класса ttk.Treeview состоит из заголовков, которые определяют каждую колонку с помощью имени. Их можно скрыть.

С помощью ttk.Treeview создадим таблицу из списка контактов, которые хранятся в CSV-файле:

Использование виджета Treeview

Создадим виджет 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(), и для каждого результата выполняем следующие шаги:

  1. С помощью метода item() получаем словарь параметров и значений выбранного элемента.
  2. Получаем первые три значения словаря item, которые соответствуют фамилии, имени и адресу электронной почты контакта.
  3. Значения форматируются и выводятся в стандартный вывод:

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 создадим базовый браузер файловой системы. Раскрываемые узлы будут представлять собой папки, а после раскрытия они будут показывать вложенные файлы и папки:

Заполнение вложенных элементов в 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)

Появились вопросы? Задайте на Яндекс Кью

У блога есть сообщество на Кью >> Python Q <<. Там я и другие участники отвечаем на вопросы по python, программированию и контенту сайта.

Обучение Python и Data Science

Профессия Python-разработчик / Skillbox

Профессия Python-разработчик / Skillbox

7 313 2 925 ₽/мес.
Профессия Python Fullstack / Skillbox

Профессия Python Fullstack / Skillbox

6 569 2 627 ₽/мес.
Профессия Data Scientist / Skillbox

Профессия Data Scientist / Skillbox

9 187 3 675 ₽/мес.
Python-фреймворк на Django / Skillbox

Python-фреймворк на Django / Skillbox

818 ₽/мес.
Профессия DS: машинное обучение / Skillbox

Профессия DS: машинное обучение / Skillbox

6172 2469 ₽/мес.
Профессия DS: анализ данных / Skillbox

Профессия DS: анализ данных / Skillbox

6172 2469 ₽/мес.

Вам помогла эта статья? Поделитесь в соцсетях или блоге. Репосты помогают сайту развиться.

Александр эксперт Яндекс.Кью
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.
Мои контакты: