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

Мы неоднократно будем возвращаться к менеджеру компоновки в этой главе и использовать его во многих примерах книги. В главе 9 мы также познакомимся с альтернативным менеджером компоновки grid и системой размещения виджетов в контейнере в табличном виде (то есть по строкам и колонкам). Третий вариант, менеджер компоновки placer, описывается в документации Tk и не описывается в данной книге - он менее популярен, чем менеджеры pack и grid, и его использование может оказаться непростым делом при создании крупных графических интерфейсов.


Запуск программ с графическим интерфейсом

Как и любой другой программный код на языке Python, модуль из примера 7.1 может быть запущен несколькими способами: путем запуска как самостоятельного сценария:

C:\...\PP4E\Gui\Intro> python gui1.py

с помощью операции импортирования из интерактивного сеанса Python или из другого файла модуля:

>>> import gui1

путем запуска его, как выполняемого файла Unix, если добавить в начало файла особую строку, начинающуюся с #!:

% gui1.py &

или любым другим способом, которым программы Python могут быть запущены на вашей платформе. Например, этот сценарий можно также запустить щелчком мыши на имени файла в проводнике Windows, или его код может быть введен в интерактивной оболочке, в ответ на приглашение >>>.33 Можно даже выполнить его из программы на языке C, вызвав соответствующую функцию прикладного интерфейса встраивания (подробности об интеграции с программами на языке C можно найти в главе 20).

Иными словами, практически нет специальных правил, которым требуется следовать, чтобы запустить графический интерфейс, реализованный на языке Python. Интерфейс tkinter (и сама библиотека Tk) связан с интерпретатором Python. Когда программа на языке Python вызывает функции GUI, они просто за кулисами передаются встроенной графической подсистеме. Это облегчает написание инструментов командной строки, которые вызывают появление всплывающих окон, -они выполняются так же, как обычные текстовые сценарии, которые изучались в предыдущей части книги.

Как избежать появления окна консоли DOS в Windows

В главах 3 и 6 отмечалось, что если имя программы имеет расширение .pyw, а не .py, запуск сценария Python в Windows не вызывает появления окна консоли DOS, которое обслуживает стандартные потоки ввода-вывода сценария, запускаемого щелчком мыши на ярлыке. Теперь, когда мы наконец-то начали создавать собственные окна, этот прием с именем файла становится еще более полезным.

Чтобы видеть только окна, создаваемые сценарием, независимо от способа его запуска, дайте файлам своих сценариев с графическим интерфейсом расширение .pyw, если они будут выполняться в Windows. Например, щелчок на файле примера 7.2 в проводнике Windows создает только окно, изображенное на рис. 7.1.

Пример 7.2. PP4E\Gui\Intro\gui1.pyw ...то же, что gui1.py...

Избежать появления всплывающих окон DOS в Windows можно также, запуская программу с выполняемым файлом pythonw.exe вместо python.exe (в действительности файлы .pyw просто зарегистрированы для открытия посредством pythonw). В Linux расширение .pyw не мешает, но и не является необходимым - в Unix-подобных системах нет понятия всплывающих окон для подключения потоков ввода-вывода. С другой стороны, если в будущем потребуется выполнять ваши сценарии с графическим интерфейсом в Windows, добавление «w» в конце имени может освободить вас от необходимости каких-то дополнительных действий по переносу. В данной книге имена файлов .py иногда используются для вызова окон консоли, чтобы видеть сообщения, выводимые в Windows.


Альтернативные приемы использования tkinter

Как можно предполагать, есть разные способы реализации примера gui1. Например, если желательно более явным образом описать в сценарии импортируемые из tkinter элементы, импортируйте весь модуль и добавляйте имя модуля ко всем относящимся к нему именам, как в примере 7.3.

Пример 7.3. PP4E\Gui\Intro\gui1b.py - import против from

import tkinter

widget = tkinter.Label(None, text='Hello GUI world!’)

widget.pack()

widget.mainloop()

В действующих примерах это может быть утомительным - библиотека tkinter экспортирует десятки классов графических переменных и констант, которые повсюду встречаются в сценариях с графическим интерфейсом на языке Python. На самом деле обычно проще использовать спецификатор *, чтобы одним махом импортировать все, находящееся в модуле tkinter. Это показано в примере 7.4.

Пример 7.4. PP4E\Gui\Intro\gui1c.py - константы и функции вместе

from tkinter import * root = Tk()

Label(root, text=’Hello GUI world!’).pack(side=TOP) root.mainloop()

Модуль tkinter старается экспортировать только то, что действительно необходимо, поэтому он один из немногих, для которых формат импорта со спецификатором * можно применять относительно безопасно.34 К примеру, константа TOP в вызове функции pack является одной из многих, экспортируемых модулем tkinter. Это просто имя переменной (TOP="top"), которой заранее присвоено значение в модуле constants, автоматически загружаемом пакетом tkinter.

При размещении графических элементов можно указать, к какому краю родительского контейнера они должны быть прикреплены, - TOP, BOTTOM, LEFT или RIGHT. Если параметр side не передается функции pack (как в предшествующих примерах), виджет по умолчанию прикрепляется к верхнему краю (TOP). В целом, более крупные графические интерфейсы на базе tkinter могут конструироваться как набор прямоугольников, прикрепляемых к надлежащим сторонам других, охватывающих их прямоугольников. Как будет показано позднее, tkinter располагает графические элементы в прямоугольнике, в соответствии с очередностью их размещения и параметром side. Если виджеты располагаются по сетке, им присваиваются номера строк и колонок. Однако все это имеет смысл, только если в окне присутствует больше одного виджета, поэтому продолжим наши исследования.

Обратите внимание, что в этой версии метод pack вызывается сразу после создания метки, которая не присваивается никакой переменной. Если нет необходимости сохранять виджет, можно разместить его таким способом на месте, и этим сэкономить строку программного кода. Мы будем использовать этот прием, если графический элемент прикрепляется к более крупной структуре и нигде в программном коде больше не используется. Сложности могут возникнуть, если присваивать результат, возвращаемый pack, но объяснение этого откладывается до того времени, когда будут изучены еще некоторые основные понятия.

Мы также используем здесь в качестве родителя экземпляр класса Tk виджета вместо None. Объект Tk представляет главное («корневое») окно программы - которое открывается вместе с запуском программы. Автоматически созданный объект Tk используется также как родительский виджет по умолчанию, когда в вызовы других методов виджетов не передается никакого родителя или когда в качестве родителя передается None. Иными словами, по умолчанию виджеты просто прикрепляются к главному окну программы. В данном сценарии это поведение по умолчанию реализуется явно, путем создания и передачи самого объекта Tk. В главе 8 будет показано, как для создания новых всплывающих окон, действующих независимо от главного окна программы, используются виджеты Toplevel.

Некоторые методы виджетов в tkinter экспортируются также в виде функций, что позволяет сократить пример 7.5 до трех строчек кода.

Пример 7.5. PP4E\Gui\Intro\gui1d.py - минимальная версия

from tkinter import *

Label(text=’Hello GUI world!’).pack() mainloop()

Функцию mainloop из модуля tkinter можно вызывать относительно виджета или непосредственно (то есть как метод или как функцию). В этом варианте мы не передавали конструктору Label аргумент родителя: если он опущен, то по умолчанию будет использоваться значение None (что, в свою очередь, по умолчанию означает автоматически создаваемый объект Tk). Но использование этого значения по умолчанию становится менее удобным при создании более крупных графических интерфейсов. Такие элементы, как метки, чаще прикрепляются к другим контейнерам виджетов.


Основы изменения размеров виджетов

Размер окон верхнего уровня - таких как окно, создававшееся во всех продемонстрированных до сих пор вариантах, обычно может изменяться пользователем - нужно просто растянуть окно с помощью мыши. На рис. 7.2 показано, как выглядит увеличенное в размерах окно.

Это не очень хорошо - метка сохраняет положение у верхнего края родительского окна, вместо того чтобы оказаться в середине, но положение легко исправить с помощью пары параметров метода pack, как показано в примере 7.6.

Пример 7.6. PP4E\Gui\Intro\gui1e.py - растягивание окна

from tkinter import *

Label(text=’Hello GUI world!’).pack(expand=YES, fill=BOTH) mainloop()

Рис. 7.2. Увеличенное окно сценария gui1

При размещении виджетов можно указывать, должен ли виджет увеличиться в размерах, чтобы заполнить все свободное пространство, и если да, то как он должен измениться, чтобы заполнить все это пространство. По умолчанию виджеты не увеличиваются вслед за родителем. Но в данном сценарии имена YES и BOTH (импортированные из модуля tkinter) указывают, что метка должна увеличиваться вместе со своим родителем, то есть с главным окном. Так оно и происходит, как видно на рис. 7.3.

Рис. 7.3. Сценарий gui1e c виджетом, изменяющим размер

Технически, менеджер компоновки назначает размер каждому отображаемому виджету, исходя из его содержимого (длины текстовой строки и так далее). По умолчанию виджет может занимать только отведенное ему пространство и не может быть больше назначенного ему размера. Параметры expand и fill позволяют более точно определять правила автоматического изменения размеров:

Параметр expand=YES

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

Параметр fill

Может использоваться для растяжения виджета, чтобы он занял все выделенное ему пространство.

Сочетания этих двух параметров производят различные эффекты расположения и изменения размеров, при этом некоторые из них имеют смысл только при наличии в окне нескольких графических элементов. Например, использование параметра expand без fill приводит к размещению виджета в центре занимаемого им пространства, а параметр fill может определять возможность изменения размеров только по вертикали (fill=Y), только по горизонтали (fill=X) или по обеим осям одновременно (fill=BOTH). Определяя эти ограничения и стороны прикрепления для всех виджетов в графическом интерфейсе наряду с порядком размещения, можно довольно точно управлять их взаимным расположением. В последующих главах будет показано, что менеджер компоновки grid использует совершенно другой протокол изменения размеров, но при необходимости может обеспечивать похожий порядок размещения.

Впервые столкнувшись с этим, можно запутаться, и мы вернемся к этому позднее. Но если вы не уверены в том, какой результат будет иметь некоторая комбинация параметров expand и fill, просто попробуйте, что получится, - в конце концов, это Python. А пока запомните, что комбинация expand=YES и fill=BOTH встречается, вероятно, чаще всего и означает «изменить размеры отведенного мне места, чтобы оно занимало все свободное пространство, и растянуть меня так, чтобы заполнить освободившееся пространство во всех направлениях». Для нашего примера «Hello World» итоговым результатом будет рост метки при увеличении размеров окна, поэтому метка всегда остается в центре.


Настройка параметров графического элемента и заголовка окна

До сих пор мы сообщали библиотеке tkinter, что должно выводиться в метке, передавая ее текст в именованном аргументе при вызове конструктора метки. Оказывается, существует еще два способа определения параметров метки. В примере 7.7 параметр text метки определяется после ее создания путем присвоения ключу text виджета - виджеты перегружают операции индексирования, чтобы доступ к параметром можно было осуществлять по ключам, подобно словарям.

Пример 7.7. PP4E\Gui\Intro\gui1f.py - параметры

from tkinter import * widget = Label()

widget[‘text’] = ‘Hello GUI world!’ widget.pack(side=TOP)

mainloop()

Но чаще параметры виджетов устанавливаются после их создания путем вызова метода config, как в примере 7.8.

Пример 7.8. PP4E\Gui\Intro\gui1g.py - методы config и title

from tkinter import *

root = Tk()

widget = Label(root)

widget.config(text=’Hello GUI world!’)

widget.pack(side=TOP, expand=YES, fill=BOTH)

root.title(‘gui1g.py’)

root.mainloop()

Метод config (допускается также использовать его синоним configure) можно вызвать в любой момент после создания виджета, чтобы на лету изменить его внешний вид. Например, можно было бы вызвать метод config еще раз, дальше в сценарии, чтобы изменить текст, который выводится в метке. Примеры такого динамического изменения параметров можно найти в последующих примерах в этой части книги.

Обратите также внимание, что в этой версии вызывается метод root.ti-tle - этот метод устанавливает текст в заголовке окна, как показано на рис. 7.4. Вообще говоря, окна верхнего уровня, такие как виджет root типа Tk, в этом примере экспортируют интерфейсы менеджера окон, имеющие отношение к рамке окна, а не к его содержимому.

Рис. 7.4. Сценарий gui1g c виджетом, изменяющим размер, и заголовком окна

Развлечения ради в этой версии метка также центрируется при изменении размеров окна путем установки значений параметров expand и fill. На самом деле здесь почти все делается явно, и это отражает типичный способ создания меток в полноценных интерфейсах - с указанием родителя, политики автоматического изменения размеров и способа прикрепления, без использования значений по умолчанию.


Еще одна версия в память о былых временах

Наконец, если вы склонны к минимализму и испытываете ностальгию по старому стилю программирования на языке Python, сценарий «Hello World» можно записать, как показано в примере 7.9.

Пример 7.9. PP4E\Gui\Intro\gui1-old.py - использование словаря

from tkinter import *

Label(None, {‘text’: ‘Hello GUI world!’, Pack: {‘side’: ‘top’}}).mainloop()

В этом примере для создания окна хватило двух строк, хотя выглядит он ужасно! Эта схема основана на старом стиле программирования, широко использовавшемся до выхода версии Python 1.3, когда параметры настройки передавались не как именованные аргументы, а в виде словаря.35 В этой схеме параметры менеджера компоновки могут передаваться как значения по ключу Pack (имя класса в модуле tkinter).

Схема вызова с использованием словаря по-прежнему действует, и ее можно обнаружить в старых сценариях на языке Python, но лучше ею не пользоваться. Для передачи параметров применяйте в своих сценариях, использующих tkinter, именованные аргументы и явный вызов метода pack. Единственная причина, по которой я не выкинул этот пример, заключается в том, что словари все еще могут быть полезны, когда требуется динамически вычислить и передать набор параметров.

С другой стороны, теперь можно использовать синтаксическую конструкцию func(*pargs, **kargs), которая также позволяет передать явный словарь именованных аргументов в третьей позиции:

options = {‘text’: ‘Hello GUI world!’} layout = {‘side’: ‘top’}

Label(None, **options).pack(**layout) # ключи должны быть строками

Даже в динамических сценариях, где параметры виджетов определяются во время выполнения, нет веских причин использовать формат вызова со словарем, который использовался с версиями, предшествовавшими tkinter 1.3.


Добавление виджетов без их сохранения

В сценарии gui1c.py из примера 7.4 я начал добавлять метки, не сохраняя их в переменных. Этот прием действует и является совершенно допустимым, но поскольку при первом взгляде такой программный код может смутить начинающих программистов, необходимо более подробно объяснить здесь, почему он работает.

В tkinter объекты классов Python соответствуют объектам, выводимым на экран, - мы заставляем объект Python создать объект на экране и вызываем методы объекта Python, чтобы настроить этот экранный объект. Благодаря такому соответствию срок жизни объекта Python должен в целом соответствовать сроку жизни соответствующего ему объекта на экране.

К счастью, сценариям на языке Python обычно не требуется беспокоиться о времени жизни объекта. Обычно они вообще не обязаны сохранять ссылки на создаваемые объекты виджетов, если только не собираются в дальнейшем изменять параметры настройки этих объектов. Например, при программировании с использованием tkinter часто сразу же после создания виджет размещают в интерфейсе, если в дальнейшем ссылка на него не понадобится:

Label(text=’hi’).pack() # ОК

Эта инструкция выполняется как обычно, слева направо. Она сначала создает новую метку и затем немедленно вызывает метод pack нового объекта, чтобы установить его расположение при отображении на экране. Обратите, однако, внимание, что в этом выражении объект Label является временным: так как он не присваивается переменной, в обычной ситуации он должен был бы быть утилизирован сборщиком мусора сразу после выполнения метода pack.

Однако поскольку при создании объектов библиотека tkinter обращается к библиотеке Tk, метка будет нарисована на экране, как положено, несмотря на то, что мы не сохранили в своем сценарии ссылку на соответствующий объект Python. В действительности внутри tkinter объекты виджетов связываются в долгоживущее дерево, которое используется для представления интерфейса, отображаемого на экране, поэтому ссылка на объект Label, созданный этой инструкцией, сохраняется, хотя и не внутри нашего сценария.36

Иными словами, сценариям обычно не нужно беспокоиться о сроке жизни объектов виджетов, и вполне допустимо создавать виджеты и сразу же размещать их одной и той же инструкцией, не сохраняя ссылки на них в переменных.

Но это не значит, что можно писать такой код:

widget = Label(text=’hi’).pack() # неверно!

...использование графического элемента...

На первый взгляд, эта инструкция должна присвоить только что размещенную в интерфейсе метку переменной widget, но это не так. На самом деле это известная ошибка новичков в tkinter. Метод pack размещает виджет, но не возвращает его. Метод pack возвращает объект None - после этой операции переменная widget будет хранить ссылку на None и все дальнейшие операции с виджетом, использующие эту переменную, окажутся безуспешными. Например, по той же причине следующая команда потерпит неудачу:

Label(text=’hi’).pack().mainloop() # неверно!

Метод pack возвращает None, поэтому попытка обратиться к его атрибуту mainloop возбудит исключение (как и должно быть). Если вы действительно хотите разместить виджет и сохранить ссылку на него, используйте такой способ:

widget = Label(text=’hi’) # тоже правильно

widget.pack()

...использование графического элемента...

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

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

В главе 8 мы встретимся с двумя исключениями из этого правила. Сценарии должны вручную получать ссылки на объекты изображений, потому что фактические данные, составляющие изображение, будут уничтожены, как только сборщик мусора в Python утилизирует объект изображения. Объекты класса Variable из библиотеки tkinter также временно сбрасывают связанные с ними переменные из библиотеки Tk при утилизации, но это случается редко и не так опасно.


Добавление кнопок и обработчиков

Пока мы научились только выводить текст в метках и попутно познакомились с базовыми понятиями tkinter. Метки хороши для обучения основам, но от пользовательских интерфейсов обычно требуется нечто большее - способность реагировать на действия пользователя. Эту способность демонстрирует программа из примера 7.10, которая создает окно, изображенное на рис. 7.5.

Пример 7-10. PP4E\Gui\Intrd\gui2.py

import sys

from tkinter import *

widget = Button(None, text=’Hello widget world’, command=sys.exit)

widget.pack()

widget.mainloop()

Рис. 7.5. Кнопка вверху

Здесь мы создаем вместо метки экземпляр класса Button. Как и прежде, он прикрепляется по умолчанию к верхнему краю TOP окна верхнего уровня. Но главное, на что нужно обратить внимание, это аргументы с параметрами кнопки: в качестве значения параметра с именем command используется функция sys.exit.

В кнопках параметр command определяет функцию обратного вызова, которая должна вызываться в дальнейшем при нажатии кнопки. Фактически с помощью параметра command регистрируется действие, которое должна вызвать библиотека tkinter, когда в виджете произойдет событие. Функция обратного вызова, использованная здесь, не представляет большого интереса: как мы узнали в главе 5, встроенная функция sys. exit просто прекращает выполнение вызвавшей ее программы. В данном случае это означает, что при нажатии на эту кнопку окно исчезнет.

Так же как и в случае с метками, есть разные способы создания кнопок. Пример 7.11 является версией, в которой кнопка размещается в интерфейсе немедленно, без сохранения ссылки на нее в переменной. Она явно прикрепляется к левому краю родительского окна, и в качестве обработчика для нее определяется root.quit - стандартный метод объекта Tk, закрывающий главное окно и тем самым завершающий программу. В действительности метод quit завершает выполнение функции mainloop цикла событий и всей программы в целом - когда мы начнем использовать сразу несколько окон верхнего уровня в главе 8, мы узнаем, что метод quit обычно закрывает все окна, а родственный ему метод destroy закрывает только одно окно.

Пример 7.11. PP4E\Gui\Intro\gui2b.py

from tkinter import * root = Tk()

Button(root, text=’press’, command=root.quit).pack(side=LEFT) root.mainloop()

Эта версия создает окно, изображенное на рис. 7.6. Мы не потребовали от кнопки, чтобы она автоматически изменяла свои размеры и занимала все свободное пространство, - она этого и не делает.

Рис. 7.6. Кнопка слева

В двух последних примерах нажатие на кнопку завершает выполнение программы. В более старых сценариях, использующих tkinter, иногда можно увидеть, как параметру command присваивается строка exit, что также вызывает закрытие окна при нажатии на кнопку. В этом случае используется инструмент, имеющийся в библиотеке Tk, но такой прием менее характерен для Python, чем sys.exit или root.quit.


Еще раз об изменении размеров виджетов: растягивание

Даже для такого простого интерфейса есть множество способов определить его внешний вид с помощью основанного на ограничениях менеджера компоновки pack. Например, чтобы поместить кнопку в центре окна, добавьте в вызов метода pack параметр expand=YES в примере 7.11, как показано ниже:

Button(root, text=’press’, command=root.quit).pack(side=LEFT, expand=YES)

В этом случае менеджер компоновки отдаст кнопке все свободное пространство, но не растянет ее. В результате получится окно, как на рис. 7.7.

Рис. 7.7. pack(side=LEFT, expand=YES)

Если необходимо, чтобы кнопке было отдано все свободное пространство и она была растянута по горизонтали, добавьте в вызов pack именованные аргументы expand=YES и fill=X. В результате получится то, что изображено на рис. 7.8.

Рис. 7.8. pack(side=LEFT, expand=YES, fill=X)

Кнопка первоначально займет все окно (выделенное ей место расширено, а сама она растянута, чтобы заполнить выделенное пространство). При этом кнопка будет растягиваться с увеличением размеров родительского окна. Как показано на рис. 7.9, кнопка в этом окне будет растягиваться при растягивании родителя, но только по горизонтальной оси X.

Рис. 7.9. Изменение размера при expand=YES, fill=X

Чтобы кнопка растягивалась в обоих направлениях, укажите в вызове pack параметры expand=YES и fill=BOTH - теперь при изменении размеров окна кнопка будет растягиваться во все стороны, как показано на рис. 7.10. Если раскрыть окно на весь экран, получится одна очень большая кнопка tkinter.

Рис. 7.10. Изменение размера при expand=YES, fill=BOTH

В более сложных интерфейсах такая кнопка будет растягиваться, только когда автоматическое изменение размеров задано для всех содержащих ее виджетов. Здесь единственным родителем кнопки является корневое окно Tk программы, поэтому вопрос об автоматическом изменении размеров родителя пока не стоит - в последующих сценариях нам потребуется также делать автоматически расширяемыми объемлющие виджеты Frame. Мы еще вернемся к менеджеру компоновки, когда встретимся с интерфейсами, содержащими несколько виджетов, и еще раз -когда будем изучать альтернативную функцию grid в главе 9.


Добавление пользовательских обработчиков

В простых примерах с кнопкой из предыдущего раздела обработчиком событий служила уже существующая функция, завершающая программу. Однако ничуть не сложнее зарегистрировать собственный обработчик, который выполняет что-нибудь более полезное. В примере 7.12 определяется собственный обработчик обратного вызова на языке Python.

Пример 7.12. PP4E\Gui\Intro\gui3.py

import sys

from tkinter import *

def quit(): # собственный обработчик событий

print(‘Hello, I must be going...’)

sys.exit() # закрыть окно и завершить процесс

widget = Button(None, text=’Hello event world’, command=quit)

widget.pack()

widget.mainloop()

Окно, создаваемое этим сценарием, изображено на рис. 7.11. Этот сценарий и воспроизводимый им графический интерфейс почти совпадают с предыдущим примером. Но здесь в параметре command передается функция, которая определена локально. При нажатии кнопки библиотека tkinter вызовет для обработки события функцию quit в этом файле. Внутри quit вызов функции print выведет сообщение в поток stdout программы, и процесс завершится, как ранее.

Рис. 7.11. Кнопка, нажатие на которую вызывает функцию Python

Как обычно, роль потока вывода stdout играет окно, из которого была запущена программа, если только он не был перенаправлен в файл.

Это будет всплывающее окно консоли DOS, если запустить программу щелчком на файле в Windows; добавьте вызов input перед sys.exit, если всплывающее окно исчезает прежде чем удается посмотреть сообщение. Ниже показано, как выглядит вывод в стандартный поток вывода при нажатии кнопки - он генерируется функцией на языке Python, которую автоматически вызывает библиотека tkinter:

C:\...\PP4E\Gui\Intro> python gui3.py

Hello, I must be going...

C:\...\PP4E\Gui\Intro>

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

Вообще говоря, обработчиками событий могут быть любые вызываемые объекты - функции; анонимные функции, создаваемые с помощью lambda-выражений; связанные методы классов или экземпляров типов или экземпляры классов, наследующие метод перегрузки оператора

__call__. Обработчики нажатия кнопки Button не получают никаких

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


lambda-выражения как обработчики событий

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

Как этим воспользоваться, будет показано далее в этой части книги, но для иллюстрации основных идей в примере 7.13 демонстрируется, как будет выглядеть приложение 7.12, если задействовать в нем инструкцию lambda вместо def.

Пример 7.13. PP4E\Gui\Intro\gui3b.py

import sys

from tkinter import * # lambda-выражение генерирует функцию

widget = Button(None, # но содержит всего лишь выражение

text=’Hello event world’,

command=(lambda: print(‘Hello lambda world’) or sys.exit()) )

widget.pack()

widget.mainloop()

В этом примере есть небольшая хитрость: lambda-выражения могут содержать только одно выражение, поэтому здесь используется оператор or, чтобы заставить выполниться два выражения (функция print вызывается первой, а поскольку в Python 3.X она стала функцией, нам не требуется использовать sys.stdout непосредственно).


Отложенные вызовы с применением инструкций lambda и ссылок на объекты

Очень часто lambda-выражения используются для передачи дополнительных данных в обработчик события (для простоты я опустил вызовы функций pack и mainloop в следующем фрагменте):

def handler(A, B): # обычно вызывается без аргументов ...использование A и B...

X = 42

Button(text='ni', command=(lambda: handler(X, 'spam'))) # lambda добавляет

# аргументы

Библиотека tkinter вызывает обработчики command, не передавая им никаких аргументов, тем не менее с помощью такого lambda-выражения можно создать косвенную анонимную функцию, которая будет играть роль оболочки для действительного обработчика и передавать ему информацию, существовавшую в момент создания графического интерфейса. Вызов фактического обработчика откладывается, благодаря чему мы получаем возможность добавлять необходимые аргументы. В данном случае значение глобальной переменной X и строка “spam” будут переданы в аргументах A и B даже при том, что библиотека tkinter вызывает функции обратного вызова без аргументов. Таким образом, инструкция lambda может использоваться для отображения вызова без аргументов в вызов с аргументами, которые поставляются самим lambda-выражением.

Если такой синтаксис приводит вас в замешательство, запомните, что такое lambda-выражение, как показано выше, обычно может быть реализовано в виде простой инструкции def. Вторая функция в следующем фрагменте выполняет ту же операцию, что и lambda-выражение в предыдущем фрагменте, - ссылка на нее в операции создания кнопки фактически откладывает вызов действительного обработчика, чтобы ему можно было передать дополнительные аргументы:

def handler(A, B): # обычно вызывается без аргументов ...использование A и B...

X = 42

def func(): # косвенная функция-обертка, добавляющая аргументы handler(X, 'spam')

Button(text='ni', command=func)

Чтобы более отчетливо понять, зачем необходимы отложенные вызовы, обратите внимание, что произойдет, если в операцию создания кнопки вставить вызов самого обработчика с аргументами без использования lambda-выражения или другой промежуточной функции, - обработчик будет вызван немедленно, в момент создания кнопки, а не когда на ней будет выполнен щелчок. Именно для того чтобы отложить вызов обработчика, обращение к нему требуется обернуть промежуточной функцией:

def handler(name): print(name)

Button(command=handler(‘spam’)) # ОШИБКА: обработчик будет вызван немедленно!

Использование lambda-выражений или ссылок на вызываемые объекты позволяет отложить вызов обработчика до появления события. Например, использование lambda-выражения для передачи дополнительных данных с помощью определения встроенной функции, вызов которой откладывается:

def handler(name): print(name)

Button(command=(lambda: handler(‘spam’))) # OK: обертывание lambda-выражением

# откладывает вызов

всегда эквивалентно более длинной и, по мнению некоторых, менее удобной форме с двумя функциями:

def handler(name): print(name)

def temp():

handler(‘spam’)

Button(command=temp) # OK: ссылка на функцию, а не ее вызов

Нам достаточно применять только какой-то один прием - либо lambda-выражение без аргументов, либо ссылку на вызываемый объект, не принимающий аргументов, но не оба сразу. Бессмысленно использовать lambda-выражение, просто вызывающее функцию, которая не принимает дополнительных аргументов, так как в этом случае будет выполняться один лишний вызов:

def handler(name): print(name)

def temp():

handler(‘spam’)

Button(command=(lambda: temp())) # БЕССМЫСЛЕННО: добавляет лишний вызов!

Как будет показано далее, допускается также использовать ссылки на другие вызываемые объекты, такие как связанные методы и вызываемые экземпляры классов, которые сохраняют необходимую информацию в своих атрибутах. Если они не принимают аргументов, их имена можно просто указывать на этапе создания виджетов и их не требуется обертывать лишними lambda-выражениями.


Проблемы с областями видимости обработчиков

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

Аргументы и глобальные переменные

Например, обратите внимание, что первая версия обработчика из предыдущего раздела сама могла бы обратиться к переменной X непосредственно, так как она является глобальной (и будет определена к моменту, когда будет вызван обработчик). Благодаря этому мы могли бы написать обработчик, принимающий единственный аргумент, и передавать ему в lambda-выражении только строку ‘spam’:

def handler(A): # X находится в глобальной области видимости

...использование глобальной переменной X и аргумента A...

X = 42

Button(text=’ni’, command=(lambda: handler('spam')))

Аргумент A также можно было бы перенести в глобальную область видимости, чтобы полностью устранить необходимость использования lambda-выражения; в этом случае мы сможем регистрировать сам обработчик и убрать промежуточную функцию.

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

Передача значений из объемлющей области видимости с помощью аргументов со значениями по умолчанию

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

def handler(A, B):

...использование A и B...

def makegui():

X = 42

Button(text=’ni’, command=(lambda: handler(X, ‘spam’))) # запоминает X

makegui()

mainloop() # в этой точке область видимости функции makegui прекратила # существование

К счастью, модель ссылок в объемлющую область видимости, реализованная в Python, автоматически сохранит значение X, находящееся в локальной области видимости объемлющей функции, где используется lambda-выражение, и обеспечит его доступность в обработчике, когда он будет вызван по событию нажатия кнопки. Обычно этот прием действует именно так, как нам хотелось бы, и автоматически обслуживает подобные ссылки на переменные.

Однако такое использование объемлющей области видимости можно сделать более явным, применив аргументы со значениями по умолчанию, которые также сохраняют значения переменных из локальной области видимости объемлющей функции после выхода из нее. Например, в следующем фрагменте аргумент с именем X (слева в выражении X=X) сохранит объект 42, потому что переменная с именем X (справа в выражении X=X) используется не только в объемлющей области видимости, но и в сгенерированной функции, которая позднее будет вызываться без аргументов:

def handler(A, B): # аргументы со значениями по умолчанию обеспечивают

...использование A и B... # сохранение информации о состоянии

def makegui():

X = 42

Button(text=’ni’, command=(lambda X=X: handler(X, 'spam')))

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

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

Передача значений из объемлющей области видимости с помощью автоматических ссылок

Аргументы со значениями по умолчанию способны более четко обозначить внешние зависимости, тем не менее можно обойтись и без них (начиная с версии Python 2.2, по крайней мере), и в современной практике они обычно не используются для этих целей. Вместо этого просто создается lambda-выражение, которое откладывает вызов действительного обработчика и передает ему дополнительные аргументы. Переменные из объемлющей области видимости, используемые в lambda-выражении, автоматически сохраняются даже после выхода из объемлющей функции.

Предыдущий фрагмент, например, в наши дни можно записать, как мы уже делали это выше - имя X в обработчике автоматически будет отображено в переменную X из объемлющей области видимости, что фактически приводит к сохранению значения, которое имела переменная X на момент создания кнопки:

def makegui(): # X запоминается

X = 42 # автоматически

Button(text=’ni’, command=(lambda: handler(X, 'spam'))) # аргументы по

# умолчанию не

# нужны

Далее мы увидим более конкретные примеры использования этого приема. При использовании классов для создания графического интерфейса, например, аргумент self является для методов локальной переменной и потому автоматически становится доступным в теле lambda-выражения. Нет никакой необходимости передавать его явно, в виде аргумента со значением по умолчанию:

class Gui:

def handler(self, A, B):

...использование self, A и B...

def makegui(self):

X = 42

Button(text=’ni’, command=(lambda: self.handler(X, 'spam')))

Gui().makegui()

mainloop()

Однако при использовании классов в качестве источника дополнительной информации для функций обратного вызова можно использовать атрибуты экземпляров и отказаться от дополнительных аргументов. Как это делается, мы увидим чуть ниже. Но сначала нам следует отклониться от темы и рассмотреть правила областей видимости в Python, чтобы понять, почему аргументы со значениями по умолчанию иногда все же бывают необходимы для передачи значений во вложенные lambda-функции, особенно в графических интерфейсах.

Но иногда вместо объемлющих областей видимости

все же необходимо использовать аргументы со значениями

по умолчанию

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

Все это так, но не совсем. Есть одна особенность. Оказывается, что внутри lambda-выражения (или внутри инструкции def) ссылки на переменные в объемлющей области видимости разрешаются в момент вызова сгенерированной функции, а не в момент ее создания. Вследствие этого, когда функция будет вызвана позднее, такие ссылки будут отражать последние значения переменных, присвоенные им где бы то ни было в объемлющей области видимости, которые не обязательно будут совпадать со значениями, присвоенными переменным на момент создания функции. Это справедливо не только для вложенных функций обратного вызова, но и для находящихся в глобальной области видимости модуля. В любом случае, все ссылки на переменные в объемлющей области видимости разрешаются в момент вызова функции, а не в момент ее создания.

Аргументы со значениями по умолчанию отличаются тем, что их значения определяются в момент создания функции, а не в момент ее вызова. Вследствие этого они по-прежнему могут быть полезным способом сохранения значений переменных из объемлющей области видимости, имевших место на момент создания функции. В отличие от ссылок на переменные в объемлющей области видимости, аргументы со значениями по умолчанию не получат другое значение, даже если переменная в объемлющей области видимости изменится между моментами создания и вызова функции. (Фактически именно этим объясняется, почему изменяемые объекты, такие как списки, передаваемые по умолчанию, сохраняются между вызовами - они создаются только один раз, в момент создания функции, и присоединяются к самой функции.)

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

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

def simple(): spam = ‘ni’ def action():

print(spam) # ссылка на переменную в объемлющей функции return action

act = simple() # создать и вернуть вложенную функцию

act() # затем вызвать ее: выведет ‘ni’

Это простейший случай ссылки на переменную в объемлющей области видимости, и он одинаково действует для вложенных функций, сгенерированных с помощью инструкций def и lambda. Но обратите внимание, что он будет действовать точно так же, если значение переменной spam будет присвоено после определения вложенной функции:

def normal(): def action():

return spam # поиск переменной будет выполняться в момент вызова spam = ‘ni’

return action

act = normal()

print(act()) # также выведет ‘ni’

Из этого примера следует, что разрешение имен в объемлющей области видимости не выполняется в момент создания вложенной функции -фактически в этот момент переменная вообще не была определена. Разрешение имени выполняется в момент вызова вложенной функции. То же справедливо и для lambda-выражений:

def weird(): spam = 42

return (lambda: spam * 2) # запомнит ссылку на spam в объемлющей # области видимости

act = weird()

print(act()) # выведет 84

Пока все неплохо. Ссылка на переменную spam внутри вложенной lambda-функции сохраняет значение, полученное переменной в объемлющей области видимости, которое остается доступным даже после выхода из объемлющей функции. Этот пример соответствует зарегистрированному обработчику в графическом интерфейсе, который будет вызываться позднее, по событиям. И снова ссылка из вложенной области видимости разрешается не в тот момент, когда в lambda-выражение создает функцию, а когда сгенерированная функция будет вызвана. Следующий фрагмент более наглядно демонстрирует действие этого правила:

def weird():

tmp = (lambda: spam * 2) # запоминает ссылку на spam, даже при том, spam = 42 # что здесь она еще не установлена

return tmp

act = weird()

print(act()) # выведет 84

Здесь вложенная функция ссылается на переменную, которой на момент создания функции еще не было присвоено значение. Действительно, при каждом вызове вложенной функции ссылки в объемлющую область видимости возвращают последние значения, присвоенные переменным. Взгляните, что происходит в следующем фрагменте:

def weird(): spam = 42

handler = (lambda: spam * 2) # функция не сохраняет текущее значение 42 spam = 50

print(handler()) # выведет 100: поиск spam выполняется именно сейчас spam = 60

print(handler()) # выведет 120: поиск spam снова выполняется именно сейчас

weird()

Теперь значение ссылки на spam внутри lambda-функции изменяется при каждом вызове сгенерированной функции! Фактически ссылка на переменную возвращает последнее значение, присвоенное в объемлющей области видимости на момент вызова вложенной функции, потому что ссылки разрешаются в момент вызова функции, а не в момент ее создания.

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

def odd(): funcs = [] for c in ‘abcdefg’:

funcs.append((lambda: c)) # поиск переменной c будет выполнен позднее return funcs # не сохраняет текущее значение c

for func in odd():

print(func(), end=’ ‘) # Опа!: выведет 7 символов g, а не a,b,c,... !

Здесь список func имитирует зарегистрированные обработчики событий графического интерфейса, подключенные к виджетам. Этот пример работает совсем не так, как могли бы ожидать многие из вас. Переменная c внутри вложенной функции здесь всегда будет иметь значение 'g', то есть значение, установленное последней итерацией цикла в объемлющей области видимости. В результате все семь сгенерированных lambda-функций будут получать при вызове одно и то же значение.

Аналогичная реализация использования дополнительных данных в lambda-функциях, вызывающих действительные обработчики графического интерфейса, будет испытывать похожие проблемы. Например, все обработчики событий от кнопок, созданных в цикле, могут в конечном итоге давать один и тот же результат! Чтобы исправить положение, необходимо передавать значения вложенным функциям в виде значений аргументов по умолчанию, которые сохраняют текущее значение переменной цикла (а не то, которое будет в будущем):

def odd(): funcs = [] for c in ‘abcdefg’:

funcs.append((lambda c=c: c)) # запомнить текущее значение c return funcs # значения по умолчанию вычисляются

# немедленно

for func in odd():

print(func(), end=’ ‘) # OK: теперь выведет a,b,c,...

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

funcs = [] # объемлющая область видимости - модуль

for c in ‘abcdefg’: # запомнить текущее значение c,

funcs.append((lambda c=c: c)) # иначе снова выведет 7 символов g

for func in funcs:

print(func(), end=’ ‘) # OK: выведет a,b,c,...

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

Мы еще будем сталкиваться с этим явлением в последующих примерах, создающих более сложные графические интерфейсы. А пока запомните, что ссылки в объемлющую область видимости не могут служить полной заменой аргументов со значениями по умолчанию - в некоторых ситуациях значения по умолчанию незаменимы для передачи значений функциям обратного вызова. Кроме того, имейте в виду, что зачастую классы обеспечивают более простой способ сохранения информации для использования в обработчиках, чем вложенные функции. Поскольку информация в классах сохраняется более явным способом, им не свойственны описанные проблемы областей видимости. Следующие два раздела детально рассказывают об использовании классов.


Связанные методы как обработчики событий

Вернемся к теме программирования графических интерфейсов. Во многих случаях вполне можно ограничиться функциями и lambda-выражениями, однако связанные методы класса тоже прекрасно справляются с ролью обработчиков событий в графических интерфейсах - они запоминают не только экземпляр, которому было послано событие, но и связанный с ним метод, который должен быть вызван. Рассмотрим пример 7.14, где приводится переработанная версия примеров 7.12 и 7.13, регистрирующая связанный метод класса вместо функции или результата lambda-выражения.

Пример 7.14. PP4E\Gui\Intro\gui3c.py

import sys

from tkinter import *

class HelloClass:

def __init__(self):

widget = Button(None, text='Hello event world’, command=self.quit) widget.pack()

def quit(self):

print(‘Hello class method world’) # self.quit - связанный метод sys.exit() # хранит пару self+quit

HelloClass()

mainloop()

При нажатии кнопки библиотека tkinter вызовет метод q u it этого класса как обычно, без аргументов. Но в действительности он получит один аргумент - оригинальный объект self - хотя tkinter не передает его явно. Поскольку связанный метод self.quit хранит обе ссылки, self и quit, он совместим с вызовом простой функции - интерпретатор Python автоматически передаст аргумент self функции метода. Напротив, регистрация несвязанного метода экземпляра в виде HelloClass.quit работать не станет, потому что в этом случае не будет объекта self, который можно передать потом при возникновении события.

Позднее мы увидим, что приемы использования классов в качестве обработчиков событий также предоставляют естественное место для сохранения данных, используемых при возникновении событий, - достаточно просто присвоить их атрибутам экземпляра self:

class someGuiClass:

def __init__(self):

self.X = 42 self.Y = ‘spam’

Button(text=’Hi’, command=self.handler)

def handler(self):

...использование self.X, self.Y...

Поскольку событие будет отправлено методу этого класса со ссылкой на оригинальный объект экземпляра, аргумент self позволит обращаться к атрибутам, содержащим исходные данные. В действительности атрибуты экземпляра сохраняют информацию для использования в процессе обработки события. Этот прием обеспечивает большую гибкость, чем глобальные переменные или дополнительные аргументы, добавляемые с помощью lambda-выражений, особенно заметную в больших графических интерфейсах.


Объекты вызываемых классов как обработчики событий

Поскольку объекты экземпляров классов в языке Python могут вызываться как функции, если они наследуют метод__call__для перехвата

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

Пример 7.15. PP4E\Gui\Intro\gui3d.py

import sys

from tkinter import * class HelloCallable:

def __init__(self): # __init__ вызывается при создании объекта

self.msg = ‘Hello __call__ world’

def __call__(self):

print(self.msg) # __call__ вызывается при попытке обратиться

sys.exit() # к объекту класса как к функции

widget = Button(None, text=’Hello event world’, command=HelloCallable())

widget.pack()

widget.mainloop()

Здесь экземпляр класса HelloCallable, зарегистрированный в command, тоже может вызываться как обычная функция - Python вызовет его

метод__call__для обработки операции вызова, выполняемой в tkinter

при нажатии кнопки. В данном случае обобщенный метод__call__

фактически замещает связанный метод. Обратите внимание, что здесь для хранения информации, используемой при обработке событий, задействуется атрибут self, msg - self, являющийся ссылкой на исходный экземпляр класса, который автоматически передается при вызове специального метода__call__.

Все четыре версии gui3 создают одинаковые окна (рис. 7.11), но при нажатии на кнопку выводят в stdout различные сообщения:

C:\...\PP4E\Gui\Intro> python gui3.py Hello, I must be going...

C:\...\PP4E\Gui\Intro> python gui3b.py

Hello lambda world

C:\...\PP4E\Gui\Intro> python gui3c.py

Hello class method world

C:\...\PP4E\Gui\Intro> python gui3d.py

Hello __call__ world

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


Другие протоколы обратного вызова в tkinter

Запомните также на будущее, что использование параметров command для перехвата генерируемых пользователем событий нажатия кнопки является лишь одним из способов регистрации обработчиков в библиотеке tkinter. На самом деле существуют различные способы перехвата событий в сценариях tkinter:

Параметр command кнопки

Как только что было показано, событие нажатия кнопки перехватывается путем передачи вызываемого объекта в параметре command виджета. То же относится к другим виджетам, похожим на кнопки, с которыми мы познакомимся в главе 8 (например, переключателям, флажкам и ползункам).

Параметры, command меню

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

Протоколы полос прокрутки

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

Обобщенные методы bind виджетов

Имеется возможность использовать более универсальный механизм метода bind для регистрации низкоуровневых обработчиков событий, таких как нажатия клавиш, перемещения и щелчки мышью и так далее. В отличие от обработчиков, зарегистрированных с помощью параметра command, обработчики, зарегистрированные методом bind, получают в качестве аргумента объект события (экземпляр класса Event из библиотеки tkinter), который предоставляет контекст события - виджет, к которому относится событие, экранные координаты и так далее.

Протоколы менеджера окон

Кроме того, сценарии могут перехватывать события менеджера окон (например, запрос на закрытие окна) путем внедрения в механизм метода protocol менеджера окон, который доступен для оконных объектов верхнего уровня. Например, установив обработчик события WM_DELETE_WINDOW, можно перехватить событие от кнопки закрытия окна.

Обработчики планируемых событий

Наконец, в сценарии, использующем библиотеку tkinter, можно также зарегистрировать обработчики, которые должны вызываться в особых случаях, например при срабатывании таймера, поступлении входных данных и холостом состоянии цикла событий. Сценарии могут также приостанавливаться и ожидать событий, связанных с изменением состояния окон и специальных переменных. С этими типами событий мы более подробно познакомимся в конце главы 9.


Связывание событий

Из всех перечисленных протоколов наиболее универсальным, но, вероятно, и наиболее сложным является метод bind. Более подробно мы изучим его потом, но чтобы получить первоначальное представление, рассмотрим пример 7.16, который создает тот же графический интерфейс, что и примеры из предыдущего раздела, но для перехвата события нажатия кнопки использует метод bind, а не параметр command.

Пример 7.16. PP4E\Gui\Intro\gui3e.py

import sys

from tkinter import * def hello(event):

print(‘Press twice to exit’) # одиночный щелчок левой кнопкой

def quit(event): # двойной щелчок левой кнопкой

print(‘Hello, I must be going...’) # event дает виджет, координаты и т.д. sys.exit()

widget = Button(None, text=’Hello event world’) widget.pack()

widget.bind(‘’, hello) # привязать обработчик щелчка

widget.bind(‘’, quit) # привязать обработчик двойного щелчка

widget.mainloop()

В этой версии параметр command для кнопки вообще не определяется. Вместо этого выполняется установка низкоуровневых обработчиков событий одинарных () и двойных щелчков левой кнопкой () внутри области отображения кнопки. Метод bind принимает большую группу таких идентификаторов событий в разнообразных форматах, с которыми мы познакомимся в главе 8.

При выполнении этого сценария снова создается то же самое окно (рис. 7.11). При одиночном щелчке на кнопке программа выводит сообщение, но не завершается - чтобы завершить программу, нужно щелкнуть на кнопке дважды. Ниже приводятся сообщения, выводимые после двух одиночных щелчков и одного двойного (двойной щелчок сначала запускает обработчик события одиночного щелчка):

C:\...\PP4E\Gui\Intro> python gui3e.py

Press twice to exit Press twice to exit Press twice to exit Hello, I must be going...

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

Рис. 7.11. Кнопка, нажатие на которую вызывает функцию Python

Более подробно мы разберем работу метода bind и все другие способы установки обработчиков событий, предоставляемые библиотекой tkinter, далее в этой книге. Однако сначала займемся построением более крупных графических интерфейсов, состоящих не только из одной кнопки, и другими способами использования классов в графических интерфейсах.


Добавление нескольких виджетов

Настало время строить интерфейсы пользователя с несколькими виджетами. Сценарий в примере 7.17 создает окно, изображенное на рис. 7.12.

Пример 7.17. PP4E\Gui\Intro\gui4.py

from tkinter import *

def greeting():

print(‘Hello stdout world!...’) win = Frame()

win.pack()

Label(win, text=’Hello container world’).pack(side=TOP) Button(win, text=’Hello’, command=greeting).pack(side=LEFT) Button(win, text=’Quit’, command=win.quit).pack(side=RIGHT)


win.mainloop()

Рис. 7.12. Окно с несколькими виджетами

Этот сценарий создает виджет Frame (еще один класс из библиотеки tkinter), к которому прикрепляются три других виджета - Label и два Button - путем передачи объекта Frame в первом аргументе. На языке tkinter это означает, что виджет Frame становится родителем для трех других виджетов. Обе кнопки этого интерфейса вызывают следующие обработчики:

• Щелчок на кнопке HeLLo запускает функцию greeting, определенную внутри этого файла, которая производит вывод в поток stdout.

• Щелчок на кнопке Quit вызывает стандартный метод tkinter quit, который виджет win наследует от класса Frame (Frame.quit имеет тот же эффект, что и использованный ранее метод Tk. quit).

Ниже приводится текст, который выводится в stdout при щелчке на кнопке HeLLo, какими бы ни были стандартные потоки ввода-вывода для этого сценария:

C:\...\PP4E\Gui\Intro> python gui4.py Hello stdout world!...

Hello stdout world!...

Hello stdout world!...

Hello stdout world!...

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


Еще раз об изменении размеров: обрезание

Ранее мы видели, как заставить виджеты расширяться вместе с родительским окном, передавая параметры expand и fill менеджеру компоновки pack. Теперь, когда у нас в окне имеется несколько виджетов, я открою вам один из полезных секретов компоновщика. Как правило, при уменьшении размеров окна виджеты, добавленные первыми, обрезаются в последнюю очередь. Это означает, что порядок добавления элементов определяет, какие из них окажутся скрытыми, если окно сделается слишком маленьким, - элементы, добавленные последними, обрезаются в первую очередь. Например, на рис. 7.13 показано, что произойдет, если окно сценария gui4 уменьшить в интерактивном режиме.

Рис. 7.13. Уменьшение размеров gui4

Попробуйте изменить порядок создания метки и кнопок в сценарии и посмотреть, что произойдет при сжатии окна: виджет, добавленный первым, всегда исчезает последним. Например, если метка добавляется последней, то, как видно на рис. 7.14, она будет обрезана первой, даже если прикреплена к верхнему краю: параметр side и порядок добавления виджетов вместе влияют на общую структуру, но только порядок упаковки имеет значение, когда окно уменьшается. Ниже показан измененный порядок создания виджетов:

Button(win, text=’Hello’, command=greeting).pack(side=LEFT)

Button(win, text=’Quit’, command=win.quit).pack(side=RIGHT)

Label(win, text=’Hello container world’).pack(side=TOP)

Рис. 7.14. Метка добавляется последней, а обрезается первой

Это обусловлено тем, что библиотека tkinter запоминает порядок добавления. Сценарии могут заранее готовиться к сжатию окон, вызывая вначале метод pack для более важных виджетов. Например, в предстоящем обзоре библиотеки tkinter мы встретим сценарий, который создает меню и панели инструментов в верхней и нижней части окна. Чтобы обеспечить их исчезновение при сжатии окна в последнюю очередь, они добавляются первыми, перед теми компонентами, которые размещаются в середине. Аналогично полосы прокрутки, содержащиеся в интерфейсах, обычно добавляются раньше, чем прокручиваемые ими элементы (например, текстовые окна или списки), чтобы сохраняться при сжатии окна.


Прикрепление виджетов к фреймам

Наиболее важным нововведением в этом примере является использование фреймов: виджеты Frame служат просто контейнерами для других виджетов, благодаря чему возникает представление о графических интерфейсах как об иерархиях, или деревьях виджетов. Здесь роль охватывающего окна для трех других элементов играет виджет win. Однако в целом, прикрепляя виджеты к фреймам, а фреймы к другим фреймам, можно строить графические интерфейсы с произвольной структурой. Просто поделите графический интерфейс на ряд прямоугольников уменьшающегося размера, реализуйте каждый из них как Frame и прикрепите к фреймам простые виджеты.

В данном сценарии, когда виджет win передается в первом аргументе конструкторам Label и Button, библиотека tkinter прикрепляет их к виджету Frame (они становятся дочерними для win). Сам объект win по умолчанию прикрепляется к окну верхнего уровня, потому что конструктору Frame не был передан родитель. Когда мы предлагаем виджету win начать выполнение (вызывая метод mainloop), библиотека tkinter отображает все графические элементы в построенном нами дереве.

Три дочерних виджета также позволяют указывать параметры pack: аргументы side говорят о том, к какой части содержащего их фрейма (то есть win) должен быть прикреплен новый виджет. Метка подвешивается к верхнему краю, а кнопки прикрепляются к боковым сторонам. TOP, LEFT и RIGHT являются строковыми переменными с предварительно присвоенными значениями, которые импортируются из tkinter. Размещение виджетов происходит немного сложнее, чем простое указание сторон, к которым они прикрепляются, но чтобы узнать почему, придется сделать краткое отступление и обсудить детали работы менеджера компоновки.


Порядок компоновки и прикрепление к сторонам

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

Вот как работает система компоновки элементов:

1. Компоновщик начинает с пустого доступного пространства, в которое входит весь родительский контейнер (например, весь фрейм или окно верхнего уровня).

2. Когда виджет прикрепляется к какому-либо краю, ему отдается весь запрашиваемый край в оставшемся пустом пространстве, и пустое пространство сокращается.

3. Последующие виджеты получают все, что осталось от этого края после добавления предыдущих виджетов.

4. После того как виджетам будет отдано все пустое пространство, expand делит оставшееся пространство, а fill и anchor растягивают и устанавливают виджеты внутри выделенной им области.

Например, изменим в сценарии gui4 логику создания дочерних виджетов, как показано ниже:

Button(win, text=’Hello’, command=greeting).pack(side=LEFT)

Label(win, text=’Hello container world’).pack(side=TOP)

Button(win, text=’Quit’, command=win.quit).pack(side=RIGHT)

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

Рис. 7.15. Метка была добавлена второй

Теперь, несмотря на значение параметра side, метка не захватывает весь верх окна, и чтобы разобраться в причине, нужно представить себе сокращающиеся пустые пространства. Так как первой в интерфейс добавляется кнопка Hello, ей выделяется весь левый край фрейма. После этого метка получает весь верх того, что осталось. Наконец, кнопка Quit получает правый край остатка - прямоугольник, находящийся справа от кнопки Hello и под меткой. При сжатии этого окна графические элементы обрезаются в порядке, противоположном их добавлению: первой исчезает кнопка Quit, за ней следует метка.37

В первоначальной версии этого примера (рис. 7.12) метка занимает весь верхний край только потому, что она добавляется первой, а не благодаря значению параметра side. Если внимательнее рассмотреть рис. 7.14, можно заметить, что он иллюстрирует действие тех же самых правил -метка находится между кнопками, потому что они уже полностью заняли левый и правый край.


Снова о параметрах expand и fill компоновки

Помимо порядка добавления на взаимное расположение виджетов оказывает влияние также уже знакомый параметр fill, позволяющий растягивать виджет так, чтобы он занимал все пространство вдоль выделенного ему пустого края, а все пустое пространство, оставшееся после расстановки элементов, поровну распределять между виджетами, при добавлении которых был указан параметр expand=YES. Например, следующий фрагмент создает окно, изображенное на рис. 7.16 (сравните с рис. 7.15).

Button(win, text=’Hello’, command=greeting).pack(side=LEFT,fill=Y)

Label(win, text=’Hello container world’).pack(side=TOP)

Button(win, text=’Quit’, command=win.quit).pack(side=RIGHT, expand=YES,fill=X)

Рис. 7.16. Компоновка с параметрами expand и fill

Чтобы все эти элементы увеличивались вместе с окном, необходимо сделать содержащий их контейнер расширяемым - виджеты расширяются за пределы своего первоначального положения, только если расширяются все их родители. Ниже демонстрируются изменения в gui4.py:

win = Frame()

win.pack(side=TOP, expand=YES, fill=BOTH)

Button(win, text=’Hello’, command=greeting).pack(side=LEFT, fill=Y)

Label(win, text=’Hello container world’).pack(side=TOP)

Button(win, text=’Quit’, command=win.quit).pack(side=RIGHT, expand=YES,fill=X)

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

Рис. 7.17. Расширяемый фрейм в увеличенном окне gui4


Использование якорей вместо растягивания

Как если бы это не обеспечивало достаточной гибкости, механизм компоновки дополнительно предоставляет для размещения графических элементов в отведенной для них области параметр anchor помимо заполнения пространства с помощью fill. Параметр anchor принимает константы из библиотеки tkinter, указывающие восемь направлений (N, NE, NW, S и так далее), или константу CENTER (например, anchor=NW). Компоновщику при этом предписывается разместить виджет в желательном месте внутри отведенного для него пространства, если это пространство больше, чем требуется для изображения данного графического элемента.

По умолчанию параметр anchor получает значение CENTER, поэтому виджеты выводятся в центре отведенного им пространства (выделенного им края пустого пространства), если только для них не определено иное местоположение с помощью параметра anchor или они не растянуты с помощью параметра fill. Для демонстрации изменим сценарий gui4, как показано ниже:

Button(win, text=’Hello’, command=greeting).pack(side=LEFT, anchor=N)

Label(win, text=’Hello container world’).pack(side=TOP)

Button(win, text=’Quit’, command=win.quit).pack(side=RIGHT)

Новым здесь является только то, что кнопка HeLLo закреплена на северной стороне отведенного ей пространства. Так как эта кнопка была добавлена первой, она получила весь левый край родительского фрейма. Места оказалось больше, чем необходимо кнопке, поэтому по умолчанию она оказывается в середине этого края, как показано на рис. 7.15 (то есть закреплена в центре). После установки якоря в значение N она смещается вверх по левому краю, как показано на рис. 7.18.

Рис. 7.18. Закрепление кнопки на севере

Имейте в виду, что параметры fill и anchor начинают приниматься во внимание только после того как виджету будет выделено место у края пустого пространства, определяемого параметром side, в соответствии с порядком добавления, и запроса дополнительного пространства expand. Путем варьирования порядка добавления, а также значений параметров, определяющих край, направление заполнения и закрепления, можно получить массу эффектов расположения и обрезания, и стоит потратить некоторое время, экспериментируя с разными вариантами, если вы этого еще не сделали. Например, в исходной версии этого примера метка занимает весь верхний край только потому, что была добавлена первой.

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

Наконец, имейте в виду, что дерево виджетов, создаваемое в этих примерах, в действительности является неявным - библиотека tkinter внутри ведет учет связей, которые образуются в результате передачи аргументов родительских виджетов. На языке ООП это называется композицией - фрейм содержит метку и несколько кнопок. А теперь рассмотрим другой вид связи - наследование.


Настройка виджетов с помощью классов

В сценариях, использующих библиотеку tkinter, совсем необязательно применять приемы ООП, но они определенно могут оказаться полезными. Как мы только что видели, графические интерфейсы на базе tkinter строятся, как деревья объектов экземпляров классов. Ниже демонстрируется еще один способ применения ООП для моделирования графического интерфейса: специализация виджетов посредством наследования. Сценарий в примере 7.18 создает окно, изображенное на рис. 7.19.

Рис. 7.19. Подкласс кнопки в действии

Пример 7.18. PP4E\Gui\Pntro\gui5.py

from tkinter import *

class HelloButton(Button):

def __init__(self, parent=None, **config): # регистрирует метод callback

Button.__init__(self, parent, **config) # и добавляет себя в интерфейс

self.pack() # можно использовать старый

self.config(command=self.callback) # стиль аргумента config

def callback(self): # действие по умолчанию при нажатии

print(‘Goodbye world...’) # переопределить в подклассах

self.quit()

if__name__== ‘__main__’:

HelloButton(text=’Hello subclass world’).mainloop()

В этом примере нет ничего необычного: он просто отображает одну кнопку, при нажатии на которую программа выводит сообщение и завершается. Но на этот раз мы сами создали виджет кнопки. Класс Hel-loButton наследует все свойства и методы класса Button, а также добавляет метод callback и логику в конструкторе, устанавливая параметру command значение self.callback - связанный метод экземпляра. При нажатии на кнопку теперь вызывается не просто функция, а метод callback нового класса виджета.

Здесь аргумент **config собирает в словарь все дополнительные именованные аргументы, которые затем передаются конструктору Button. Конструкция **config в вызове конструктора Button распаковывает словарь в список именованных аргументов (в действительности в этом нет необходимости, благодаря поддержке устаревшей формы вызова со словарем, встречавшейся нам ранее, но и вреда никакого не будет). Мы уже встречались с вызовом метода config виджетов в конструкторе HelloBut-ton: это просто альтернативный способ передачи параметров настройки после создания виджета (вместо передачи аргументов конструктору).


Стандартизация поведения и внешнего вида

Какой же смысл в создании подклассов виджетов? В двух словах, благодаря такому подходу появляется возможность создавать виджеты, выглядящие и действующие одинаково, путем создания подклассов, и использовать преимущества модели ООП в языке Python. Этот прием может оказаться достаточно мощным, чтобы использовать его в крупных программах.

Общие черты поведения

Сценарий в примере 7.18 стандартизирует поведение - он демонстрирует возможность настройки виджетов путем создания подклассов вместо передачи параметров. Экземпляр класса HelloButton является настоящей кнопкой - при ее создании параметры настройки, такие как text, передаются как обычно. Но можно также определить обработчик событий, переопределив в подклассе метод callback, как показано в примере 7.19.

Пример 7.19. PP4E\Gui\Intro\gui5b.py

from gui5 import HelloButton

class MyButton(HelloButton): # подкласс класса HelloButton

def callback(self): # переопределяет метод обработчика

print("Ignoring press!...”) # события нажатия кнопки

if__name__== ‘__main__’:

MyButton(None, text=’Hello subclass world’).mainloop()

Этот сценарий создает то же самое окно, но вместо завершения программы при нажатии кнопки MyButton происходит вывод сообщения в поток stdout и программа продолжает работу. Ниже показаны сообщения, выведенные в стандартный поток вывода после нескольких нажатий:

C:\...\PP4E\Gui\Intro> python gui5b.py

Ignoring press!...

Ignoring press!...

Ignoring press!...

Ignoring press!...

Решить, как проще настраивать графические элементы - создавая подклассы или путем передачи им параметров, - это дело вкуса. Но нужно отметить, что библиотека Tk приобретает в Python настоящие объектноориентированные черты, поскольку объектно-ориентированным является сам язык Python - используя привычные приемы ООП, мы можем создавать свои подклассы виджетов. Такой подход позволяет определять не только общие черты поведения, но и общий внешний вид.

Общий внешний вид

Например, хотя мы и не будем углубляться в изучение параметров настройки виджетов до следующей главы, тем не менее подобный класс кнопки мог бы обеспечить стандартизацию внешнего вида, отличного от внешнего вида, придаваемого библиотекой tkinter по умолчанию, для каждого своего экземпляра, и приблизиться к реализации таких понятий, как «стили» или «темы», используемых в некоторых других инструментах создания графических интерфейсов:

class ThemedButton(Button): # настраивает стиль

def __init__(self, parent=None, **configs): # для всех экземпляров

Button.__init__(self, parent, **configs) # описание параметров

self.pack() # смотрите в главе 8

self.config(fg=’red’, bg=’black’,

font=(‘courier’, 12), relief=RAISED, bd=5)

B1 = ThemedButton(text=’spam’, command=onSpam) # обычные виджеты кнопок B2 = ThemedButton(text=’eggs’) # но наследуют общий стиль

B2.pack(expand=YES, fill=BOTH)

Этот фрагмент приводится лишь для предварительного ознакомления - полную версию вы найдете в файле gui5b-themed.py в дереве примеров, а дополнительные сведения о параметрах настройки виджетов -в главе 8. Но он иллюстрирует возможность придания общего внешнего вида за счет создания подклассов виджетов - все кнопки, созданные из этого класса, будут выглядеть одинаково, и автоматически будут изменять внешний вид при любых изменениях в классе.

Прием создания подклассов - это, безусловно, инструмент программиста, но мы можем сделать возможность настройки доступной и для пользователей графических интерфейсов. В крупных программах, демонстрируемых далее в этой книге (например, PyEdit, PyClock и PyMail-GUI), мы иногда будем добиваться похожего эффекта за счет импортирования настроек из модулей и применения их к виджетам, как если бы они были встроенными настройками. Если такие внешние настройки использовать в подклассах виджетов, таких как наш класс ThemedBut-ton выше, они будут применяться ко всем экземплярам и подклассам (для справки: полная версия следующего фрагмента находится в файле gui5b-themed-user.py):

from user_preferences import bcolor, bfont, bsize # получить настройки

class ThemedButton(Button):

def __init__(self, parent=None, **configs):

Button.__init__(self, parent, **configs)

self.pack()

self.config(bg=bcolor, font=(bfont, bsize))

ThemedButton(text=’spam’, command=onSpam) # обычные виджеты кнопок, но

ThemedButton(text=’eggs’, command=onEggs) # наследуют настройки пользователя

class MyButton(ThemedButton): # подклассы также наследуют

def __init__(self, parent=None, **configs): # настройки пользователя

ThemedButton.__init__(self, parent, **configs)

self.config(text='subclass')

MyButton(command=onSpam)

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


Повторно используемые компоненты и классы

Крупные графические интерфейсы часто строятся как подклассы Frame с реализацией обработчиков событий в виде методов. Благодаря такой структуре возникает естественное место для хранения информации между событиями: атрибуты экземпляров классов. Она позволяет также специализировать графические интерфейсы за счет переопределения методов в новых подклассах и прикреплять их к более крупным структурам интерфейса, чтобы повторно использовать в качестве общих компонентов. Например, текстовый редактор с графическим интерфейсом, реализованный как подкласс Frame, можно прикреплять к любому числу других графических интерфейсов и настраивать с их помощью - при правильном использовании такой текстовый редактор можно встроить в любой интерфейс пользователя, где требуются средства редактирования текста.

С таким текстовым редактором в виде компонента мы встретимся в главе 11. А пока проиллюстрируем идею простым примером 7.20. Сценарий gui6.py создает окно, изображенное на рис. 7.20.

Пример 7.20. PP4E\Gui\Intro\gui6.py

from tkinter import *

class Hello(Frame): # расширенная версия класса Frame

def __init__(self, parent=None):

Frame.__init__(self, parent) # вызвать метод __init__ суперкласса

self.pack() self.data = 42

self.make_widgets() # прикрепить виджеты к себе

def make_widgets(self):

widget = Button(self, text=’Hello frame world!’, command=self.message) widget.pack(side=LEFT)

def message(self): self.data += 1

print(‘Hello frame world %s!’ % self.data) if __name__ == ‘__main__’: Hello().mainloop()

Рис. 7.20. Нестандартный фрейм в действии

Этот сценарий выводит окно с единственной кнопкой. При ее нажатии вызывается связанный метод self, message, который снова выводит сообщение в stdout. Ниже показаны сообщения, выведенные после четырехкратного нажатия кнопки - обратите внимание, что атрибут self.data (в данном случае простой счетчик) сохраняет информацию о состоянии между нажатиями:

C:\...\PP4E\Gui\Intro> python gui6.py Hello frame world 43!

Hello frame world 44!

Hello frame world 45!

Hello frame world 46!

Это может показаться окольным методом отображения кнопки типа Button (в примерах 7.10, 7.11 и 7.12 то же достигалось меньшим числом строк). Но класс Hello предоставляет контейнерную организационную структуру для построения графического интерфейса. В примерах, предшествовавших предыдущему разделу, графические интерфейсы создавались с применением процедурного подхода: мы вызывали конструкторы виджетов, как если бы они были функциями, и связывали виджеты воедино, указывая родителя при вызове конструктора. Не было никакого представления о внешнем контексте, помимо глобальной области видимости файла модуля, содержащего вызовы методов виджетов. Такой подход годится для простых графических интерфейсов, но при построении структур более крупных интерфейсов служит причиной хрупкости кода.

При создании подкласса, наследующего класс Frame, как было продемонстрировано, этот класс становится охватывающим контекстом графического интерфейса:

• Виджеты добавляются путем прикрепления объектов к self, экземпляру подкласса контейнера Frame (например, Button).

• Обработчики событий регистрируются как связанные методы объекта self, вследствие чего вызовы направляются обратно в реализацию класса (например, self.message).

• Информация о состоянии сохраняется между событиями путем присвоения атрибутам объекта self и доступна всем обработчикам событий в классе (например, self.data).

• Легко можно создать несколько экземпляров такого компонента графического интерфейса, даже внутри одного и того же процесса, потому что каждый экземпляр класса является отдельным пространством имен.

• Классы обладают естественной возможностью настройки благодаря возможности наследования и композиции.

В некотором смысле графические интерфейсы целиком становятся специализированными объектами Frame с расширениями, соответствующими их применению. Классы также позволяют реализовать протоколы построения виджетов (например, метод make_widgets в данном случае), выполнять стандартные задачи настройки (например, установка параметров настройки менеджера окон) и так далее. Одним словом, подклассы Frame дают простой способ организации совокупностей объектов других классов виджетов.


Прикрепление классов компонентов

Более важно, вероятно, что подклассы Frame являются настоящими виджетами: их можно и дальше расширять и модифицировать, создавая подклассы, и прикреплять к контейнерным виджетам. Например, чтобы прикрепить весь пакет виджетов, создаваемых классом, к чему-либо еще, нужно создать экземпляр класса, передав ему родительский виджет. Это иллюстрируется примером 7.21, при запуске которого создается окно, изображенное на рис. 7.21.

Пример 7.21. PP4E\Gui\Intro\gui6b.py

from sys import exit

from tkinter import * # импортировать классы виджетов Tk

from gui6 import Hello # импортировать подкласс фрейма

parent = Frame(None) # создать контейнерный виджет

parent.pack()

Hello(parent).pack(side=RIGHT) # прикрепить виджет Hello, не запуская его

Button(parent, text=’Attach’, command=exit).pack(side=LEFT) parent.mainloop()

Рис. 7.21. Прикрепленный компонент класса справа

В этом сценарии кнопка Hello добавляется к правому краю родителя parent - контейнера Frame. На самом деле, кнопка в правой части этого окна является встроенным компонентом: его кнопка действительно представляет прикрепленный объект класса Python. Нажатие кнопки встроенного класса справа, как и ранее, выводит сообщение; нажатие новой кнопки закрывает окно вызовом sys.exit:

C:\...\PP4E\Gui\Intro> python gui6b.py

Hello frame world 43!

Hello frame world 44!

Hello frame world 45!

Hello frame world 46!

В более сложных интерфейсах можно прикреплять большие подклассы Frame к другим компонентам-контейнерам и разрабатывать каждый из них независимо. В примере 7.22 демонстрируется еще один специализированный подкласс класса Frame, который прикрепляет экземпляр класса Hello более объектно-ориентированным способом. При запуске в виде самостоятельного сценария он создает окно, идентичное изображенному на рис. 7.21.

Пример 7.22. PP4E\Gui\Intro\gui6c.py

from tkinter import * # импортировать классы виджетов Tk

from gui6 import Hello # импортировать подкласс фрейма

class HelloContainer(Frame):

def __init__(self, parent=None):

Frame.__init__(self, parent)

self.pack()

self.makeWidgets()

def makeWidgets(self):

Hello(self).pack(side=RIGHT) # прикрепить объект класса Hello к себе Button(self, text=’Attach’, command=self.quit).pack(side=LEFT)

if __name__ == ‘__main__’: HelloContainer().mainloop()

Выглядит и действует этот сценарий в точности как gui6b, но в качестве обработчика события добавленной кнопки он регистрирует метод self. quit, который является стандартным методом quit виджетов, унаследованным от Frame. На этот раз окно демонстрирует действие двух классов Python - виджеты встроенного компонента справа (оригинальная кнопка Hello) и графические элементы контейнера слева.

Конечно, это простой пример (мы прикрепили здесь только одну кнопку). Но в более практических примерах набор прикрепляемых таким способом виджетов может быть значительно больше. Представьте себе, что мы заменили вызов конструктора Hello в этом сценарии вызовом, прикрепляющим готовый и полностью отлаженный объект калькулятора, и мощь данной парадигмы станет вам более понятна. Если все свои компоненты графического интерфейса создавать как классы, они автоматически образуют библиотеку многократно используемых виджетов, которые всегда можно будет вставить в другие приложения.


Расширение классов компонентов

При построении графических интерфейсов с помощью классов существует ряд способов повторного их использования в других приложениях. Чтобы расширить класс Hello, а не прикреплять его, можно переопределить некоторые его методы в новом подклассе (который сам станет специализированным виджетом Frame). Этот прием демонстрируется в примере 7.23.

Пример 7.23. PP4E\Gui\Intro\gui6d.py

from tkinter import * from gui6 import Hello

class HelloExtender(Hello):

def make_widgets(self): # расширение метода

Hello.make_widgets(self)

Button(self, text=’Extend’, command=self.quit).pack(side=RIGHT) def message(self):

print(‘hello’, self.data) # переопределение метода if __name__ == ‘__main__’: HelloExtender().mainloop()

Метод make_widgets этого подкласса сначала создает виджеты обращением к методу суперкласса, а затем добавляет справа вторую кнопку Extend, как показано на рис. 7.22.

Рис. 7.22. Модифицированные виджеты класса слева

Поскольку этот подкласс переопределяет метод message, нажатие кнопки исходного суперкласса, расположенной слева, теперь выводит в stdout другую строку (когда осуществляется поиск вверх по дереву наследования, начиная от объекта self, первым обнаруживается атрибут message в этом подклассе, а не в суперклассе):

C:\...\PP4E\Gui\Intro> python gui6d.py

hello 42

hello 42

hello 42

hello 42

Но нажатие новой кнопки Extend справа, добавленной этим подклассом, приводит к немедленному выходу из приложения, потому что обработчиком событий от добавленной кнопки является метод quit (унаследованный от Hello, который в свою очередь наследует его от Frame). Таким образом, этот класс модифицирует исходный класс, добавляя новую кнопку и изменяя поведение метода message.

Это довольно простой пример, но он наглядно демонстрирует технологию, обеспечивающую широкие возможности на практике: чтобы изменить поведение графического интерфейса, можно не изменять существующую реализацию, а написать новый класс, который модифицирует некоторые части интерфейса. Основная реализация отлаживается только один раз, а затем модифицируется с помощью подклассов, когда возникают особые потребности.

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


Автономные классы-контейнеры

Прежде чем двинуться дальше, хочу отметить, что большая часть перечисленных выше преимуществ от создания компонентов на основе классов может быть получена в результате создания автономных классов, не являющихся производными от класса Frame или других классов виджетов из библиотеки tkinter. Так, класс из примера 7.24 создает окно, изображенное на рис. 7.23.

Пример 7.24. PP4E\Gui\Intro\gui7.py

from tkinter import *

class HelloPackage: # не является подклассом виджета

def __init__(self, parent=None):

self.top = Frame(parent) # встроить фрейм Frame

self.top.pack() self.data = 0

self.make_widgets() # прикрепить виджеты к self.top

def make_widgets(self):

Button(self.top, text=’Bye’, command=self.top.quit).pack(side=LEFT) Button(self.top, text=’Hye’, command=self.message).pack(side=RIGHT)

def message(self): self.data += 1

print(‘Hello number’, self.data) if __name__ == ‘__main__’: HelloPackage().top.mainloop()

Рис. 7.23. Автономный класс в действии

Если запустить этот сценарий, кнопка Hye будет производить вывод в stdout, а Bye - закрывать окно и завершать работу программы, как и раньше:

C:\...\PP4E\Gui\Intro> python gui7.py

Hello number 1

Hello number 2

Hello number 3

Hello number 4

Так же как и раньше, атрибут self.data сохраняет информацию о состоянии между событиями, а для обработки событий вызывается метод self.message, имеющийся в этом классе. Но в отличие от того, что было раньше, сам класс HelloPackage не является подклассом виджета Frame.

Он вообще не является подклассом какого-либо виджета - он служит только для создания пространства имен, хранящего действительные объекты виджетов и информацию о состоянии. По этой причине виджеты прикрепляются к объекту self.top (встроенному фрейму Frame), а не к self. Более того, все ссылки на объект как на виджет должны передаваться вниз, встроенному фрейму, как, например, вызов метода top. mainloop для запуска интерфейса в конце сценария.

Это приводит к тому, что внутри класса приходится писать чуть больше программного кода, но устраняет возможные конфликты имен между атрибутами, добавляемыми к self в структуре tkinter и существующими методами виджетов tkinter. Например, если определить в своем классе метод config, он замаскирует вызов config, экспортируемый библиотекой tkinter. Для автономных классов, как в этом примере, доступны будут только те методы и атрибуты экземпляров, которые определены в этих классах.

В действительности в библиотеке tkinter используется не очень много имен, поэтому обычно это не создает больших проблем.38 Конечно, такая вероятность существует, но, честно говоря, за 18 лет программирования на языке Python я не встречался с конфликтами имен tkinter в подклассах виджетов. Кроме того, использование автономных классов не лишено своих недостатков. Хотя в целом их можно прикреплять и создавать производные от них классы, они не вполне совместимы с действительными объектами виджетов. Например, вызовы настройки, выполняемые в примере 7.21 для подкласса Frame, будут терпеть неудачу в примере 7.25.

Пример 7.25. PP4E\Gui\Intro\gui7b.py

from tkinter import *

from gui7 import HelloPackage # или from gui7c, где добавлен __getattr__

frm = Frame() frm.pack()

Label(frm, text=’hello’).pack() part = HelloPackage(frm)

part.pack(side=RIGHT) # НЕУДАЧА! Должно быть part.top.pack(side=RIGHT)

frm.mainloop()

Этот сценарий вообще не будет работать, потому что part не является настоящим виджетом. Чтобы работать с ним как с виджетом, нужно спуститься в part.top, прежде чем настраивать интерфейс, и рассчитывать на то, что имя top не будет изменено разработчиком класса. Другими словами, требуется знать внутреннее устройство класса. Лучше всего эти действия реализовать в самом классе, определив метод, всегда направляющий обращения к неизвестным атрибутам встроенному объекту класса Frame, как показано в примере 7.26.

Пример 7.26. PP4E\Gui\Intro\gui7c.py

import gui7

from tkinter import *

class HelloPackage(gui7.HelloPackage): def __getattr__(self, name):

return getattr(self.top, name) # передать вызов настоящему виджету

if __name__ == ‘__main__’: HelloPackage().mainloop() # вызовет __getattr__!

Этот сценарий создает такое же окно, как на рис. 7.23. Однако изменения в примере 7.25, выражающиеся в импортировании расширенной версии класса HelloPackage из модуля gui7c, обеспечивают корректную работу интерфейса, изображенного на рис. 7.24.

Рис. 7.24. Автономный класс в действии

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


Завершение начального обучения

В данной главе мы изучили основы использования библиотеки tkinter в сценариях на языке Python и познакомились с рядом простых виджетов - метками, кнопками, фреймами, а также с менеджером компоновки. Этого достаточно для создания простых интерфейсов, но в действительности мы лишь поверхностно ознакомились с набором виджетов tkinter.

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


Таблица 7.1. Классы виджетов tkinter


Классы виджетов

Описание

Label

Простая область для вывода текста

Button

Простая кнопка с меткой

Frame

Контейнер для прикрепления и размещения других виджетов

Toplevel, Tk

Новое окно, управляемое менеджером окон

Message

Метка с несколькими строками

Entry

Простое однострочное поле ввода текста

Checkbutton

Кнопка с двумя состояниями; обычно используется для организации для выбора нескольких вариантов

Radiobutton

Кнопка с двумя состояниями; обычно используется для организации выбора одного варианта из нескольких

Scale

Ползунок со шкалой

PhotoImage

Объект графического изображения, используемый для вывода полноцветных изображений на других виджетах

BitmapImage

Объект графического изображения, используемый для вывода растровых изображений на других виджетах

Menu

Набор вариантов выбора, связанных с Menubutton или с окном верхнего уровня


Таблица 7.1 (продолжение)


Классы виджетов

Описание

Menubutton

Кнопка, открывающая меню или подменю с вариантами выбора

Scrollbar

Элемент управления для прокрутки содержимого других виджетов (например, списка, холста, текста)

Listbox

Список имен, доступных для выбора

Text

Виджет для просмотра/редактирования многострочного текста, поддерживающий шрифты и так далее

Canvas

Область для изображения графики с поддержкой возможности рисования линий, окружностей, фотографий, текста и так далее.


В этой главе мы уже встречались с виджетами Label, Button и Frame. Для облегчения усвоения оставшийся материал разбит на две главы: глава 8 освещает элементы в верхней части табл. 7.1, вплоть до Menu, а в главе 9 представлены виджеты, находящиеся в нижней части таблицы.

Помимо классов виджетов, представленных в табл. 7.1, в библиотеке tkinter содержатся дополнительные классы и инструменты, многие из которых также будут исследованы в двух следующих главах:

Управление компоновкой

pack, grid, place

Связанные переменные tkinter

StringVar, IntVar, DoubleVar, BooleanVar Улучшенные виджеты. Tk

Spinbox, LabelFrame, PanedWindow Составные виджеты.

Dialog, ScrolledText, OptionMenu Планируемые обратные вызовы

Методы виджетов after, wait и update Прочие инструменты.

Стандартные диалоги, буфер обмена, bind и Event, параметры настройки виджетов, пользовательские и модальные диалоги, анимационные эффекты

Большинство виджетов tkinter принадлежит к числу знакомых интерфейсных элементов. Некоторые из них обладают очень богатой функциональностью. Например, класс Text реализует сложный виджет многострочного текста, поддерживающий шрифты, цвета и спецэффекты, мощности которого достаточно для реализации веб-броузера. Аналогично класс Canvas предоставляет множество инструментов, достаточно мощных для создания приложений отображения и обработки изображений. Кроме того, расширения для библиотеки tkinter, такие как Pmw, Tix и ttk, добавляют в инструментарий разработчика графических интерфейсов виджеты с еще более богатыми возможностями.


Соответствие между Python/tkinter и Tcl/Tk

В начале этой главы я упомянул, что библиотека tkinter является интерфейсом Python к библиотеке Tk, первоначально написанной для языка Tcl. В помощь читателям, переходящим на Python с Tcl, и для подведения итогов некоторых основных тем, встретившихся в этой главе, в данном разделе сравниваются интерфейсы Python и Tcl к Tk. Кроме того, такое сопоставление сделает для разработчиков программ на языке Python более полезными справочники по Tk, написанные для других языков.

Вообще говоря, ориентированность Tcl на применение строковых команд сильно отличается от подхода к программированию на языке Python, основанного на объектах. Однако, что касается использования Tk, синтаксические различия невелики. Ниже приводятся некоторые главные отличия интерфейса Python к tkinter:

Создание

Виджеты создаются как экземпляры классов при вызове конструктора класса виджета.

Владельцы (родители)

Родителями являются ранее созданные объекты, передаваемые конструкторам классов виджетов.

Параметры виджетов

Параметры являются именованными аргументами конструктора или метода config либо ключами словарей.

Операции

Операции виджетов (действия) становятся методами объектов классов виджетов tkinter.

Обратные вызовы

Обработчиком события может быть любой вызываемый объект: функция, метод, lambda-выражение и так далее.

Расширение

Виджеты расширяются с использованием механизма наследования классов в языке Python.

Композиция

Интерфейсы конструируются путем прикрепления объектов, а не конкатенации имен.

Связанные переменные (следующая глава)

Переменные, ассоциируемые с виджетами, являются объектами классов tkinter с методами.

Команды создания виджетов в языке Python (например, button) являются именами классов, начинающимися с заглавной буквы (например, Button), операции с виджетами, состоящие из двух слов (например, add command), становятся одним именем метода с подчеркиванием внутри (например, add_command), а метод «configure» можно кратко записывать «config», как в Tcl. В главе 8 будет также показано, что «переменные» библиотеки tkinter, ассоциируемые с виджетами, принимают форму объектов экземпляров классов (например, StringVar, IntVar) с методами get и set, а не просто именами переменных Python или Tcl. В табл. 7.2 более конкретно приведены основные соответствия между языками.


Таблица 7.2. Соответствие между Tk и tkinter


Операция

Tcl/Tl

Python/tkinter

Создание

Frame .panel

panel = Frame()

Владелец

button .panel.quit

quit = Button(panel)

Параметры

button .panel.go -fg black

go = Button(panel, fg=' black’)

Настройка

.panel.go config -bg red

g o. c o n f i g (b g=’ r e d ’) go[‘bg’] = ‘red’

Действия

.popup invoke

popup.invoke()

Компоновка

pack .panel -side left -fill x

panel.pack(side=LEFT, fill=X)


Конечно, некоторые из этих различий касаются не только синтаксиса. Например, Python строит внутреннее дерево объектов графических элементов, исходя из аргументов, определяющих родителя и передаваемых конструктору, не требуя объединения строк путей к виджетам. После создания виджета к нему можно обращаться непосредственно, по ссылке. При программировании на языке Tcl иногда можно спрятать записанные через точку имена путей вручную в переменных, но это не то же самое, что чисто объектно-ориентированная модель Python.

Как только вы напишете на языке Python несколько сценариев, использующих библиотеку tkinter, отличия объектов Python наверняка будут казаться вам тривиальными. В то же время, поддержка объектноориентированных приемов в языке Python вносит совершенно новую составляющую в разработку для Tk: вы получаете те же самые виджеты плюс поддержку возможности структурирования и многократного использования программного кода.

8

Экскурсия по tkinter, часть 1


«Виджеты, гаджеты, графические интерфейсы... Бог мой!»

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

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


Темы этой главы

Формально мы уже использовали ряд простых виджетов в главе 7. Пока мы познакомились с классами Label, Button, Frame и Tk и попутно изучили понятия управления компоновкой в методе pack. Несмотря на свою простоту, все эти классы достаточно полно представляют интерфейсы библиотеки tkinter в целом и служат рабочими лошадками в типичных графических интерфейсах. Например, контейнеры Frame служат основой иерархической структуры отображения.

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

• Виджеты Toplevel и Tk

• Виджеты Message и Entry

• Виджеты Checkbutton, Radiobutton и Scale

• Изображения: объекты PhotoImage и BitmapImage

• Параметры настройки виджетов и окон

• Диалоги: стандартные и пользовательские

• Низкоуровневое связывание событий

• Объекты связанных переменных tkinter

• Использование библиотеки Python обработки изображений - расширения PIL (Python Imaging Library) - для работы с изображениями других типов

Глава 9 завершает краткий рассказ, представляя остальные элементы инструментария библиотеки tkinter: меню, текст, холсты, анимацию и другие.

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


Настройка внешнего вида виджетов

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

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

Пример 8.1. PP4E\Gui\Tour\config-label.py

from tkinter import * root = Tk()

labelfont = (‘times’, 20, ‘bold’) # семейство, размер, стиль widget = Label(root, text=’Hello config world’) widget.config(bg=’black’, fg=’yellow’) # желтый текст на черном фоне widget.config(font=labelfont) # использовать увеличенный шрифт

widget.config(height=3, width=20) # начальный размер: строк,символов

widget.pack(expand=YES, fill=BOTH) root.mainloop()

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

Рис. 8.1. Нестандартный внешний вид метки

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

Цвет

Установкой параметра bg метки определяется черный цвет ее фона. Аналогично параметр fg изменяет цвет переднего плана (текста) метки на желтый. Эти параметры цвета присутствуют у большинства виджетов tkinter, и в них указывается простое название цвета (например, ‘blue’) или шестнадцатеричная строка. Поддерживается большинство знакомых названий цветов (если только вам не довелось работать для компании Crayola39). Чтобы более точно определить значение цвета, в этих параметрах можно также передавать строки с шестнадцатеричными значениями - они должны начинаться с символа # и содержать значения насыщенности красного, зеленого и голубого цветов с одинаковым количеством битов для каждого. Например, строка ‘#ff0000’ содержит по восемь битов для каждого цвета и определяет чистый красный цвет - «f» означает в шестнадцатеричном виде четыре единичных бита. Мы еще вернемся к шестнадцатеричному формату, когда встретимся с диалоговым окном выбора цвета далее в этой главе.

Размер

Для метки определяется точный размер в виде количества строк в высоту и символов в ширину путем установки параметров height и width. С помощью этих параметров можно увеличивать размеры метки по сравнению с теми, что устанавливаются менеджером компоновки по умолчанию.

Шрифт

В этом сценарии выбирается нестандартный шрифт для текста метки путем записи в параметр font кортежа из трех элементов, определяющих семейство шрифта, его размер и стиль (в данном случае: Times, 20 пунктов, полужирный). Стиль шрифта может принимать значения normal, bold, roman, italic, underline, overstrike и их сочетания (например, “bold italic”). Библиотека tkinter гарантирует возможность использования названий семейств шрифтов Times, Courier и Helvetica на всех платформах, однако в некоторых системах могут использоваться и другие (например, system - системный шрифт в Windows). Такие настройки шрифта будут действовать для всех виджетов, содержащих текст, например меток, кнопок, полей ввода, списков и Text (последний может одновременно выводить текст, отображаемый различными шрифтами, с помощью «тегов»). Параметр font сохраняет возможность определять шрифт с помощью более старых определений в стиле X Window - длинных строк с дефисами и звездочками, однако более новая форма определения параметров шрифта в виде кортежа более независима от платформы.

Параметры, компоновки

Наконец, метка может быть сделана расширяемой и растягиваемой в целом путем установки параметров компоновщика pack, таких как expand и fill, с которыми мы познакомились в предыдущей главе: метка увеличивается вместе с окном. При распахивании окна черный фон заполняет весь экран, а желтый текст помещается в центр -можете попробовать.

В данном сценарии итоговым результатом настроек является метка, по своему внешнему виду коренным образом отличающаяся от тех, что мы создавали до сих пор. Она больше не следует стандартам внешнего вида Windows, но такая согласованность не всегда важна. Для справки отмечу, что библиотека tkinter предоставляет дополнительные способы настройки внешнего вида, не используемые в данном сценарии, но которые могут встретиться вам в других сценариях:

Загрузка...