Рис. 9.37. Добавлена загрузка данных из файла
По умолчанию класс создает сетку размером 5 на 5, но существует возможность определять другие размерности, как в конструкторе класса, так и в командной строке сценария. При нажатии кнопки Load выводится стандартный диалог выбора файла, с которым мы встречались ранее (рис. 9.38).
Файл данных grid5-data1.txt содержит семь строк и шесть колонок данных:
C:\...\PP4E\Gui\Tour\Grid>type grid5-data1.txt
1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6
При загрузке его в наш графический интерфейс соответствующим образом изменяются размеры сетки - класс просто заново выполняет логику создания виджетов после удаления прежних элементов ввода с помощью метода grid_forget. Метод grid_forget отвязывает виджеты в сетке и в результате удаляет их с экрана. Другие способы удаления и перерисовки компонентов графического интерфейса предоставляют методы
Рис. 9.38. Диалог открытия файла в сценарии SumGrid
Рис. 9.39. Файл с данными загружен, отображен и просуммирован
pack_forget виджетов и withdraw окна, которые используются в обработчике события after примеров «будильников» в следующем разделе.
На рис. 9.39 показано, как выглядит окно после операций удаления и перерисовки виджетов, выполненных в результате щелчков на кнопках Load и Sum.
Файл с данными grid5-data2.txt имеет те же размерности, но в двух колонках он содержит не просто числа, а выражения. Так как этот сценарий преобразует значения полей ввода с помощью встроенной функции eval, в полях этой таблицы допускается использовать любые выражения Python, если они могут быть вычислены в области видимости метода onSum:
C:\...\PP4E\Gui\Tour\Grid> type grid5-data2.txt
1 2 3 2*2 5 6 1 3-1 3 2<<1 5 6 1 5%3 3 pow(2,2) 5 6 1 2 3 2**2 5 6 1 2 3 [4,3][0] 5 6 1 {‘a’:2}[‘a’] 3 len(‘abcd’) 5 6 1 abs(-2) 3 eval(‘2+2’) 5 6
При суммировании этих полей выполняется содержащийся в них программный код на языке Python, что иллюстрирует рис. 9.40. Эта особенность может оказаться достаточно мощной. Представьте себе, например, полноценную сетку электронной таблицы - значения полей могут быть «фрагментами» программного кода на языке Python, которые динамически вычисляют значения, вызывают функции из модулей и даже загружают текущие котировки акций из Интернета с помощью инструментов, с которыми мы познакомимся в следующей части книги.
Однако эта особенность может представлять опасность - в поле может содержаться выражение, удаляющее содержимое вашего жесткого диска!45 Если вы не до конца уверены в том, какими могут быть полученные выражения, не используйте функцию eval (осуществляйте преобразование, применяя более ограниченные функции, такие как int и float) или обеспечьте выполнение процесса Python с ограниченными правами доступа к системным компонентам, которые было бы нежелательно подвергать опасности.
Рис. 9.40. Выражения на языке Python в данных и таблице
Конечно, этому сценарию еще очень далеко до настоящей электронной таблицы. Сценарий подсчитывает суммы по колонкам и способен загружать данные из файлов, но ячейки не могут содержать формулы, ссылающиеся на другие ячейки. Однако из-за недостатка места в книге дальнейшие улучшения для достижения этой цели я оставляю читателям в качестве упражнения.
Я должен также отметить, что о размещении по сетке можно сказать больше, чем позволяет объем книги. Например, путем создания вложенных фреймов с собственными сетками можно строить более сложные структуры в виде иерархий компонентов, во многом подобно тому, как размещает вложенные фреймы менеджер компоновки pack. А теперь перейдем к последней теме обзора виджетов.
Инструменты синхронизации, потоки выполнения и анимация
Последняя остановка в нашей экскурсии по виджетам, вероятно, самая необычная. Библиотека tkinter предоставляет ряд инструментов, которые связаны с моделью программирования, управляемого событиями, а не с отображением графики на экране.
Некоторым приложениям с графическим интерфейсом требуется периодически выполнять действия в фоновом режиме. Например, чтобы придать виджету «мерцающий» вид, можно зарегистрировать обработчик, который будет вызываться через равные промежутки времени. Аналогично при выполнении длительной операции с файлом неверно было бы заблокировать прочие действия в графическом интерфейсе -если бы удалось заставить цикл событий периодически выполнять обновления, графический интерфейс мог бы оставаться активным. В библиотеке tkinter есть средства для планирования таких отложенных действий и принудительного обновления интерфейса:
widget.after(milliseconds, function, *args)
Этот инструмент планирует вызов указанной функции по истечении заданного числа миллисекунд. Данная форма вызова не останавливает программу - функция обратного вызова будет запущена позднее из обычного цикла событий tkinter, а вызывающая программа продолжит свою работу как обычно и графический интерфейс останется активным, пока функция ожидает вызова. Как уже говорилось в главе 5, в отличие от объекта Timer из модуля threading, события widget.after распространяются в главном потоке выполнения графического интерфейса и потому могут выполнять в нем любые изменения.
Аргумент function может быть любым вызываемым объектом Python: функцией, связанным методом, lambda-выражением и так далее. Аргумент milliseconds определяет интервал времени в миллисекундах и является целым числом - если разделить значение этого аргумента на 1000, получится эквивалентное число секунд. Любые значения в кортеже args будут переданы функции function в виде позиционных аргументов.
На практике вместо отдельных аргументов можно использовать lambda-выражения, чтобы сделать связь аргументов с функцией более явной, но это не является обязательным. Когда в качестве функции передается связанный метод, он может получать дополнительную информацию из атрибутов объекта, а не из аргументов. Метод after возвращает идентификатор, который можно передать методу after_cancel, чтобы отменить вызов обработчика. Метод after используется очень часто, поэтому несколько ниже о нем будет рассказываться более подробно и с примерами.
widget.after(milliseconds)
Этот инструмент останавливает выполнение программы на заданное количество миллисекунд. Например, если передать в аргументе число 5000, программа будет приостановлена на 5 секунд. В сущности, это то же самое, что библиотечная функция Python time. sleep(seconds), и обе функции могут применяться для создания задержки при отображении (например, в анимационных программах, таких как PyDraw и более простых примерах ниже).
widget.after_idle(function, *args)
Этот инструмент планирует вызов указанной функции при отсутствии других событий, которые должны обрабатываться. То есть функция function становится обработчиком холостого времени, который вызывается, когда графический интерфейс не занят ничем другим.
widget.after_cancel(id)
Этот инструмент отменяет вызов обработчика, запланированный методом after до того, как он произойдет. Аргумент id - значение, возвращаемое методом after.
widget.update()
Этот инструмент вынуждает библиотеку tkinter обработать все ожидающие события, имеющиеся в очереди событий, в том числе изменение геометрии окна, а также обновление и перерисовку виджетов. Его можно периодически вызывать из долго выполняющегося обработчика, чтобы обновить экран и произвести те изменения, которые уже запросил ваш обработчик. Если этого не делать, произведенные обработчиком изменения появятся на экране только после выхода из него. На время работы обработчика, выполняющегося продолжительное время, интерфейс может вообще зависнуть, если не обновлять его вручную (обработчики не выполняются в отдельных потоках, о чем говорится в следующем разделе); окно даже не будет перерисовывать себя при перекрытии или открытии другими окнами, пока не произойдет возврат из обработчика.
Например, программы, осуществляющие анимацию путем последовательного перемещения объекта и приостановки, должны вызывать этот метод, не дожидаясь конца анимации, иначе на экране можно будет увидеть только конечное положение объекта. Что еще хуже, интерфейс окажется совершенно неактивным, пока не произойдет возврат из обработчика анимации (смотрите простые примеры воспроизведения анимации далее в этой главе и в программе PyDraw в главе 11).
widget.update_idletasks()
Этот инструмент запускает обработку всех событий холостого времени. Иногда он безопаснее, чем метод after, который в некоторых случаях может стать причиной возникновения состояния гонки за ресурсами (race conditions). События холостого времени используются виджетами Tk для отображения самих себя.
_tkinter.createfilehandler(file, mask, function)
Этот инструмент назначает функцию, которая будет вызываться при изменении состояния файла. Функция может быть вызвана, когда в файле появятся данные для чтения, когда он станет доступным для записи или когда будет возбуждено исключение. В аргументе file передается объект Python файла или сокета (формально - любой объект с методом fileno()) или целочисленный дескриптор файла; аргументе mask - значение tkinter. READABLE или tkinter. WRITABLE, определяющее режим; а в аргументе function передается функция обратного вызова, принимающая два аргумента - признак готовности файла к выполнению операции и маску. Обработчики файлов часто используются для обработки каналов и сокетов, так как обычные функции ввода/вывода могут блокировать вызывающую программу.
Этот метод недоступен в Windows и потому не будет рассматриваться в данной книге. Поскольку он доступен только в Unix, для разработки переносимых графических интерфейсов лучше использовать циклы таймера after для проверки готовности к выполнению операции и порождать потоки выполнения, которые будут читать данные и помещать их в очередь при необходимости; более подробно этот прием описывается в главе 10. Потоки выполнения являются более универсальным механизмом выполнения неблокирующих операций передачи данных.
widget.wait_variable(var)
widget.wait_window(win)
widget.wait_visibility(win)
Эти инструменты приостанавливают выполнение вызвавшей программы до момента, когда переменная tkinter изменит свое значение, будет разрушено окно или окно станет видимым. Все они входят в локальный цикл событий, благодаря чему функция mainloop приложения продолжает обработку событий. Обратите внимание, что аргумент var является объектом переменной tkinter (о которых рассказывалось ранее), а не простой переменной Python. Для использования в модальных диалогах сначала следует вызвать widget.focus() (чтобы установить фокус ввода) и widget.grab() (чтобы сделать окно единственным активным).
Некоторые из этих инструментов мы будем использовать в примерах, но не станем вникать во все их особенности здесь. За дополнительной информацией обращайтесь к другой документации по библиотекам Tk и tkinter.
Использование потоков выполнения в графических интерфейсах tkinter
Имейте в виду, что во многих программах поддержка потоков выполнения в Python, с которой мы познакомились в главе 5, способна отчасти решать те же задачи, что и инструменты tkinter, перечисленные в предыдущем разделе, и даже позволяет использовать их. Например, чтобы избежать блокировки интерфейса (и не заставлять пользователей бездействовать) во время продолжительных операций обмена данными через файлы или сокеты, этот обмен можно выполнять в дочерних потоках выполнения, при этом остальная часть программы будет выполняться, как обычно. Аналогично графические интерфейсы, ожидающие появления данных в канале или в сокете, могут использовать для этих целей потоки выполнения, обработчики, устанавливаемые методом after, или их комбинации, и тем самым избежать блокирования графического интерфейса.
Однако при использовании потоков выполнения в программах на основе библиотеки tkinter обращения к графическому интерфейсу должны выполняться только из главного потока (в котором был создан интерфейс и запущена функция mainloop). По крайней мере, потоки не должны пытаться одновременно изменять графический интерфейс. Например, метод update, описанный в предыдущем разделе, исторически является источником проблем в многопоточных графических интерфейсах - если вызывать его (или другой метод, вызывающий update) в порожденных потоках выполнения, он иногда может приводить к неожиданному и даже эффектному краху программы.
Чтобы увидеть простые и яркие примеры, демонстрирующие небезопасность обращения к графическим интерфейсам на базе tkinter из нескольких потоков выполнения, загляните в следующие сценарии, находящиеся в дереве примеров к книге, и попробуйте запустить их:
..\PP4E\Gui\Tour\threads-demoAU-frm.py
..\PP4E\Gui\Tour threads-demoAll-win.py
Эти сценарии являются версиями примеров 8.32 и 8.33 из предыдущей главы, которые конструируют четыре демонстрационных компонента графического интерфейса в параллельно выполняющихся потоках. Оба они зависают в Windows и требуют принудительного завершения. Но, хотя некоторые операции с графическим интерфейсом могут безопасно выполняться параллельно в разных потоках (например, смотрите пример 9.32, где выполняется перемещение элементов на холсте), тем не менее в целом библиотека tkinter не поддерживает многопоточную модель выполнения. (Дополнительные доказательства этого утверждения вы найдете в обсуждении многопоточной реализации циклов обновления в следующей главе, сразу после примера 10.28 - в этом примере поток выполнения пытается вывести новое окно, что вызывает сбой в работе графического интерфейса.)
Такое отношение к потокам выполнения со стороны реализации поддержки графических интерфейсов может измениться со временем, но на сегодняшний день оно налагает некоторые структурные ограничения. Например, порожденные потоки выполнения обычно не могут производить опеарции с графическим интерфейсом, поэтому, в случае необходимости, они должны взаимодействовать с главным потоком программы, используя глобальные переменные или разделяемые объекты, такие как очереди. Например, поток, ожидающий получения данных из сокета, может добавлять их в разделяемые очереди или просто устанавливать глобальные переменные и инициировать изменения в графическом интерфейсе через обработчик, устанавливаемый методом after. А обработчик может обрабатывать результаты, полученные в порождаемых потоках.
Некоторые операции над графическим интерфейсом поддерживают возможность выполнения в многопоточном режиме, тем не менее программы с графическим интерфейсом лучше делить на главный поток, управляющий графическим интерфейсом, и ряд «рабочих» потоков, не связанных с интерфейсом, что поможет избежать потенциальных конфликтов и решить проблему поддержки многопоточности в целом. Программа PyMailGUI, представленная далее в книге, например, вызывает функции-обработчики, сохраняемые потоками выполнения в очереди.
Не забывайте также, что независимо от наличия поддержки многопоточной модели выполнения в графических интерфейсах многопоточные программы с графическим интерфейсом должны следовать тем же правилам, что и любые другие многопоточные программы. Как мы узнали в главе 5, такие программы всегда должны синхронизировать доступ к совместно используемым данным, если есть вероятность, что сразу несколько потоков попытаются изменить их. Модель организации потоков производитель/потребитель, основанная на очередях, способна снять множество проблем, тем не менее в программах, порождающих потоки выполнения, изменяющие информацию, которую использует главный поток управления графическим интерфейсом, по-прежнему может требоваться использовать блокировки, чтобы избежать проблем, связанных с попытками одновременного изменения совместно используемых данных.
Подробнее многопоточные графические интерфейсы мы будем рассматривать в главе 10, а в четвертой части книги познакомимся с более реалистичными примерами многопоточных графических интерфейсов, таких как PyMailGUI в главе 14. В PyMailGUI, например, чтобы избежать блокирования интерфейса, для выполнения продолжительных операций используются потоки, все действия с графическим интерфейсом производятся только в главном потоке, а для предотвращения конфликтов, возможных при изменении совместно используемых данных, применяются блокировки.
Использование метода after
Из всех инструментов, перечисленных выше, наиболее интересным является метод after. Он позволяет сценариям назначить обработчик, который будет вызван в некоторый момент времени в будущем. Несмотря на его простоту, он будет часто использоваться в последующих примерах. В частности, в главе 11 мы познакомимся с программой часов, которая с помощью метода after просыпается 10 раз в секунду и получает текущее время, а также с программой показа слайдов, которая с помощью метода after устанавливает интервал смены фотографий (программы PyClock и PyView). Для иллюстрации основ планирования вызова обработчиков служит пример 9.27.
Пример 9.27. PP4E\Gui\Tour\alarm.py
# мигает и издает сигнал каждую секунду, используя цикл с методом after()
from tkinter import *
class Alarm(Frame):
def __init__(self, msecs=1000): # по умолчанию = 1 секунда
Frame.__init__(self)
self.msecs = msecs self.pack()
stopper = Button(self, text='Stop the beeps!’, command=self.quit) stopper.pack()
stopper.config(bg=’navy’, fg=’white’, bd=8)
self.stopper = stopper
self.repeater()
def repeater(self): # каждые N миллисекунд
self.bell() # подать сигнал
self.stopper.flash() # мигнуть кнопкой
self.after(self.msecs, self.repeater) # запланировать следующий вызов
if __name__ == ‘__main__’: Alarm(msecs=1000).mainloop()
Этот сценарий создает окно, изображенное на рис. 9.41, и периодически вызывает метод flash кнопки, заставляющий кнопку мигнуть (изменяет ее цвет на короткое время), и метод bell, который обращается к функции подачи звукового сигнала. Метод repeater вызывает методы beep и flash и с помощью метода after устанавливает обработчик, который будет выполнен через определенный промежуток времени.
Рис. 9.41. Прекратите пищать!
Метод after не останавливает вызывающий сценарий: обработчик вызывается в фоновом режиме, в то время как программа выполняет другую работу, - технически с того момента, когда цикл событий Tk получит возможность обнаружить изменение времени. Для этого метод repeater каждый раз вызывает after и заново устанавливает обработчик. Отложенные события являются одноразовыми: чтобы повторить событие, его нужно снова запланировать.
В итоге этот сценарий начинает подавать звуковые сигналы и мигать кнопкой, как только будет выведено его окно с одной кнопкой, и продолжает подавать сигналы и мигать снова и снова. Прочие действия и операции с графическим интерфейсом не влияют на это. Даже если свернуть окно, сигналы будут продолжаться, потому что события таймера tkinter генерируются в фоновом режиме. Чтобы прекратить сигналы, нужно закрыть окно или щелкнуть на кнопке. Изменив задержку msecs, можно заставить сигнал звучать так часто или так редко, как позволяет система (максимально допустимая частота может зависеть от платформы). Предупреждаю заранее, что это не лучшая демонстрационная программа для запуска в многолюдном помещении.
Скрытие и перерисовка виджетов и окон
Метод flash кнопки вызывает кратковременное изменение цвета виджета, но с помощью метода config так же просто можно динамически изменять и другие параметры внешнего вида виджетов, таких как кнопки, метки и текст,. Например, эффекта мигания можно добиться путем инвертирования цветов переднего и заднего плана элементов вручную, вызывая метод config в обработчиках, установленных методом after. Ради забавы в примере 9.28 приводится версия сценария, подающего звуковой сигнал, в которой сделан еще один шаг.
Пример 9.28. PP4E\Gui\Tour\alarm-hide.py
# стирает и отображает кнопку в обработчике, устанавливаемом методом after()
from tkinter import * import alarm
class Alarm(alarm.Alarm): # измените обработчик таймера
def __init__(self, msecs=1000): # по умолчанию = 1 секунда
self.shown = False alarm.Alarm.__init__(self, msecs)
def repeater(self): # каждые N миллисекунд
self.bell() # подать сигнал
if self.shown:
self.stopper.pack_forget() # скрыть кнопку
else: # или изменить цвет, мигнуть...
self.stopper.pack()
self.shown = not self.shown # изменить до следующего раза
self.after(self.msecs, self.repeater) # переустановить обработчик
if __name__ == ‘__main__’: Alarm(msecs=500).mainloop()
Если запустить этот сценарий, на экране появится то же самое окно, но теперь при каждом событии от таймера кнопка поочередно будет стираться и отображаться вновь. Метод pack_forget виджета стирает нарисованный элемент, а метод pack отображает его снова - методы grid_ forget и grid аналогичным образом скрывают и отображают виджеты в сетке. Метод pack_forget удобно использовать для динамического изменения графического интерфейса. Например, можно решить, какие компоненты должны отображаться в тот или иной момент времени, создавать виджеты заранее и отображать их только по мере надобности. В данном случае это просто значит, что пользователь должен щелкнуть на кнопке, пока она видна, иначе шум будет продолжаться.
Сценарий в примере 9.29 идет еще дальше. Здесь с помощью нескольких методов реализовано скрытие и появления всего окна:
• Чтобы скрыть и отобразить не какой-то отдельный виджет, а целое окно, можно воспользоваться методами withdraw и deiconify этого окна. Метод withdraw, используемый в примере 9.29, полностью стирает окно и его ярлык (если необходимо, чтобы ярлык окна оставался видимым, используйте метод iconify).
• Метод lift поднимает окно над всеми другими окнами или над определенным окном, переданным методу в виде аргумента. Этот метод также может также вызываться под именем tkraise, но не raise - его именем в Tk, потому что raise в языке Python является зарезервированным словом.
• Метод state возвращает или изменяет текущее состояние окна - он принимает значения normal, iconic, zoomed (на весь экран) и withdrawn.
Поэкспериментируйте с этими методами, чтобы понять, чем они отличаются. Их также можно использовать для динамического вывода предварительно созданных диалогов, однако практическая ценность этого приема невелика.
Пример 9.29. PP4E\Gui\Tour\alarm-withdraw.py
# то же самое, но скрывает и отображает окно целиком
from tkinter import * import alarm
class Alarm(alarm.Alarm):
def repeater(self): # каждые N миллисекунд
self.bell() # подать сигнал
if self.master.state() == ‘normal’: # окно отображается?
self.master.withdraw() # скрыть окно, без ярлыка
else: # iconify свертывает в ярлык
self.master.deiconify() # иначе перерисовать окно
self.master.lift() # и поднять над остальными
self.after(self.msecs, self.repeater) # переустановить обработчик
if __name__ == ‘__main__’: Alarm().mainloop() # master = корневое окно Tk
# по умолчанию
Этот сценарий действует точно так же, за исключением того, что при подаче сигнала появляется или исчезает все окно - закрывать его надо тогда, когда оно видно. Реализацию обработчика, вызываемого по таймеру, можно разнообразить массой других эффектов. Будете ли вы добиваться, чтобы ваши кнопки и окна мигали и исчезали, зависит скорее от мнения пользователей, чем от возможностей библиотеки tkinter.
Простые приемы воспроизведения анимации
Все графические интерфейсы, представленные до сих пор в этой книге, за исключением примера canvasDraw с непосредственным перемещением фигур, были довольно статичными. В данном, последнем разделе будет показано, как можно изменить эту ситуацию, добавив в пример 9.16 несколько простых анимаций перемещения фигуры на холсте.
Здесь также демонстрируется и расширяется понятие тегов холста -операции перемещения в этом примере применяются сразу ко всем объектам на холсте, связанным с тегом. Все овалы перемещаются при нажатии клавиши O, а все прямоугольники - при нажатии клавиши R. Как уже отмечалось ранее, методы холста принимают не только идентификаторы объектов, но и имена тегов.
Но главная задача сейчас состоит в том, чтобы проиллюстрировать простые приемы анимации с помощью инструментов, основанных на измерении интервалов времени и описанных выше в этом разделе. Существует три основных способа перемещения объектов по холсту:
• С помощью циклов, использующих функцию time.sleep для приостановки на доли секунды между последовательными операциями перемещения, наряду с вызовами метода update вручную. Сценарий выполняет перемещение, приостанавливается, передвигает объект еще немного и так далее. Функция time.sleep приостанавливает работу вызывающей программы и не возвращает управление в цикл событий графического интерфейса - обработка операций с интерфейсом, выполняемых во время перемещения, откладывается. Из-за этого после каждого перемещения нужно вызывать метод canvas. update, чтобы перерисовать экран, иначе экран не обновится, пока не закончится весь цикл перемещения в обработчике и не произойдет возврат. Это классический пример обработчика, выполняющегося продолжительное время. Без вызова метода обновления экрана вручную никакие другие события графического интерфейса не будут обработаны до возврата из обработчика (даже перерисовка окна).
• С помощью метода widget.after, планирующего выполнение операций перемещения через каждые несколько миллисекунд. Поскольку этот подход основан на расписании событий, которые библиотека tkinter отправляет обработчикам, он допускает параллельное осуществление нескольких перемещений и не требует вызова метода canvas.update. Для выполнения перемещений используется цикл событий, поэтому приостановка программы не требуется и графический интерфейс не блокируется.
• С помощью потоков выполнения, в которых выполняется несколько экземпляров циклов с приостановкой вызовом метода time.sleep, как в первом подходе. Так как потоки выполняются параллельно, приостановка любого из потоков не блокирует ни графический интерфейс, ни другие потоки, выполняющие перемещения. Как уже описывалось выше, графический нтерфейс вообще не следует обновлять из порожденных потоков, но некоторые методы холста, в частности метод move, в настоящее время допускают возможность вызова из потоков выполнения.
Из этих трех схем первая обеспечивает самое плавное воспроизведение анимации, но она замедляет другие операции во время перемещения. Вторая схема дает более замедленное перемещение, чем остальные, но в целом безопаснее, чем использование потоков выполнения, и обе последние схемы позволяют одновременно передвигать несколько объектов.
Использование циклов time.sleep
В следующих трех разделах поочередно демонстрируется структура программного кода для всех трех подходов, создающая новые подклассы примера canvasDraw, с которым мы познакомились в примере 9.16. Обращайтесь к этому примеру за информацией о привязке других событий и об основах выполнения операций рисования, перемещения и стирания. Здесь объекты, создаваемые на холсте, ассоциируются с тегами, а также добавляются новые операции и выполняется привязка новых событий. Пример 9.30 иллюстрирует первый подход.
Пример 9.30. PP4E\Gui\Tour\canvasDraw_tags.py
перемещение с применением тегов и функции time.sleep (без помощи метода widget. after или потоков выполнения); функция time.sleep не блокирует цикл событий графического интерфейса на время паузы, но интерфейс не обновляется до выхода из обработчика или вызова метода widget.update; текущему вызову обработчика onMove уделяется исключительное внимание, пока он не вернет управление: если в процессе перемещения нажать клавишу ‘R’ или ‘O’;
from tkinter import * import canvasDraw, time
class CanvasEventsDemo(canvasDraw.CanvasEventsDemo):
def __init__(self, parent=None):
canvasDraw.CanvasEventsDemo.__init__(self, parent)
self.canvas.create_text(100, 10, text=’Press o and r to move shapes’) self.canvas.master.bind(‘
def create_oval_tagged(self, x1, y1, x2, y2):
objectId = self.canvas.create_oval(x1, y1, x2, y2) self.canvas.itemconfig(objectId, tag=’ovals’, fill=’blue’) return objectId
def create_rectangle_tagged(self, x1, y1, x2, y2):
objectId = self.canvas.create_rectangle(x1, y1, x2, y2) self.canvas.itemconfig(objectId, tag=’rectangles’, fill=’red’) return objectId
def onMoveOvals(self, event): print(‘moving ovals’)
self.moveInSquares(tag=’ovals’) # переместить все овалы с данным тегом
def onMoveRectangles(self, event): print(‘moving rectangles’) self.moveInSquares(tag=’rectangles’)
def moveInSquares(self, tag): # 5 повторений по 4 раза в секунду
for i in range(5):
for (diffx, diffy) in [(+20, 0), (0, +20), (*20, 0), (0, *20)]: self.canvas.move(tag, diffx, diffy)
self.canvas.update() # принудительно обновить изображение
time.sleep(0.25) # пауза, не блокирующая интерфейс
if __name__ == ‘__main__’:
CanvasEventsDemo()
mainloop()
Все три сценария в этом разделе при вытягивании новых фигур с помощью левой кнопки мыши создают окно с голубыми овалами и красными прямоугольниками. Сама реализация вытягивания наследуется из суперкласса. Щелчок правой кнопкой мыши немедленно перемещает одну фигуру, а двойной щелчок левой кнопкой по-прежнему очищает холст - эти операции также унаследованы из суперкласса. В действительности в этом новом сценарии лишь изменены методы, создающие объекты, - теперь они ассоциируют создаваемые объекты с тегами и окрашивают их в соответствующие цвета, добавлено текстовое поле в верхней части холста и добавлены обработчики событий, выполняющие перемещение. На рис. 9.42 показано, как выглядит окно этого подкласса после создания нескольких фигур.
Рис. 9.42. Нарисованные объекты готовы к анимации
С помощью клавиш O и R начинается анимация всех нарисованных овалов и прямоугольников соответственно. Например, при нажатии клавиши O начинают синхронно перемещаться все голубые овалы. Объекты, которые подвергаются анимации, помечаются пятью квадратами вокруг своего местоположения, и перемещаются со скоростью четыре шага в секунду. Новые объекты, которые вытягиваются, когда другие находятся в движении, тоже начинают перемещаться, потому что помечены тегами. Вам обязательно следует запустить этот сценарий, чтобы получить представление о простой анимации, которую он реализует (можно, конечно, попробовать подвигать влево-вправо и вверх-вниз книгу, но это все-таки не совсем то, что нужно, да и может глупо выглядеть на людях).
Использование событий widget.after
Главный недостаток первого подхода в том, что одновременно может происходить только одна анимация: если нажать клавишу R или O во время движения, новый запрос приостанавливает предыдущее перемещение до своего окончания, потому что каждый обработчик операции перемещения допускает только один поток управления при своей работе. То есть в каждый конкретный момент времени может выполняться только один цикл, использующий time.sleep, а новый вызов этой функции из метода update фактически является рекурсивным вызовом, который приостанавливает уже выполняющийся цикл.
Обновление экрана во время перемещений тоже происходит несколько замедленно, потому что производится, только когда метод update вызывается вручную (попробуйте вытянуть фигуру или перекрыть окно другим окном во время перемещения и вы в этом убедитесь сами). Фактически если закомментировать вызов метода update в примере 9.30, графический интерфейс вообще перестанет откликаться во время выполнения операций перемещения - он не будет перерисовываться при перекрытии другими окнами, не будет откликаться на действия пользователя и никакого эффекта анимации воспроизводиться не будет (по истечении времени вы просто увидите окно в заключительном состоянии). Это полноценная имитация влияния операций, выполняющихся продолжительное время, на графический интерфейс.
Пример 9.31 переопределяет метод moveInSquares, чтобы снять такие ограничения, - применяя метод after, он обеспечивает перемещение практически без пауз. Кроме того, он демонстрирует наиболее часто используемый (и, вероятно, лучший) способ обработки событий от таймера в графических интерфейсах на основе библиотеки tkinter. Разбиение задания на части вместо того чтобы выполнять его целиком, позволяет выполнить естественное распределение частей по времени и выполнять несколько заданий одновременно.
Пример 9.31. PP4E\Gui\Tour\canvasDraw_tags_after.py
аналогично, но с применением метода widget.after() вместо циклов time.sleep; поскольку это планируемые события, появляется возможность перемещать овалы и прямоугольники _одновременно_ и отпадает необходимость вызывать метод update для обновления графического интерфейса; движение станет беспорядочным, если еще раз нажать ‘o’ или ‘r’ в процессе воспроизведения анимации: одновременно начнут выполняться несколько операций перемещения;
from tkinter import * import canvasDraw_tags
class CanvasEventsDemo(canvasDraw_tags.CanvasEventsDemo): def moveEm(self, tag, moremoves):
(diffx, diffy), moremoves = moremoves[0], moremoves[1:] self.canvas.move(tag, diffx, diffy) if moremoves:
self.canvas.after(250, self.moveEm, tag, moremoves)
def moveInSquares(self, tag):
allmoves = [(+20, 0), (0, +20), (*20, 0), (0, *20)] * 5 self.moveEm(tag, allmoves)
if __name__ == ‘__main__’:
CanvasEventsDemo()
mainloop()
Эта версия наследует все изменения из предыдущей версии и при этом позволяет перемещать овалы и прямоугольники одновременно - нарисуйте несколько овалов и прямоугольников, а затем нажмите клавишу O и затем сразу клавишу R. Попробуйте нажать обе клавиши несколько раз - чем больше нажатий, тем интенсивнее движение, потому что генерируется много событий, перемещающих объекты из того места, в котором они находятся. Если во время перемещения нарисовать новую фигуру, она, как и раньше, начнет перемещаться немедленно.
Использование нескольких потоков выполнения с циклами time.sleep
Иногда того же эффекта можно добиться выполнением анимации в потоках. Как уже говорилось выше, в целом обновлять интерфейс из порожденного потока выполнения опасно, но в данном примере этот прием действует (по крайней мере, на платформах, участвовавших в тестировании). В примере 9.32 каждая задача анимации выполняется как независимый и параллельный поток. Это означает, что при каждом нажатии клавиши O или R для запуска анимации порождается новый поток, который выполняет эту задачу.
Пример 9.32. PP4E\Gui\Tour\canvasDraw_tags_thread.py
аналогично, но анимация воспроизводится с применением циклов time.sleep, выполняемых параллельно в разных потоках, а не с помощью обработчиков событий, устанавливаемых методом after(), или одного активного цикла time. sleep; поскольку потоки выполняются параллельно, эта версия также позволяет перемещать овалы и прямоугольники _одновременно_ и не требует вызывать метод update для обновления графического интерфейса: фактически вызов метода .update() в этой версии приводит к краху, хотя некоторые методы холста можно безопасно использовать в потоках, иначе все это вообще не работало бы;
from tkinter import * import canvasDraw_tags import _thread, time
class CanvasEventsDemo(canvasDraw_tags.CanvasEventsDemo): def moveEm(self, tag): for i in range(5):
for (diffx, diffy) in [(+20, 0), (0, +20), (*20, 0), (0, *20)]: self.canvas.move(tag, diffx, diffy)
time.sleep(0.25) # приостанавливает только этот поток
def moveInSquares(self, tag):
_thread.start_new_thread(self.moveEm, (tag,))
if __name__ == ‘__main__’:
CanvasEventsDemo()
mainloop()
В этой версии возможно одновременное перемещение фигур, как и в примере 9.31, но на этот раз оно выполняется с помощью параллельных потоков. На самом деле используется та же схема, что и в первой версии time.sleep. Однако в данном случае активных потоков управления может быть несколько, поэтому периоды выполнения обработчиков перемещений могут перекрываться во времени - функция time.sleep блокирует только вызвавший ее поток, а не программу в целом.
В настоящее время этот пример прекрасно работает в Windows, но в Linux он однажды потерпел у меня неудачу - интерфейс не обновлялся при изменении его в потоках, и никаких изменений не наблюдалось до появления последующих событий. Правило, которое гласит, что желательно избегать изменения графического интерфейса в порождаемых потоках выполнения, остается верным. Обычно надежнее использовать потоки только для вычислений, а какое-либо обновление экрана производить в главном потоке (создавшем графический интерфейс). Тем не менее даже в этой модели главный поток выполнения может следить за результатами работы других потоков с помощью метода after, как в примере 9.31, не приостанавливаясь в периоды ожидания (подробнее об этом рассказывается в следующем разделе и в следующей главе).
Не исключено, что реализация составных частей, участвующих в создании анимации, изменится со временем, и не исключено, что возможность обновления графического интерфейса из потоков выполнения будет лучше поддерживаться в следующих версиях tkinter, поэтому ищите дополнительные сведения об изменениях в поддержке многопоточной модели в новых выпусках.
Другие темы, связанные с анимацией
Мы снова обратимся к анимации в примере PyDraw в главе 11. В нем будут возрождены все три приема - задержки, таймеры и потоки - для перемещения фигур, текста и фотографий в произвольные точки холста, помечаемые щелчком мыши. И хотя система абсолютных координат холста - главная рабочая лошадка для реализации большинства нетривиальных анимаций, в целом возможности анимации, основанные на библиотеке tkinter, ограничены лишь вашим воображением. В заключение скажу еще несколько слов, чтобы обозначить некоторые из имеющихся возможностей.
Другие анимационные эффекты
Помимо анимационных эффектов, которые создаются с применением холста, различные анимационные эффекты можно также создавать с помощью инструментов настройки виджетов. Как было показано ранее в примерах сценариев alarm (пример 9.28), где использовались эффекты скрытия и мигания виджетов, с помощью метода after можно легко динамически изменять внешний вид и других виджетов. С помощью циклов на основе таймера можно организовать мигание виджетов, динамически стирать и перерисовывать виджеты и окна, инвертировать или изменять цвет виджетов и так далее. Еще один пример из этой категории, где динамически изменяется шрифт и цвет (хотя эргономика этого примера вызывает большое сомнение), приводится во врезке «На досуге...», в главе 1.
Потоки выполнения и анимация
Приемы выполнения продолжительных операций в параллельных потоках приобретают особое значение, когда анимация должна оставаться активной, пока приложение выполняет какую-либо работу. Например, представьте себе программу, которая загружает большой объем данных из сети, производит тяжелые математические вычисления или выполняет другие продолжительные операции. Если графический интерфейс такой программы должен воспроизводить анимацию или как-то иначе отображать ход выполнения операции, периодически изменяя внешний вид виджета или перемещая объекты на холсте, - просто используйте метод after, как было показано выше, для периодического вызова обработчика, который будет изменять графический интерфейс. В обработчике, установленном с помощью метода after, можно, к примеру, обновлять индикатор хода выполнения или счетчик.
Кроме того, саму продолжительную операцию, вероятно, лучше выполнять в параллельном потоке, чтобы графический интерфейс оставался активным, и воспроизводить анимацию, ожидая завершения операции. В противном случае графический интерфейс будет оставаться неактивным, пока операция не завершится и не вернет управление. Внутри обработчика, установленного вызовом метода after, главный поток выполнения, управляющий графическим интерфейсом, мог бы проверять переменные или объекты, изменяемые потоком, выполняющим продолжительную операцию, чтобы определить момент ее завершения.
В частности, когда в приложении одновременно выполняется более одной продолжительной операции, порожденные потоки выполнения могут также взаимодействовать с главным потоком графического интерфейса, сохраняя информацию в объекте Python Queue, которая будет обрабатываться реализацией графического интерфейса внутри обработчика, устанавливаемого методом after. В общем случае в очередь Queue допускается помещать даже объекты функций, которые могут вызываться реализацией графического интерфейса для его обновления.
В главе 10 мы еще раз вернемся к обсуждению приемов реализации многопоточных графических интерфейсов и будем использовать их в примере PyMailGUI, далее в этой книге. А пока имейте в виду, что организация вычислений в отдельных потоках выполнения позволяет графическому интерфейсу оставаться активным и воспроизводить анимацию или реагировать на действия пользователя, ожидая окончания вычислений.
Инструменты отображения графики и реализации игровых программ
Если только вы не прекратили играть в видеоигры сразу после появления игры Pong46, вы наверняка понимаете, что приемы перемещения и воспроизведения анимации, продемонстрированные в этой главе, могут использоваться для реализации игровых программ, но только самых простых. Для реализации игровых программ с более высокими требованиями в Python имеются дополнительные инструменты поддержки графики и игр, которые мы не рассматривали здесь.
Если требуется более сложная трехмерная анимация, следует обратить внимание на поддержку пакетом расширения PIL распространенных форматов файлов анимации и фильмов, таких как FLI и MPEG. Другие сторонние инструменты, такие как OpenGL, Blender, PyGame, Maya и VPython, обеспечивают еще более высокоуровневые средства отображения графики и анимации. Кроме того, система PyOpenGL обеспечивает поддержку Tk для построения графических интерфейсов. Ссылки на эти и другие инструменты ищите на веб-сайтах PyPI или в поисковых системах.
Если вас интересует разработка игровых программ, обратите внимание на PyGame и другие пакеты поддержки разработки игр на языке Python, а также обращайтесь к другим книгам и веб-ресурсам, посвященным этой тематике. Язык Python нечасто используется в качестве единственного языка для реализации игровых программ, интенсивно использующих графику, но его все же можно применять в качестве языка, на котором пишутся прототипы и сценарии для таких продуктов.47 А при интеграции с библиотеками трехмерной графики его роль может быть расширена еще больше. Ссылки на другие имеющиеся расширения для этой области можно найти на сайте http://www.python.org.
Конец экскурсии
На этом мы завершаем наш обзор библиотеки tkinter. Вы познакомились со всеми основными виджетами и инструменты, предварительный обзор которых был сделан в конце главы 7 (вернитесь туда, чтобы просмотреть общее описание области, рассмотренной в этом путешествии). Дополнительные сведения вы получите, когда все представленные здесь инструменты вновь появятся в более крупных примерах в главах 10 и 11 и в оставшейся части книги в целом. В некотором смысле последние несколько глав заложили основу для перехода к более крупным программам, следующим далее.
Другие виджеты и их параметры
Однако следует заметить, что наш тур не был исчерпывающим. Мы познакомились со всеми основными виджетами в арсенале tkinter и попутно овладели основами построения графических интерфейсов, тем не менее мы пропустили ряд более новых и более совершенных виджетов, появившихся в библиотеке tkinter недавно:
Spinbox
Поле ввода Entry для выбора значения из множества или из диапазона
LabelFrame
Фрейм с заголовком и рамкой вокруг группы элементов
PanedWindow
Виджет менеджера компоновки, который может содержать множество виджетов, изменяющих свои размеры при перемещении линий-разделителей мышью
Кроме того, мы даже не упомянули ни об одном из виджетов в популярных расширениях библиотеки tkinter, таких как Pmw, Tix и ttk (описываются в главе 7), и не коснулись ни одного стороннего пакета. Например:
• Расширения Tix и ttk реализуют дополнительные параметры виджетов, обозначенные в главе 7, которые теперь входят в состав стандартной библиотеки Python.
• Перечень сторонних пакетов имеет тенденцию изменяться со временем, тем не менее уже сейчас они предоставляют виджеты деревьев, средства отображения разметки HTML, диалоги выбора шрифта, таблицы и многое другое, а также огромный пакет виджетов Pmw.
• Многие программы с графическим интерфейсом на основе библиотеки tkinter, такие как стандартная среда IDLE, включают диалоги выбора шрифта, виджеты деревьев и многое другое, что с успехом может использоваться вами в ваших приложениях.
Поскольку такие расширения пока еще для нас слишком сложны, чтобы их можно было охватить с пользой для дела, в интересах экономии места в книге мы оставим их освещение за другими ресурсами. В поисках более богатых возможностей для своих графических интерфейсов обязательно ознакомьтесь с описанием дополнительных виджетов в документации по tkinter, Tk, Tix, ttk и Pmw, посетите веб-сайт PyPI по адресу http://python.org/ или поищите сторонние расширения для tkinter в Интернете.
Следует также заметить, что виджеты также обладают дополнительными параметрами настройки, о которых не упоминалось в этом обзоре. Ищите описания этих параметров в ресурсах по библиотекам Tk и tkinter. В библиотеке tkinter имеются и другие инструменты, аналогичные представленным здесь, тем не менее пространство в книге, которое я могу отвести для их освещения, ограничено, во-первых, моим издателем, а во-вторых - небесконечностью древесных ресурсов.
iq
Приемы программирования графических интерфейсов
«Создание улучшенной мышеловки»
В этой главе мы продолжаем изучать создание графических интерфейсов пользователей с помощью Python и стандартной библиотеки tkinter, путем представления коллекции более сложных шаблонов и приемов программирования графических интерфейсов. В трех предшествующих главах мы познакомились с основами использования библиотеки tkinter. Здесь мы применим полученные знания для конструирования структур более высокого уровня, которые пригодятся в создании более крупных программ. То есть здесь мы перейдем к написанию собственного программного кода, реализующего программный слой над и вне базового набора инструментов tkinter, который с пользой будет применен в более практичных примерах далее в книге.
В этой главе мы рассмотрим следующие приемы:
• Реализация типичных операций с графическим интерфейсом в виде подмешиваемых классов
• Конструирование меню и панелей инструментов из шаблонных структур данных
• Добавление графических интерфейсов к инструментам командной строки
• Перенаправление потоков ввода-вывода в виджеты
• Динамическая переустановка обработчиков графического интерфейса
• Обертывание и автоматизация интерфейсов окон верхнего уровня
• Применение потоков выполнения и очередей для устранения блокирования графических интерфейсов
• Создание всплывающих окон в программах, не имеющих графического интерфейса
• Добавление графических интерфейсов в виде отдельных программ, подключаемых через сокеты и каналы
Как и другие главы этой книги, данная глава преследует двойную цель - не только изучение программирования графических интерфейсов, но и изучение более общих понятий программирования на языке Python, таких как объектно-ориентированное программирование (ООП) и повторное использование программного кода. Как мы увидим далее, создавая инструменты для работы с графическими интерфейсами на языке Python, мы упрощаем их применение в самых разных контекстах и программах.
Для связки со следующей главой эта глава завершается знакомством с панелями запуска PyDemos и PyGadgets - графических интерфейсов, используемых для запуска демонстрационных примеров. В книге приводится лишь малая часть этих программ, тем не менее мы рассмотрим их структуру достаточно подробно, чтобы вы могли самостоятельно исследовать их, отыскав в пакете с примерами.
Два предварительных замечания: во-первых, обязательно читайте программный код в листингах, выясняя подробности, отсутствующие в описании. Во-вторых, несмотря на небольшой объем примеров в этой главе, они демонстрируют приемы, которые найдут практическое применение в более реалистичных программах. Мы будем использовать эти приемы в более крупных примерах в следующей главе и на протяжении всей оставшейся части книги. Фактически разработанные здесь модули мы часто будем повторно использовать как инструменты в других программах из этой книги - программное обеспечение многократного использования должно использоваться снова и снова. Но для начала давайте приложим максимум усилий и создадим некоторые инструменты.
GuiMixin: универсальные подмешиваемые классы
Если вы читали предыдущие три главы, то наверняка заметили, что программный код, конструирующий нетривиальный графический интерфейс, может быть очень длинным, если каждый виджет создавать вручную. Приходится не только вручную связывать все виджеты, но нужно помнить десятки параметров, которые должны быть установлены. При такой стратегии программирование графических интерфейсов часто становится упражнением по вводу с клавиатуры и и выполнению операций копирования и вставки в текстовом редакторе.
Функции создания виджетов
Вместо того чтобы выполнять все операции вручную, правильнее было бы создать оболочку или как-то иначе максимально автоматизировать процесс построения графического интерфейса. Одним из решений является создание функций, обеспечивающих создание виджетов с типичными настройками и автоматизирующих процесс конструирования. Например, можно было бы определить функцию создания кнопки, реализующую все тонкости настройки и поддерживающую большинство необходимых нам кнопок. В примере 10.1 демонстрируется группа таких функций, создающих виджеты.
Пример 10.1. PP4E\Gui\Tools\widgets.py
##############################################################################
функции-обертки, упрощающие создание виджетов и опирающиеся на некоторые допущения (например, режим растягивания); используйте словарь **extras именованных аргументов для передачи таких параметров настройки, как ширина, шрифт/цвет и других, и повторно компонуйте возвращаемые виджеты, если компоновка по умолчанию вас не устраивает;
##############################################################################
from tkinter import *
def frame(root, side=TOP, **extras): widget = Frame(root)
widget.pack(side=side, expand=YES, fill=BOTH) if extras: widget.config(**extras) return widget
def label(root, side, text, **extras):
widget = Label(root, text=text, relief=RIDGE) # настройки по умолчанию widget.pack(side=side, expand=YES, fill=BOTH) # компонуется автоматически if extras: widget.config(**extras) # применить все
return widget # дополнительные параметры
def button(root, side, text, command, **extras):
widget = Button(root, text=text, command=command) widget.pack(side=side, expand=YES, fill=BOTH) if extras: widget.config(**extras) return widget
def entry(root, side, linkvar, **extras):
widget = Entry(root, relief=SUNKEN, textvariable=linkvar) widget.pack(side=side, expand=YES, fill=BOTH) if extras: widget.config(**extras) return widget
if__name__== ‘__main__’:
app = Tk()
frm = frame(app, TOP) # программного кода теперь требуется намного меньше! label(frm, LEFT, ‘SPAM’)
button(frm, BOTTOM, ‘Press’, lambda: print(‘Pushed’)) mainloop()
Этот модуль опирается на некоторые допущения, касающиеся его использования клиентами, и обеспечивает автоматизацию типичных последовательностей операций конструирования виджетов, такие как размещение методом pack. В результате применение этого модуля позволяет уменьшить объем программного кода в импортирующих его программах. Если запустить модуль из примера 10.1 как самостоятельный сценарий, он создаст простое окно с меткой в выступающей рамке слева и с кнопкой справа, в случае щелчка на которой в поток stdout выводится сообщение. Оба виджета растягиваются вместе с окном. Запустите этот пример у себя - его окно действительно не содержит ничего нового для нас, а его программный код организован скорее как библиотека, чем сценарий, который позднее будет повторно использоваться в программе PyCalc, в главе 19.
Такое решение, основанное на функциях, может сократить объем необходимого программного кода. Однако функции не обеспечивают возможность специализации, как это позволяют классы при объектноориентированном подходе. Кроме того, они не являются методами и не обладают доступом к информации о состоянии объекта, представляющего элемент графического интерфейса.
Вспомогательные подмешиваемые классы
Другим способом может быть реализация общих методов в классе и наследование их при необходимости. Такие классы обычно называют подмешиваемыми (mixin), потому что их методы «подмешиваются» в другие классы. Подмешиваемые классы служат своего рода пакетами полезных во многих случаях инструментов, оформленных в виде методов. Эта идея близка к импортированию модулей, однако подмешиваемые классы могут обращаться к конкретному экземпляру, self, используя состояние конкретного объекта и унаследованные методы. Сценарий в примере 10.2 демонстрирует, как это делается.
Пример 10.2. PP4E\Gui\Tools\guimixin.py
##############################################################################
класс, "подмешиваемый” во фреймы: реализует общие методы вызова стандартных диалогов, запуска программ, простых инструментов отображения текста и так далее; метод quit требует, чтобы этот класс подмешивался к классу Frame (или его производным)
############################################################################## from tkinter import *
from tkinter.messagebox import *
from tkinter.filedialog import *
from PP4E.Gui.Tour.scrolledtext import ScrolledText # или tkinter.scrolledtext from PP4E.launchmodes import PortableLauncher, System # или используйте модуль
# multiprocessing
class GuiMixin:
def infobox(self, title, text, *args): # используются стандартные диалоги return showinfo(title, text) # *args для обратной совместимости
def errorbox(self, text): showerror(‘Error!’, text)
def question(self, title, text, *args):
return askyesno(title, text) # вернет True или False
def notdone(self):
showerror(‘Not implemented’, ‘Option not available’)
def quit(self):
ans = self.question(‘Verify quit’, ‘Are you sure you want to quit?’) if ans:
Frame.quit(self) # нерекурсивный вызов quit!
def help(self): # переопределите более
self.infobox(‘RTFM’, ‘See figure 1...’) # подходящим
def selectOpenFile(self, file=””, dir=”.”): # испол-ся стандартные диалоги return askopenfilename(initialdir=dir, initialfile=file)
def selectSaveFile(self, file=””, dir=”.”):
return asksaveasfilename(initialfile=file, initialdir=dir)
def clone(self, args=()): # необязательные аргументы конструктора
new = Toplevel() # создать новую версию
myclass = self.__class__ # объект класса экземпляра (самого низшего)
myclass(new, *args) # прикрепить экземпляр к новому окну
def spawn(self, pycmdline, wait=False):
if not wait: # запустить новый процесс
PortableLauncher(pycmdline, pycmdline)() # запустить программу else:
System(pycmdline, pycmdline)() # ждать ее завершения
def browser(self, filename):
new = Toplevel() # создать новое окно
view = ScrolledText(new, file=filename) # Text с полосой прокрутки view.text.config(height=30, width=85) # настроить Text во фрейме view.text.config(font=(‘courier’, 10, ‘normal’)) # моноширинный шрифт
new.title("Text Viewer”) # атрибуты менеджера окон
new.iconname(“browser”) # текст из файла будет
# вставлен автоматически
def browser(self, filename): # на случай, если импортирован new = Toplevel() # модуль tkinter.scrolledtext
text = ScrolledText(new, height=30, width=85) text.config(font=(‘courier’, 10, ‘normal’)) text.pack(expand=YES, fill=BOTH) new.title(“Text Viewer”) new.iconname(“browser”)
text.insert(‘0.0’, open(filename, ‘ r').read() ) if__name__== ‘__main__’:
class TestMixin(GuiMixin, Frame): # автономный тест
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.pack()
Button(self, text=’quit’, command=self.quit).pack(fill=X)
Button(self, text=’help’, command=self.help).pack(fill=X)
Button(self, text=’clone’, command=self.clone).pack(fill=X) Button(self, text=’spawn’, command=self.other).pack(fill=X) def other(self):
self.spawn(‘guimixin.py’) # запустить себя в отдельном процессе
TestMixin().mainloop()
Хотя пример 10.2 и ориентирован на графические интерфейсы, кроме этого он иллюстрирует архитектурные идеи. Класс GuiMixin реализует обычные операции со стандартными интерфейсами, которые не подвержены влиянию возможных изменений в реализации. На деле реализации некоторых методов этого класса все-таки изменились - при переходе от первого ко второму изданию этой книги вызовы функций из устаревшего модуля Dialog были заменены вызовами новых стандартных диалогов Tk; в четвертом издании изменился компонент для просмотра содержимого текстовых файлов и он теперь использует другой класс текстового виджета с прокруткой. Так как интерфейс класса в примере скрывает подобные детали, отпадает необходимость изменять использующие его программы, чтобы сделать доступными в них новые приемы.
В данном виде класс GuiMixin предоставляет методы для вызова стандартных диалогов, клонирования окон, запуска программ, просмотра текстовых файлов и так далее. Позднее, если обнаружится, что одни и те же методы приходится писать снова и снова, их можно будет добавить в такой подмешиваемый класс, и они немедленно станут доступны везде, где импортируется и внедряется этот класс. Более того, методы класса GuiMixin можно наследовать и использовать в существующем
виде либо переопределять в подклассах. Таковы естественные преимущества классов перед функциями.
Здесь есть несколько тонкостей, которые следует отметить особо:
• Метод quit выполняет отчасти ту же задачу, что и кнопка многократного использования Quitter в предыдущих главах. Так как в подмешиваемых классах могут определяться большие библиотеки многократно используемых методов, они обеспечивают более мощный способ упаковки многократно используемых компонентов, чем отдельные классы. При правильном применении подмешиваемый класс может дать значительно больше, чем обработчик единственной кнопки.
• Метод clone создает новый экземпляр самого нижнего в иерархии класса, который подмешивает класс GuiMixin, в новом окне верхнего
уровня (self.__class__- это объект класса, из которого был создан
экземпляр). Предполагается, что конструктор класса не требует никаких других аргументов, кроме ссылки на родительский контейнер. Он открывает новый независимый экземпляр окна (и передает конструктору любые дополнительные аргументы).
• Метод browser открывает в новом окне объект ScrolledText, который мы создали в главе 9, и заполняет его текстом из файла, который нужно просмотреть. Как отмечалось в предыдущей главе, существует также стандартный виджет ScrolledText, находящийся в модуле tkinter.scrolledtext, но он имеет иной интерфейс, не загружает содержимое файла автоматически и, возможно, будет объявлен устаревшим (хотя этого не происходит уже многие годы). Для справки в класс включена реализация метода, использующая этот виджет.
• Метод spawn запускает программу на языке Python в новом процессе и либо ждет его завершения, либо нет (в зависимости от аргумента wait, со значением по умолчанию False - обычно графический интерфейс не должен ждать завершения дочерней программы). Этот метод прост потому, что тонкости запуска скрыты в модуле launchmodes, представленном в конце главы 5. Класс GuiMixin способствует применению и сам применяет на практике приемы повторного использования программного кода.
Назначение класса GuiMixin состоит в том, чтобы служить библиотекой многократно используемых инструментальных методов, и как самостоятельный класс он, в сущности, бесполезен. В действительности для использования его нужно подмешивать в классы, наследующие класс Frame: метод quit предполагает, что он смешивается с классом Frame, а метод clone предполагает, что он смешивается с классом виджета. Чтобы удовлетворить этим ограничениям, находящаяся в конце реализация самотестирования объединяет класс GuiMixin с виджетом Frame.
На рис. 10.1 изображена картина, которая возникает при самотестировании после щелчка на кнопках cLone и spawn, а затем на кнопке heLp в одной из трех копий окна. Поскольку щелчок на кнопке spawn запускает отдельный процесс, окно, созданное таким способом, остается на экране после закрытия всех остальных окон, а его закрытие не оказывает влияния на другие окна. Окно, созданное щелчком на кнопке cLone, напротив, закрывается при закрытии главного окна, однако щелчок на кнопке X в копии окна закрывает только это окно. Не забудьте включить путь к каталогу PP4E в переменную окружения PYTHONPATH, чтобы обеспечить возможность импортирования пакетов в этом и в последующих примерах.
Рис. 10.1. Реализация самотестирования класса GuiMixin в действии
Мы снова встретимся с классом GuiMixin в роли подмешиваемого класса в последующих примерах - в конце концов, в этом весь смысл повторного использования кода. Хотя функции часто бывают полезными, тем не менее поддержка наследования классами, возможность доступа к информации в экземпляре и обеспечение дополнительной организационной структуры оказываются особенно полезными при создании графических интерфейсов. Например, если многие методы класса Gui-Mixin можно было бы заменить простыми функциями, то методы clone и quit - нет. В следующем разделе рассматриваются еще более широкие возможности подмешиваемых классов.
GuiMaker: автоматизация создания меню и панелей инструментов
Подмешиваемый класс из предыдущего раздела упрощает выполнение стандартных задач, но не решает проблем сложности связывания в таких виджетах, как меню и панели инструментов. Конечно, при наличии инструмента структурирования графического интерфейса, который генерировал бы программный код на языке Python, проблем бы не было. Мы бы проектировали виджеты интерактивно, нажимали кнопку и добавляли бы реализацию обработчиков.
Однако при использовании такого относительно простого инструмента, как tkinter, сгодится и подход, основанный на программировании. Хотелось бы иметь возможность, имея в окне шаблон для меню и панелей инструментов, наследовать некоторый класс, который сам выполнял бы всю черновую работу по конструированию. Ниже демонстрируется один из возможных способов, использующий деревья простых объектов. Класс в примере 10.3 интерпретирует структуры данных, содержащих представление меню и панелей инструментов, и автоматически создает необходимые виджеты.
Пример 10.3. PP4E\Gui\Tools\guimaker.py
##############################################################################
Расширенный Frame, автоматически создающий меню и панели инструментов в окне.
GuiMakerFrameMenu предназначен для встраивания компонентов (создает меню на
основе фреймов).
GuiMakerWindowMenu предназначен для окон верхнего уровня (создает меню Tk8.0).
Пример древовидной структуры приводится в реализации самотестирования (и
в PyEdit).
##############################################################################
import sys
from tkinter import * # классы виджетов
from tkinter.messagebox import showinfo
class GuiMaker(Frame):
menuBar = [] # значения по умолчанию
toolBar = [] # изменять при создании подклассов
helpButton = True # устанавливать в start()
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.pack(expand=YES, fill=BOTH) # растягиваемый фрейм self.start() # в подклассе: установить меню/панель инстр.
self.makeMenuBar() # здесь: создать полосу меню
self.makeToolBar() # здесь: создать панель инструментов
self.makeWidgets() # в подклассе: добавить середину
def makeMenuBar(self):
создает полосу меню вверху (реализация меню Tk8.0 приводится ниже) expand=no, fill=x, чтобы ширина оставалась постоянной
menubar = Frame(self, relief=RAISED, bd=2) menubar.pack(side=TOP, fill=X)
for (name, key, items) in self.menuBar:
mbutton = Menubutton(menubar, text=name, underline=key)
mbutton.pack(side=LEFT)
pulldown = Menu(mbutton)
self.addMenuItems(pulldown, items)
mbutton.config(menu=pulldown)
if self.helpButton:
Button(menubar, text = ‘Help’, cursor = ‘gumby’, relief = FLAT,
command = self.help).pack(side=RIGHT)
def addMenuItems(self, menu, items):
for item in items: # сканировать список вложенных элем.
if item == ‘separator’: # строка: добавить разделитель
menu.add_separator({})
elif type(item) == list: # список: неактивных элементов for num in item:
menu.entryconfig(num, state=DISABLED) elif type(item[2]) != list:
menu.add_command(label = item[0], # команда: метка
underline = item[1], # горячая клавиша
command = item[2]) # обр-к: вызыв. объект
else:
pullover = Menu(menu)
self.addMenuItems(pullover, item[2]) # подменю: menu.add_cascade(label = item[0], # создать подменю
underline = item[1], # добавить каскад
menu = pullover)
def makeToolBar(self):
создает панель с кнопками внизу, если необходимо
expand=no, fill=x, чтобы ширина оставалась постоянной
можно добавить поддержку изображений: смотрите главу 9,
для чего придется создать минатюры в формате FIF или использовать
расширение PIL
if self.toolBar:
toolbar = Frame(self, cursor=’hand2’, relief=SUNKEN, bd=2)
toolbar.pack(side=BOTTOM, fill=X)
for (name, action, where) in self.toolBar:
Button(toolbar, text=name, command=action).pack(where)
def makeWidgets(self):
‘средняя’ часть создается последней, поэтому меню/панель инструменто всегда остаются вверху/внизу и обрезаются в последнюю очередь;
переопределите этот метод,
для pack: прикрепляйте середину к любому краю;
для grid: компонуйте середину по сетке во фрейме, который
прикрепляется методом pack
name = Label(self,
width=40, height=10, relief=SUNKEN, bg=’white’,
text = self.__class__.__name__,
cursor = ‘crosshair’)
name.pack(expand=YES, fill=BOTH, side=TOP)
def help(self):
“переопределите в подклассе”
showinfo(‘Help’, ‘Sorry, no help for ‘ + self.__class__.__name__)
def start(self):
“переопределите в подклассе: связать меню/панель инструментов с self” pass
##############################################################################
# Специализированная версия для полосы меню главного окна Tk 8.0 ##############################################################################
GuiMakerFrameMenu = GuiMaker # используется для меню встраиваемых
# компонентов
class GuiMakerWindowMenu(GuiMaker): # используется для меню окна def makeMenuBar(self): # верхнего уровня
menubar = Menu(self.master) self.master.config(menu=menubar)
for (name, key, items) in self.menuBar: pulldown = Menu(menubar) self.addMenuItems(pulldown, items)
menubar.add_cascade(label=name, underline=key, menu=pulldown)
if self.helpButton:
if sys.platform[:3] == ‘win’:
menubar.add_command(label=’Help’, command=self.help) else:
pulldown = Menu(menubar) # В Linux требуется настоящее меню pulldown.add_command(label=’About’, command=self.help) menubar.add_cascade(label=’Help’, menu=pulldown)
##############################################################################
# Реализация самотестирования, которая выполняется, если запустить модуль как
# самостоятельный сценарий: ‘python guimaker.py’ ##############################################################################
if__name__== ‘__main__’:
from guimixin import GuiMixin # встроить метод help
menuBar = [
(‘File’, 0,
[(‘Open’, 0, lambda:0), # lambda:0 - пустая операция (‘Quit’, 0, sys.exit)]), # здесь использовать sys, а не self (‘Edit’, 0,
[(‘Cut’, 0, lambda:0),
(‘Paste’, 0, lambda:0)]) ] toolBar = [(‘Quit’, sys.exit, {‘side’: LEFT})]
class TestAppFrameMenu(GuiMixin, GuiMakerFrameMenu): def start(self):
self.menuBar = menuBar self.toolBar = toolBar
class TestAppWindowMenu(GuiMixin, GuiMakerWindowMenu): def start(self):
self.menuBar = menuBar self.toolBar = toolBar
class TestAppWindowMenuBasic(GuiMakerWindowMenu): def start(self):
self.menuBar = menuBar
self.toolBar = toolBar # help из GuiMaker, а не из GuiMixin root = Tk()
TestAppFrameMenu(Toplevel())
TestAppWindowMenu(Toplevel())
TestAppWindowMenuBasic(root)
root.mainloop()
Чтобы понять принцип действия этого модуля, необходимо знакомство с основами создания меню, изложенными в главе 9. При соблюдении этого условия программный код будет прост и понятен: класс GuiMaker просто выполняет обход структур с описанием меню и панели инструментов и попутно создает соответствующие виджеты. В реализацию самотестирования этого модуля включен простой пример структур данных, использованных для компоновки меню и панели инструментов:
Шаблоны меню
Списки и вложенные подсписки кортежей (метка, горячая_клави-ша, обработчик). Если обработчик является подсписком, а не функцией или методом, предполагается, что это каскадное подменю.
Шаблоны панелей инструментов
Список кортежей (метка, обработчик, параметры_компоновки). Параметры компоновки определяются в виде словаря параметров, передаваемых методу pack виджета, - словарь можно записать в виде литерала {‘k’:v} или использовать вызов функции dict(k=v) с именованными аргументами. Метод pack принимает словари, однако словари можно трансформировать в именованные аргументы, используя синтаксис вызова func(**kargs). В данной реализации метки определяются как текст, но точно так же можно было бы реализовать поддержку изображений (смотрите раздел «BigGui: клиентская демонстрационная программа» ниже)
Для разнообразия предусмотрено изменение внешнего вида указателя мыши в зависимости от его местоположения: при наведении на панель инструментов указатель приобретает вид руки, в средней части - вид перекрестия, а при наведении на кнопку Help в меню, основанном на фрейме, - другой вид (можете настроить по вашему желанию).
Протоколы подклассов
Помимо структур меню и панелей инструментов клиенты этого класса могут вмешиваться и изменять реализованные в нем методы и протоколы компоновки:
Атрибуты, шаблона
Предполагается, что клиенты этого класса установят атрибуты menuBar и toolBar в каком-то месте в цепочке наследования до момента завершения метода start.
Инициализация
Метод start может переопределяться для динамического создания шаблонов меню и панели инструментов, поскольку ему доступна ссылка self. Метод start служит также местом, где осуществляется общая инициализация - конструктор__init__класса GuiMixin дол
жен вызываться, но не переопределяться.
Добавление виджетов
Метод makeWidgets может быть переопределен и создает виджеты в средней части окна - между полосой меню и панелью инструментов. По умолчанию makeWidgets помещает в середине метку с именем ближайшего класса, но по сути это абстрактный метод и предполагается его специализация в подклассах.
Протокол компоновки методом pack
В специализированном методе makeWidgets клиенты могут прикреплять виджеты средней части к любому краю self (Frame), так как полоса меню и панель инструментов уже захватили верх и низ контейнера к моменту выполнения makeWidgets. Если виджеты, составляющие среднюю часть, компонуются с помощью метода pack, она не обязательно должна быть вложенным фреймом. Полоса меню и панель инструментов автоматически компонуются первыми, чтобы при сжатии окна они обрезались в последнюю очередь.
Протокол компоновки методом grid
Размещение виджетов в средней части может осуществляться по сетке, если эта сетка помещена во вложенный фрейм, который добавляется в родительский контейнер self. (Напомню, что на каждом уровне контейнеров можно применять любой из методов, grid или pack, но не оба вместе, а self является фреймом, в котором к моменту вызова makeWidgets меню и панель инструментов уже скомпонованы с применением метода pack.) Так как фрейм GuiMaker сам компонует себя в родительском контейнере с помощью метода pack, по аналогичным причинам его нельзя непосредственно встраивать в контейнер с элементами, располагаемыми по сетке, - для использования его в таком контексте добавьте промежуточный фрейм с сеткой.
Классы GuiMaker
В ответ на выполнение условий по протоколам и шаблонам GuiMaker клиентские подклассы получают фрейм, который умеет автоматически строить свои меню и панели инструментов по структурам данных шаблона. Если вы смотрели примеры создания меню в предыдущих главах, то сможете понять, что это большой шаг вперед, в смысле уменьшения объема программного кода. Класс GuiMaker также достаточно сообразителен и может экспортировать интерфейсы меню обоих стилей, с которыми мы встречались в главе 9:
GuiMakerWindowMenu
Реализует меню окон верхнего уровня в стиле Tk 8.0, которые удобно использовать в самостоятельных программах и всплывающих окнах.
GuiMakerFrameMenu
Реализует альтернативные меню, основанные на виджетах Frame/ Menubutton, которые удобно использовать для создания меню объектов, встраиваемых в виде компонентов в более крупные графические интерфейсы.
Оба класса создают панели инструментов, экспортируют одни и те же протоколы и ожидают получить одни и те же структуры шаблонов - они отличаются только способом обработки шаблонов меню. В действительности один из них является подклассом другого, специализирующим метод создания меню - два стиля отличаются только обработкой меню верхнего уровня (Menu с каскадами Menu, вместо Frame с Menubuttons).
Программный код самотестирования GuiMaker
Как и в случае с классом GuiMixin, если запустить пример 10.3 как самостоятельный сценарий, будет выполнена логика самотестирования, находящаяся в конце файла. На рис. 10.2 изображены получаемые при этом окна. На экране создаются три окна, представляющие классы TestApp. Все три окна имеют меню и панель инструментов, параметры которых определены в структурах данных шаблонов, создаваемых программным кодом самотестирования: раскрывающиеся меню File и Edit,
Рис. 10.2. Программный код самотестирования GuiMaker в действии
а также кнопка панели инструментов Quit и стандартная кнопка меню HeLp. На рисунке меню FiLe одного из окон оторвано, а меню Edit другого окна раскрыто. Нижнее окно растянуто для наглядности.
Класс GuiMaker можно смешивать с другими суперклассами, но в первую очередь он предназначен служить тем же целям расширения и встраивания, что и класс Frame из библиотеки tkinter (особенно если учесть, что в действительности он является специализированным классом Frame, реализующим дополнительные протоколы конструирования). Фактически программный код самотестирования объединяет фрейм GuiMaker с инструментами из класса GuiMixin, представленного в предыдущем разделе.
Связи между суперклассами устанавливаются в программном коде так, что два из трех окон получают обработчик help из класса GuiMix-in, а TestAppWindowMenuBasic получает его из класса GuiMaker. Обратите внимание, что порядок, в котором смешиваются эти два класса, имеет большое значение: так как метод quit определен в обоих классах, Gui-Mixin и Frame, класс, от которого мы хотим его получить, нужно указать первым в строке заголовка смешанного класса, поскольку при множественном наследовании поиск производится слева направо. Чтобы обеспечить преимущество методов класса GuiMixin, его следует указывать перед суперклассом, производным от виджетов.
Более практическое применение класс GuiMaker найдет в таких примерах, как PyEdit в главе 11. В следующем разделе демонстрируется другой способ использования шаблонов класса GuiMaker для построения усложненного интерфейса, который служит еще одной проверкой его функциональных возможностей.
BigGui: клиентская демонстрационная программа
Рассмотрим программу, представляющую лучшее применение тех двух классов автоматизации, которые мы написали. Класс Hello, реализованный в примере 10.4, является наследником обоих классов, GuiMixin и GuiMaker. Класс GuiMaker обеспечивает связь с виджетом Frame и логику создания меню/панели инструментов. Класс GuiMixin обеспечивает дополнительные методы стандартного поведения. В действительности класс Hello служит образцом еще одного способа расширения виджета Frame, поскольку является производным от класса GuiMaker. Чтобы бесплатно получить меню и панель инструментов, он просто следует протоколам, определенным в классе GuiMaker, - устанавливает атрибуты menuBar и toolBar в методе start и переопределяет метод makeWidgets, помещая в середину нестандартную метку.
Пример 10.4. PP4E\Gui\Tools\big_gui.py
реализация графического интерфейса - объединяет GuiMaker, GuiMixin и данный класс
import sys, os
from tkinter import * # классы виджетов
from PP4E.Gui.Tools.guimixin import * # подмешиваемые методы: quit, spawn...
from PP4E.Gui.Tools.guimaker import * # фрейм плюс построение меню/панели
# инструментов
class Hello(GuiMixin, GuiMakerWindowMenu): # или GuiMakerFrameMenu
def start(self): self.hellos = 0
self.master.title("GuiMaker Demo”) self.master.iconname(“GuiMaker”)
def spawnme(): self.spawn(‘big_gui.py’) # отложен. вызов вместо lambda
self.menuBar = [ # дерево: 3 раскр. меню
(‘File’, 0, # (раскр. меню)
[(‘New...’, 0, spawnme),
(‘Open...’, 0, self.fileOpen), # [список элементов меню]
(‘Quit’, 0, self.quit)] # метка,клавиша,обработчик
),
(‘Edit’, 0,
[(‘Cut’, -1, self.notdone), # без клавиши| обработчика
(‘Paste’, -1, self.notdone), # lambda:0 тоже можно
‘separator’, # добавить разделитель
(‘Stuff’, -1,
[(‘Clone’, -1, self.clone),# каскадное подменю (‘More’, -1, self.more)]
),
(‘Delete’, -1, lambda:0),
[5]] # отключить ‘delete’
),
(‘Play’, 0,
[(‘Hello’, 0, self.greeting),
(‘Popup...’, 0, self.dialog),
(‘Demos’, 0,
[(‘Toplevels’, 0,
lambda: self.spawn(r’..\Tour\toplevel2.py’)),
(‘Frames’, 0,
lambda: self.spawn(r’..\Tour\demoAll-frm-ridge.py’)), (‘Images’, 0,
lambda: self.spawn(r’..\Tour\buttonpics.py’)),
(‘Alarm’, 0,
lambda: self.spawn(r’..\Tour\alarm.py’, wait=False)), (‘Other...’, -1, self.pickDemo)]
)]
)]
self.toolBar = [ # добавить 3 кнопки
(‘Quit’, self.quit, dict(side=RIGHT)), # или {‘side’: RIGHT} (‘Hello’, self.greeting, dict(side=LEFT)),
(‘Popup’, self.dialog, dict(side=LEFT, expand=YES)) ]
def makeWidgets(self): # переопределить метод
middle = Label(self, text=’Hello maker world!’, # создания виджетов width=40, height=10, # в середине окна
relief=SUNKEN, cursor=’pencil’, bg=’white’) middle.pack(expand=YES, fill=BOTH)
def greeting(self): self.hellos += 1 if self.hellos % 3: print(“hi”) else:
self.infobox(“Three”, ‘HELLO!’) # каждый третий щелчок
def dialog(self):
button = self.question(‘OOPS!’,
‘You typed “rm*” ... continue?’, # старый стиль ‘questhead’, (‘yes’, ‘no’)) # аргументы
[lambda: None, self.quit][button]() # игнорируются
def fileOpen(self):
pick = self.selectOpenFile(file=’big_gui.py’)
if pick:
self.browser(pick) # просмотр файла модуля или другого файла
def more(self):
new = Toplevel()
Label(new, text=’A new non-modal window’).pack()
Button(new, text=’Quit’, command=self.quit).pack(side=LEFT)
Button(new, text=’More’, command=self.more).pack(side=RIGHT)
def pickDemo(self):
pick = self.selectOpenFile(dir=’..’) if pick:
self.spawn(pick) # запустить любую программу Python
if __name__ == ‘__main__’: Hello().mainloop() # создать, запустить
Этот сценарий создает довольно объемное меню и панель инструментов, а также добавляет собственные методы обратного вызова, которые выводят сообщения в поток stdout, отображают средства просмотра текстовых файлов и новые окна и запускают другие программы. Однако многие из этих методов не делают ничего, кроме запуска метода not-Done, унаследованного от класса GuiMixin. Данный пример предназначен в основном для демонстрации возможностей классов GuiMaker и GuiMixin.
Если запустить big_gui как самостоятельный сценарий, он создаст окно с четырьмя раскрывающимися меню вверху и панелью инструментов с тремя кнопками внизу, как показано на рис. 10.3, где также изображены некоторые всплывающие окна, созданные обработчиками. В меню имеются разделители, неактивные элементы и каскадные подменю в полном соответствии с шаблоном menuBar, который передается классу GuiMaker, и кнопка Quit, унаследованная от класса GuiMixin, щелчок на которой вызывает появление диалога с просьбой подтвердить завершение работы. И это лишь часть инструментов, которые мы бесплатно получаем в свое распоряжение.
На рис. 10.4 снова изображено окно этого сценария после того как через раскрывающееся меню Play были запущены два демонстрационных сценария, которые мы написали в главах 8 и 9, выполняющиеся независимо. Эти демонстрационные сценарии были запущены с помощью переносимых инструментов, которые мы написали в главе 5 и приобрели из класса GuiMixin. Если у вас появится желание запустить какую-либо другую демонстрационную программу, выберите пункт Other в меню PLay, который откроет стандартный диалог открытия файла, и выберите файл требуемой программы. Примечание: изображение ярлыка, используемое демонстрационным сценарием, запуск которого производится из меню Play, я скопировал в каталог с этим сценарием - позднее мы напишем инструменты, которые будут пытаться отыскивать его автоматически.
Наконец, хочу заметить, что класс GuiMaker можно перепроектировать так, чтобы в нем использовались деревья вложенных экземпляров классов, умеющих применять себя к строящемуся дереву виджетов tkinter вместо ветвления по типам элементов в структурах данных шаблона. Однако ввиду недостатка места в данном издании мы отнесем это расширение к числу самостоятельных упражнений.
Рис. 10.3. Сценарий big_gui с несколькими всплывающими окнами
Рис. 10.4. Сценарий big_gui и несколько запущенных им демонстрационных программ
Коль скоро речь зашла о расширениях, - в главе 9 я демонстрировал, как вместо текстовых меток помещать на кнопки в панели инструментов изображения. Создание подкласса, наследующего класс GuiMaker, реализующего такую возмож-
ность за счет переопределения метода создания панели инструментов, стало бы не только отличным упражнением, но и позволило бы получить полезный инструмент. Однако если я буду добавлять реализацию каждой такой особенности, эта книга может разрастись настолько, что превратится в неподъемный груз...
ShellGui: графические интерфейсы к инструментам командной строки
Демонстрационные программы - это здорово, но, чтобы явственнее продемонстрировать практическую пользу таких инструментов, как класс GuiMixin, требуется найти ему более реальное применение. Вот одно из них: допустим, что мы написали комплект сценариев командной строки для решения административных задач, например, таких, как мы написали во второй части книги. Там, если помните, сценарии запускались из командной строки, но требовали от нас запоминать все параметры, которые можно передавать им при запуске, - для меня (и, вероятно, для вас) это означает, что при обращении к сценариям после долгого перерыва потребуется детально изучить их программный код.
Вместо того чтобы требовать от пользователей таких инструментов вводить загадочные команды в оболочке, почему бы для запуска этих программ не создать простой в использовании графический интерфейс на базе tkinter? Такой интерфейс мог бы запрашивать параметры командной строки, не требуя, чтобы пользователь помнил их. И если мы ставим такую задачу, почему бы вообще не обобщить идею запуска инструментов командной строки из графического интерфейса для обеспечения поддержки инструментов, которые появятся в будущем?
Обобщенный графический интерфейс инструментов оболочки
Примеры с 10.5 по 10.11 - два сценария командной строки, один вспомогательный модуль для реализации графического интерфейса, два диалога, главный графический интерфейс и модуль формы ввода параметров - представляют конкретную реализацию этих абстрактных размышлений. Так как я хотел, чтобы это был инструмент общего назначения, способный запустить любую программу командной строки, его конструкция разбита на модули, которые становятся все более специфическими для приложений по мере углубления в иерархию программного обеспечения. А на самом верху все должно быть максимально универсальным, как показано в примере 10.5.
Пример 10.5. PP4E\Gui\ShettGui\shettguipy
#!/usr/local/bin/python
##############################################################################
инструмент запуска; использует шаблоны GuiMaker, стандартный диалог завершения GuiMixin; это просто библиотека классов: чтобы вывести графический интерфейс, запустите сценарий mytools;
##############################################################################
from tkinter import * # импортировать виджеты
from PP4E.Gui.Tools.guimixin import GuiMixin # импортировать quit, а не done
from PP4E.Gui.Tools.guimaker import * # конструктор меню/панели
# инструментов
class ShellGui(GuiMixin, GuiMakerWindowMenu): # фрейм + конструктор +
def start(self): # подмешиваемые методы
self.setMenuBar() # для компонентов использовать
self.setToolBar() # GuiMaker
self.master.title(“Shell Tools Listbox”)
self.master.iconname("Shell Tools”)
def handleList(self, event): # двойной щелчок на списке
label = self.listbox.get(ACTIVE) # получить выбранный текст
self.runCommand(label) # и выполнить операцию
def makeWidgets(self): # добавить список в середину
sbar = Scrollbar(self) # связать sbar со списком
list = Listbox(self, bg=’white’) # или использ. Tour.ScrolledList
sbar.config(command=list.yview) list.config(yscrollcommand=sbar.set)
sbar.pack(side=RIGHT, fill=Y) # первым добавлен = посл. обрезан
list.pack(side=LEFT, expand=YES, fill=BOTH) # список обрез-ся первым for (label, action) in self.fetchCommands(): # добавляется в список, list.insert(END, label) # в меню и на панель инстр.
list.bind(‘
def forToolBar(self, label): # поместить на панель инстр.?
return True # по умолчанию = все
def setToolBar(self): self.toolBar = []
for (label, action) in self.fetchCommands(): if self.forToolBar(label):
self.toolBar.append((label, action, dict(side=LEFT))) self.toolBar.append((‘Quit’, self.quit, dict(side=RIGHT)))
def setMenuBar(self):
toolEntries = [] self.menuBar = [
(‘File’, 0, [(‘Quit’, -1, self.quit)]), # имя раскрывающегося меню (‘Tools’, 0, toolEntries) # список элементов меню
] # метка,клавиша,обработчик
for (label, action) in self.fetchCommands():
toolEntries.append((label, -1, action)) # добавить приложения
# в меню
##############################################################################
# делегирование операций шаблонным подклассам с разным способом хранения
# перечня утилит, которые в свою очередь делегируют операции
# подклассам, реализующим запуск утилит
##############################################################################
class ListMenuGui(ShellGui):
def fetchCommands(self): # myMenu устанавливается в подклассе
return self.myMenu # список кортежей (метка, обработчик)
def runCommand(self, cmd):
for (label, action) in self.myMenu: if label == cmd: action()
class DictMenuGui(ShellGui): def fetchCommands(self):
return self.myMenu.items() def runCommand(self, cmd): self.myMenu[cmd]()
Класс ShellGui, находящийся в этом модуле, знает, как с помощью интерфейсов GuiMaker и GuiMixin создать окно для выбора, которое выводит имена утилит в меню, в списке с прокруткой и на панели инструментов. Он также предоставляет переопределяемый метод forToolBar, позволяющий подклассам указывать, какие утилиты должны добавляться на панель инструментов, а какие - нет (на панели инструментов может быстро закончиться свободное место). Однако он умышленно оставлен в неведении относительно имен утилит, которые должны быть выведены в указанных местах, и операций, которые должны быть выполнены при выборе имен утилит.
Вместо этого класс ShellGui использует подклассы ListMenuGui и DictMenu-Gui, находящиеся в этом же файле, чтобы получить список имен утилит через их методы fetchCommands и управлять операциями по именам с помощью их методов runCommand. Эти два подкласса в действительности лишь предоставляют интерфейс к наборам утилит, представленным в виде списков и словарей, - они по-прежнему не знают, какие имена утилит будут реально отображены в графическом интерфейсе. Это сделано умышленно: так как отображаемые наборы утилит определяются подклассами более низкого уровня, мы получаем возможность использовать класс ShellGui для отображения различных наборов утилит.
Классы наборов утилит
Чтобы получить фактические наборы утилит, нужно спуститься на один уровень ниже. Модуль в примере 10.6 определяет подклассы двух специфических по типу классов, наследующих класс ShellGui, чтобы предоставить наборы доступных инструментов в виде списка и словаря (обычно достаточно одного из них, но модуль иллюстрирует применение обоих). Кроме того, именно этот модуль запускает графический интерфейс - модуль shellgui является всего лишь библиотекой классов.
Пример 10.6. PP4E\Gui\ShellGui\mytools.py
#!/usr/local/bin/python
############################################################################## реализует два набора инструментов, специфичных для типов ##############################################################################
from shellgui import * # интерфейсы, специфичные для типов
from packdlg import runPackDialog # диалоги для ввода данных
from unpkdlg import runUnpackDialog # оба используют классы приложений
class TextPak1(ListMenuGui): def __init__(self):
self.myMenu = [(‘Pack ‘, runPackDialog), # простые функции
(‘Unpack’, runUnpackDialog), # длина меток одинаковая (‘Mtool ‘, self.notdone)] # метод из GuiMixin
ListMenuGui.__init__(self)
def forToolBar(self, label):
return label in {‘Pack ‘, ‘Unpack’} # синтаксис множеств в 3.x
class TextPak2(DictMenuGui): def __init__(self):
self.myMenu = {‘Pack ‘: runPackDialog, # или использовать input...
‘Unpack’: runUnpackDialog, # вместо диалогов ввода ‘Mtool ‘: self.notdone}
DictMenuGui.__init__(self)
if __name__ == ‘__main__’: # реализация самопроверки...
from sys import argv # ‘menugui.py list|^'
if len(argv) > 1 and argv[1] == ‘list’: print(‘list test’)
TextPak1().mainloop()
else:
print(‘dict test’)
TextPak2().mainloop()
Классы в этом модуле являются конкретными наборами утилит. Чтобы вывести другой набор имен утилит, нужно просто написать и использовать новый подкласс. Разделение логики приложения на такие отдельные подклассы и модули повышает возможность повторного использования программного обеспечения.
На рис. 10.5 изображено главное окно ShellGui, создаваемое при запуске сценария mytools с классом структуры меню на основе списка в Windows 7, а также оторванные меню, демонстрирующие свое содержание. Меню и панель инструментов этого окна построены с помощью класса GuiMaker, а кнопки Quit и HeLp и пункты меню, вызывающие методы quit и help, унаследованы из класса GuiMixin через суперклассы Shell-Gui модуля. Надеюсь, вы начинаете понимать, почему в этой книге столь часто проповедуется повторное использование программного кода?
Рис. 10.5. Элементы mytools в окне ShellGui
Добавление графических интерфейсов к инструментам командной строки
К настоящему моменту мы создали библиотеку универсальных инструментальных классов, а также модуль с комплектом инструментов для запуска конкретных приложений, который определяет имена обработчиков в пунктах меню. Чтобы закончить картину, нам необходимо теперь реализовать эти обработчики, а также сами сценарии, которые они будут запускать.
Сценарии командной строки
Для проверки способности графического интерфейса запускать сценарии командной строки нам, конечно же, необходимо создать несколько таких сценариев. Следующие два сценария, находящиеся в самом низу иерархии, реализуют архивацию текстовых файлов, используя системные инструменты и приемы, описанные во второй части книги. Первый сценарий, представленный в примере 10.7, просто объединяет содержимое нескольких текстовых файлов в один файл, добавляя предопределенные строки-разделители между ними.
Пример 10.7. PP4E\Gui\ShellGui\packer.py
# упаковывает текстовые файлы в единый файл, добавляя строки-разделители
# (простейшая архивация)
import sys, glob
marker = ‘:’ * 20 + ‘textpak=>’ # надеемся, что это уникальная строка
def pack(ofile, ifiles):
output = open(ofile, ‘w’) for name in ifiles:
print(‘packing:’, name)
input = open(name, ‘r’).read() # открыть следующий входной файл
if input[-1] != ‘\n’: input += ‘\n’ # гарантировать наличие \n в конце output.write(marker + name + ‘\n’) # записать строку-разделитель
output.write(input) # и содержимое входного файла
if__name__== ‘__main__’:
ifiles = []
for patt in sys.argv[2:]: # в Windows не выполняется автоматическая
ifiles += glob.glob(patt) # подстановка по шаблону pack(sys.argv[1], ifiles) # упаковать файлы, перечисленные
# в командной строке
Второй сценарий, который приводится в примере 10.8, сканирует файлы архивов, созданные первым сценарием, и восстанавливает оригинальные файлы.
Пример 10.8. PP4E\Gui\ShellGui\unpacker.py
# распаковывает архивы, созданные сценарием packer.py
# (простейшие архивы текстовых файлов)
import sys
from packer import marker # использовать общую строку-разделитель mlen = len(marker) # имена файлов следуют за строкой-разделителем
def unpack(ifile, prefix=’new-’):
for line in open(ifile): # по всем строкам входного файла
if line[:mlen] != marker:
output.write(line) # действительные строки записать
else:
name = prefix + line[mlen:-1] # или создать новый выходной файл print(‘creating:’, name) output = open(name, ‘w’)
if __name__ == ‘__main__’: unpack(sys.argv[1])
Это чрезвычайно простые сценарии, и в этой части книги предполагается, что вы уже прочитали главы, посвященные системным инструментам, поэтому мы не будем вдаваться в подробности их реализации. Варианты этих сценариев появились еще в первом издании этой книги, в 1996 году. Я пользовался ими на заре моей карьеры программиста на языке Python для архивации файлов, еще до того, как на всех моих компьютерах появились такие инструменты, как tar и zip (и еще до того, как в стандартной библиотеке Python появились модули поддержки tar и zip). Принцип действия этих сценариев чрезвычайно прост. Возьмем следующие три текстовых файла:
C:\...\PP4E\Gui\ShellGui> type spam.txt
spam
Spam
SPAM
C:\...\PP4E\Gui\ShellGui> type eggs.txt eggs
C:\...\PP4E\Gui\ShellGui> type ham.txt h
a
m
Если запустить сценарий packer из командной строки, он объединит эти файлы в один общий файл, а сценарий unpacker извлечет их оттуда. Сценарий packer должен предусматривать обработку шаблонов имен файлов, потому что командная оболочка в Windows не выполняет автоматическое расширение шаблонов:
C:\...\PP4E\Gui\ShellGui> packer.py packed.txt *.txt
packing: eggs.txt packing: ham.txt packing: spam.txt
C:\...\PP4E\Gui\ShellGui> unpacker.py packed.txt
creating: new-eggs.txt creating: new-ham.txt creating: new-spam.txt
Файлы, извлекаемые из архива, по умолчанию получают уникальные имена (с дополнительным префиксом, чтобы избежать случайного затирания оригинальных файлов, что особенно важно на этапе тестирования), и вы получаете то, что было упаковано в архив:
C:\...\PP4E\Gui\ShellGui> type new-spam.txt
spam
Spam
SPAM
C:\...\PP4E\Gui\ShellGui> type packed.txt ::::::::::::::::::::textpak=>eggs.txt
eggs
::::::::::::::::::::textpak=>ham.txt
h
a
m
::::::::::::::::::::textpak=>spam.txt
spam
Spam
SPAM
Эти сценарии не предназначены для архивации двоичных файлов, не выполняют сжатие или что-то еще, а служат лишь иллюстрацией сценариев командной строки с обязательными аргументами командной строки. Они могут использоваться самостоятельно, как было показано выше (и запускаться с помощью таких инструментов в языке Python, как os.popen и subprocess), или как модули, которые могут импортироваться и вызываться другими программами. В нашем графическом интерфейсе мы будем использовать второй, более прямой интерфейс вызовов.
Диалоги ввода
Нам осталось реализовать заключительную часть. Сценарии упаковывания и распаковывания прекрасно справляются со своей работой как инструменты командной строки. Однако обработчики, имена которых указаны в сценарии mytools.py из примера 10.6, должны делать нечто, ориентированное на использование графического интерфейса. Поскольку оригинальные сценарии packer и unpacker живут в мире текстовых потоков ввода-вывода и командных оболочек, нам необходимо обернуть их программным кодом, который будет принимать входные параметры из графического интерфейса. В частности, нам необходимы диалоги, запрашивающие обязательные аргументы командной строки.
В первую очередь рассмотрим модуль, представленный в примере 10.9, и клиентский сценарий в примере 10.10, который использует приемы создания модального диалога, рассматривавшиеся в главе 8, чтобы отобразить форму ввода параметров для сценария packer. Программный код в примере 10.9 был выделен в отдельный модуль, потому что он может найти более широкое применение. Фактически мы будем повторно использовать его в реализации диалога для сценария unpacker и еще раз - в приложении PyEdit, в главе 11.
Этот модуль демонстрирует еще один способ автоматизации конструирования графических интерфейсов - его использование для создания рядов формы ввода позволяет заменить 7 или более строк программного кода для каждого ряда (6 - если не использовать связанную переменную или кнопку вызова диалога выбора файла) ровно на 1 строку. В модуле form.py, в главе 12, мы увидим другой, еще более автоматизированный способ конструирования форм. Однако уже такой автоматизации вполне достаточно, чтобы сэкономить десятки строк программного кода при создании нетривиальных форм.
Пример 10.9. PP4E\Gui\ShellGui\formrows.py
создает фрейм-ряд с меткой и полем ввода и дополнительной кнопкой, вызывающей диалог выбора файла; эта реализация была выделена в отдельный модуль, потому что она может с успехом использоваться и в других программах; вызывающая программа (или обработчики событий, как в данном случае) должна сохранять ссылку на связанную переменную на все время использования ряда;
from tkinter import * # виджеты и константы
from tkinter.filedialog import askopenfilename # диалог выбора файла
def makeFormRow(parent, label, width=15, browse=True, extend=False): var = StringVar() row = Frame(parent)
lab = Label(row, text=label + ‘?’, relief=RIDGE, width=width) ent = Entry(row, relief=SUNKEN, textvariable=var) row.pack(fill=X) # используются фреймы-ряды
lab.pack(side=LEFT) # с метками фиксированной длины
ent.pack(side=LEFT, expand=YES, fill=X) # можно использовать
if browse: # grid(row, col)
btn = Button(row, text=’browse...’) btn.pack(side=RIGHT) if not extend:
btn.config(command=
lambda: var.set(askopenfilename() or var.get()) )
else:
btn.config(command=
lambda: var.set(var.get() + ‘ ‘ + askopenfilename()) ) return var
Далее, функция runPackDialog в примере 10.10 является фактическим обработчиком, который вызывается при выборе имени инструмента в главном окне ShellGui. Она использует модуль конструирования рядов формы из примера 10.9 и применяет приемы создания модальных диалогов, которые мы изучали ранее.
Пример 10.10. PP4E\Gui\ShellGui\packdlg.py
# выводит диалог ввода параметров для сценария packer и запускает его
from glob import glob # расширение шаблонов имен файлов
from tkinter import * # виджеты графического интерфейса
from packer import pack # использовать сценарий/модуль packer
from formrows import makeFormRow # использовать инструмент создания форм
def packDialog(): # новое окно верхнего уровня
win = Toplevel() # с 2 фреймами-рядами + кнопка ok win.title(‘Enter Pack Parameters’)
var1 = makeFormRow(win, label=’Output file’)
var2 = makeFormRow(win, label=’Files to pack’, extend=True)
Button(win, text=’OK’, command=win.destroy).pack() win.grab_set()
win.focus_set() # модальный: захватить мышь, фокус ввода,
win.wait_window() # ждать закрытия окна диалога;
# иначе возврат произойдет немедленно return var1.get(), var2.get() # извлечь значения связанных переменных
def runPackDialog():
output, patterns = packDialog() # вывести диалог и ждать щелчка на
if output != “” and patterns != “”: # кнопке ok или закрытия окна
patterns = patterns.split() # выполнить действия не связанные с filenames = [] # графическим интерфейсом
for sublist in map(glob, patterns): # вып. расширение шаблона вручную filenames += sublist # командные оболочки Unix
print(‘Packer:’, output, filenames) # делают это автоматически pack(ofile=output, ifiles=filenames) # вывод также можно показать в
# графическом интерфейсе
if__name__== ‘__main__’:
root = Tk()
Button(root, text=’popup’, command=runPackDialog).pack(fill=X)
Button(root, text=’bye’, command=root.quit).pack(fill=X) root.mainloop()
Если запустить сценарий из примера 10.10 и щелкнуть на кнопке popup, он создаст форму ввода, как показано на рис. 10.6, - это тот же диалог, который будет показан в ответ на выбор инструмента в главном окне сценария mytools.py. Пользователь может ввести имена входных и выходных файлов с клавиатуры или щелкнуть на кнопке browse... чтобы открыть стандартный диалог выбора файла. Допускается вводить шаблоны имен файлов - вызов функции glob в этом сценарии выполнит подстановку шаблона и отфильтрует имена несуществующих файлов. Командные оболочки в Unix осуществляют такую подстановку шаблонов автоматически, если запускать сценарий packer.py из командной строки, в отличие от Windows.
Рис. 10.6. Форма вводаpackdlg
После того как пользователь заполнит форму и щелкнет на кнопке OK, параметры будут переданы главной функции сценария packer, представленного выше, для выполнения операции слияния файлов.
Графический интерфейс диалога ввода параметров для сценария unpacking выглядит проще, потому что в нем присутствует только одно поле ввода - имя файла архива. Здесь мы снова используем модуль конструирования рядов формы ввода, разработанного для диалога к сценарию packer, потому что эти две задачи очень похожи. Сценарий в примере 10.11 (и его главная функция, вызываемая графическим интерфейсом выбора инструмента в сценарии mytools.py) создает форму ввода, изображенную на рис. 10.7.
Рис. 10.7. Форма ввода unpkdlg
Пример 10.11. PP4E\Gui\ShellGui\unpkdlg.py
# выводит диалог ввода параметров для сценария unpacker и запускает его
from tkinter import * # классы виджетов
from unpacker import unpack # использовать сценарий/модуль unpacker
from formrows import makeFormRow # инструмент создания полей формы
def unpackDialog(): win = Toplevel()
win.title(‘Enter Unpack Parameters’) var = makeFormRow(win, label=’Input file’, width=11) win.bind(‘
win.focus_set() # сделать себя модальным
win.wait_window() # ждать возврата из диалога
return var.get() # или закрытия его окна
def runUnpackDialog():
input = unpackDialog() # получить входные параметры из диалога
if input != ‘’: # выполнить действия, не связанные с
print(‘Unpacker:’, input) # графическим интерфейсом, передав имя unpack(ifile=input, prefix=’’) # файла из диалога
if__name__== “__main__”:
Button(None, text=’popup’, command=runUnpackDialog).pack() mainloop()
Кнопка browse... на рис. 10.7 выводит диалог выбора файла так же, как форма packdlg. Вместо кнопки OK этот диалог связывает событие нажатия клавиши Enter с операцией закрытия окна и с завершением ожидания закрытия модального диалога; при этом имя файла архива передается экземпляру класса главной функции сценария unpacker, представленного выше, для выполнения фактической процедуры сканирования файла.
Возможные улучшения
Весь комплекс действует, как и было обещано, - благодаря такой доступности утилит командной строки в графическом виде они становятся значительно более привлекательными для пользователей, привычной средой обитания которых является графический интерфейс. Мы фактически добавили простой графический интерфейс к инструментам командной строки. И все же в данной конструкции есть два аспекта, которые можно было бы усовершенствовать.
Во-первых, оба диалога ввода используют общий программный код конструирования рядов в их формах ввода, который был реализован для данного конкретного случая. Мы могли бы существенно упростить создание диалогов, импортировав более обобщенный модуль создания форм. Мы встречались с обобщенной реализацией конструктора форм в главах 8 и 9 и будем встречаться с ней далее - смотрите также указания по обобщению создания форм в модуле form. py, в главе 12.
Во-вторых, в тот момент, когда пользователь передает данные, введенные в той или иной диалоговой форме, след графического интерфейса теряется - главное окно блокируется, а сообщения поступают в окно консоли. Графический интерфейс блокируется по техническим причинам и не может обновлять себя, пока работают утилиты packer и unpacker. Хотя эти операции и выполняются достаточно быстро с моими файлами, но при обработке очень больших файлов можно было бы запускать их в отдельных потоках выполнения, чтобы сохранить графический интерфейс активным (подробнее о потоках выполнения рассказывается далее в этой главе).
Проблема с консолью является более насущной: сообщения от утилит packer и unpacker все так же выводятся в поток stdout, а не в графический интерфейс (все имена файлов здесь включают полные пути к ним, если выбирать их с помощью стандартного диалога выбора файлов, открываемого щелчком на кнопке browse...):
C:\...\PP4E\Gui\ShellGui\temp> python ..\mytools.py list
PP4E scrolledtext list test
Packer: packed.all [‘spam.txt’, ‘ham.txt’, ‘eggs.txt’]
packing: spam.txt
packing: ham.txt
packing: eggs.txt
Unpacker: packed.all
creating: spam.txt
creating: ham.txt creating: eggs.txt
Это далеко не идеальное решение для пользователей, привыкших работать с графическим интерфейсом, - они могут не ожидать (или даже не в состоянии отыскать) появления полезной информации в окне консоли. Лучшим решением было бы перенаправить поток вывода stdout в объект, который выводит полученный текст в окно графического интерфейса. О том, как это сделать, можно прочесть в следующем разделе.
GuiStreams: перенаправление потоков данных в виджеты
Следующий прием программирования графических интерфейсов. В ответ на проблему, поставленную в конце предыдущего раздела, сценарий в примере 10.12 организует отображение входных и выходных потоков во всплывающие окна приложения, применяя практически тот же способ, который мы использовали со строками в темах, связанных с перенаправлением потоков ввода-вывода в главе 3. Хотя этот модуль служит лишь базовым прототипом и сам нуждается в усовершенствовании (например, для ввода каждой входной строки отображается новый диалог - не самое эргономичное решение), тем не менее он демонстрирует идею в целом.
Объекты этого модуля, GuiOutput и GuiInput, определяют методы, позволяющие им маскироваться под файлы везде, где ожидаются настоящие файлы. Как мы узнали в главе 3, это осуществляется с помощью средств доступа к стандартным потокам ввода-вывода, таких как встроенные функции print и input, и явных вызовов read и write. Типичные случаи использования обслуживаются в этом модуле двумя высокоуровневыми интерфейсами:
• Функция redirectedGuiFunc благодаря такой совместимости с файлами позволяет выполнять любые функции так, что их стандартные потоки ввода и вывода целиком отображаются в окна графического интерфейса, а не в окно консоли (или туда, куда эти потоки отображались бы в обычном случае).
• Функция redirectedGuiShellCmd аналогичным образом направляет в окно графического интерфейса вывод программы, запускаемой из командной строки. Она может использоваться для отображения в графическом интерфейсе вывода любых программ, включая программы на языке Python.
Классы GuiInput и GuiOutput, реализованные в модуле, можно использовать или специализировать непосредственно в клиентах, где требуется обеспечить более непосредственный интерфейс методов файлов или более полное управление процессом.
Пример 10.12. PP4E\Gui\Tools\guiStreams.py
##############################################################################
начальная реализация классов, похожих на файлы, которые можно использовать для перенаправления потоков ввода и вывода в графические интерфейсы; входные данные поступают из стандартного диалога (единый интерфейс вывод+ввод или постоянное поле Entry для ввода были бы удобнее); кроме того, некорректно берутся строки в запросах входных данных, когда количество байтов > ^(строки); в GuiInput
можно было бы добавить методы __iter__/__next__, для поддержки итераций по
строкам, как в файлах, но это способствовало бы порождению большого количества всплывающих окон;
##############################################################################
from tkinter import *
from tkinter.simpledialog import askstring
from tkinter.scrolledtext import ScrolledText # или PP4E.Gui.Tour.scrolledtext class GuiOutput:
font = (‘courier’, 9, ‘normal’) # в классе - для всех, self - для одного
def __init__(self, parent=None):
self.text = None
if parent: self.popupnow(parent) # сейчас или при первой записи
def popupnow(self, parent=None): # сейчас в родителе, Toplevel потом
if self.text: return
self.text = ScrolledText(parent or Toplevel())
self.text.config(font=self.font)
self.text.pack()
def write(self, text): self.popupnow()
self.text.insert(END, str(text)) self.text.see(END)
self.text.update() # обновлять после каждой строки
def writelines(self, lines): # строки уже включают ‘\n’
for line in lines: self.write(line) # или map(self.write, lines)
class GuiInput:
def __init__(self):
self.buff = ‘’
def inputLine(self):
line = askstring(‘GuiInput’, ‘Enter input line +
return ‘’ # диалог для ввода каждой строки
else: # кнопка cancel означает eof
return line + ‘\n’ # иначе добавить символ ‘\n’
def read(self, bytes=None): if not self.buff:
self.buff = self.inputLine()
if bytes: # читать по счетчику байтов,
text = self.buff[:bytes] # чтобы не захватить лишние строки
self.buff = self.buff[bytes:] else:
text = ‘’ # читать до eof
line = self.buff while line:
text = text + line
line = self.inputLine() # до cancel=eof=’’ return text
def readline(self):
text = self.buff or self.inputLine() # имитировать методы чтения файла self.buff = ‘’ return text
def readlines(self):
lines = [] # читать все строки
while True:
next = self.readline() if not next: break lines.append(next) return lines
def redirectedGuiFunc(func, *pargs, **kargs):
import sys # отображает потоки функции
saveStreams = sys.stdin, sys.stdout # во всплывающие окна
sys.stdin = GuiInput() # выводит диалог при необходимости
sys.stdout = GuiOutput() # новое окно для каждого вызова
sys.stderr = sys.stdout
result = func(*pargs, **kargs) # это блокирующий вызов
sys.stdin, sys.stdout = saveStreams return result
def redirectedGuiShellCmd(command): import os
input = os.popen(command, ‘r’) output = GuiOutput()
def reader(input, output): # показать стандартный вывод
while True: # команды оболочки в новом
line = input.readline() # окне с виджетом Text;
if not line: break # вызов readline может
output.write(line) # блокироваться
reader(input, output)
if __name__ == ‘__main__’: # код самотестирования
def makeUpper(): # использовать стандартные потоки
while True: # ввода-вывода
try:
line = input(‘Line? ‘) except: break
print(line.upper()) print(‘end of file’)
def makeLower(input, output): # использовать файлы
while True:
line = input.readline() if not line: break output.write(line.lower()) print(‘end of file’)
root = Tk()
Button(root, text=’test streams’,
command=lambda: redirectedGuiFunc(makeUpper)).pack(fill=X)
Button(root, text=’test files ‘,
command=lambda: makeLower(GuiInput(), GuiOutput()) ).pack(fill=X) Button(root, text=’test popen ‘,
command=lambda: redirectedGuiShellCmd(‘dir *’)).pack(fill=X) root.mainloop()
Класс GuiOutput прикрепляет объект ScrolledText (из стандартной библиотеки Python) либо к указанному родительскому контейнеру, либо выводит новое окно верхнего уровня, которое должно служить контейнером, при первом обращении к методу записи. Класс GuiInput выводит новый стандартный диалог ввода каждый раз, когда метод read запрашивает новую входную строку. Ни одна из этих схем не будет оптимальной для всех ситуаций (ввод лучше было бы отобразить на более долгоживущий виджет), но они доказывают правильность общей идеи.
На рис. 10.8 изображена картина, создаваемая реализацией самотестирования этого сценария, после перехвата вывода команды оболочки dir в Windows (слева) и двух интерактивных проверок цикла (окно с приглашениями «Line?» и прописными буквами представляет работу теста makeUpper перенаправления потоков). Диалог ввода выведен для демонстрации нового теста интерфейса файлов makeLower.
Возможно, эта картина не настолько захватывающая, чтобы на нее можно было смотреть часами, но она отражает автоматическое отображение файловых операций ввода-вывода в виджеты графического интерфейса - как будет показано чуть ниже, это решает большую часть последней проблемы, обозначенной в предыдущем разделе.
Но прежде чем двинуться дальше, необходимо отметить, что реализованный в этом модуле вызов функции, для которой выполняется перенаправление потоков ввода-вывода, а также его цикл чтения вывода по-
Рис. 10.8. Сценарий guiStreams направляет потоки во всплывающие окна
рожденной команды оболочки, могут быть заблокированы - они не возвращают управление циклу событий графического интерфейса, пока не завершится функция или запущенная команда оболочки. И хотя класс GuiOutput предусматривает вызов метода update из библиотеки tkinter после записи каждой строки, тем не менее в целом этот модуль не имеет возможности контролировать продолжительность выполнения функций или команд, которые он запускает.
В функции redirectedGuiShellCmd, например, вызов метода input.read-line будет вызывать задержку, пока от порожденной программы не будет принята полная выходная строка, и графический интерфейс в течение этого времени не будет откликаться на действия пользователя. Поскольку объект вывода output вызывает метод update, изображение на экране будет обновляться в процессе выполнения программы (метод update немедленно вызывает цикл событий Tk), но не чаще, чем будут приниматься строки от порожденной программы. Кроме того, из-за наличия цикла в этой функции графический интерфейс полностью подчиняет себя запущенной им команде, пока она не завершится.
Вызовы функций в redirectedGuiFunc также подвержены подобным блокировкам. Кроме того, на протяжении всего времени работы вызываемой функции графический интерфейс обновляется не чаще, чем функция будет выводить данные. Иными словами, эта упрощенная блокирующая модель может стать источником проблем в крупных графических интерфейсах. Мы еще вернемся к этой теме далее, когда встретимся с потоками выполнения. А пока данная реализация вполне соответствует нашим текущим целям.
Использование перенаправления для сценариев архивирования
Теперь, чтобы использовать эти инструменты перенаправления для отображения вывода сценария командной строки в графический интерфейс, просто выполним вызовы и команды оболочки через две функции этого модуля. Пример 10.13 демонстрирует один из способов обертывания вызова диалога архивирования, реализация которого представлена в примере 10.10, благодаря которому вывод операции оказывается во всплывающем окне вместо консоли.
Пример 10.13. PP4E\Gui\ShellGui\packdlg-redirect.py
# обертывает запуск сценария командной строки инструментом перенаправления его
# вывода в графический интерфейс
from tkinter import *
from packdlg import runPackDialog
from PP4E.Gui.Tools.guiStreams import redirectedGuiFunc
def runPackDialog_Wrapped(): # обработчик для использования в
redirectedGuiFunc(runPackDialog) # модуле mytools.py, обертывает прежний
# обработчик целиком
if__name__== ‘__main__’:
root = Tk()
Button(root, text=’pop’, command=runPackDialog_Wrapped).pack(fill=X) root.mainloop()
Можете проверить работу этого сценария, запустив его непосредственно, без привлечения окна ShellGui. На рис. 10.9 изображено получившееся окно stdout после закрытия диалога ввода параметров для операции архивирования. Окно появляется, как только сценарий создаст вывод, и предоставляет пользователю несколько более дружественный графический интерфейс, чем при отлове сообщений в консоли. Аналогичный