Пример 8.31. PP4E\Gui\Tour\demo-seale-simple.py

from tkinter import * root = Tk()

scl = Scale(root, from_=-100, to=100, tickinterval=50, resolution=10) scl.pack(expand=YES, fill=Y)

def report():

print(scl.get())

Button(root, text=’state’, command=report).pack(side=RIGHT) root.mainloop()

На рис. 8.31 изображены два экземпляра этой программы, запущенные в Windows, окно одной из них растянуто, а другой - нет (ползунки

Рис. 8.31. Простая шкала без переменных

настроены так, чтобы они растягивались по вертикали). Шкала имеет диапазон значений от -100 до 100, использует параметр resolution для изменения текущей позиции на 10 единиц вверх или вниз при каждом перемещении, а параметр tickinterval установлен так, чтобы рядом со шкалой отображались метки с шагом 50. Если щелкнуть на кнопке State в окне этого сценария, будет вызван метод get шкалы для получения и вывода текущего значения без всяких переменных или обратных вызовов:

C:\...\PP4E\Gui\Tour> python demo-scale-simple.py

0

60

-70

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

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


Три способа использования графических интерфейсов

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


Прикрепление к фреймам

Чтобы проиллюстрировать иерархическое построение графического интерфейса в более крупном масштабе, чем это делалось до сих пор, пример 8.32 объединяет все четыре сценария панелей запуска диалогов из этой главы в одном контейнере. В нем повторно используется программный код примеров 8.9, 8.22, 8.25 и 8.30.

Пример 8.32. PP4E\Gui\Tour\demoAll-frm.py

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

from tkinter import * from quitter import Quitter

demoModules = [‘demoDlg’, ‘demoCheck’, ‘demoRadio’, ‘demoScale’] parts = []

def addComponents(root): for demo in demoModules:

module = __import__(demo) # импортировать по имени в виде строки

part = module.Demo(root) # прикрепить экземпляр

part.config(bd=2, relief=GROOVE) # или передать параметры

# конструктору Demo()

part.pack(side=LEFT, expand=YES, fill=BOTH) # растягивать

# вместе с окном

parts.append(part) # добавить в список

def dumpState():

for part in parts:

print(part.__module__ + ‘:’, end=’ ‘)

if hasattr(part, ‘report’): # вызвать метод report,

part.report() # если имеется

else:

print(‘none’)

root = Tk() # явно создать корневое окно

root.title(‘Frames’)

Label(root, text=’Multiple Frame demo’, bg=’white’).pack()

Button(root, text=’States’, command=dumpState).pack(fill=X)

Quitter(root).pack(fill=X)

addComponents(root)

root.mainloop()

Поскольку все четыре демонстрационные панели запуска реализованы в виде фреймов, которые могут прикрепляться к родительским виджетам, объединить их в одном графическом интерфейсе намного проще, чем вы думаете. Для этого нужно лишь передать один и тот же родительский виджет (в данном случае окно root) во все четыре вызова конструкторов демонстрационных примеров, после чего скомпоновать и настроить созданные демонстрационные объекты желаемым образом. На рис. 8.32 показано, как выглядит результат - одно окно, в которое встроены экземпляры всех четырех знакомых нам демонстрационных панелей запуска диалогов. В данном примере все четыре встроенных панели изменяют свои размеры при изменении размеров окна (попробуйте убрать параметр expand=YES, чтобы панели сохраняли свои размеры постоянными).

Рис. 8.32. demoAll_frm: вложенные фреймы

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

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

C:\...\PP4E\Gui\Tour> python demoAll_frm.py

in onMove 0 in onMove 0 demoDlg: none demoCheck: 0 0 0 0 0 demoRadio: Error demoScale: 0 you pressed Input result: 1.234 in onMove 1 demoDlg: none demoCheck: 1 0 1 1 0 demoRadio: Input demoScale: 1 you pressed Query result: yes in onMove 2 You picked 2 None

in onMove 3 You picked 3

C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Launcher.py

3

Query 1 1 1 1 0 demoDlg: none demoCheck: 1 1 1 1 0 demoRadio: Query demoScale: 3

Импортирование по имени в виде строки

Единственным хитроумным приемом в этом сценарии является использование встроенной функции__import__, импортирующей модуль по

его имени в виде строки. Взглянем на следующие две строки из функции addComponents сценария:

module = __import__(demo) # импортировать по имени в виде строки

part = module.Demo(root) # прикрепить экземпляр, созданный его классом Demo

Они эквивалентны следующим строкам:

import ‘demoDlg’

part = ‘demoDlg’.Demo(root)

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

dComponents обходит список строк с именами и с помощью__import__

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

import demoDlg, demoRadio, demoCheck, demoScale

part = demoDlg.Demo(root)

part = demoRadio.Demo(root)

part = demoCheck.Demo(root)

part = demoScale.Demo(root)

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

for demo in demoModules:

exec(‘from %s import Demo’ % demo) # сконструировать и выполнить from part = eval(‘Demo’)(root) # получить ссылку на импортированный

# объект по его имени в виде строки

Функция exec компилирует и выполняет строку с инструкцией Python (в данном случае from, загружающей класс Demo из модуля). Она действует, как если бы вместо вызова функции exec в исходный текст была вставлена строка с инструкцией. Следующий фрагмент позволяет добиться того же эффекта, но конструируя инструкцию import:

for demo in demoModules:

exec(‘import %s’ % demo) # сконструировать и выполнить import part = eval(demo).Demo(root) # получить ссылку на объект модуля

# также по имени в виде строки

Так как функции exec/eval поддерживают любые инструкции Python, этот прием оказывается более универсальным, чем использование

функции__import__, но он может замедлять работу, потому что при

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

1 Как будет показано дальше, exec может представлять опасность, если выполняет строки, полученные от пользователей или по сети. Это не относится к жестко определенным строкам в данном примере.

Настройка на этапе конструирования

Еще одна альтернатива, о которой следует упомянуть: обратите внимание, как в примере 8.32 выполняется настройка и компоновка каждого прикрепляемого демонстрационного фрейма:

def addComponents(root): for demo in demoModules:

module = __import__(demo) # импортировать по имени в виде строки

part = module.Demo(root) # прикрепить экземпляр

part.config(bd=2, relief=GROOVE) # или передать параметры

# конструктору Demo()

part.pack(side=LEFT, expand=YES, fill=BOTH) # растягивать

# вместе с окном

Однако благодаря тому что демонстрационные классы поддерживают параметры настройки, используя аргумент **options, мы могли бы выполнять настройки прямо на этапе создания. Например, если изменить реализацию сценария, как показано ниже, он воспроизведет несколько отличающееся окно, изображенное на рис. 8.33 (для иллюстрации несколько растянутое по горизонтали; вы найдете эту реализацию в файле demoAll-frm-ridge.py в пакете с примерами):

def addComponents(root): for demo in demoModules:

module = __import__(demo) # импортировать по имени в виде строки

part = module.Demo(root, bd=6, relief=RIDGE) # прикрепить, настроить part.pack(side=LEFT, expand=YES, fill=BOTH) # экземпляр так, чтобы он

# растягивался с окном

Рис. 8.33. demoAll_frm: настройка на этапе конструирования

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

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


Независимые окна

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

Пример 8.33. PP4E\Gui\Tour\demoAll-win.py

4 демонстрационных класса в независимых окнах верхнего уровня; не процессы: при завершении одного щелчком на кнопке Quit завершаются все остальные, потому что все окна выполняются в одном и том же процессе; здесь первое окно Tk создается вручную, иначе будет создано пустое окно

from tkinter import *

demoModules = [‘demoDlg’, ‘demoRadio’, ‘demoCheck’, ‘demoScale’]

def makePopups(modnames): demoObjects = [] for modname in modnames:

module = __import__(modname) # импортировать по имени в виде строки

window = Toplevel() # создать новое окно

demo = module.Demo(window) # родительским является новое окно

window.title(module.__name__)

demoObjects.append(demo) return demoObjects

def allstates(demoObjects): for obj in demoObjects:

if hasattr(obj, ‘report’):

print(obj.__module__, end=’ ‘)

obj.report()

root = Tk() # явно создать корневое окно

root.title(‘Popups’)

demos = makePopups(demoModules)

Label(root, text=’Multiple Toplevel window demo’, bg=’white’).pack()

Button(root, text=’States’, command=lambda: allstates(demos)).pack(fill=X) root.mainloop()

Мы уже встречались с классом Toplevel - каждый его экземпляр создает на экране новое окно. Получаемый результат изображен на рис. 8.34 -каждая демонстрационная панель выполняется не в общем, а в собственном окне.

Рис. 8.34. demoAll_win: новые окна Toplevel

Главное корневое окно на этом рисунке находится в левом нижнем углу. На нем есть кнопка States, которая вызывает метод report каждого демонстрационного объекта, выводя в stdout примерно такой текст:

C:\...\PP4E\Gui\Tour> python demoAll_win.py

in onMove 0 in onMove 0 in onMove 1 you pressed Open

result: C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Launcher.py demoRadio Open demoCheck 1 1 0 0 0 demoScale 1

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


Запуск программ

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

При запуске их таким образом каждая из них получает имя__main__,

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

Пример 8.34. PP4E\Gui\Tour\demoAll-prg.py

4 демонстрационных класса, выполняемых как независимые процессы: команды; если теперь одно окно будет завершено щелчком на кнопке Quit, остальные продолжат работу; в данном случае не существует простого способа вызвать все методы report (впрочем, для организации взаимодействий между процессами можно было бы воспользоваться сокетами и каналами), а кроме того, некоторые способы запуска могут сбрасывать поток stdout дочерних программ и разрывать связь между родителем и потомком;

from tkinter import *

from PP4E.launchmodes import PortableLauncher

demoModules = [‘demoDlg’, ‘demoRadio’, ‘demoCheck’, ‘demoScale’]

for demo in demoModules: # смотрите главу 5

PortableLauncher(demo, demo + ‘.py’)() # запуск в виде программ верхнего

# уровня

root = Tk()

root.title(‘Processes’)

Label(root, text=’Multiple program demo: command lines’, bg=’white’).pack() root.mainloop()

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

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

Рис. 8.35. demoAll_prg: независимые программы

Запуск графических интерфейсов как самостоятельных программ другими способами: модуль multiprocessing

Если вернуться к главе 5 и рассмотреть реализацию модуля запуска процессов переносимым способом, использованным в примере 8.34, можно заметить, что в Windows он использует функцию os.spawnv, а в других системах - os.fork/exec. То есть графические интерфейсы в данном примере запускаются выполнением команд оболочки. Эти способы прекрасно справляются со своей задачей, но, как мы узнали в главе 5, они входят в состав более обширного набора инструментов запуска программ, в число которых также входят os.popen, os.system, os.startfile и модули subprocess и multiprocessing. Эти инструменты могут отличаться деталями подключения к окну консоли, реакцией на завершение родительского процесса и так далее.

Например, модуль multiprocessing, с которым мы познакомились в главе 5, предоставляет похожий переносимый способ запуска других графических интерфейсов в виде независимых процессов, как показано в примере 8.35. Если запустить его, он воспроизведет точно такое же окно, как на рис. 8.35, но с другими метками в главном окне.

Пример 8.35. PP4E\Gui\Tour\demoAll-prg-multi.py

4 демонстрационных класса, выполняемых как независимые процессы: multiprocessing;

модуль multiprocessing позволяет запускать только именованные функции с аргументами - он не может работать с lambda-выражениями, поскольку в Windows они не могут быть сериализованы (глава 5); кроме того, модуль multiprocessing имеет собственные инструменты взаимодействий между процессами, такие как каналы;

from tkinter import *

from multiprocessing import Process

demoModules = [‘demoDlg’, ‘demoRadio’, ‘demoCheck’, ‘demoScale’]

def runDemo(modname): # запускается в новом процессе

module = import (modname) # создать GUI с нуля

module.Demo().mainloop()

if__name__== ‘__main__’:

for modname in demoModules: # только в __main__!

Process(target=runDemo, args=(modname,)).start()

root = Tk() # граф. интерфейс родительского процесса

root.title(‘Processes’)

Label(root, text=’Multiple program demo: multiprocessing’, bg=’white’).pack() root.mainloop()

При запуске в Windows эта версия имеет только следующие функциональные отличия:

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

• Сценарий не завершается, если хотя бы один дочерний процесс продолжает выполнение: окно консоли в данном примере блокируется при попытке закрыть окно главного процесса, пока дочерние процессы продолжают работу, если только не установить флаг daemon дочерних процессов в значение True перед их запуском, как было показано в главе 5, - в этом случае все дочерние процессы автоматически будут завершены вместе с их родителем (но родитель по-прежнему может пережить своих потомков).

Обратите также внимание, как мы запускаем простую именованную функцию в новом процессе Process. Как мы узнали в главе 5, в Windows в аргументе target допускается передавать только сериализуемые выполняемые объекты (то есть те, которые можно импортировать), поэтому мы не можем использовать lambda-выражения для передачи дополнительных данных, как мы обычно делали это в обработчиках событий tkinter. Следующие два варианта реализации будут терпеть неудачу в Windows:

Process(target=(lambda: runDemo(modname))).start() # оба терпят неудачу!

Process(target=(lambda: __import__(modname).Demo().mainloop())).start()

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

Обмен данными между программами

Запуск графических интерфейсов в виде самостоятельных программ -вершина независимости кода, но это затрудняет связь между компонентами. Например, поскольку демонстрационные примеры выполняются здесь в виде отдельных программ, нет простого способа вызвать все их методы report из окна запускающего сценария, изображенного слева вверху на рис. 8.35. Кнопки States теперь нет, и в поток stdout попадают только сообщения от экземпляра PortableLauncher:

C:\...\PP4E\Gui\Tour> python demoAll_prg.py

demoDlg

demoRadio

demoCheck

demoScale

На некоторых платформах сообщения, выводимые демонстрационными программами (в том числе собственными кнопками State), могут появиться в исходном окне консоли, в котором запущен сценарий. В Windows функция os.spawnv, используемая в модуле launchmodes для запуска программ, полностью отключает поток stdout дочерней программы от родителя. В любом случае нет прямого способа одновременно вызвать методы report во всех демонстрационных программах - это отдельные программы, выполняющиеся в отдельных адресных пространствах, а не импортированные модули.

Однако существует возможность организовать вызов методов report в порожденных программах с помощью некоторых механизмов IPC, с которыми мы познакомились в главе 5. Например:

• Демонстрационные программы могут быть оснащены механизмом приема сигнала, в ответ на который они будут вызывать свой метод

report.

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

• Независимые программы могут общаться подобным образом с помощью сокетов, универсальным инструментом, представленным в главе 5, который мы будем подробно изучать в четвертой части книги. Главный сценарий мог бы отправлять запрос на получение отчета и принимать ответ в тот же самый сокет (и контактировать с демонстрационными сценариями, выполняющимися удаленно).

• При использовании модуля multiprocessing становятся доступны его собственные инструменты IPC, такие как каналы и очереди объектов, представленные в главе 5, которые также можно было бы задействовать для организации обмена данными: демонстрационные сценарии могли бы также прослушивать каналы этого типа.

Исходя из управляемой событиями природы, программы с графическим интерфейсом должны избегать перехода в состояние ожидания -они не должны блокироваться, ожидая появления запросов в механизмах IPC, иначе они не будут откликаться на действия пользователя (и даже не смогут перерисовывать себя). Поэтому может потребоваться дополнить их потоками выполнения, обработчиками, вызываемыми по таймеру, выполнять операции чтения в неблокирующем режиме или использовать комбинации этих инструментов для периодической проверки таких входящих сообщений в каналах, fifo или сокетах. Как мы увидим далее, метод after из библиотеки tkinter, описываемый ближе к концу следующей главы, является идеальным средством для этого: он позволяет регистрировать функции обратного вызова для периодической проверки наличия входящих запросов.

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

Программирование, обеспечивающее повторное использование

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

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

Лишние виджеты

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

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

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

Ограничения режима использования

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

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

Пример 8.36. PP4E\Gui\Tour\bwttonbars.py

классы панелей флажков и переключателей для приложений, которые запрашивают информацию о состоянии позднее;

передается список вариантов выбора, вызывается метод state(), работа с переменными выполняется автоматически

from tkinter import * class Checkbar(Frame):

def __init__(self, parent=None, picks=[], side=LEFT, anchor=W):

Frame.__init__(self, parent)

self.vars = [] for pick in picks: var = IntVar()

chk = Checkbutton(self, text=pick, variable=var) chk.pack(side=side, anchor=anchor, expand=YES) self.vars.append(var) def state(self):

return [var.get() for var in self.vars] class Radiobar(Frame):

def __init__(self, parent=None, picks=[], side=LEFT, anchor=W):

Frame.__init__(self, parent)

self.var = StringVar() self.var.set(picks[0]) for pick in picks:

rad = Radiobutton(self, text=pick, value=pick, variable=self.var) rad.pack(side=side, anchor=anchor, expand=YES) def state(self):

return self.var.get()

if__name__== ‘__main__’:

root = Tk()

lng = Checkbar(root, [‘Python’, ‘C#’, ‘Java’, ‘C++’])

gui = Radiobar(root, [‘win’, ‘x11’, ‘mac’], side=TOP, anchor=NW)

tgl = Checkbar(root, [‘All’])

gui.pack(side=LEFT, fill=Y) lng.pack(side=TOP, fill=X) tgl.pack(side=LEFT) lng.config(relief=GROOVE, bd=2) gui.config(relief=RIDGE, bd=2)

def allstates():

print(gui.state(), lng.state(), tgl.state())

from quitter import Quitter Quitter(root).pack(side=RIGHT)

Button(root, text=’Peek’, command=allstates).pack(side=RIGHT) root.mainloop()

Для повторного использования этих классов в сценариях нужно импортировать их и вызвать со списком вариантов выбора, которые должны появиться на панелях флажков или переключателей. Программный код самотестирования модуля, находящийся в конце, демонстрирует особенности использования этих классов. Если запустить этот пример как самостоятельный сценарий, на экране появится окно верхнего уровня, изображенное на рис. 8.36, с двумя встроенными панелями Checkbar, одной панелью Radiobar, кнопкой Quitter для завершения, а также кнопкой Peek для вывода информации о состоянии панелей.

Рис. 8.36. Окно самотестирования сценария buttonbars

Ниже приводится содержимое стандартного потока вывода stdout после щелчка на кнопке Peek - результат вызова методов state этих классов:

x11 [1, 0, 1, 1] [0]

win [1, 0, 0, 1] [1]

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

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


Изображения

В библиотеке tkinter графические изображения отображаются за счет создания независимых объектов PhotoImage или BitmapImage и прикрепления их к другим виджетов путем установки атрибута image. Кнопки, метки, холсты, текстовые виджеты и меню - все они могут выводить изображения, связывая таким способом готовые графические объекты. Для иллюстрации сценарий в примере 8.36 выводит картинку на кнопке.

Пример 8.37. PP4E\Gui\Tour\imgButton.py

gifdir = "../gifs/” from tkinter import * win = Tk()

igm = PhotoImage(file=gifdir + “ora-pp.gif”)

Button(win, image=igm).pack() win.mainloop()

Трудно было бы придумать более простой пример: этот сценарий всего лишь создает объект PhotoImage для GIF-файла, хранящегося в другом каталоге, и связывает его с параметром image виджета Button. Результат изображен на рис. 8.37.

Рис. 8.37. Сценарий imgButton в действии

Объект PhotoImage и его собрат BitmapImage просто загружают графические файлы и позволяют прикреплять полученные изображения к другим типам виджетов. Чтобы открыть файл с картинкой, его имя необходимо передать в атрибуте file этих виджетов изображений. Несмотря на всю простоту, прикрепление изображений к кнопкам может найти применение во множестве ситуаций - в главе 9, например, мы будем использовать эту простую идею при реализации кнопок для панелей инструментов в нижней части окна.

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

Рис. 8.38. Изображение на холсте

Пример 8.38. PP4E\Gui\Tour\imgCanvas.py

gifdir = “../gifs/” from tkinter import * win = Tk()

img = PhotoImage(file=gifdir + “ora-lp4e.gif”)

can = Canvas(win)

can.pack(fill=BOTH)

can.create_image(2, 2, image=img, anchor=NW) # координаты x, y win.mainloop()

Размеры кнопок автоматически изменяются в соответствии с размерами изображений, холсты свои размеры не изменяют (потому что в холсты можно добавлять объекты, как будет показано в главе 9). Чтобы размер холста соответствовал размерам изображения, нужно установить его размер, исходя из значений, возвращаемых методами width и height объектов изображений, как в примере 8.39. Эта версия сценария при необходимости делает холст больше или меньше, чем размер, устанавливаемый по умолчанию; позволяет передавать имя графического файла в аргументе командной строки и может использоваться в качестве простой утилиты просмотра изображений. Окно, создаваемое этим сценарием, изображено на рис. 8.39.

Пример 8.39. PP4E\Gui\Tour\imgCanvas2.py

gifdir = “../gifs/” from sys import argv from tkinter import *

filename = argv[1] if len(argv) > 1 else ‘ora-lp4e.gif’ # имя файла

# в командной строке?

Рис. 8.39. Изменение размера холста соответственно картинке

win = Tk()

img = PhotoImage(file=gifdir + filename)

can = Canvas(win)

can.pack(fill=BOTH)

can.config(width=img.width(), height=img.height()) # размер соответственно can.create_image(2, 2, image=img, anchor=NW) # картинке

win.mainloop()

Чтобы просмотреть другое изображение, нужно запустить этот сценарий, передав ему имя другого файла (попробуйте сами, на своем компьютере):

C:\...\PP4E\Gui\Tour> imgCanvas2.py ora-ppr-german.gif

Вот и все. В главе 9 будет показано, как помещать изображения в элементы меню, в кнопки на панели инструментов, приведены другие примеры с объектом Canvas и дружественный к изображениям виджет Text. В последующих главах они встретятся в программе просмотра слайдов (PyView), графическом редакторе (PyDraw) и в других. В графических интерфейсах Python/tkinter очень легко добавлять графику.

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

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

В настоящее время виджет PhotoImage поддерживает только файлы форматов GIF, PPM и PGM, а BitmapImage поддерживает файлы растровых изображений .xbm в стиле X Window. В последующих версиях количество поддерживаемых форматов может расшириться, и, конечно же, вы можете предварительно преобразовать свои изображения в указанные форматы. Но как будет показано далее в этой главе, поддержку дополнительных форматов изображений легко можно обеспечить с помощью открытого пакета PIL и его класса PhotoImage.

Берегите свои фотографии!

В отличие от других виджетов, tkinter изображение будет безвозвратно утрачено, если соответствующий объект изображения будет утилизирован сборщиком мусора. Это значит, что необходимо сохранять явные ссылки на объекты с графикой в течение всего времени, когда они могут понадобиться программе (например, присвоить их долгоживущей переменной, атрибуту объекта или компоненту структуры данных). Интерпретатор не сохраняет автоматически ссылку на графическое изображение, даже если оно связано с другими компонентами графического интерфейса, отображающими его. Кроме того, методы деструкторов объектов изображений стирают их из памяти. Мы уже видели раньше, что переменные tkinter тоже ведут себя неожиданным образом при уничтожении, но для графики этот эффект еще неприятнее и еще более вероятен. В будущих версиях Python такое поведение может измениться; при этом существуют веские причины не держать в памяти большие графические файлы неопределенно долгое время. На данный же момент элементы с графикой таковы, что если не используются, то могут быть утеряны.


Развлечения с кнопками и картинками

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

Пример 8.40. PP4E\Gui\Tour\bwttonpics-func.py

from tkinter import * # импортировать базовый набор виджетов,

from glob import glob # чтобы получить список файлов по расширению

import demoCheck # прикрепить демонстрационный пример с флажками

import random # выбрать случайную картинку

gifdir = ‘../gifs/’ # каталог по умолчанию с GIF-файлами

def draw():

name, photo = random.choice(images)

lbl.config(text=name)

pix.config(image=photo)

root=Tk()

lbl = Label(root, text=”none”, bg=’blue’, fg=’red’)

pix = Button(root, text=”Press me”, command=draw, bg=’white’) lbl.pack(fill=BOTH)

pix.pack(pady=10)

demoCheck.Demo(root, relief=SUNKEN, bd=2).pack(fill=BOTH)

files = glob(gifdir + “*.gif”) # имеющиеся GIF-файлы

images = [(x, PhotoImage(file=x)) for x in files] # загрузить и сохранить print(files) root.mainloop()

В этом примере используются несколько встроенных инструментов из библиотеки Python:

• Модуль glob, с которым мы впервые встретились в главе 4, позволяет получить список всех файлов с расширением .gif в каталоге - иными словами, всех GIF-файлов, которые там хранятся.

• Модуль random используется для выбора случайного GIF-файла из числа имеющихся в каталоге: функция random.choice случайным образом выбирает и возвращает элемент из списка.

• Чтобы изменить отображаемое изображение (и имя GIF-файла в метке в верхней части окна), сценарий просто вызывает метод config виджета с новыми значениями параметров - такое изменение динамически изменяет вид графического элемента.

Для разнообразия этот сценарий также прикрепляет экземпляр демонстрационной панели флажков demoCheck, который, в свою очередь, прикрепляет экземпляр кнопки Quitter, написанной нами ранее в примере 8.7. Конечно, это искусственный пример, но он еще раз демонстрирует мощь классов компонентов.

Обратите внимание, что все изображения, создаваемые в этом сценарии, сохраняются в списке images. В данном случае генератор списков применяет вызов конструктора PhotoImage к каждому файлу .gif в каталоге с картинками и возвращает список кортежей (filename, image-object), ссылка на который сохраняется в глобальной переменной (то же самое можно реализовать с помощью функции map, использующей lambda-функцию с одним аргументом). Напомню, что это убережет объекты изображений от утилизации сборщиком мусора в течение всего времени выполнения программы. На рис. 8.40 изображено окно этого сценария в Windows.

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

Рис. 8.40. Сценарий buttonpics в действии

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

Рис. 8.42. Сценарий buttonpics обретает политический подтекст

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

Пример 8.41. PP4E\Gui\Tour\bwttonpics.py

from tkinter import * # импортировать базовый набор виджетов,

from glob import glob # чтобы получить список файлов по расширению import demoCheck # прикрепить демонстрационный пример с флажками

import random # выбрать случайную картинку

gifdir = ‘../gifs/’ # каталог по умолчанию с GIF-файлами

class ButtonPicsDemo(Frame):

def __init__(self, gifdir=gifdir, parent=None):

Frame.__init__(self, parent)

self.pack()

self.lbl = Label(self, text=”none”, bg=’blue’, fg=’red’)

self.pix = Button(self, text=”Press me”, command=self.draw, bg=’white’)

self.lbl.pack(fill=BOTH)

self.pix.pack(pady=10)

demoCheck.Demo(self, relief=SUNKEN, bd=2).pack(fill=BOTH) files = glob(gifdir + “*.gif”)

self.images = [(x, PhotoImage(file=x)) for x in files] print(files)

def draw(self):

name, photo = random.choice(self.images)

self.lbl.config(text=name)

self.pix.config(image=photo)

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

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


Отображение и обработка изображений с помощью PIL

Как упоминалось ранее, сценарии на языке Python, использующие библиотеку tkinter, отображают изображения, связывая независимо созданные объекты изображений с действующими виджетами. На момент написания данной книги библиотека tkinter была способна отображать графические файлы в форматах GIF, PPM и PGM с помощью объекта PhotoImage, а также растровые файлы в стиле X11 (обычно с расширением .xbm) с помощью объекта BitmapImage.

Данное множество поддерживаемых форматов файлов ограничено лежащей в основе библиотекой Tk, а не самой библиотекой tkinter, и может быть расширено в будущем. Но если вам нужно сейчас вывести файлы в другом формате (например, в популярном формате JPEG), можно либо преобразовать файлы в один из поддерживаемых форматов с помощью программы обработки изображений, либо установить пакет расширения Python PIL, о котором говорилось в начале главы 7.

Пакет PIL, Python Imaging Library, - система, распространяемая с исходными текстами, поддерживает в данное время около 30 форматов графических файлов (в том числе GIF, JPEG, TIFF, PNG и BMP). В дополнение к расширению диапазона поддерживаемых форматов графических файлов пакет PIL также предоставляет инструменты для обработки изображений, включая геометрические преобразования, создание миниатюр, преобразование из одного формата в другой и многое другое.


Основы PIL

Чтобы воспользоваться инструментами из этого пакета, его сначала необходимо получить и установить: инструкции вы найдете на сайте http://www.pythonware.com (или поищите по строке «PIL», воспользовавшись поисковой системой). Затем нужно просто использовать особые объекты PhotoImage и BitmapImage, импортируемые из модуля ImageTk пакета PIL, чтобы открывать файлы в других графических форматах. Это совместимая замена для одноименных классов из библиотеки tkinter, которую можно использовать везде, где tkinter предполагает использование объекта PhotoImage или BitmapImage (то есть в настройках объектов меток, кнопок, холстов, текстовых виджетов и меню).

Это означает, что стандартный программный код, использующий tkinter, как показано ниже:

from tkinter import *

imgobj = PhotoImage(file=imgdir + “spam.gif”)

Button(image=imgobj).pack()

можно заменить таким программным кодом:

from tkinter import * from PIL import ImageTk

photoimg = ImageTk.PhotoImage(file=imgdir + “spam.jpg”) Button(image=photoimg).pack()

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

from tkinter import *

from PIL import Image, ImageTk

imageobj = Image.open(imgdir + “spam.jpeg”)

photoimg = ImageTk.PhotoImage(imageobj)

Button(image=photoimg).pack()

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

from tkinter import *

from PIL.ImageTk import PhotoImage # <== добавьте эту строку

imgobj = PhotoImage(file=imgdir + “spam.png”)

Button(image=imgobj).pack()

Особенности установки пакета PIL зависят от платформы. В Windows достаточно лишь загрузить и запустить самоустанавливающийся файл. В результате пакет PIL будет сохранен в каталоге установки Python Lib\site-packages, а поскольку мастер установки автоматически добавит каталог пакета в путь поиска модулей, никаких дополнительных настроек путей не потребуется. Просто запустите мастер установки и затем импортируйте модули из пакета PIL. На других платформах вам может потребоваться распаковать загруженный архив с исходным

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

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

Дополнительную информацию вы найдете на сайте http://www.python-ware.com, а также в комплекте электронной документации по PIL и tkinter. Однако, чтобы помочь вам начать, мы закроем эту главу несколькими интересными примерами использования пакета PIL для отображения и обработки изображений.


Отображение других типов графических изображений с помощью PIL

В предыдущих примерах работы с изображениями мы прикрепляли виджеты к кнопкам и холстам, однако стандартный набор инструментов библиотеки tkinter позволяет прикреплять изображения к виджетам различных типов, включая простые метки, текстовые виджеты и элементы меню. Так, сценарий в примере 8.42 отображает изображение в метке, находящейся в главном окне приложения, используя только средства библиотеки tkinter. Этот сценарий предполагает, что изображения хранятся в подкаталоге images, а также позволяет передавать имя файла с изображением в аргументе командной строки (при отсутствии аргументов по умолчанию используется файл spam.gif). Кроме того, для большей переносимости он объединяет имена файлов и каталогов с помощью os.path.join и выводит высоту и ширину изображения в пикселях в стандартный поток вывода, исключительно чтобы предоставить дополнительную информацию.

Пример 8.42. PP4E\Gui\PIL\viewer-tk.py

отображает изображение с помощью стандартного объекта PhotoImage из библиотеки tkinter; данная реализация может работать с GIF-файлами, но не может обрабатывать изображения в формате JPEG; использует файл с изображением, имя которого указано в командной строке, или файл по умолчанию; используйте Canvas вместо Label, чтобы обеспечить возможность прокрутки, и т.д.

import os, sys

from tkinter import * # использовать стандартный объект PhotoImage

# работает с форматом GIF, а для работы с форматом JPEG

# требуется пакет PIL

imgdir = ‘images’ imgfile = ‘london-2010.gif’

if len(sys.argv) > 1: # аргумент командной строки задан?

imgfile = sys.argv[1] imgpath = os.path.join(imgdir, imgfile)

win = Tk()

win.title(imgfile)

imgobj = PhotoImage(file=imgpath)

Label(win, image=imgobj).pack() # прикрепить к метке Label

print(imgobj.width(), imgobj.height()) # вывести размеры в пикселях, win.mainloop() # пока объект не уничтожен

На рис. 8.43 изображено окно этого сценария в Windows 7, где отображается изображение из GIF-файла по умолчанию. Запустите его из консоли, передав имя файла в виде аргумента командной строки, чтобы просмотреть другое изображение из подкаталога images (например, python viewer_tk.py filename.gif).

Рис. 8.43. Отображение картинки в формате GIF средствами tkinter

Сценарий в примере 8.42 может работать только с изображениями, форматы которых поддерживаются базовым набором инструментов в библиотеке tkinter. Для отображения изображений в других форматах, таких как JPEG, необходимо установить пакет PIL и использовать его альтернативную реализацию класса PhotoImage. С точки зрения программного кода, для этого достаточно добавить всего одну инструкцию import, как показано в примере 8.43

Пример 8.43. PP4E\Gui\PIL\viewer-pil.py

отображает изображение с помощью альтернативного объекта из пакета PIL поддерживает множество форматов изображений; предварительно установите пакет PIL: поместите его в каталог Lib\site-packages

import os, sys from tkinter import *

from PIL.ImageTk import PhotoImage # <== использовать альтернативный класс из

# PIL, остальной программный код

# без изменений

imgdir = ‘images’

imgfile = ‘florida-2009-1.jpg’ # поддерживает gif, jpg, png, tiff, и др. if len(sys.argv) > 1: imgfile = sys.argv[1] imgpath = os.path.join(imgdir, imgfile)

win = Tk() win.title(imgfile)

imgobj = PhotoImage(file=imgpath) # теперь поддерживает и JPEG!

Label(win, image=imgobj).pack() win.mainloop()

print(imgobj.width(), imgobj.height()) # показать размер в пикселях при выходе

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


Отображение всех изображений в каталоге

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

Puc. 8.44. Отображение картинки в формате GIF средствами tkinter и PIL

Пример 8.44. PP4E\Gui\PIL\viewer-dir.py

выводит все изображения, найденные в каталоге, открывая новые окна GIF-файлы поддерживаются стандартными средствами tkinter, но JPEG-файлы буду-пропускаться при отсутствии пакета PIL

import os, sys from tkinter import *

from PIL.ImageTk import PhotoImage # <== требуется для JPEG и др. форматов imgdir = ‘images’

if len(sys.argv) > 1: imgdir = sys.argv[1]

imgfiles = os.listdir(imgdir) # не включает полный путь к каталогу

main = Tk() main.title(‘Viewer’)

quit = Button(main, text=’Quit all’, command=main.quit, font=(‘courier’, 25))

quit.pack()

savephotos = []

for imgfile in imgfiles:

imgpath = os.path.join(imgdir, imgfile)

win = Toplevel()

win.title(imgfile)

try:

imgobj = PhotoImage(file=imgpath)

Label(win, image=imgobj).pack()

print(imgpath, imgobj.width(), imgobj.height()) # размер в пикселях savephotos.append(imgobj) # сохранить ссылку

except:

errmsg = ‘skipping %s\n%s’ % (imgfile, sys.exc_info()[1])

Label(win, text=errmsg).pack()

main.mainloop()

Запустите этот сценарий у себя, чтобы посмотреть создаваемые им окна. При запуске он создает одно главное окно с кнопкой Quit, щелчок на которой закрывает все дополнительные окна, количество которых совпадает с количеством файлов изображений в каталоге. Этот сценарий удобно использовать для быстрой организации просмотра, но он определенно не является образцом дружественного отношения к пользователю, особенно если в каталоге содержится огромное количество изображений! Каталог images, находящийся в дереве примеров и использовавшийся при тестировании, содержит 59 изображений. В результате при просмотре этого каталога сценарий порождает 60 окон, а каталоги, где вы храните свои снимки, сделанные цифровой фотокамерой, могут содержать гораздо больше изображений. Чтобы улучшить сценарий, перейдем к следующему разделу.


Создание миниатюр изображений с помощью пакета PlL

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

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

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

Пример 8.45. PP4E\Gui\PIL\viewer_thumbs.py

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

import os, sys, math from tkinter import *

from PIL import Image # <== required for thumbs

from PIL.ImageTk import PhotoImage # <== required for JPEG display

def makeThumbs(imgdir, size=(100, 100), subdir=’thumbs’):

создает миниатюры для всех изображений в каталоге; для каждого изображения создается и сохраняется новая миниатюра или загружается существующая; при необходимости создает каталог thumb;

возвращает список кортежей (имя_файла_изображения, объект_миниатюры); для получения списка файлов миниатюр вызывающая программа может также воспользоваться функцией listdir в каталоге thumb; для неподдерживаемых типов файлов может возбуждать исключение IOError, или другое;

ВНИМАНИЕ: можно было бы проверять время создания файлов;

thumbdir = os.path.join(imgdir, subdir) if not os.path.exists(thumbdir): os.mkdir(thumbdir)

thumbs = []

for imgfile in os.listdir(imgdir):

thumbpath = os.path.join(thumbdir, imgfile) if os.path.exists(thumbpath):

thumbobj = Image.open(thumbpath) # использовать существующую

thumbs.append((imgfile, thumbobj)) else:

print(‘making’, thumbpath)

imgpath = os.path.join(imgdir, imgfile)

try:

imgobj = Image.open(imgpath) # создать новую миниатюру imgobj.thumbnail(size, Image.ANTIALIAS) # фильтр, дающий

# лучшее качество при

# уменьшении размеров

imgobj.save(thumbpath) # тип определяется расширением

thumbs.append((imgfile, imgobj)) except: # не всегда IOError

print(“Skipping: “, imgpath) return thumbs

class ViewOne(Toplevel):

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

def __init__(self, imgdir, imgfile):

Toplevel.__init__(self)

self.title(imgfile)

imgpath = os.path.join(imgdir, imgfile) imgobj = PhotoImage(file=imgpath)

Label(self, image=imgobj).pack()

print(imgpath, imgobj.width(), imgobj.height()) # размер в пикселях self.savephoto = imgobj # сохранить ссылку

# на изображение

def viewer(imgdir, kind=Toplevel, cols=None):

создает окно с миниатюрами для каталога с изображениями: по одной кнопке с миниатюрой для каждого изображения;

используйте параметр kind=Tk, чтобы вывести миниатюры в главном окне, или Frame (чтобы прикрепить к фрейму); значение imgfile изменяется в каждой итерации цикла: ссылка на значение должна сохраняться по умолчанию; объекты PhotoImage должны сохраняться: иначе при утилизации изображения будут уничтожены;

компонует в ряды фреймов (в противоположность сеткам, фиксированным размерам, холстам);

win = kind()

win.title(‘Viewer: ‘ + imgdir)

quit = Button(win, text=’Quit’, command=win.quit, bg=’beige’) # добавить quit.pack(fill=X, side=BOTTOM) # первой, чтобы урезалась последней

thumbs = makeThumbs(imgdir) if not cols:

cols = int(math.ceil(math.sqrt(len(thumbs)))) # фиксированное или N x N

savephotos = [] while thumbs:

thumbsrow, thumbs = thumbs[:cols], thumbs[cols:] row = Frame(win) row.pack(fill=BOTH) for (imgfile, imgobj) in thumbsrow: photo = PhotoImage(imgobj) link = Button(row, image=photo)

handler = lambda savefile=imgfile: ViewOne(imgdir, savefile)

link.config(command=handler)

link.pack(side=LEFT, expand=YES)

savephotos.append(photo) return win, savephotos

if __name__ == ‘__main__’:

imgdir = (len(sys.argv) > 1 and sys.argv[1]) or ‘images’

main, save = viewer(imgdir, kind=Tk)

main.mainloop()

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

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

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

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

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

Рис. 8.45. Простой графический интерфейс выбора миниатюр, простые ряды фреймов


Рис. 8.46. Окно с полноразмерным изображением

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

Производительность: сохранение миниатюр в файлах

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

Для небольших коллекций изображений разница в скорости выполнения будет незаметна. Однако если испытать эти альтернативы на больших коллекциях изображений, можно будет заметить, что оригинальная версия в примере 8.45, сохраняющая и загружающая миниатюры из файлов, дает значительное преимущество в скорости. В одном из тестов с большой коллекцией файлов изображений на моем компьютере (примерно 320 цифровых фотографий на весьма, по общему мнению, медлительном ноутбуке) оригинальному сценарию потребовалось всего 5 секунд, чтобы открыть графический интерфейс (после первого запуска, в ходе которого было выполнено кэширование миниатюр), тогда как версии, представленной в примере 8.46, потребовалась 1 минута и 20 секунд: в 16 раз больше. Загрузка миниатюр из файлов выполняется значительно быстрее, чем операция изменения размеров.

Пример 8.46. PP4E\Gui\PIL\viewer-thumbs-nosave.py

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

import os, sys from PIL import Image from tkinter import Tk import viewer_thumbs

def makeThumbs(imgdir, size=(100, 100), subdir=’thumbs’): создает миниатюры в памяти, но не сохраняет их в файлах thumbs = []

for imgfile in os.listdir(imgdir):

imgpath = os.path.join(imgdir, imgfile) try:

imgobj = Image.open(imgpath) # создать новую миниатюру

imgobj.thumbnail(size) thumbs.append((imgfile, imgobj)) except:

print(“Skipping: “, imgpath) return thumbs

if __name__ == ‘__main__’:

imgdir = (len(sys.argv) > 1 and sys.argv[1]) or ‘images’

viewer_thumbs.makeThumbs = makeThumbs

main, save = viewer_thumbs.viewer(imgdir, kind=Tk)

main.mainloop()

Варианты компоновки: по сетке

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

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

Пример 8.47. PP4E\Gui\PIL\viewer-thumbs-grid.py

то же, что и viewer_thumbs, но использует менеджер компоновки grid, чтобы добиться более стройного размещения миниатюр; того же эффекта можно добиться с применением фреймов и менеджера pack, если кнопки будут иметь фиксированный и одинаковый размер;

import sys, math

from tkinter import *

from PIL.ImageTk import PhotoImage

from viewer_thumbs import makeThumbs, ViewOne def viewer(imgdir, kind=Toplevel, cols=None):

измененная версия, размещает миниатюры по сетке win = kind()

win.title(‘Viewer: ‘ + imgdir) thumbs = makeThumbs(imgdir) if not cols:

cols = int(math.ceil(math.sqrt(len(thumbs))))# фиксированное или N x N

rownum = 0 savephotos = [] while thumbs:

thumbsrow, thumbs = thumbs[:cols], thumbs[cols:] colnum = 0

for (imgfile, imgobj) in thumbsrow: photo = PhotoImage(imgobj) link = Button(win, image=photo)

handler = lambda savefile=imgfile: ViewOne(imgdir, savefile) link.config(command=handler) link.grid(row=rownum, column=colnum) savephotos.append(photo) colnum += 1 rownum += 1

Button(win, text=’Quit’, command=win.quit).grid(columnspan=cols, stick=EW) return win, savephotos

if __name__ == ‘__main__’:

imgdir = (len(sys.argv) > 1 and sys.argv[1]) or ‘images’

main, save = viewer(imgdir, kind=Tk)

main.mainloop()

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

Варианты компоновки: кнопки фиксированного размера

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

Рис. 8.47. Графический интерфейс выбора миниатюр с размещением по сетке

Пример 8.48. PP4E\Gui\PIL\viewer-thumbs-fixed.py

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

import sys, math

from tkinter import *

from PIL.ImageTk import PhotoImage

from viewer_thumbs import makeThumbs, ViewOne

def viewer(imgdir, kind=Toplevel, cols=None):

измененная версия, выполняет размещение с использованием кнопок фиксированного размера

win = kind()

win.title(‘Viewer: ‘ + imgdir) thumbs = makeThumbs(imgdir) if not cols:

cols = int(math.ceil(math.sqrt(len(thumbs))))# фиксированное или N x N

savephotos = [] while thumbs:

thumbsrow, thumbs = thumbs[:cols], thumbs[cols:]

row = Frame(win)

row.pack(fill=BOTH)

for (imgfile, imgobj) in thumbsrow:

size = max(imgobj.size) # ширина, высота

photo = PhotoImage(imgobj) link = Button(row, image=photo)

handler = lambda savefile=imgfile: ViewOne(imgdir, savefile) link.config(command=handler, width=size, height=size) link.pack(side=LEFT, expand=YES) savephotos.append(photo)

Button(win, text=’Quit’, command=win.quit, bg=’beige’).pack(fill=X) return win, savephotos

if __name__ == ‘__main__’:

imgdir = (len(sys.argv) > 1 and sys.argv[1]) or ‘images’

main, save = viewer(imgdir, kind=Tk)

main.mainloop()

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

Прокрутка и холсты (забегая вперед)

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

Рис. 8.48. Графический интерфейс выбора миниатюр с кнопками фиксированного размера и рядами фреймов

дут получаться слишком большими и неудобными в работе (при этом часть миниатюр вообще может не поместиться в окне).

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

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

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

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

Рис. 8.41. Сценарий buttonpics, отображающий более высокую картинку


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


«Меню дня: Spam, Spam и еще раз Spam»

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

• Виджеты Menu, Menubutton и OptionMenu

• Виджет Scrollbar: для прокрутки текста, списков и холстов

• Виджет Listbox: список с возможностью выбора нескольких вариантов

• Виджет Text: универсальный инструмент отображения и редактирования текста

• Виджет Canvas: универсальный инструмент вывода графики

• Менеджер компоновки grid, принцип действия которого основан на использовании таблиц

• Инструменты измерения интервалов времени: after, update, wait и потоки выполнения

• Основы анимации в tkinter

• Буферы обмена, удаление виджетов и окон и так далее.

К концу этой главы вы будете знакомы с основным содержанием библиотеки tkinter и овладеете всей информацией, необходимой для самостоятельного построения больших переносимых интерфейсов пользователя. Также вы будете готовы справиться с объемными примерами, представленными в главах 10 и 11. А сейчас продолжим обзор виджетов.


Меню

Меню представляют собой раскрывающиеся списки, которые обычно можно увидеть в верхней части окна (или всего экрана, если вы работаете на Macintosh). Переместите указатель мыши на панель меню, щелкните на имени (например, Файл (FiLe)), и под этим именем появится список вариантов выбора (например, Открыть (Open), Сохранить (Save)). Пункты меню могут запускать какие-либо действия, как щелчок на кнопке. Они могут также открывать другие «каскадные» подменю, выводящие дополнительные списки вариантов, показывать окна диалогов и так далее. В библиотеке tkinter есть два типа меню, которые можно добавлять в сценарии: меню окон верхнего уровня и меню, основанные на фреймах. Первый вид лучше подходит для окон в целом, а второй может использоваться в качестве вложенных компонентов.


Меню окон верхнего уровня

Во всех последних версиях Python (где используется библиотека Tk версии 8.0 и выше) можно связывать горизонтальную панель меню с объектом окна верхнего уровня (например, Tk или Toplevel). В Windows и Unix (X Window ) эта строка меню выводится вдоль верхнего края окна. В некоторых версиях Mac OS это меню при выборе окна заменяет то, что отображается в верхней части экрана. Иными словами, меню окон выглядят так, как принято на той платформе, на которой выполняется сценарий.

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

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

2. Для каждого раскрывающегося меню создать новый объект Menu как дочерний для самого верхнего меню и добавить его как каскадный для самого верхнего меню с помощью метода add_cascade.

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

4. Добавить каскадные подменю, создавая новые виджеты Menu как дочерние для того объекта Menu, который должен расширяться каскад-но, и связывая родительский и дочерний объекты с помощью метода

add_cascade.

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

Пример 9.1. PP4E\Gui\Tour\menu_win.py

# меню окна верхнего уровня в стиле Tk8.0

from tkinter import * # импортировать базовый набор виджетов

from tkinter.messagebox import * # импортировать стандартные диалоги

def notdone():

showerror(‘Not implemented’, ‘Not yet available’) def makemenu(win):

top = Menu(win) # win = окно верхнего уровня

win.config(menu=top) # установить его параметр menu

file = Menu(top)

file.add_command(label=’New...’, command=notdone, underline=0) file.add_command(label=’Open...’, command=notdone, underline=0) file.add_command(label=’Quit’, command=win.quit, underline=0) top.add_cascade(label=’File’, menu=file, underline=0)

edit = Menu(top, tearoff=False)

edit.add_command(label=’Cut’, command=notdone, underline=0) edit.add_command(label=’Paste’, command=notdone, underline=0) edit.add_separator()

top.add_cascade(label=’Edit’, menu=edit, underline=0) submenu = Menu(edit, tearoff=True)

submenu.add_command(label=’Spam’, command=win.quit, underline=0) submenu.add_command(label=’Eggs’, command=notdone, underline=0) edit.add_cascade(label=’Stuff’, menu=submenu, underline=0)

if__name__== ‘__main__’:

root = Tk() # или Toplevel()

root.title(‘menu_win’) # информация для менеджера окон

makemenu(root) # создать строку меню

msg = Label(root, text=’Window menu basics’) # добавить что-нибудь ниже msg.pack(expand=YES, fill=BOTH)

msg.config(relief=SUNKEN, width=40, height=7, bg=’beige’) root.mainloop()

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

top = Menu(win) # прикрепить Menu к окну

win.config(menu=top) # связать окно и меню

file = Menu(top) # прикрепить Menu к Menu верх. ур.

top.add_cascade(label=’File’, menu=file) # связать родителя с потомком

Помимо построения дерева объектов меню этот сценарий демонстрирует некоторые часто встречающиеся параметры конфигурации меню:

Линии-разделители

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

Линии отрыва

Сценарий запрещает отрыв раскрывающегося меню Edit, передавая параметр tearoff=0 при создании виджета Menu. Линии отрыва - это пунктирные линии, по умолчанию появляющиеся в меню tkinter верхнего уровня. Щелчок на этой линии создает новое окно, содержащее меню. Они могут служить удобным средством упрощения навигации (можно сразу щелкнуть на пункте отрывного меню, не блуждая по дереву вложенных пунктов), но не на всех платформах принято их использовать.

Горячие клавиши

Сценарий использует параметр underline, чтобы назначить уникальную букву в пункте меню горячей клавишей. Параметр задает смещение буквы в строке метки пункта меню. Например, в Windows пункт Quit в меню FiLe этого сценария можно выбрать, как обычно, с помощью мыши, а также нажатием клавиши Alt, затем f и затем q. Использовать параметр underline не обязательно - в Windows первая буква имени раскрывающегося меню автоматически становится горячей клавишей, а кроме того, для перемещения по меню и выбора раскрывающихся пунктов можно использовать клавиши со стрелками и Enter. Но явное определение горячих клавиш может облегчить использование больших меню. Например, последовательность клавиш Alt+E+S+S выполняет действие по завершению программы, предусмотренное пунктом Spam во вложенном подменю Stuff, без каких-либо перемещений с помощью мыши или клавиш со стрелками.

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

На рис. 9.2 изображено, что происходит при выборе раскрывающегося меню File. Обратите внимание, что виджеты Menu связываются, а не присоединяются с помощью менеджера компоновки - в действительности менеджер компоновки здесь вообще не участвует. Если запустить этот

Рис. 9.1. Сценарий menu_win: строка меню окна верхнего уровня


Рис. 9.2. Открытое меню File

сценарий, можно также заметить, что все элементы меню либо завершают выполнение программы, либо выводят стандартный диалог ошибки «Not Implemented» (Не реализовано). Этот пример просто демонстрирует работу с меню, но на практике обработчики событий выбора пунктов меню обычно выполняют более полезные вещи.

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

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

Рис. 9.3. Отрывное меню File и каскадное Edit


Рис. 9.4. Несколько окон верхнего уровня с меню

Пример 9.2. PP4E\Gui\Towr\menu_win-multi.py

from menu_win import makemenu # повторно использовать функцию создания меню from tkinter import *

root = Tk()

for i in range(3): # три всплывающих окна с меню

win = Toplevel(root) makemenu(win)

Label(win, bg=’black’, height=5, width=25).pack(expand=YES, fill=BOTH) Button(root, text=”Bye”, command=root.quit).pack() root.mainloop()


Меню на основе виджетов Frame и Menubutton

Хотя это не совсем обычно для окон верхнего уровня, но допустимо создание строки меню в виде горизонтального фрейма Frame. Однако прежде чем показывать, как это делается, я хочу объяснить, зачем это может понадобиться. Поскольку такая схема, основанная на фреймах, не зависит от протоколов окон верхнего уровня, она может применяться для добавления меню в качестве вложенных компонентов более крупных интерфейсов. Иными словами, она в основном применяется не к окнам верхнего уровня. Например, текстовый редактор PyEdit из главы 11 может использоваться как программа и как прикрепляемый компонент. Мы реализуем выбор в PyEdit с помощью оконных меню при выполнении его как самостоятельной программы и с помощью меню, основанного на фрейме, когда PyEdit будет встраиваться в интерфейсы PyMailGUI и PyView. Поэтому стоит знать обе схемы.

Для меню на основе фреймов требуется написать несколько дополнительных строчек программного кода, но они ненамного сложнее оконных меню. Для создания такого меню нужно разместить виджеты Me-nubutton в контейнере Frame, связать виджеты Menu и Menubutton и присоединить Frame к верхней части окна-контейнера. В примере 9.3 создается такое же меню, как в примере 9.2, но с использованием фрейма.

Пример 9.3. PP4E\Gui\Tour\menu_frm.py

# Меню на основе фреймов: пригодно для окон верхнего уровня и компонентов

from tkinter import * # импортировать базовый набор виджетов

from tkinter.messagebox import * # импортировать стандартные диалоги

def notdone():

showerror(‘Not implemented’, ‘Not yet available’) def makemenu(parent):

menubar = Frame(parent) # relief=RAISED, bd=2...

menubar.pack(side=TOP, fill=X)

fbutton = Menubutton(menubar, text=’File’, underline=0)

fbutton.pack(side=LEFT)

file = Menu(fbutton)

file.add_command(label=’New...’, command=notdone, underline=0) file.add_command(label=’Open...’, command=notdone, underline=0) file.add_command(label=’Quit’, command=parent.quit, underline=0) fbutton.config(menu=file)

ebutton = Menubutton(menubar, text=’Edit’, underline=0)

ebutton.pack(side=LEFT)

edit = Menu(ebutton, tearoff=False)

edit.add_command(label=’Cut’, command=notdone, underline=0)

edit.add_command(label=’Paste’, command=notdone, underline=0)

edit.add_separator()

ebutton.config(menu=edit)

submenu = Menu(edit, tearoff=True)

submenu.add_command(label=’Spam’, command=parent.quit, underline=0) submenu.add_command(label=’Eggs’, command=notdone, underline=0) edit.add_cascade(label=’Stuff’, menu=submenu, underline=0) return menubar

if __name__ == ‘__main__’:

root = Tk() # или TopLevel, или Frame

root.title(‘menu_frm’) # информация для менеджера окон

makemenu(root) # создать строку меню

msg = Label(root, text=’Frame menu basics’) # добавить что-нибудь ниже msg.pack(expand=YES, fill=BOTH)

msg.config(relief=SUNKEN, width=40, height=7, bg=’beige’) root.mainloop()

Снова выделим здесь логику связывания, чтобы не отвлекали другие детали. Для меню FiLe она сводится к следующему:

menubar = Frame(parent) # создать Frame для строки меню

fbutton = Menubutton(menubar, text=’File’) # прикрепить Menubutton к Frame file = Menu(fbutton) # прикрепить Menu к Menubutton

fbutton.config(menu=file) # связать кнопку и меню

В этой схеме появился дополнительный виджет Menubutton, но это не сделало ее намного сложнее создания оконных меню верхнего уровня. На рис. 9.5 и 9.6 изображено, как этот сценарий выполняется в Windows.

Рис. 9.5. Сценарий menu_frm: фрейм и полоса меню Menubutton

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

Рис. 9.6. С выбранным меню Edit

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

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

Рис. 9.7. Несколько меню на основе фреймов в одном окне

Пример 9.4. PP4E\Gui\Tour\menu_frm-multi.py

from menu_frm import makemenu # здесь нельзя использовать menu_win--одно окно from tkinter import * # но можно прикреплять меню на основе фреймов

root = Tk()

for i in range(2): # 2 меню в одном окне

mnu = makemenu(root) mnu.config(bd=2, relief=RAISED)

Label(root, bg=’black’, height=5, width=25).pack(expand=YES, fill=BOTH) Button(root, text=”Bye”, command=root.quit).pack() root.mainloop()

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

Пример 9.5. PP4E\Gui\Tour\menu_frm-multi2.py

from menu_frm import makemenu # нельзя использовать menu_win--корень=Frame from tkinter import * root = Tk()

for i in range(3): # три меню, вложенные в контейнеры

frm = Frame()

mnu = makemenu(frm)

mnu.config(bd=2, relief=RAISED)

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

Label(frm, bg=’black’, height=5, width=25).pack(expand=YES, fill=BOTH)

Button(root, text=”Bye”, command=root.quit).pack()

root.mainloop()

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

В действительности меню, основанные на Menubutton, являются еще более универсальными, чем следует из примера 9.3, - они могут появляться в любом месте интерфейса, где может располагаться обычная кнопка, а не только в строке меню во фрейме Frame. В примере 9.6 создается раскрывающийся список Menubutton, который отображается самостоятельно и прикреплен к корневому окну. На рис. 9.8 приведен графический интерфейс, создаваемый этим примером.

Пример 9.6. PP4E\Gui\Tour\mbwtton.py

from tkinter import * root = Tk()

mbutton = Menubutton(root, text=’Food’) # отдельное раскрывающееся меню

picks = Menu(mbutton)

mbutton.config(menu=picks)

picks.add_command(label=’spam’, command=root.quit) picks.add_command(label=’eggs’, command=root.quit) picks.add_command(label=’bacon’, command=root.quit) mbutton.pack()

mbutton.config(bg=’white’, bd=4, relief=RAISED) root.mainloop()

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

Рис. 9.8. Виджет Menubutton сам по себе

Пример 9.7 иллюстрирует типичное использование виджета Optionmenu и создает интерфейс, изображенный на рис. 9.9. Щелчок на любой из первых двух кнопок открывает раскрывающееся меню. Щелчок на третьей кнопке state выводит текущие значения, отображаемые на первых двух.

Пример 9.7. PP4E\Gui\Tour\optionmenu.py

from tkinter import * root = Tk()

var1 = StringVar() var2 = StringVar()

opt1 = OptionMenu(root, var1, ‘spam’, ‘eggs’, ‘toast’) # как и Menubutton,

opt2 = OptionMenu(root, var2, ‘ham’, ‘bacon’, ‘sausage’) # но отображает

opt1.pack(fill=X) # выбранный вариант

opt2.pack(fill=X) var1.set(‘spam’) var2.set(‘ham’)

def state(): print(var1.get(), var2.get()) # связанные переменные

Button(root, command=state, text=’state’).pack()

root.mainloop()

Рис. 9.9. Виджет Optionmenu в действии

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

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


Окна с меню и панелью инструментов

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

В примере 9.8 представлен один из способов добавления панели инструментов в окно. Он демонстрирует также, как добавлять изображения в пункты меню (присвоить атрибуту image ссылку на объект PhotoImage) и как делать пункты меню недоступными для выбора, изображая их в серых тонах (вызвать метод меню entryconfig, передав ему индекс отключаемого пункта; отсчет начинается с 1). Обратите внимание, что объекты PhotoImage сохраняются в виде списка - напомню, что в отличие от других виджетов, они будут утеряны, если не сохранить ссылки на них (загляните в главу 8, если вам требуется освежить память).

Пример 9.8. PP4E\Gui\Tour\menuDemo.py

#!/usr/local/bin/python

главное меню окна в стиле Tk8.0

строка меню и панель инструментов прикрепляются к окну в первую очередь, fill=X (прикрепить первым = обрезать последним); добавляет изображения в элементы меню; смотрите также: add_checkbutton, add_radiobutton

from tkinter import * # импортировать базовый набор виджетов

from tkinter.messagebox import * # импортировать стандартные диалоги

class NewMenuDemo(Frame): # расширенный фрейм

def __init__(self, parent=None): # прикрепляется к корневому окну?

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

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

self.createWidgets() # прикрепить фреймы/виджеты

self.master.title("Toolbars and Menus”) # для менеджера окон self.master.iconname(“tkpython”) # текст метки при свертывании

def createWidgets(self): self.makeMenuBar() self.makeToolBar()

L = Label(self, text=’Menu and Toolbar Demo’)

L.config(relief=SUNKEN, width=40, height=10, bg=’white’) L.pack(expand=YES, fill=BOTH)

def makeToolBar(self):

toolbar = Frame(self, cursor=’hand2’, relief=SUNKEN, bd=2) toolbar.pack(side=BOTTOM, fill=X)

Button(toolbar, text=’Quit’, command=self.quit ).pack(side=RIGHT) Button(toolbar, text=’Hello’, command=self.greeting).pack(side=LEFT)

def makeMenuBar(self):

self.menubar = Menu(self.master)

self.master.config(menu=self.menubar) # master^^ верхнего уровня

self.fileMenu()

self.editMenu()

self.imageMenu()

def fileMenu(self):

pulldown = Menu(self.menubar)

pulldown.add_command(label=’Open...’, command=self.notdone) pulldown.add_command(label=’Quit’, command=self.quit) self.menubar.add_cascade(label=’File’, underline=0, menu=pulldown)

def editMenu(self):

pulldown = Menu(self.menubar)

pulldown.add_command(label=’Paste’, command=self.notdone) pulldown.add_command(label=’Spam’, command=self.greeting) pulldown.add_separator()

pulldown.add_command(label=’Delete’, command=self.greeting) pulldown.entryconfig(4, state=DISABLED)

self.menubar.add_cascade(label=’Edit’, underline=0, menu=pulldown) def imageMenu(self):

photoFiles = (‘ora-lp4e.gif’, ‘pythonPowered.gif’,

‘python_conf_ora.gif’) pulldown = Menu(self.menubar) self.photoObjs = [] for file in photoFiles:

img = PhotoImage(file=’../gifs/’ + file) pulldown.add_command(image=img, command=self.notdone) self.photoObjs.append(img) # сохранить ссылку

self.menubar.add_cascade(label=’Image’, underline=0, menu=pulldown)

def greeting(self):

showinfo(‘greeting’, ‘Greetings’) def notdone(self):

showerror(‘Not implemented’, ‘Not yet available’) def quit(self):

if askyesno(‘Verify quit’, ‘Are you sure you want to quit?’): Frame.quit(self)

if __name__ == ‘__main__’: NewMenuDemo().mainloop() # если запущен как

# самостоятельный сценарий

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

Использование изображений в панелях инструментов

Как видно на рис. 9.11, пункты меню легко могут быть украшены графическими изображениями. Хотя это и не было продемонстрировано в примере 9.8, тем не менее элементы панелей инструментов могут также снабжаться картинками, как и элементы меню Image в примере. Для этого достаточно просто поместить небольшие изображения на кнопки в панели инструментов, как мы делали это в примерах с миниатюрами, в последнем разделе главы 8. Как вы уже знаете, при наличии предварительно созданных изображений для кнопок на панели инструментов не составляет никакого труда ассоциировать их с кнопками. Фактически для динамического создания таких изображений требуется приложить ненамного больше труда - навыки создания миниатюр с помощью пакета PIL, полученные нами в предыдущей главе, придутся кстати и в этом контексте.

Рис. 9.10. Сценарий menuDemo: меню и панели инструментов


Рис. 9.11. Изображения в меню и отрывные меню в действии

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

# изменяет размеры изображений для кнопок на панели инструментов с помощью PIL def makeToolBar(self, size=(40, 40)):

from PIL.ImageTk import PhotoImage, Image # для jpeg или новых миниатюр imgdir = r’../PIL/images/’

toolbar = Frame(self, cursor=’hand2’, relief=SUNKEN, bd=2)

toolbar.pack(side=BOTTOM, fill=X)

photos = ‘ora-lp4e-big.jpg’, ‘PythonPoweredAnim.gif’,

‘python_conf_ora.gif’ self.toolPhotoObjs = [] for file in photos:

imgobj = Image.open(imgdir + file) # создать новую миниатюру

imgobj.thumbnail(size, Image.ANTIALIAS) # фильтр с лучшим качеством img = PhotoImage(imgobj)

btn = Button(toolbar, image=img, command=self.greeting) btn.config(relief=RAISED, bd=2) btn.config(width=size[0], height=size[1]) btn.pack(side=LEFT)

self.toolPhotoObjs.append((img, imgobj)) # сохранить ссылку Button(toolbar, text=’Quit’, command=self.quit).pack(side=RIGHT, fill=Y)

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

Рис. 9.12. Сценарий menuDemo2: добавление изображений в панель инструментов с помощью PIL

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

Пакет PIL можно не использовать при наличии изображений в формате GIF или в поддерживаемом растровом формате, созданных вручную. Достаточно просто загружать изображения из файлов, используя стандартный объект PhotoImage из библиотеки tkinter, как показано в следующей альтернативной реализации метода конструирования панели инструментов (эта версия сценария сохранена в файле menuDemo3.py в пакете с примерами):

# использует подготовленные изображения gif и стандартные средства tkinter

def makeToolBar(self, size=(30, 30)): imgdir = r’../gifs/’

toolbar = Frame(self, cursor=’hand2’, relief=SUNKEN, bd=2) toolbar.pack(side=BOTTOM, fill=X)

photos = ‘ora-lp4e.gif’, ‘pythonPowered.gif’, ‘python_conf_ora.gif’ self.toolPhotoObjs = [] for file in photos:

img = PhotoImage(file=imgdir + file)

btn = Button(toolbar, image=img, command=self.greeting)

btn.config(bd=5, relief=RIDGE)

btn.config(width=size[0], height=size[1])

btn.pack(side=LEFT)

self.toolPhotoObjs.append(img) # сохранить ссылку

Button(toolbar, text=’Quit’, command=self.quit).pack(side=RIGHT, fill=Y)

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

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

Автоматизация создания меню

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

Рис. 9.13. Сценарий menuDemo3: заранее подготовленные изображения GIF в панели инструментов

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


Виджеты Listbox и Scrollbar

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

Пример 9.9. PP4E\Gui\Tour\scrolledlist.py

“простой настраиваемый компонент окна списка с прокруткой”

from tkinter import * class ScrolledList(Frame):

def __init__(self, options, parent=None):

Frame.__init__(self, parent)

self.pack(expand=YES, fill=BOTH) # сделать растягиваемым self.makeWidgets(options)

def handleList(self, event):

index = self.listbox.curselection() # при двойном щелчке на списке label = self.listbox.get(index) # извлечь выбранный текст

self.runCommand(label) # и вызвать действие

# или get(ACTIVE)

def makeWidgets(self, options): sbar = Scrollbar(self) list = Listbox(self, relief=SUNKEN)

sbar.config(command=list.yview) # связать sbar и list

list.config(yscrollcommand=sbar.set) # сдвиг одного = сдвиг другого sbar.pack(side=RIGHT, fill=Y) # первым добавлен - посл. обрезан

list.pack(side=LEFT, expand=YES, fill=BOTH) # список обрезается первым pos = 0

for label in options: # добавить в виджет списка

list.insert(pos, label) # или insert(END,label)

pos += 1 # или enumerate(options)

#list.config(selectmode=SINGLE, setgrid=1) # режимы выбора, измен. разм. list.bind(‘’, self.handleList) # установить обр-к события self.listbox = list

def runCommand(self, selection): # необходимо переопределить

print(‘You selected:’, selection)

if __name__ == ‘__main__’:

options = ((‘Lumberjack-%s’ % x) for x in range(20)) # или map/lambda,

ScrolledList(options).mainloop() # [...]

Этот модуль можно запускать как самостоятельный сценарий, чтобы поэкспериментировать с этими виджетами, или использовать в качестве библиотечного объекта. Передавая различные списки выбора в аргументе options и переопределяя метод runCommand в подклассе, можно повторно использовать определенный здесь класс компонента ScrolledList всякий раз когда потребуется вывести список с прокруткой. Мы еще будем использовать этот класс в главе 11, в примере программы PyEdit. При грамотном подходе можно легко расширить библиотеку tkinter классами на языке Python таким способом.

Если запустить этот пример как самостоятельный сценарий, он создаст окно, подобное изображенному на рис. 9.14, которое было получено в Windows 7. Это фрейм Frame со списком Listbox в левой части, содержащим 20 сгенерированных элементов (на пятом выполнен щелчок) и связанным с виджетом Scrollbar в правой части, предназначенным для прокрутки списка. Если переместить ползунок в полосе прокрутки, список также будет прокручиваться, и наоборот.

Рис. 9.14. Сценарий scrolledlist в действии


Программирование виджетов списков

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

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

list.insert(pos, label) pos = pos + 1

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

list.insert(‘end’, label) # добавление в конец: подсчет позиций не нужен list.insert(END, label) # END - константа со значением ‘end’ в tkinter

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

В обработчике двойного щелчка этот сценарий получает выделенный в списке элемент с помощью следующей пары методов:

index = self.listbox.curselection() # получить индекс выделенного элемента label = self.listbox.get(index) # текст, соответствующий этому индексу

Эту операцию можно реализовать иначе. Обе следующие строки дают одинаковый результат: они получают содержимое строки с индексом ‘active’, то есть выбранной в данный момент:

label = self.listbox.get(‘active’) # получить по индексу active label = self.listbox.get(ACTIVE) # в tkinter ACTIVE=’active’

Для иллюстрации, метод класса по умолчанию runCommand выводит выбранное значение при каждом двойном щелчке на элементе списка -сценарий получает его в виде строки с текстом выбранного элемента:

C:\...\PP4E\Gui\Tour> python scrolledlist.py

You selected: Lumberjack-2 You selected: Lumberjack-19 You selected: Lumberjack-4 You selected: Lumberjack-12

Виджеты списков могут служить отличными инструментами ввода данных даже без полос прокрутки. Они принимают также параметры настройки, определяющие цвет, шрифт и рельеф. Наряду с режимом выбора единственного элемента они также поддерживают возможность выбора нескольких элементов одновременно. По умолчанию используется режим выбора единственного элемента, но вы можете передать в аргументе selectmode четыре значения: SINGLE, BROWSE, MULTIPLE и EXTENDED (по умолчанию: BROWSE). Первые два из них определяют режимы выбора единственного элемента, а последние два позволяют выбирать сразу несколько элементов.

Эти режимы имеют очень тонкие отличия. Например, режим BROWSE напоминает SINGLE, но дополнительно позволяет перетаскивать выделенный элемент. Щелчок на элементе в режиме MULTIPLE изменяет его состояние, не оказывая влияния на состояние других элементов. Режим EXTENDED также позволяет выбирать несколько элементов, но использует порядок выделения, как принято в интерфейсе проводника файлов Windows - первый элемент выбирается простым щелчком, несколько элементов - щелчком, при удерживаемой клавише CtrL, а диапазон элементов - щелчком, при удерживаемой клавише Shift. Режим множественного выбора можно реализовать, как показано ниже:

listbox = Listbox(window, bg=’white’, font=(‘courier’, fontsz)) listbox.config(selectmode=EXTENDED)

listbox.bind(‘’, (lambda event: onDoubleClick()))

# onDoubeClick: извлекает сообщения, выбранные в списке

selections = listbox.curselection() # кортеж строк чисел, 0..N-1

selections = [int(x)+1 for x in selections] # преобразует в int,

# переводит в диапазон 1..N

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

Вы можете самостоятельно поэкспериментировать с альтернативными режимами выбора, раскомментировав строку в примере 9.9, где определяется параметр selectmode, и изменяя значение. При этом двойной щелчок мышью в режиме множественного выбора может порождать сообщение об ошибке, потому что методу get будет передаваться кортеж более чем с одним индексом выбранного элемента (выведите его, чтобы убедиться в этом). Режим множественного выбора мы будем использовать в примере PyMailGUI, далее в этой книге (в главе 14), поэтому дальнейшее обсуждение этой темы я отложу до будущих примеров.


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

Однако самое большое таинство в примере 9.9 свершается в следующих двух строках:

sbar.config(command=list.yview) # вызвать list.yview при перемещении list.config(yscrollcommand=sbar.set) # вызвать sbar.set при перемещении

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

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

• При вертикальном перемещении в окне списка вызывается обработчик, зарегистрированный в его параметре yscrollcommand. В данном сценарии встроенный метод sbar.set пропорционально настраивает полосу прокрутки.

Иными словами, прокрутка в одном виджете автоматически вызывает прокрутку в другом. В tkinter у всех прокручиваемых элементов - Listbox, Entry, Text и Canvas - есть встроенные методы yview и xview для обработки прокрутки по вертикали и по горизонтали, а также параметры yscrollcommand и xscrollcommand, в которых определяются обработчики связанной с ними полосы прокрутки. У полос прокрутки есть параметр command, в котором указывается обработчик, вызываемый при прокрутке. Библиотека tkinter передает этим методам информацию, определяющую новое положение (например, «прокрутить вниз на 10%»), но программисту не требуется опускаться в сценариях до таких деталей.

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

Рис. 9.15. Прокрутка до середины списка


Компоновка полос прокрутки

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

Рис. 9.16. Список уменьшился

рис. 9.16, при уменьшении окна сценария отрезается часть списка, но полоса прокрутки сохраняется.

В то же время, обычно не требуется, чтобы полоса прокрутки расширялась вместе с окном, поэтому компоновка ее должна выполняться с одним параметром fill=Y (или fill=X для прокрутки по горизонтали), без expand=YES. В частности, расширение окна этого примера увеличивает окно списка, но изменяет ширину полосы прокрутки, прикрепленной справа.

В примерах этой и последующих глав мы неоднократно будем встречаться с полосами прокрутки и виджетами списков (можно заглянуть вперед и посмотреть примеры PyEdit, PyMailGUI, PyForm, PyTree и ShellGui). И хотя основы их применения охватываются в данной главе, следует отметить, что за кадром осталось многое, что могут предложить эти виджеты.

Например, столь же легко к прокручиваемым виджетам можно добавить горизонтальные полосы прокрутки. Они программируются почти так же, как вертикальные, только имена обработчиков начинаются с «х», а не «y» (например, xscrollcommand), а для объекта полосы прокрутки устанавливается параметр orient= ’horizontal’. Чтобы добавить сразу две полосы прокрутки, вертикальную и горизонтальную, и связать их с виджетом, можно использовать такой прием:

window = Frame(self)

vscroll = Scrollbar(window)

hscroll = Scrollbar(window, orient=’horizontal’)

listbox = Listbox(window)

# прокрутить список при перемещении движка в полосе прокрутки vscroll.config(command=listbox.yview, relief=SUNKEN) hscroll.config(command=listbox.xview, relief=SUNKEN)

# переместить движок в полосе прокрутки при прокрутке списка listbox.config(yscrollcommand=vscroll.set, relief=SUNKEN) listbox.config(xscrollcommand=hscroll.set)

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


Виджет Text

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

В главе 11 мы воспользуемся двумя этими виджетами для реализации текстового редактора (PyEdit), графического редактора (PyDraw), часов с графическим интерфейсом (PyClock) и программ для просмотра изображений (PyPhoto и PyView). Однако в этой, экскурсионной главе мы будем использовать эти виджеты в более простых примерах. В примере 9.10 реализован простой интерфейс отображения текста с прокруткой, который может вывести строку текста или файл.

Пример 9.10. PP4E\Gui\Tour\scrolledtext.py

"простой компонент просмотра текста или содержимого файла”

print(‘PP4E scrolledtext’)

from tkinter import *

class ScrolledText(Frame):

def __init__(self, parent=None, text=’’, file=None):

Frame.__init__(self, parent)

self.pack(expand=YES, fill=BOTH) # сделать растягиваемым

self.makewidgets()

self.settext(text, file)

def makewidgets(self): sbar = Scrollbar(self) text = Text(self, relief=SUNKEN)

sbar.config(command=text.yview) # связать sbar и text

text.config(yscrollcommand=sbar.set) # сдвиг одного = сдвиг другого sbar.pack(side=RIGHT, fill=Y) # первым добавлен - посл. обрезан

text.pack(side=LEFT, expand=YES, fill=BOTH) # Text обрезается первым self.text = text

def settext(self, text=’’, file=None): if file:

text = open(file, ‘r’).read()

self.text.delete(‘1.0’, END) # удалить текущий текст

self.text.insert(‘1.0’, text) # добавить в стр. 1, кол. 0

self.text.mark_set(INSERT, ‘1.0’) # установить курсор вставки

self.text.focus() # сэкономить щелчок мышью

def gettext(self): # возвращает строку

return self.text.get(‘1.0’, END+’-1c’) # от начала до конца

if__name__== ‘__main__’:

root = Tk() if len(sys.argv) > 1:

st = ScrolledText(file=sys.argv[1]) # имя файла в командной строке else:

st = ScrolledText(text=’Words\ngo here’) # иначе: две строки def show(event):

print(repr(st.gettext())) # вывести как простую строку

root.bind(‘’, show) # esc = выводит дамп текста

root.mainloop()

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

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

Загрузка...