Эффективный оптимизированный графический элемент управления типа "дерево", написанный на Python и Tkinter

Он полностью написан на Python, таким образом для его нормальной работы необходимо и достаточно, чтобы в системе были установлены Python и Tkinter. Никакие другие инструментальные стредства, элементы управления или библиотеки не требуются. Поскольку аналогов предлагаемому элементу управления пока нет, я решил сделать его общедоступным, разместив в Интернете.

История
Графический элемент управления типа "дерево" был создан как часть программы для просмотра графических файлов. Отсутствие подобного элемента управления в системах Unix сразу бросается в глаза на фоне очень гибкого "стандартного элемента управления" ("common control"), являющегося частью Windows 95. (Как бы там ни критиковали Microsoft, они сумели создать несколько новых изящных и очень полезных элементов управления, таких как элемент управления типа "дерево" (tree control), элемент управления типа "закладка" (tab control) и выпадающее комбинированное окно с возможностью прокрутки (drop-down scrollable combo box). Наборам графических элементов управления в системе X11 нужно наверстывать упущенное.) example tree display

Я начал с поисков подходящего элемента управления, написанного на Python с применением Tkinter, и нашел довольно примитивный прототип в разделе contrib основного сайта Python. "Элегантное решение в духе современной компьютерной науки" заключалось в использовании рекурсивного алгоритма, полностью перерисовывающего все дерево всякий раз, когда менялось состояние одного из узлов-каталогов. Для файловой системы с тысячами файлов и каталогов это, очевидно, совершенно неприемлемо.

В ходе поисков, обратив внимание, что такой элемент управления нужен и другим людям, я решил создать свой собственный для свободного распространения.

Сначала я попытался воспользоваться элементом управления Text (текст), так что дерево могло перемещаться вниз, освобождая место для развертывания его ветвей, просто за счет вставки новых текстовых строк. Все соединительные линии строились кусочно, почти как символы текстовой графики MS-DOS. При этом возникли проблемы с быстродействием и чрезвычайно неприятным дрожанием изображения при его перерисовке во время прокрутки. Плюс ко всему выглядело все это просто отвратительно.

Следующая попытка была связана с элементом управления Canvas (холст). Прежде я избегал этого подхода, поскольку не представлял, как можно перемещать большие "ветви" дерева, нарисованные с помощью растровой графики, на лету, не затрачивая на это миллиарды циклов процессора, на таком интерпретируемом языке как Python. Прорыв произошел, когда обнаружилось, что можно пометить "тегами" такие объекты, как линии и растровые рисунки, и затем переместить их в новое положение одной командой Tkinter. Также оказалось, что функция addtag() воспринимает параметры поиска ограничивающего прямоугольника (bounding box), так что очень легко сформулировать инструкцию: "Переместить все, что располагается ниже данного узла, на x пикселей вниз"

Получившаяся конструкция была быстрой, сильно не зависела от размеров дерева и выглядела очень привлекательно с плавной прокруткой. Сворачивание или разворачивание узлов происходило быстро, код остался небольшим и простым для понимания.

Первоначально я создал подкласс элемента управления Frame, в котором линейки прокрутки имелись по умолчанию, но это делало чрезвычайно трудной настройку внутреннего элемента управления Canvas (холст) и навязывало пользователям мои собственные представления о стиле. Такой выбор был продиктован тем, что считается "неправильным" создавать подклассы у других элементов кроме Frame. Тем не менее, затем я создал подкласс элемента Canvas (холст), и мир не перевернулся, а программный код стал более ясным. Возможно, это лишь вопрос выбора стиля программирования. Я был бы рад услышать аргументы за и против. Возникали мысли сделать "дерево" частью набора элементов управления PMW, но в конце концов я рассудил, что элемент будет проще всего использовать, если он останется стандартным холстом (Canvas) со встроенным в него деревом.

Никаких проблем при взаимодействии с элементами управления Tk, Python или оконными системами, с которыми мне приходилось иметь дело, замечено не было. Какой приятный сюрприз.

Протестировать предлагаемый элемент управления можно, запустив файл tree.py интерактивно из командной строки. Если все в порядке, на экране должно появиться интерактивное дерево вашей файловой системы.

Привязки клавиш клавиатуры и мыши
Щелчок мышью по иконке или ярлыку папки вызывает ее сворачивание или разворачивание. [На самом деле и в Windows и в Linux щелкать нужно по крестику, расположенному в узле дерева.- Прим. пер.]

Значение многих клавиш достаточно очевидно: Установка
Загрузите Tree.py и разместите его в каталоге site-python [наверное, имеется в виду каталог Python24\Lib\site-packages.- Прим. пер.] или в любом месте, где Python ищет свои модули. Для этого проще всего в браузере щелкнуть правой кнопкой мыши по ссылке и выбрать опцию "Сохранить ссылку как...".

Методы элемента Tree (дерево)
Ниже перечисляются методы, которые поддерживаются элементом управления "дерево" в дополнение к обычным методам элемента Canvas (холст).
Tree(master, root_id, root_label, get_contents_callback, [dist_x], [dist_y], [text_offset], [line_flag], [expanded_icon], [collapsed_icon], [regular_icon], [expandable_icon], [collapsible_icon], [node_class], [drop_callback])
Создает новый объект Tree (дерево). Это подкласс элемента управления Tkinter Canvas (холст). Таким образом, аргумент master относится именно к этому родительскому элементу Tkinter, и все остальные ключевые слова-аргументы обрабатываются классом Canvas. Будет неплохой идеей использовать в своих программах все ключевые слова при обращении к Tkinter, поскольку порядок следования аргументов может меняться.

expanded_icon и collapsed_icon будут значками по умолчанию для развертываемых узлов, созданных в этом элементе управления. Развертываемые узлы также дополняются иконками - индикаторами состояния (развернутый/свернутый) на конце соединительных линий; управлять этими значками можно с помощью аргументов expandable_icon и collapsible_icon. regular_icon будет значком по умолчанию для неразвертываемых узлов.

root_id и root_label - это идентификатор и текст метки для корневого узла, соответственно.

get_contents_callback - функция, вызываемая перед сжатием/расширением узла. Обработав узел, меняющий свое состояние, она вызывает add_node(), чтобы создать потомков этого узла.

dist_x - расстояние в пикселах, на которое иконки дочернего дерева смещаются по отношению к иконкам родительской ветви. Вертикальные соединительные линии будут разделяться интервалами, кратными этому расстоянию. dist_y - это расстояние между узлом и его родителем или между сестринскими узлами по вертикали. text_offset - расстояние по горизонтали между иконкой узла и его меткой.

line_flag делает соединительные линии невидимыми, когда имеет значение "false" ("ложь"). По умолчанию имеет значение "true" ("истина"), т.е. соединительные линии видимы.

node_class - по умолчанию вспомогательный класс Node (узел). С его помощью можно создать подкласс Node и вставить в него свой собственный класс.

drop_callback вызывается после корректной операции "drag and drop" ("перетащи и оставь"). В качестве аргументов при обращении используются узел-источник и узел назначения.

add_node([name], [id], [flag], [expanded_icon], [collapsed_icon])
Данный метод используется пользовательской функцией get_contents_callback() для создания дочерних узлов у развернутого в текущий момент времени узла. Если не задать изображения для иконок, будут использоваться картинки по умолчанию.

add_list([list], [name], [id], [flag], [expanded_icon], [collapsed_icon])
Этот метод похож на add_node(), за исключением того, что к заданному списку добавляется информация о новом узле. Метод полезен для генерации списков новых узлов при использовании insert_before(), insert_after() и insert_children().

Обратите внимание, что рассматриваемый метод возвращает список и не требует аргумента в виде списка, так что выражение: n.insert_after(add_list(name='foo')) будет корректно осуществлять операцию быстрой вставки одиночных узлов.

find_full_id()
Возвращает первый узел, соответствующий заданному списку атрибутов full_id(), или значение None (ничего), если не удается найти ни одного такого узла.

see(node)
Перемещает изображение, пока узел node не станет полностью видимым.

move_cursor(node)
Перемещает курсор к узлу node.

cursor_node()
Возвращает узел, находящийся под курсором.

toggle()
Переключает состояние узла (открытый/закрытый) под курсором.

next()
Перемещает курсор к следующему (располагающемуся ниже) узлу.

prev()
Перемещает курсор к предыдущему (располагающемуся выше) узлу.

ascend()
Перемещает курсор к непосредственному предку текущего узла.

descend()
Пытается раскрыть узел под курсором и перемещается к его первому потомку. Если это невозможно, переходит к следующему лежащему ниже узлу.

first()
Перемещает курсор к корневому узлу.

last()
Перемещает курсор к последнему узлу.

pageup()
Перемещает курсор на одну "страницу" вверх, причем страницей считается текущая высота окна.

pagedown()
Перемещает курсор на одну "страницу" вниз подобно pageup().

Методы, относящиеся к узлам
Дерево состоит из отдельных объектов - узлов.

Обратите внимание, что в коде встречаются методы, относящиеся к узлам, с префиксом "PVT_". Такие методы являются служебными, частными для данного модуля и могут быть кардинально изменены в будущем по желанию программиста.
Node(parent_node, id, x, y, [parent_widget], [collapsed_icon], [expanded_icon], [label], [expandable_flag])
Создается новый объект - узел Node, относящийся к родительскому элементу parent_widget, в точке с координатами x,y с текстовой меткой label справа от иконки. collapsed_icon и expanded_icon - это изображения, создаваемые путем вызова PhotoImage(); если они не определены, то рисунки заимствуются у родительского элемента. Если значение expandable_flag - "истина", данный узел может быть развернут/свернут двойным щелчком мышки.

set_collapsed_icon(icon)
Задает изображение, показывающее, что данный узел свернут. Изображение должно быть создано заранее путем обращения к PhotoImage() или же элемент tree (дерево) воспользуется картинкой, заложенной в него по умолчанию. Также имеется изображение для обозначения нераскрываемых узлов.

set_expanded_icon(icon)
Задает изображение, показывающее, что данный узел развернут. Изображение должно быть создано заранее путем обращения к PhotoImage() или же элемент tree (дерево) воспользуется картинкой, заложенной в него по умолчанию.

parent()
Возвращает узел-родитель текущего узла.

prev_sib()
Возвращает предыдущий сестринский узел. Это узел, расположенный непосредственно над текущим узлом и имеющий того же самого родителя. Если текущий узел является первым дочерним узлом своего предка, то возвращается значение None (никакой).

next_sib()
Возвращает следующий сестринский узел. Это узел, расположенный непосредственно под текущим узлом и имеющий того же самого родителя. Если текущий узел является последним дочерним узлом своего предка, то возвращается значение None (никакой).

prev_visible()
Возвращает узел непосредственно над текущим узлом.

next_visible()
Возвращает узел непосредственно под текущим узлом.

children()
Возвращает список дочерних узлов.

get_label()
Возвращает текстовую метку, связанную с данным узлом, в виде строки.

set_label(text)
Определяет текстовую метку, связанную с данным узлом, присаивая ей значение в виде текстовой строки.

full_id()
Возвращает идентификатор текущего узла и идентификаторы всех родителей в виде списке.

expanded()
Возвращает значение "true" ("истина"), если узел развернут, "false" ("ложь") - в противном случае.

expandable()
Возвращает значение "true" ("истина"), если узел возможно развернуть, "false" ("ложь") - в противном случае.

expand()
Развертывает узел, если у него установлен флаг, показывающий, что данный узел может быть развернут, выводит на экран узлы-потомки (если таковые имеются) и меняет иконку на изображение для развернутого узла. Перед началом этой операции вызывается функция get_contents_callback(), которая, как предполагается, выполняет ряд обращений к add_child(), чтобы определить, какие узлы-потомки будут отображаться на экране.

collapse()
Свертывает узел, если он развернут, удаляя всех его потомков с экрана и меняя иконку на изображение для свернутого узла.

toggle_state()
Изменяет состояние узла с развернутого на свернутое и наоборот в соответствии с правилами для expand() и collapse().

delete()
Удаляет узел и всех его потомков из дерева.

insert_before(nodes)
Вставляет список узлов непосредственно перед заданным. Поскольку операции вставки очень ресурсоемкие, список новых узлов вставляется за один шаг. Для генерации списка узлов достаточно вызвать функцию дерева add_node().

insert_after(nodes)
Вставляет список узлов непосредственно после заданного.

insert_children(nodes)
Вставляет список узлов в начало списка узлов-потомков.

Изображения иконок
Если есть необходимость поддерживать свои собственные иконки различного размера, по-видимому, нужно задать dist_x, dist_y и text_offset для компенсации. По умолчанию иконки имеют размер примерно 15x15 пикселов.

Для начала здесь приводятся прозрачные файлы в формате GIF89a, которые применяются для создания иконок по умолчанию. Поскольку функция PhotoImage распознает только GIF-файлы, которые закодированы в соответствии со стандартом MIME и выглядят как текстовая строка, воспользуйтесь модулем Python "base64", чтобы получить строку, которую можно будет вставлять в программный код. Такая техника позволяет избежать использования нескольких небольших вспомогательных файлов в формате GIF.

>>> import base64
>>> x=open('mini-file.gif').read()
>>> base64.encodestring(x).strip()
'R0lGODlhCwAOAJEAAAAAAICAgP///8DAwCH5BAEAAAMALAAAAAALAA4AAAIphA+jA+JuVgtUtMQe PJlWCgSN9oSTV5lkKQpo2q5W+wbzuJrIHgw1WgAAOw=='

page-style regular file icon Иконка файла
manila folder style closed folder icon Иконка закрытой папки
manila folder style open folder icon Иконка открытой папки
expandable node icon Расширяемая иконка
collapsible node icon Сжимаемая иконка

Файл treedemo-icons.py - это демонстрация возможностей управления внешним видом элемента управления "дерево". Я сделал прозрачные GIF-файлы из картинок, используемых менеджером файлов KDE, убрав соединительные линии, чтобы добиться большего сходства. Также в примере введены 3 различных цвета для иконок для различения символических связей, обычных файлов и специальных файлов. Первоначальные иконки для файлов приводятся ниже:
triangle pointing down open.gif
triangle pointing right close.gif
white file file1.gif
green file file2.gif
red file file3.gif

Drag'n'Drop ("перетащи и оставь")
Пользоваться технологией drag'n'drop ("перетащи и оставь") довольно просто. Прежде всего, нужно передать некоторую функцию аргументу drop_callback конструктора Tree. Эта функция будет вызываться с двумя аргументами. Первый аргумент - это перетаскиваемый узел (источник), второй аргумент - узел, на который "бросается" перетаскиваемый узел (цель). Во время вызова этой функции необходимо обновить внутренние структуры данных вашего приложения, чтобы отразить изменения, инициированные операцией drag'n'drop. К примеру, элемент управления файл-менеджера должен произвести реальное перемещение какого-либо файла или директории.

Также данная функция в случае необходимости выполняет обновление самого дерева. Если возникает, исчезает или перемещается какой-либо узел, функция, о которой идет речь, обязательно должна это сделать.

При этом применяется Tkdnd, так что любые другие элементы управления, которые также используют Tkdnd, будут способны к взаимодействию, и над ними можно производить операцию drag'n'drop. Допустим, веб-браузер должен иметь возможность вставлять ссылки в дерево закладок. В этом случае источником будет объект-ссылка, а не узел. Функция обратного вызова создаст узел, включающий в себя новую закладку и вставит ее в нужное место.

treedemo-dnd.py демонстрирует метод "drag'n'drop" на примере двух полностью независимых друг от друга деревьев в отдельных окнах.

Дополнительные примеры
treedemo-dirs.py - это автономный пример, делающий то же самое, что и пример в самом модуле. Разобраться в нем будет легче, чем копаться в коде самого элемента управления.

treedemo-complex.py иллюстрирует использование подкласса Node (узел) для добавления к узлам дерева контекстного меню в стиле Windows, а также методов вставки/удаления.


Python для инженеров и исследователей

Автор: Charles E. "Gene" Cash
Web-адрес: http://home.cfl.rr.com/genecash/
[Сама ссылка не работает. Осталась ее копия в архиве Интернета. Зато у этого проекта есть продолжение, сделанное уже другими людьми,- Multi select Tree Control for Tkinter.- Прим. пер.]

Перевод на русский язык: Филипп Занько
Лицензия перевода: Разрешается свободное распространение и использование настоящего перевода для любых целей при условии сохранения текста перевода в неизменном виде.

О замеченных ошибках, неточностях, опечатках просьба сообщать по электронному адресу:
russianlutheran@gmail.com