Рис. 10.9. Перенаправление вывода сценария во всплывающие окна с графическим интерфейсом
программный код можно написать и для диалога ввода параметров разархивирования, чтобы направить вывод во всплывающее окно. Просто измените сценарий mytools.py в примере 10.6, зарегистрировав представленную здесь функцию-обертку в качестве обработчика.
Фактически этот прием можно использовать для перенаправления вывода любой функции или команды оболочки во всплывающее окно. Как обычно, идея совместимых интерфейсов объектов в значительной мере обусловливает гибкость Python.
Динамическая перезагрузка обработчиков
Следующий прием программирования, который мы рассмотрим, касается изменения графического интерфейса в процессе его работы. Функция imp. reload в языке Python позволяет динамически изменять и перезагружать модули программы, не останавливая ее. Например, можно вызвать текстовый редактор, изменить отдельные части системы во время ее выполнения и увидеть, как проявляются эти изменения, сразу после перезагрузки измененного модуля.
Это мощная возможность, особенно при разработке программ, перезапуск которых мог бы занять длительное время. Программы, которые подключаются к базам данных или сетевым серверам, инициализируют крупные объекты или проходят длинную последовательность шагов, чтобы снова запустить обработчик, являются первыми кандидатами на использование функции reload. Эта функция может существенно сократить время разработки.
Однако в графическом интерфейсе при регистрации обработчиков сохраняются ссылки на объекты, а не имена модулей и объектов, поэтому перезагрузка функций обработчиков после их регистрации не даст желаемого эффекта. Операция imp.reload действует путем изменения содержимого объекта модуля в памяти. Однако, так как библиотека tkinter запоминает указатель на зарегистрированный объект обработчика, ей неизвестно о перезагрузке модуля, в котором находится обработчик. Это означает, что tkinter по-прежнему будет ссылаться на старые объекты модуля, даже если модуль был изменен и перезагружен.
Это тонкий момент, но в действительности достаточно только запомнить, что для динамической перезагрузки функций обработчиков требуется выполнить особые действия. Необходимо не только явно выполнить перезагрузку измененных модулей, но и предоставить некоторый косвенный слой, маршрутизирующий обратные вызовы от зарегистрированных объектов в модули, чтобы перезагрузка возымела эффект.
Например, сценарий в примере 10.14 выполняет дополнительные действия, перенаправляя обратные вызовы функциям в явно перезагруженном модуле. Обработчики, зарегистрированные в библиотеке tkinter, являются объектами методов, которые всего лишь осуществля-
ют перезагрузку и снова отправляют вызов. Так как доступ к действительным функциям обработчиков происходит через объект модуля, перезагрузка этого модуля приводит к обращению к последним версиям этих функций.
Пример 10.14. PP4E\Gui\Tools\rad.py
# перезагружает обработчики динамически
from tkinter import *
import radactions # получить первоначальные обработчики from imp import reload # в Python 3.X была перемещена в модуль imp
class Hello(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.make_widgets()
def make_widgets(self):
Button(self, text=’message1’, command=self.message1).pack(side=LEFT) Button(self, text=’message2’, command=self.message2).pack(side=RIGHT)
def message1(self):
reload(radactions) # перезагрузить модуль radactions перед вызовом
radactions.message1() # теперь щелчок на кнопке вызовет новую версию
def message2(self):
reload(radactions) # изменения в radactions.py возымеют эффект
# благодаря перезагрузке
radactions.message2(self) # вызовет свежую версию; передать self def method1(self):
print(‘exposed method...’) # вызывается из функции в модуле radactions Hello().mainloop()
Если запустить этот сценарий, он создаст окно с двумя кнопками, вызывающими методы message1 и message2. Пример 10.15 содержит фактическую реализацию обработчика. Его функции получают аргумент self, который обеспечивает доступ к объекту класса Hello, как если бы это были действительные методы. Можно многократно изменять этот файл во время выполнения сценария rad; каждое такое действие изменяет поведение графического интерфейса при нажатии кнопки.
Пример 10.15. PP4E\Gui\Tools\radactions.py
# обработчики: перезагружаются перед каждым вызовом
def message1(): # изменить себя
print(‘spamSpamSPAM’) # можно было бы вывести диалог...
def message2(self):
print(‘Ni! Ni!’) # изменить себя
self.method1() # обращение к экземпляру ‘Hello’...
Попробуйте запустить сценарий rad и изменять сообщения, которые выводит radactions, в другом окне. Вы должны увидеть, как при нажатии кнопок в окно консоли будут выводиться новые сообщения. Данный пример намеренно сделан простым, для иллюстрирации идеи, но на практике перезагружаемые таким способом операции могут выводить диалоги, новые окна верхнего уровня и так далее. Перезагрузка программного кода, создающего такие окна, позволяет динамически изменять их внешний вид.
Существуют и другие способы изменения графического интерфейса во время его выполнения. Например, в главе 9 мы видели, что внешний вид в любой момент можно изменить, вызвав метод config виджета, а сами виджеты можно динамически добавлять и удалять с экрана такими методами, как pack_forget и pack (и родственными им для менеджера компоновки grid). Кроме того, передача нового значения параметра command=action методу config может динамически установить в качестве обработчика обратного вызова новый вызываемый объект - при наличии соответствующей поддержки это может оказаться реальной альтернативой использованной выше обходной схеме повышения эффективности перезагрузки в графических интерфейсах.
Разумеется, далеко не все графические интерфейсы должны быть настолько динамичными. Однако представьте себе игру, которая позволяет модифицировать персонажи, - динамическая перезагрузка в таких системах может оказаться очень полезной. (Я оставляю задачу расширения этого примера на многопользовательские ролевые игры как самостоятельное упражнение.)
Обертывание интерфейсов окон верхнего уровня
Интерфейсы окон верхнего уровня были представлены в главе 8. Данный раздел продолжает обсуждение с того места, где это представление завершилось, и демонстрирует обертывание этих интерфейсов классами, автоматизирующими большую часть операций по созданию окон верхнего уровня, - установку заголовков, поиск и отображение ярлыков окон, выполнение действий по закрытию окна исходя из его назначения, перехват события щелчка мышью на кнопке закрытия окна в его заголовке и так далее.
В примере 10.16 приводятся определения классов-оберток для наиболее часто используемых типов окон - главного окна приложения, временного всплывающего окна и окна встроенного компонента графического интерфейса. Эти типы окон несколько отличаются друг от друга реализациями операции закрытия, но наследуют множество общих черт: ярлыки, заголовки и кнопки закрытия. Используя непосредственно, подмешивая или наследуя класс окна требуемого типа, вы бесплатно получаете в свое распоряжение все логику настройки.
Пример 10.16. PP4E\Gui\Tools\windows.py
##############################################################################
Классы, инкапсулирующие интерфейсы верхнего уровня.
Позволяют создавать главные, всплывающие или присоединяемые окна; эти классы могут наследоваться непосредственно, смешиваться с другими классами или вызываться непосредственно, без создания подклассов; должны подмешиваться после (то есть правее) более конкретных прикладных классов: иначе подклассы будут получать методы (destroy, okayToQuit) из этих, а не из прикладных классов, и лишатся возможности переопределить их. ##############################################################################
import os, glob
from tkinter import Tk, Toplevel, Frame, YES, BOTH, RIDGE from tkinter.messagebox import showinfo, askyesno
class _window:
подмешиваемый класс, используется классами главных и всплывающих окон
foundicon = None # совместно используется всеми экземплярами
iconpatt = ‘*.ico’ # может быть сброшен
iconmine = ‘py.ico’
def configBorders(self, app, kind, iconfile):
if not iconfile: # ярлык не был передан?
iconfile = self.findIcon() # поиск в тек. каталоге и в каталоге
title = app # модуля
if kind: title += ‘ - ‘ + kind self.title(title) # на рамке окна
self.iconname(app) # при свертывании
if iconfile: try:
self.iconbitmap(iconfile) # изображение ярлыка окна except: # проблема с интерпретатором или
pass # платформой
self.protocol(‘WM_DELETE_WINDOW’, self.quit) # не закрывать без
# подтверждения
def findIcon(self):
if _window.foundicon: # ярлык уже найден?
return _window.foundicon
iconfile = None # сначала искать в тек. каталоге
iconshere = glob.glob(self.iconpatt) # допускается только один if iconshere: # удалить ярлык с красными
iconfile = iconshere[0] # буквами Tk
else: # поиск в каталоге модуля
mymod = __import__(__name__) # импортировать, получить каталог
path = __name__.split(’.’) # возможно, путь пакета
for mod in path[1:]: # по всему пути до конца
mymod = getattr(mymod, mod) # только самый первый mydir = os.path.dirname(mymod.__file__)
myicon = os.path.join(mydir, self.iconmine) # исп. myicon, а не tk if os.path.exists(myicon): iconfile = myicon _window.foundicon = iconfile # не выполнять поиск вторично
return iconfile
class MainWindow(Tk, _window):
главное окно верхнего уровня
def __init__(self, app, kind=’’, iconfile=None):
Tk.__init__(self) self.__app = app
self.configBorders(app, kind, iconfile) def quit(self):
if self.okayToQuit(): # потоки запущены?
if askyesno(self.__app, ‘Verify Quit Program?’):
self.destroy() # завершить приложение
else:
showinfo(self.__app, ‘Quit not allowed’) # или в okayToQuit?
def destroy(self): # просто завершить
Tk.quit(self) # переопределить, если необходимо
def okayToQuit(self): # переопределить, если используются
return True # потоки выполнения
class PopupWindow(Toplevel, _window):
вторичное всплывающее окно
def __init__(self, app, kind=’’, iconfile=None):
Toplevel.__init__(self)
self.__app = app
self.configBorders(app, kind, iconfile)
def quit(self): # переопределить, если потребуется изменить
if askyesno(self.__app, ‘Verify Quit Window?’): # или вызвать destroy self.destroy() # чтобы закрыть окно
def destroy(self): # просто закрыть окно
Toplevel.destroy(self) # переопределить, если необходимо
class QuietPopupWindow(PopupWindow):
def quit(self):
self.destroy() # закрывать без предупреждения
class ComponentWindow(Frame):
при присоединении к другим интерфейсам
def __init__(self, parent): # если не фрейм
Frame.__init__(self, parent) # предоставить контейнер self.pack(expand=YES, fill=BOTH)
self.config(relief=RIDGE, border=2) # перенастроить при необходимости def quit(self):
showinfo(‘Quit’, ‘Not supported in attachment mode’)
# destroy из фрейма: просто удалить фрейм # переопределить, если
# необходимо
Почему бы просто не определять ярлык приложения и заголовок окна непосредственно вызовом методов? С одной стороны, особенности такого рода нелегко запомнить (в результате вам придется большую часть времени тратить на копирование и вставку кода). С другой стороны, эти классы добавляют высокоуровневые функциональные возможности, реализацию которых иначе пришлось бы добавлять снова и снова. Кроме всего прочего, эти классы обеспечивают автоматический запрос подтверждения завершения и поиск ярлыка. Например, классы окон всего один раз пытаются отыскать файл ярлыка в текущем рабочем каталоге и в каталоге, где находится данный модуль.
Используя классы, инкапсулирующие - то есть скрывающие - такие подробности, мы получаем всю мощь инструмента, без необходимости задумываться об их реализации в будущем. Кроме того, с помощью этих классов мы можем придавать нашим приложениям стандартизованный внешний вид и поведение. А если в будущем их потребуется изменить, нам достаточно будет изменить программный код только в одном месте, а не во всех реализованных нами окнах.
Для тестирования этого модуля в примере 10.17 приводится сценарий, использующий эти классы в различных режимах - в качестве подмешиваемых классов, в качестве суперклассов и непосредственно - из обычного процедурного программного кода.
Пример 10.17. PP4E\Gui\Tools\windows-test.py
# модуль windows должен импортироваться, иначе атрибут __name__ будет иметь
# значение __main__ в функции findIcon
from tkinter import Button, mainloop
from windows import MainWindow, PopupWindow, ComponentWindow def _selftest():
# использовать, как подмешиваемый класс class content:
"используется так же, как Tk, Toplevel и Frame” def __init__(self):
Button(self, text=’Larch’, command=self.quit).pack()
Button(self, text=’Sing ‘, command=self.destroy).pack()
class contentmix(MainWindow, content): def __init__(self):
MainWindow.__init__(self, ‘mixin’, ‘Main’)
content.__init__(self) contentmix()
class contentmix(PopupWindow, content): def __init__(self):
PopupWindow.__init__(self, ‘mixin’, ‘Popup’)
content.__init__(self) prev = contentmix()
class contentmix(ComponentWindow, content):
def __init__(self): # вложенный фрейм
ComponentWindow.__init__(self, prev) # в предыдущем окне content.__init__(self) # кнопка Sing стирает фрейм
contentmix()
# использовать в подклассах class contentsub(PopupWindow):
def __init__(self):
PopupWindow.__init__(self, ‘popup’, ‘subclass’)
Button(self, text=’Pine’, command=self.quit).pack()
Button(self, text=’Sing’, command=self.destroy).pack() contentsub()
# использование в процедурном программном коде win = PopupWindow(‘popup’, ‘attachment’)
Button(win, text=’Redwood’, command=win.quit).pack()
Button(win, text=’Sing ‘, command=win.destroy).pack() mainloop()
if __name__ == ‘__main__’:
_selftest()
Если запустить этот тест, он создаст четыре окна, как показано на рис. 10.10. Все окна автоматически получат ярлык с голубыми буквами «PY» и будут перехватывать и запрашивать подтверждение при попытке закрыть их щелчком на кнопке X в правом верхнем углу, благодаря логике поиска и настройки, унаследованной из классов окон в модуле. Некоторые кнопки в окнах, воспроизводимых тестовым сценарием, закрывают только вмещающее их окно, некоторые - все приложение, некоторые стирают только присоединенное окно, а некоторые выводят диалог с просьбой подтвердить закрытие окна. Запустите этот сценарий у себя на компьютере, чтобы увидеть, как действуют различные кнопки, и получить возможность сопоставить их поведение с реализацией в сценарии - действия, выполняемые при закрытии окна, зависят от его типа.
Рис. 10.10. Интерфейс сценария windows-test
Мы будем использовать эти классы-обертки в следующей главе, в примере PyClock, и еще раз - в главе 14, где они будут использоваться, чтобы уменьшить сложность программы PyMailGUI. Отчасти преимущество применения приемов ООП в языке Python состоит в том, что благодаря им мы можем позднее не вспоминать детали реализации.
Графические интерфейсы, потоки выполнения и очереди
В главе 5 мы познакомились с потоками выполнения и механизмом очередей, который обычно используется для организации обмена данными между потоками. Там же было дано краткое описание применения этих идей в приложениях с графическим интерфейсом. В главе 9 мы продолжили развитие этих тем применительно к библиотеке tkinter, используемой в этой книге, и расширили модель многопоточных графических интерфейсов в целом, рассмотрев поддержку многопоточной модели выполнения (или ее отсутствие) и назначение очередей и блокировок.
Теперь, когда мы приобрели определенный опыт разработки графических интерфейсов, мы можем, наконец, перейти к воплощению этих идей в программный код. Если в свое время вы пропустили описание этих тем в главе 5 или 9, вам, вероятно, лучше вернуться назад и прочитать их - мы не будем повторно рассматривать здесь основы программирования многопоточных приложений или применения очередей.
Многопоточная модель имеет самое прямое отношение к графическим интерфейсам. Напомню, что продолжительные операции вообще должны выполняться в параллельных потоках, чтобы избежать блокирования графического интерфейса и обеспечить его отзывчивость на действия пользователя. Под продолжительными операциями обычно понимаются вызовы функций, которые выполняются значительное время, операции загрузки данных с серверов, блокирующие операции ввода-вывода и любые другие, которые могут вызвать заметную задержку. В обсуждении нашего примера со сценариями архивирования/ разархивирования, представленного выше в этой главе, например, отмечалось, что функции, выполняющие фактическую обработку файлов, в целом должны выполняться в отдельных потоках, чтобы не блокировать главный поток графического интерфейса до их завершения.
В общем случае, когда графический интерфейс ожидает завершения какой-либо операции, он становится полностью неотзывчивым - окно не сможет изменить размер, свернуться и даже просто перерисовать себя, если он был перекрыт другим окном, которое потом ушло. Чтобы исключить возможность такого блокирования, программы с графическим интерфейсом должны выполнять продолжительные операции параллельно, обычно с применением механизма потоков выполнения, который позволяет совместно использовать данные программы. При таком подходе главный поток выполнения графического интерфейса получает возможность обновлять изображение на экране и откликаться на действия пользователя, в то время как другие потоки заняты решением других задач. Как мы уже видели, в некоторых ситуациях на помощь может прийти метод update, но его можно считать решением проблем, только когда есть возможность вызвать его, - потоки выполнения обеспечивают по-настоящему параллельное выполнение продолжительных операций и предлагают более универсальное решение.
Однако, как мы узнали в главе 9, только главный поток выполнения должен обновлять графический интерфейс - потоки выполнения, выполняющие продолжительные операции, не должны делать этого. Вместо этого они должны помещать данные в очередь (или передавать их через иной механизм), чтобы главный поток мог их извлечь и отобразить. Для этого в главном потоке обычно выполняется цикл опроса через определенные интервалы времени, в котором проверяется поступление новых результатов для отображения. Дочерние потоки производят данные и помещают их в очередь, но они ничего не знают о графическом интерфейсе, - главный поток получает данные и отображает их, но не занимается их генерированием.
Вследствие такого разделения труда мы обычно называем эту модель производитель/потребитель - вычислительные потоки производят данные, которые потребляются главным потоком графического интерфейса. Потоки, выполняющие продолжительные операции, иногда называют рабочими потоками, потому что они выполняют работу по производству результатов, которые затем представляются пользователю с помощью графического интерфейса. В некотором смысле, графический интерфейс является клиентом рабочих потоков-серверов, хотя обычно эта терминология используется в более узком смысле - серверы являются долгоживущими источниками данных, не имеющими тесной связи с клиентами (хотя графический интерфейс также может отображать данные, полученные от независимых серверов). Но, независимо от своего названия, данная модель позволяет исключить блокирование графического интерфейса и параллельно выполнять другие задачи, которые не выполняют непосредственного обновления графического интерфейса.
В качестве конкретного примера представим, что наш графический интерфейс должен отображать данные телеметрии, получаемые со спутника в масштабе реального времени через сокеты (механизм взаимодействий между процессами, представленный в главе 5). Такая программа должна быть достаточно отзывчивой, чтобы не потерять входящие данные, и при этом не должна блокироваться в периоды ожидания или обработки данных. Чтобы достичь обеих целей, можно породить дочерние потоки выполнения, которые будут получать поступающие данные и помещать их в очередь, а главный поток графического интерфейса будет периодически извлекать данные из очереди и отображать их. При таком разделении труда графический интерфейс не блокируется данными со спутника и наоборот - сам графический интерфейс будет выполняться независимо от потоков данных, а поскольку потоки обработки данных могут выполняться на полной скорости, они смогут принимать входные данные с той же скоростью, с какой они будут отправляться. Вообще, циклы событий графических интерфейсов не настолько отзывчивы, чтобы обрабатывать поступающие данные в масштабе реального времени. Без дополнительных потоков выполнения мы могли бы потерять часть телеметрии, а с ними мы сумеем принимать все отправляемые данные и отображать их, как только цикл событий графического интерфейса найдет время, чтобы извлечь их из очереди, - достаточно быстро, чтобы не вызвать ощущения задержки у пользователя. В отсутствие данных только дочерние потоки будут ожидать их появления, но не графический интерфейс.
В других ситуациях дополнительные потоки могут потребоваться, только чтобы графический интерфейс оставался активным в ходе выполнения продолжительных операций. Загружая данные с веб-сервера, например, графический интерфейс должен иметь возможность перерисовывать себя при перекрытии другими окнами или при изменении размеров. По этой причине вызов функции загрузки не может быть простым вызовом функции - функция должна выполняться параллельно остальной программе, обычно в отдельном потоке. По окончании загрузки поток должен известить графический интерфейс, что данные готовы для отображения, поместив их в очередь, - главный поток обнаружит их при следующей же проверке очереди в обработчике, вызываемом по таймеру. Например, мы будем использовать потоки и очереди таким способом в программе PyMailGUI, в главе 14, чтобы обеспечить возможность параллельного выполнения нескольких операций передачи почты без блокирования графического интерфейса.
Помещение данных в очередь
Обменивается ли ваш графический интерфейс данными со спутниками, веб-серверами или с чем-то еще, эта многопоточная модель легко воплощается в программный код. В примере 10.18 приводится графический интерфейс, эквивалентный многопоточной программе с очередями, с которой мы встречались в главе 5 (сравните его с примером 5.14). В данном случае графический интерфейс является потоком-потребителем, а потоки-производители добавляют данные для отображения в общую очередь. Для проверки появления результатов в очереди главный поток вместо явного цикла использует метод after из библиотеки.
Пример 10.18. PP4E\Gui\Tools\queuetest-gui.py
# графический интерфейс, отображающий данные, производимые рабочими потоками
import _thread, queue, time
dataQueue = queue.Queue() # бесконечной длины
def producer(id):
for i in range(5): time.sleep(0.1) print(‘put’)
dataQueue.put(‘[producer id=%d, count=%d]’ % (id, i))
def consumer(root): try:
print(‘get’)
data = dataQueue.get(block=False) except queue.Empty: pass else:
root.insert(‘end’, ‘consumer got => %s\n’ % str(data)) root.see(‘end’)
root.after(250, lambda: consumer(root)) # 4 раза в секунду
def makethreads(): for i in range(4):
_thread.start_new_thread(producer, (i,))
if __name__ == ‘__main__’:
# главный поток: порождает группу рабочих потоков на каждый щелчок мыши from tkinter.scrolledtext import ScrolledText root = ScrolledText() root.pack()
root.bind(‘
consumer(root) # запустить цикл проверки очереди в главном потоке окна
root.mainloop() # вход в цикл событий
Обратите внимание, что здесь при каждом событии от таймера из очереди извлекается только один элемент данных. Это было сделано сознательно. Можно было бы при каждом событии от таймера извлекать все элементы из очереди в цикле. Но в критических случаях, при наличии большого количества данных в очереди, это легко могло бы привести к блокированию графического интерфейса (представьте быстрый интерфейс телеметрии, который внезапно передал в очередь сразу сотни или даже тысячи результатов измерений). Обрабатывая по одному элементу за раз, мы гарантируем, что цикл событий графического интерфейса будет получать управление, обновлять изображение на экране и обрабатывать ввод пользователя. Недостаток такого подхода состоит в том, что при большом количестве элементов данных в очереди их обработка может занять продолжительное время. В подобных ситуациях можно воспользоваться гибридными схемами, например извлекать из очереди до N элементов данных при каждом событии от таймера, - мы увидим пример реализации такой схемы далее в этом разделе (пример 10.20).
Если запустить этот сценарий, главный поток графического интерфейса начнет извлекать данные из очереди и отображать их в окне Scrolled-Text, как показано на рис. 10.11. При каждом щелчке левой кнопкой мыши в окне будет запускаться новая группа из четырех потоков-производителей. Потоки выполнения выводят сообщения «get» и «put» в стандартный поток вывода (в данном примере эта операция не синхронизируется между потоками - на некоторых платформах, включая Windows, выводимые сообщения могут перемешиваться). Для имитации выполнения продолжительных операций, таких как получение почты, извлечение результатов запроса или ожидание поступления данных через сокет (дополнительно о сокетах рассказывается ниже в этой главе), потоки-производители вызывают функцию sleep. Я выполнил несколько щелчков левой кнопкой мыши, чтобы обеспечить перекрытие потоков выполнения, как показано на рис. 10.11.
Реализация с классами и связанными методами
Пример 10.19 представляет дальнейшее развитие этой модели, реализовав ее в виде класса, обеспечив тем самым возможность специализации и повторного использования. По своему действию и внешнему виду окна этот пример ничем не отличается от процедурной версии, но очередь в нем проверяется чаще и ничего не выводится в стандартный поток вывода. Обратите внимание, что в качестве обработчика событий от мыши и функции потока выполнения здесь используются связанные методы - связанные методы хранят в себе ссылку на экземпляр и на сам метод, поэтому при выполнении в потоке он обладает возможно-
Рис. 10.11. Изображение обновляется главным потоком графического интерфейса
стью доступа к информации в объекте, включая очередь. Это позволило перенести очередь и окно из глобальных переменных в атрибуты экземпляра.
Пример 10.19. PP4E\Gui\Tools\queuetest-gui-class.py
# графический интерфейс, отображающий данные, производимые рабочими потоками
# (на основе классов)
import threading, queue, time
from tkinter.scrolledtext import ScrolledText # или PP4E.Gui.Tour.scrolledtext
class ThreadGui(ScrolledText): threadsPerClick = 4
def __init__(self, parent=None):
ScrolledText.__init__(self, parent)
self.pack()
self.dataQueue = queue.Queue() # бесконечной длины
self.bind(‘
# главном потоке выполнения
def producer(self, id): for i in range(5): time.sleep(0.1)
self.dataQueue.put(‘[producer id=%d, count=%d]’ % (id, i))
def consumer(self): try:
data = self.dataQueue.get(block=False) except queue.Empty: pass else:
self.insert(‘end’, ‘consumer got => %s\n’ % str(data)) self.see(‘end’)
self.after(100, self.consumer) # 10 раз в секунду
def makethreads(self, event):
for i in range(self.threadsPerClick):
threading.Thread(target=self.producer, args=(i,)).start()
if __name__ == ‘__main__’:
root = ThreadGui() # в главном потоке: создать GUI, запустить цикл таймера
root.mainloop() # войти в цикл событий tk
Рассмотрите внимательнее организацию потоков, использование общей очереди и извлечение данных в цикле от таймера, так как с этими приемами мы еще встретимся далее в этой главе, а также в главе 11, в примере программы PyEdit. В программе PyEdit мы будем использовать эти приемы для организации поиска внешних файлов в потоках, чтобы избежать блокирования графического интерфейса и обеспечить возможность одновременного поиска сразу нескольких файлов. Кроме того, мы повторно воспользуемся классической многопоточной моделью произ-водитель/потребитель в более реалистичном примере, представленном далее в этой главе. позволяющей избежать блокирования графического интерфейса, который должен читать данные из стандартного потока ввода, связанного с потоком вывода другой программы.
Завершение потоков в графических интерфейсах
Кроме всего прочего, в примере 10.19 вместо модуля _thread используется модуль threading. Это означает, что в отличие от предыдущей версии, программа не завершится, пока выполняются какие-либо потоки-производители, если только они не были запущены как потоки-демоны, установкой их флагов daemon в значение True. Напомню, что при использовании модуля threading программы завершаются, только когда остаются одни потоки-демоны, - потоки-производители наследуют значение False в атрибуте daemon от потока, создавшего их, что препятствует завершению программы, пока они продолжают выполняться.
Однако в данном примере дочерние потоки выполнения завершаются слишком быстро, чтобы можно было заметить задержку при завершении программы. Измените в сценарии вызов функции time.sleep, чтобы он выполнял задержку на 2 секунды, имитируя долгоживущий рабочий поток, и попробуйте запустить пример. Попытка закрыть окно после щелчка левой кнопкой мыши сотрет окно, но сама программа продолжит выполняться еще в течение примерно 10 секунд (это можно наблюдать, например, в виде паузы в окне консоли). Если сделать то же самое в предыдущей версии, использующей модуль _thread, или в этой версии установить флаги daemon потоков в значение True, программа будет завершаться немедленно.
При решении практических задач может потребоваться взвесить различные политики управления завершением в контексте действующих потоков и программировать их соответствующим образом; при необходимости отложить завершение программы можно использовать потоки, запущенные со значением False в атрибуте daemon, или блокировки. Напротив, использование потоков threading может препятствовать желательному завершению программы, если забыть установить флаг daemon в значение True. Дополнительно о завершении программ и о потоках-демонах (и о других пугающих темах!) рассказывается в главе 5.
Помещение обработчиков в очередь
В примерах предыдущего раздела данные, помещаемые в очередь, всегда были строками. Этого вполне достаточно для простых приложений, где могут существовать производители только одного типа. Однако если в программе могут иметься потоки, выполняющие различные функции и производящие данные различных типов, это может осложнить обработку данных. Вероятно, вам придется сопровождать данные некоторой дополнительной информацией, которая поможет главному потоку графического интерфейса определить, как обрабатывать эти данные.
Представьте, например, почтовый клиент, где несколько операций отправки и приема почты могут выполняться одновременно. Если все потоки совместно используют одну и ту же очередь, информация, помещаемая в нее, должна иметь какие-то отличительные признаки, которые позволят определить, какое событие она представляет, - загруженное сообщение для отображения, информацию для индикатора хода выполнения операции, сообщение об успешной отправке или что-то еще. Это надуманный пример: мы столкнемся с этой проблемой в приложении PyMailGUI, представленном в главе 14.
К счастью, очереди поддерживают не только строки - в очередь можно помещать объекты Python любых типов. Наиболее универсальными из них, вероятно, являются вызываемые объекты: помещая в очередь функцию или другой вызываемый объект, поток-производитель может самым непосредственным способом сообщить графическому интерфейсу, как обрабатывать данные. Графическому интерфейсу остается просто извлечь объект из очереди и вызвать его. Поскольку все потоки выполняются в пределах одного и того же процесса, в очередь могут помещаться вызываемые объекты любых типов - простые функции, результаты lambda-выражений и даже связанные методы, объединяющие в себе функции и объекты, обеспечивающие доступ к их данным и методам. Любые изменения в объектах, выполняемые такими функциями обратного вызова, будут доступны всему процессу.
Благодаря возможности в языке Python универсальным способом обрабатывать функции и списки их аргументов, передача их через очередь выглядит гораздо проще, чем могло бы показаться. Так, в примере 10.20 демонстрируется один из способов передачи функций обратного вызова через очередь, который будет использоваться в приложении PyMailGUI в главе 14. Этот модуль содержит также ряд полезных инструментов. Класс ThreadCounter можно использовать как совместно используемый счетчик и логический флаг (например, для управления операциями, перекрывающимися во времени). Однако наиболее важным здесь является реализация интерфейса передачи функций через очередь - в двух словах, данная реализация позволяет клиентам запускать потоки выполнения, которые помещают в очередь свои функции завершения для передачи главному потоку.
До определенной степени этот пример можно считать лишь разновидностью примера из предыдущего раздела - здесь мы по-прежнему выполняем цикл событий от таймера, в котором главный поток извлекает данные из очереди. Для большей эффективности за одно событие от таймера из очереди извлекается уже не один (что может оказаться слишком долгим, при большом количестве элементов данных, или слишком накладным, при коротком интервале срабатывания таймера) и не все (что может привести к блокированию графического интерфейса, если данные поступают слишком быстро), а до N элементов данных. Мы дополним этот прием пакетной обработки данных в PyMailGUI возможностью выполнения множественных изменений в графическом интерфейсе без необходимости выделять ресурсы процессора для обработки коротких событий от таймера, в чем обычно нет необходимости.
Однако здесь главное отличие, на которое следует обратить внимание, заключается в том, что мы вызываем объекты, которые потоки-производители обобщенным способом помещают в очередь для обработки успешного или неудачного выполнения операции в ответ на благополучное завершение или возникшее исключение. Кроме того, функции, выполняемые в потоках-производителях, принимают функцию, определяющую информацию о протекании операции, которая при вызове просто помещает в очередь обработчик индикатора хода выполнения операции, предназначенный для вызова в контексте главного потока выполнения. Эту особенность можно использовать, например, чтобы показать в графическом интерфейсе ход выполнения загрузки данных по сети.
Пример 10.20. PP4E\Gui\Tools\threadtools.py
##############################################################################
Общесистемные утилиты поддержки многопоточной модели выполнения для графических
интерфейсов.
Реализует единую очередь обработчиков и цикл обработки событий от таймера для ее проверки, совместно используемые всеми окнами в программе; рабочие потоки помещают в очередь свои обработчики завершения и протекания операции для вызова в главном потоке; эта модель не блокирует графический интерфейс - он просто выполняет операции в порождаемых дочерних потоках и обрабатывает события завершения и продолжения операций; рабочие потоки могут перекрываться во времени с главным потоком и с другими рабочими потоками.
На практике передача функций-обработчиков с аргументами через очереди намного удобнее, чем передача простых данных, если в программе одновременно могут действовать разнотипные потоки выполнения, - каждый тип может подразумевать выполнение различных действий при завершении.
Библиотеки создания графических интерфейсов не полностью поддерживают многопоточную модель, поэтому, вместо того чтобы напрямую вызывать обработчики, производящие изменение графического интерфейса после выполнения основной операции в потоке, они помещаются в общую очередь и вызываются не в дочерних потоках, а в цикле обработки событий от таймера в главном потоке; это также обеспечивает регулярность и предсказуемость моментов обновления графического интерфейса; требуется, чтобы логика потока разбивалась на основную операцию, завершающие действия и операцию, возвращающую информацию о протекании процесса.
Предполагается, что в случае неудачи функция потока возбуждает исключение и принимает в аргументе ‘progress’ функцию обратного вызова, если поддерживает возможность передачи информации о ходе выполнения операции; предполагается также, что все обработчики выполняются очень быстро, либо производят обновление графического интерфейса в процессе работы, и эта очередь будет содержать функции обратного вызова (или другие вызываемые объекты) для использования в приложениях с графическим интерфейсом, - требуется наличие виджетов, чтобы обеспечить работу цикла на основе метода ‘after’; для использования данной модели в сценариях без графического интерфейса можно было бы использовать простой таймер. ##############################################################################
# запустить, даже если нет потоков # сейчас, если модуль threads
try: # недоступен в стандартной библиотеке,
import _thread as thread # возбуждает исключение ImportError
except ImportError: # и блокирует графический интерфейс
import _dummy_thread as thread # тот же интерфейс без потоков
# общая очередь
# в глобальной области видимости, совместно используется потоками import queue, sys
threadQueue = queue.Queue(maxsize=0) # infinite size
##############################################################################
# ГЛАВНЫЙ ПОТОК - периодически проверяет очередь; выполняет действия,
# помещаемые в очередь, в контексте главного потока; один потребитель (GUI) и
# множество производителей (загрузка, удаление, отправка); простого списка
# было бы вполне достаточно, если бы операции list.append и list.pop были
# атомарными; 4 издание: в процессе обработки каждого события от таймера
# выполняет до N операций: обход в цикле всех обработчиков, помещенных в
# очередь, может заблокировать графический интерфейс, а при выполнении
# единственной операции вызов всех обработчиков может занять продолжительное
# время или привести к неэффективному расходованию ресурсов процессора на
# обработку событий от таймера (например, информирование о ходе выполнения
# операций); предполагается, что обработчики выполняются очень быстро или
# выполняют обновление графического интерфейса в процессе работы (вызывают
# метод update): после вызова обработчика планируется очередное событие от
# таймера и управление возвращается в цикл событий; поскольку этот цикл
# выполняется в главном потоке, он не препятствует завершению программы; ##############################################################################
def threadChecker(widget, delayMsecs=100, perEvent=1): # 10 раз/сек, 1/таймер for i in range(perEvent): # передайте другие значения,
try: # чтобы повысить скорость
(callback, args) = threadQueue.get(block=False) # выполнить до N
except queue.Empty: # обработчиков
break # очередь пуста?
else:
callback(*args) # вызвать обраб.
widget.after(delayMsecs, # переустановить
lambda: threadChecker(widget, delayMsecs, perEvent))# таймер и
# вернуться в цикл
# событий
##############################################################################
# НОВЫЙ ПОТОК - выполняет задание, помещает в очередь обработчик завершения и
# обработчик, возвращающий информацию о протекании процесса; вызывает функцию
# основной операции с аргументами, затем планирует вызов функций on* с
# контекстом; запланированные вызовы добавляются в очередь и выполняются в
# главном потоке, чтобы избежать параллельного обновления графического
# интерфейса; позволяет программировать основные операции вообще без учета
# того, что они будут выполняться в потоках; не вызывайте обработчики в
# потоках: они могут обновлять графический интерфейс в потоке, поскольку
# передаваемая функция будет вызвана в потоке; обработчик ‘progress’ просто
# должен добавлять в очередь функцию обратного вызова с передаваемыми ей
# аргументами; не обновляйте текущие счетчики здесь: обработчик завершения
# будет извлечен из очереди и выполнен функцией threadChecker в главном
# потоке;
##############################################################################
def threaded(action, args, context, onExit, onFail, onProgress): try:
if not onProgress: # ждать завершения этого потока
action(*args) # предполагается, что в случае неудачи будет else: # возбуждено исключение
def progress(*any):
threadQueue.put((onProgress, any + context)) action(progress=progress, *args)
except:
threadQueue.put((onFail, (sys.exc_info(), ) + context)) else:
threadQueue.put((onExit, context))
def startThread(action, args, context, onExit, onFail, onProgress=None): thread.start_new_thread(
threaded, (action, args, context, onExit, onFail, onProgress))
##############################################################################
# счетчик или флаг с поддержкой многопоточной модели выполнения: удобно
# использовать, чтобы избежать выполнения перекрывающихся во времени операций,
# когда потоки изменяют другие общие данные, помимо тех, которые изменяются
# обработчиками, помещаемыми в очередь
##############################################################################
class ThreadCounter: def __init__(self): self.count = 0
self.mutex = thread.allocate_lock() # или Threading.semaphore def incr(self):
self.mutex.acquire() # или с помощью self.mutex:
self.count += 1 self.mutex.release() def decr(self):
self.mutex.acquire() self.count -= 1 self.mutex.release()
def __len__(self): return self.count # True/False, если используется,
# как флаг
##############################################################################
# реализация самотестирования: разбивает поток на основную операцию,
# операцию завершения и операцию информирования о ходе выполнения задания ##############################################################################
if __name__ == ‘__main__’: # самотестирование при запуске в виде сценария
import time # или PP4E.Gui.Tour.scrolledtext
from tkinter.scrolledtext import ScrolledText
def onEvent(i): # реализация порождения потоков
myname = ‘thread-%s’ % i startThread(
action = threadaction,
args = (i, 3),
context = (myname,),
onExit = threadexit,
onFail = threadfail,
onProgress = threadprogress)
# основная операция, выполняемая потоком def threadaction(id, reps, progress): # то, что делает поток for i in range(reps):
time.sleep(1)
if progress: progress(i) # обработчик progress: в очередь
if id % 2 == 1: raise Exception # ошибочный номер: неудача
# обработчики завершения/информирования о ходе выполнения задания:
# передаются главному потоку через очередь
def threadexit(myname):
text.insert(‘end’, ‘%s\texit\n’ % myname) text.see(‘end’)
def threadfail(exc_info, myname):
text.insert(‘end’, ‘%s\tfail\t%s\n’ % (myname, exc_info[0])) text.see(‘end’)
def threadprogress(count, myname):
text.insert(‘end’, ‘%s\tprog\t%s\n’ % (myname, count)) text.see(‘end’)
text.update() # допустимо: выполняется в главном потоке
# создать графический интерфейс и запустить цикл обработки событий от
# таймера в главном потоке
# порождать группу рабочих потоков в ответ на каждый щелчок мышью:
# выполнение их может перекрываться во времени
text = ScrolledText()
text.pack()
threadChecker(text) # запустить цикл обработки потоков
text.bind(‘
# для range - нет
text.mainloop() # вход в цикл событий
Реализация модуля описывается в комментариях, а программный код самотестирования демонстрирует порядок его использования. Обратите внимание, что реализация потока выполнения разбита на основные операции, операции, выполняемые при выходе, и необязательные операции информирования о ходе выполнения задания, - основные операции выполняются в новом потоке, но другие помещаются в очередь и выполняются в главном потоке. То есть чтобы воспользоваться этим модулем, вам, по сути, необходимо разделить реализацию функции потока на действия, выполняемые в самом потоке, выполняемые после его завершения и необязательные действия информирования о ходе выполнения задания. В целом, продолжительным может быть только этап, выполняемый в пределах потока.
Если пример 10.20 запустить, как самостоятельный сценарий, по каждому щелчку мышью на виджете ScrolledTest он будет запускать шесть новых потоков выполнения, каждый из которых будет выполнять функцию threadaction. Функция, выполняемая в потоке, вызывает передаваемую ей функцию информирования о ходе выполнения задания, помещающую обработчик обратного вызова в очередь, который вызывает функцию threadprogress в главном потоке. Когда функция потока завершается, интерфейсный уровень помещает в очередь обработчик, который вызовет threadfail или threadexit в главном потоке, в зависимости от того, возбудила функция потока исключение или нет. Так как все обработчики, помещаемые в очередь, извлекаются и выполняются в цикле обработки событий от таймера в главном потоке, это означает, что все изменения в графическом интерфейсе будут производиться только в главном потоке выполнения и не будут перекрываться во времени.
На рис. 10.12 приводится фрагмент вывода, сгенерированного щелчком мыши на окне. Все сообщения о завершении, неудаче и информирующие о ходе выполнения задания, создаются обработчиками, добавляемыми в очередь дочерними потоками и вызываемыми в цикле обработки событий от таймера в главном потоке.
Рис. 10.12. Сообщения, создаваемые обработчиками, извлекаемыми из очереди
Внимательно рассмотрите этот пример и попробуйте последить логику работы программного кода самотестирования. Это достаточно сложное задание, и вам, вероятно, придется не один раз пробежать по нему глазами, чтобы понять, как он действует. Однако как только вы уловите суть этой парадигмы, вы поймете общую схему работы с разнородными потоками выполнения обобщенным способом. В PyMailGUI, например, когда требуется запустить прием/передачу почты, выполняются операции, похожие на операции в функции onEvent в реализации самотестирования в данном примере.
Передача связанных методов в очередь
Технически, чтобы обеспечить еще более высокую гибкость этой схемы, в приложении PyMailGUI из главы 14 с помощью этого модуля в очередь будут помещаться связанные методы - вызываемые объекты, которые, как упоминалось, хранят ссылку на функцию метода и на экземпляр объекта, что обеспечивает возможность доступа к данным объекта и другим его методам. При таком подходе программный код управления потоками выполнения в клиентском сценарии выглядит примерно так, как показано в примере 10.21 - версии реализации самотестирования из предыдущего примера, использующей классы и методы.
Пример 10.21. PP4E\Gui\Tools\threadtools-test-classes.py
# тест очереди обработчиков, но для реализации операций используются
# связанные методы
import time
from threadtools import threadChecker, startThread from tkinter.scrolledtext import ScrolledText
class MyGUI:
def __init__(self, reps=3):
self.reps = reps # используется окно Tk по умолчанию
self.text = ScrolledText() # сохранить виджет в атрибуте
self.text.pack()
threadChecker(self.text) # запустить цикл проверки потоков
self.text.bind(‘
# результатов map, для range - нет
def onEvent(self, i): # метод, запускающий поток
myname = ‘thread-%s' % i startThread(
action = self.threadaction,
args = (i, ),
context = (myname,),
onExit = self.threadexit,
onFail = self.threadfail,
onProgress = self.threadprogress)
# основная операция, выполняемая потоком
def threadaction(self, id, progress): # то, что делает поток
for i in range(self.reps): # доступ к данным в объекте
time.sleep(1)
if progress: progress(i) # обработчик progress: в очередь
if id % 2 == 1: raise Exception # ошибочный номер: неудача
# обработчики: передаются главному потоку через очередь def threadexit(self, myname):
self.text.insert(‘end’, ‘%s\texit\n' % myname) self.text.see(‘end’)
def threadfail(self, exc_info, myname): # имеет доступ к данным объекта self.text.insert(‘end’, ‘%s\tfail\t%s\n’ % (myname, exc_info[0])) self.text.see(‘end’)
def threadprogress(self, count, myname):
self.text.insert(‘end’, ‘%s\tprog\t%s\n’ % (myname, count)) self.text.see(‘end’)
self.text.update() # допустимо: выполняется в главном потоке
if __name__ == ‘__main__’: MyGUI().text.mainloop()
В этой версии в качестве обработчиков завершения и информирования о ходе выполнения задания, помещаемых в очередь, а также основной операции, выполняемой потоком, используются связанные методы. Как мы узнали в главе 5, благодаря тому что все потоки выполняются в пределах одного и того же процесса и в одной и той же области памяти, связанные методы ссылаются на оригинальные экземпляры объектов, а не на их копии. Это позволяет им напрямую обновлять графический интерфейс и другую информацию. Кроме того, связанные методы являются нормальными вызываемыми объектами, которые могут использоваться взамен обычных функций, поэтому нет никаких препятствий к использованию их в очередях и в потоках выполнения. Помимо всего прочего, широкие возможности совместного использования данных являются одним из основных преимуществ потоков выполнения перед процессами.
Приложение PyMailGUI в главе 14 демонстрирует более практичное применение этого модуля, где он служит базовым механизмом обслуживания событий завершения потоков и информирования о ходе выполнения задания. Там основные операции в потоках также выполняются с помощью связанных методов, что позволяет потокам и обработчикам, помещаемым ими в очередь, использовать одни и те же данные. Как мы увидим далее, действия, выполняемые обработчиками из очереди, автоматически становятся безопасными в многопоточном окружении, потому что они выполняются только в контексте главного потока. Однако другие изменения в совместно используемых объектах, производимые дочерними потоками, все еще может потребоваться синхронизировать отдельно, если они выполняются без применения очереди обработчиков и есть вероятность, что они будут перекрываться во времени. Непосредственное обновление кэша электронной почты, например, может заблокировать выполнение других операций до его завершения.
Другие способы добавления GUI к сценариям командной строки
Иногда потребность в графическом интерфейсе возникает совершенно неожиданно. Возможно, вы еще не умеете программировать графические интерфейсы или просто хотите оставаться в старых добрых временах. Но, предположим, вы написали программу, взаимодействующую с пользователем посредством консоли, а потом решили, что взаимодействие через графический интерфейс сделает ее привлекательнее. Что делать в этом случае?
Вероятно, наиболее очевидный ответ состоит в том, чтобы преобразовать программу командной строки, - добавить в программный код инициализацию виджетов при запуске, вызвать функцию mainloop, чтобы запустить цикл обработки событий и отобразить главное окно, и переместить всю логику программы в обработчики, запускаемые в ответ на действия пользователя. Все операции, выполняемые оригинальной программой, превращаются в обработчики событий, а главный поток управления конструирует главное окно, вызывает цикл событий один раз и переходит в состояние ожидания.
Это традиционный способ организации программ с графическим интерфейсом, и он полностью соответствует ожиданиям пользователя - окна появляются по запросу, а не случайным, на первый взгляд, образом. Однако если вы не готовы стиснуть зубы и выполнить такое структурное преобразование, можно пойти другим путем. Например, в разделе, посвященном примеру ShellGui, выше в этой главе, мы видели, как можно добавить к сценариям архивирования файлов окна для ввода входных параметров (пример 10.5 и следующие за ним). Позднее мы также видели, как перенаправлять вывод таких сценариев в графические интерфейсы с помощью класса GuiOutput (пример 10.13). Этот подход можно использовать, когда сценарий командной строки, обертываемый в графический интерфейс, выполняет единственную операцию. Для организации более динамичного взаимодействия с пользователем может потребоваться использовать другие приемы.
Вполне возможно, например, запускать графический интерфейс из сценария командной строки, вызывая функцию mainloop из библиотеки tkinter всякий раз когда требуется отобразить окно. Возможно также использовать более фундаментальный подход и создать отдельную программу, реализующую графический интерфейс для вашего приложения. В заключение нашего обзора методик программирования графических интерфейсов мы по очереди рассмотрим все эти схемы.
Вывод окон графического интерфейса по требованию
Если вам необходимо добавить простой графический интерфейс для взаимодействия с пользователем к существующему сценарию командной строки (например, диалог выбора файла), это можно сделать, настроив виджеты и вызвав функцию mainloop из программы командной строки, когда это будет необходимо. По сути, этот прием добавляет поддержку графического интерфейса в программу, не имеющую постоянного главного окна. Проблема в том, что функция mainloop не возвращает управление, пока главное окно не будет закрыто пользователем (или не будет вызван метод quit). Поэтому вы не сможете получить данные, введенные пользователем, из виджетов, уже уничтоженных к моменту возврата из функции mainloop. Чтобы обойти эту проблему, достаточно просто сохранить ввод пользователя в объекте Python: объект останется существовать после уничтожения графического интерфейса. Пример 10.22 демонстрирует один из способов реализации этой идеи на языке Python.
Пример 10.22. PP4E\Gui\Tools\mainloopdemo.py
демонстрирует запуск двух отдельных циклов mainloop; каждый из них возвращает управление после того как главное окно будет закрыто; ввод пользователя сохраняется в объекте Python перед тем, как графический интерфейс будет закрыт; обычно в программах с графическим интерфейсом настройка виджетов и вызов mainloop выполняется всего один раз, а вся их логика распределена по обработчикам событий; в этом демонстрационном примере вызовы функции mainloop производятся для обеспечения модальных взаимодействий с пользователем из программы командной строки; демонстрирует один из способов добавления графического интерфейса к существующим сценариям командной строки без реорганизации программного кода;
from tkinter import *
from tkinter.filedialog import askopenfilename, asksaveasfilename
class Demo(Frame):
def __init__(self,parent=None):
Frame.__init__(self,parent)
self.pack()
Label(self, text =”Basic demos”).pack()
Button(self, text=’open’, command=self.openfile).pack(fill=BOTH) Button(self, text=’save’, command=self.savefile).pack(fill=BOTH) self.open_name = self.save_name = “”
def openfile(self): # сохранить результаты пользователя
self.open_name = askopenfilename() # указать параметры диалога здесь def savefile(self):
self.save_name = asksaveasfilename(initialdir=’C:\\Python31’)
if__name__== “__main__”:
# вывести окно print(‘popup1...’)
mydialog = Demo() # присоединить фрейм к окну Tk() по умолчанию
mydialog.mainloop() # отобразить; вернуться после закрытия окна
print(mydialog.open_name) # имена сохраняются в объекте, когда окно уже print(mydialog.save_name) # будет закрыто
# Раздел программы без графического интерфейса, использующей mydialog
# отобразить окно еще раз print(‘popup2...’)
mydialog = Demo() # повторно создать виджеты
mydialog.mainloop() # повторно отобразить окно
print(mydialog.open_name) # в объекте будут сохранены новые значения print(mydialog.save_name)
# Раздел программы без графического интерфейса,
# где снова используется mydialog print(‘ending...’)
Эта программа дважды конструирует и отображает простое окно с двумя кнопками, как показано на рис. 10.13, нажатие которых вызывает появление диалогов выбора файла. Вывод программы, который производится при закрытии окна графического интерфейса, выглядит примерно, как показано ниже:
C:\...\PP4E\Gui\Tools> mainloopdemo.py
popup1...
C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Gui/Tools/widgets.py
C:/Python31/python.exe
popup2...
C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Gui/Tools/guimixin.py
C:/Python31/Lib/tkinter/__init__.py
ending...
Рис. 10.13. Окно, которое выводится программой командной строки
Обратите внимание, что в этой программе функция mainloop вызывается дважды, для организации двух модальных взаимодействий с пользователем из сценария командной строки, не имеющего графического интерфейса. Нет ничего криминального в том, что функция mainloop вызывается более одного раза, но при этом сценарию приходится заново создавать виджеты перед очередным вызовом, потому что они уничтожаются после предыдущего вызова mainloop (виджеты уничтожаются внутри библиотеки Tk, даже если соответствующие им объекты Python продолжают существование). Напомню, что такая реализация графического интерфейса не соответствует ожиданиям пользователей, в сравнении с традиционными графическими интерфейсами, - создается впечатление, что окна появляются из ниоткуда, - но это самый быстрый способ добавить графический интерфейс без глубокой реорганизации программного кода.
Обратите внимание, что данный пример отличается от случая использования вложенных (рекурсивных) вызовов функции mainloop для реализации модальных диалогов, с которым мы столкнулись в главе 8. При таком подходе вложенные вызовы mainloop возвращают управление, когда вызывается метод quit диалога, но при этом продолжается выполнение объемлющего вызова mainloop, и мы остаемся в сфере программирования, управляемого событиями. Сценарий в примере 10.22, напротив, производит два независимых вызова функции mainloop, дважды вступая и выходя из модели, управляемой событиями.
Наконец, обратите внимание, что такая схема подходит только для ситуаций, когда не требуется выполнять какие-либо операции, не связанные с графическим интерфейсом, пока окно остается открытым, потому что на период выполнения mainloop основной поток управления сценария остается неактивным и блокируется. Вы не сможете, например, применить этот подход для добавления графического интерфейса к утилитам, подобным тем, что используются в модуле guiStreams, представленном выше в этой главе, предназначенном для передачи функций взаимодействия с пользователем из сценариев командной строки в графический интерфейс. Классы GuiInput и GuiOutput в том примере предполагают, что где-то уже был произведен вызов mainloop (в конце концов, они опираются на использование графического интерфейса). Но как только будет вызвана функция mainloop, чтобы вывести окна, вы не сможете вернуть управление обычному программному коду сценария командной строки, чтобы взаимодействовать с пользователем или с графическим интерфейсом, пока этот графический интерфейс не будет закрыт и функция mainloop не вернет управление. Таким образом, эти классы могут использоваться только в контексте программ, полностью опирающихся на графический интерфейс.
Но на самом деле это неестественный способ использования библиотеки tkinter. Сценарий в примере 10.22 действует только потому, что графический интерфейс может взаимодействовать с пользователем совершенно независимо, - сценарий может позволить себе отдать управление функции mainloop из библиотеки tkinter и ждать результатов. Эта схема непригодна, когда требуется выполнять программный код, не имеющий отношения к графическому интерфейсу, в то время, когда окно остается открытым. Из-за этих ограничений в большинстве графических интерфейсов вам придется использовать модель главное-окно-плюс-обработчики-событий - обработчики вызываются в ответ на действия пользователя, пока окно графического интерфейса остается открытым. При таком подходе ваш программный код может действовать, пока окно остается открытым. Например, смотрите представленный ранее в этой главе способ запуска сценариев командной строки архивирования и разархивирования из графического интерфейса, с выводом результатов в графическом интерфейсе, - технически эти сценарии запускаются из обработчиков событий графического интерфейса, а их вывод перенаправляется в виджет.
Реализация графического интерфейса в виде отдельной программы: сокеты (вторая встреча)
Как отмечалось ранее, возможно также реализовать графический интерфейс приложения в виде отдельной программы. Это наиболее тернистый путь, но в некоторых ситуациях он может упростить интеграцию слабо связанных компонентов. Этот способ может, например, помочь решить проблемы, свойственные примеру guiStreams из предыдущего раздела, при условии, что входные и выходные данные будут передаваться графическому интерфейсу через механизмы взаимодействий между процессами (Inter-Process Communication, IPC), а для обнаружения выходных данных будет использован метод after виджетов (или подобный ему). В этом случае работа сценария командной строки не блокируется вызовом функции mainloop.
Графический интерфейс может запускаться сценарием командной строки как отдельная программа, и обмениваться результатами взаимодействий с пользователем с основным сценарием посредством каналов, сокетов, файлов или других механизмов IPC, представленным в главе 5. Преимущество такого подхода состоит в том, что он обеспечивает разделение представления и реализации - в сценарий, который иначе может использоваться как обычная утилита командной строки, достаточно будет добавить всего лишь запуск графического интерфейса и организовать прием ввода пользователя. Кроме того, работа сценария командной строки не будет блокироваться на время работы функции mainloop (функция mainloop будет выполняться только в процессе, реализующем графический интерфейс), а сам графический интерфейс может сохраняться на экране и после того, как пользователь введет все необходимые данные, что позволит уменьшить количество всплывающих окон.
Другой вариант - когда графический интерфейс запускает сценарий командной строки и организует прием данных от него с применением механизмов IPC, подключаемых к потоку стандартного вывода сценария. В еще более сложных решениях графический интерфейс и сценарий командной строки могут организовать двусторонний обмен данными.
Примеры 10.23, 10.24 и 10.25 демонстрируют простые варианты реализации этих подходов: вывод сценария командной строки отправляется в графический интерфейс. В них представлены реализации сценариев командной строки и графического интерфейса, которые взаимодействуют друг с другом через сокеты - механизм сетевых взаимодействий, который коротко рассматривался в главе 5 и подробно будет исследоваться в следующей части книги. При изучении этих файлов особое внимание обратите на то, как организована связь между программами: когда сценарий командной строки выводит что-то в стандартный поток вывода, текст отправляется графическому интерфейсу через сетевое соединение. Кроме того, что он импортирует модуль и вызывает функцию перенаправления вывода в сокет, сценарий командной строки ничего не знает ни о графических интерфейсах, ни о сокетах, а графический интерфейс ничего не знает о сценарии, вывод которого он отображает. Так как эта модель не требует полностью переписывать существующие сценарии, чтобы добавить к ним поддержку графического интерфейса, она является идеальной для сценариев, которые живут и действуют в мире командных оболочек и командной строки.
С точки зрения реализации, нам сначала необходимо создать механизм IPC, который свяжет сценарий с графическим интерфейсом. Пример 10.23 содержит реализацию подключения к сокетам на стороне клиента, используемую сценарием командной строки. В данный момент представлена только частичная реализация модуля (обратите внимание на оператор многоточия ... в последних нескольких функциях - своего рода фразу «Подлежит реализации» на языке Python; этот оператор является эквивалентом инструкции pass в данном контексте). Так как подробно сокеты будут рассматриваться только в главе 12, мы отложим реализацию других режимов перенаправления до этого момента; там также будет представлена остальная часть реализации этого модуля. Версия модуля, представленная здесь, реализует перенаправление в сокет только потока стандартного вывода и отлично подходит для графического интерфейса, которому требуется перехватить вывод сценария командной строки.
Пример 10.23. PP4E\Gui\Tools\socket_stream_redirect0.py
[частичная реализация] Инструменты подключения потоков ввода-вывода сценариев командной строки к сокетам, которые могут использоваться графическими интерфейсами (и другими сценариями) для взаимодействий с этими сценариями; более полное обсуждение и реализацию вы найдете в главе 12 и в каталоге PP4E\Sockets\ Internet
import sys
from socket import * port = 50008 host = ‘localhost’
def redirectOut(port=port, host=host):
подключает стандартный поток вывода вызывающего сценария к сокету, для передачи данных графическому интерфейсу, прослушивающему сетевое соединение;
вызывающий сценарий должен запускаться после того как будет запущен сценарий или графический интерфейс, прослушивающий сетевое соединение, иначе connect потерпит неудачу до того, как будет выполнена функция accept
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((host, port))# вызывающий сценарий играет роль клиента file = sock.makefile(‘w’) # интерфейс файлов: текстовый режим, буферизация sys.stdout = file # заставить функцию print выводить текст
# с помощью sock.send
def redirectIn(port=port, host=host): ... # см. главу 12
def redirectBothAsClient(port=port, host=host): ... # см. главу 12 def redirectBothAsServer(port=port, host=host): ... # см. главу 12
Далее, пример 10.24 использует пример 10.23 для перенаправления потока стандартного вывода в сетевое соединение, которое может прослушиваться серверной программой, реализующей графический интерфейс. Для этого требуется добавить в начало сценария всего две строки программного кода, которые выполняются в зависимости от наличия аргумента командной строки (если сценарий запускается без аргументов, он выполняется в обычном режиме):
Пример 10.24. PP4E\Gui\Tools\socket-nongui.py
# сценарий командной строки: подключает поток вывода к сокету
# и действует как обычно
import time, sys
if len(sys.argv) > 1: # подключаться к GUI только при явном требовании
from socket_stream_redirect0 import * # подключить sys.stdout к сокету redirectOut() # GUI должен запускаться первым
# программный код, не связанный с графическим интерфейсом
while True: # выводит данные в stdout:
print(time.asctime()) # передать процессу GUI через сокет
sys.stdout.flush() # требуется для передачи: буферизация!
time.sleep(2.0) # небуферизованный режим отсутствует
# ключ -u не решает проблему
И наконец, в примере 10.25 приводится реализация графического интерфейса, участвующего в обмене данными. Этот сценарий создает графический интерфейс для отображения текста, выводимого программой командной строки, но он ничего не знает о логике работы другой программы. Для отображения получаемого текста графический интерфейс использует объект перенаправления потока вывода в виджет, с которым мы встречались выше в этой главе (пример 10.12), - поскольку данная программа вызывает функцию mainloop, этот объект «просто работает».
Кроме того, для проверки наличия входных данных в сокете здесь используется цикл обработки событий от таймера, вместо того чтобы ждать завершения программы командной строки. Поскольку сокет настраивается на работу в неблокирующем режиме, операция ввода не ждет, пока появятся данные, и не блокирует графический интерфейс.
Пример 10.25. PP4E\Gui\Tools\socket-gui.py
# сервер GUI: читает и отображает текст, полученный
# от сценария командной строки
import sys, os
from socket import * # включая socket.error
from tkinter import Tk
from PP4E.launchmodes import PortableLauncher
from PP4E.Gui.Tools.guiStreams import GuiOutput myport = 50008
sockobj = socket(AF_INET, SOCK_STREAM) # GUI - сервер, сценарий - клиент sockobj.bind((‘’, myport)) # сервер настраивается перед
sockobj.listen(5) # запуском клиента
print(‘starting’)
PortableLauncher(‘nongui’, ‘socket-nongui.py -gui’)() # запустить сценарий print('accepting’)
conn, addr = sockobj.accept() # ждать подключения клиента
conn.setblocking(False) # неблокирующий сокет (False=0)
print('accepted')
def checkdata(): try:
message = conn.recv(1024) # попытка ввода не блокируется #output.write(message + ‘\n’) # можно также сделать sys.stdout=output
print(message, file=output) # если текст получен - вывести в окне except error: # возбудит socket.error, если нет данных
print(‘no data’) # вывести в sys.stdout
root.after(1000, checkdata) # проверять раз в секунду
root = Tk()
output = GuiOutput(root) # текст из сокета отображается здесь
checkdata()
root.mainloop()
Запустите сценарий из примера 10.25, чтобы протестировать весь комплекс. Когда запустятся оба процесса, графический интерфейс и сценарий командной строки, графический интерфейс примерно каждые две секунды будет получать через сокет новые сообщения и отображать их в окне, как показано на рис. 10.14. Цикл обработки событий от таймера в графическом интерфейсе проверяет поступление новых данных примерно раз в секунду, но сценарий командной строки отправляет сообщения только раз в две секунды, из-за задержки, организованной с помощью функции time.sleep. Ниже приводится пример вывода в окно консоли - сообщения «no data» в консоли и новые строки в графическом интерфейсе появляются каждую секунду:
C:\...\PP4E\Gui\Tools> socket-gui.py
starting
nongui
accepting
accepted
no data
no data
no data
no data
...часть строк опущена...
Рис. 10.14. Сообщения от сценария командной строки, выводимые графическим интерфейсом (сокеты)
Обратите внимание на рис. 10.14, что мы отображаем строки типа bytes, - несмотря на то, что сценарий командной строки выводит текст, сценарий графического интерфейса получает строки байтов, потому что читает их, используя низкоуровневый интерфейс сокетов, а сокеты в Python 3.X обрабатывают данные в виде строк двоичных байтов.
Запустите этот сценарий у себя на компьютере, чтобы посмотреть, как он действует. В общих чертах, сценарий графического интерфейса запускает сценарий командной строки и отображает окно, в которое выводит текст, печатаемый сценарием командной строки (дата и время). Сценарий командной строки может по-прежнему выполнять линейный процедурный программный код и воспроизводить данные, потому что только процесс графического интерфейса выполняет цикл событий mainloop.
Кроме того, в отличие от ранее исследованных нами приемов перенаправления, когда мы просто подключали потоки ввода-вывода сценария к объектам графического интерфейса, данный подход деления на два процесса предотвращает блокирование графического интерфейса в ожидании, пока сценарий выведет какие-либо данные. Процесс графического интерфейса остается полностью независимым и активным и просто извлекает новые результаты по мере их поступления (подробнее об этом рассказывается в следующем разделе). Данная модель по духу напоминает предыдущие примеры с потоками выполнения и очередями, только здесь главными действующими лицами являются отдельные программы, связанные с помощью сокета, а не вызовы функций в контексте единого процесса.
Мы не будем подробно рассматривать сокеты в этой главе, чтобы объяснить их применение в этом программном коде, тем не менее следует подчеркнуть несколько наиболее важных моментов:
• Вероятно, этот пример следовало бы дополнить возможностью определения признака конца файла, отправляемого дочерним сценарием, и завершать цикл обработки событий от таймера.
• Сценарий командной строки мог бы сам запускать графический интерфейс, но в мире сокетов серверный конец (графический интерфейс) должен быть настроен на прием входящих соединений раньше, чем клиент (сценарий командной строки) попытается соединиться с ним. Так или иначе, графический интерфейс должен быть запущен еще до того, как сценарий командной строки попытается установить соединение, иначе соединение не будет установлено и сценарий потерпит неудачу.
• Из-за поддержки буферизации в текстовом режиме, свойственной объектам socket.makefile, используемым здесь для перенаправления потока вывода, клиентская программа обязательно должна выталкивать выходной буфер с помощью sys.stdout.flush, чтобы отправить данные графическому интерфейсу, - без вызова этого метода графический интерфейс ничего не будет получать и отображать. Как будет показано в главе 12, этот прием не обязательно применять при использовании каналов, но обязательно - при работе с обертками сокетов, как в данном примере. В Python 3.X эти обертки не поддерживают небуферизованные режимы и не имеют ключа командной строки, такого как -u, для данного контекста (дополнительные сведения о ключе -u и о каналах приводятся в следующем разделе).
Дополнительную информацию к этому примеру и по данной теме вы найдете в главе 12. Модель клиент-сервер на основе сокетов неплохо подходит для соединения графического интерфейса со сценариями командной строки, но существуют и другие альтернативы, которые мы рассмотрим в следующем разделе, прежде чем двинуться дальше.
Реализация графического интерфейса в виде отдельной программы: каналы
Объединение двух программ в предыдущем разделе напоминает программу с графическим интерфейсом, которая читает вывод команды, запущенной с помощью os.popen (или с помощью интерфейса subprocess. Popen, который опирается на эту функцию). Как будет показано далее, сокеты также поддерживают возможность обмена данными с независимыми серверами и могут использоваться для соединения программ, выполняющихся на разных компьютерах в сети, однако эту идею мы будем рассматривать в главе 12.
Пожалуй, еще более тонким и важным для нашего исследования графических интерфейсов является тот факт, что без цикла обработки со-
бытий от таймера на основе метода after и неблокирующей операции чтения данных, подобной той, что использовалась в предыдущем разделе, графический интерфейс может блокироваться в ожидании поступления данных от программы командной строки и оказаться неспособным обрабатывать более одного потока данных.
Предлагаю взглянуть на функцию redirectedGuiShellCmd в примере 10.12, перенаправляющую вывод команды оболочки, запускаемой с помощью os.popen, в окно графического интерфейса. Мы могли бы использовать простейший программный код, как в примере 10.26, чтобы перехватить вывод порожденной программы на языке Python и отобразить его в окне отдельной программы с графическим интерфейсом. Решение получилось таким компактным благодаря тому, что оно опирается на цикл чтения/записи и на класс GuiOutput из примера 10.12 для управления графическим интерфейсом и чтения данных из канала. Это решение, по сути, повторяет один из вариантов реализации самотестирования в том примере, но здесь мы читаем вывод программы на языке Python.
Пример 10.26. PP4E\Gui\Tools\pipe-gui1.py
# графический интерфейс: перенаправляет стандартный вывод порождаемой
# программы в окно GUI
from PP4E.Gui.Tools.guiStreams import redirectedGuiShellCmd # исп-ет GuiOutput
redirectedGuiShellCmd(‘python -u pipe-nongui.py’) # -u: без
# буферизации
Обратите внимание на ключ -u командной строки интерпретатора Python, используемый здесь: он принудительно отключает буферизацию потока стандартного вывода запускаемой программы, поэтому мы получаем печатаемый текст немедленно и нам не приходится ждать завершения дочерней программы.
Мы говорили об этой возможности в главе 5, когда обсуждали каналы и состояния взаимоблокировки. Напомню, что функция print выводит текст в sys.stdout, который обычно предусматривает буферизацию при подключении к каналу таким способом. Если бы мы здесь не использовали ключ -u и порожденная программа не вызывала бы метод sys. stdout.flush, мы ничего не увидели бы в графическом интерфейсе, пока дочерняя программа не завершилась бы или пока не переполнился буфер. Если дочерняя программа выполняет бесконечный цикл, нам может потребоваться ждать очень долго, пока вывод появится в канале и, соответственно, в графическом интерфейсе.
Такой подход значительно упрощает реализацию сценария командной строки, как показано в примере 10.27: он просто выводит текст в стандартный поток вывода и ему не требуется выполнять подключение к сокету. Сравните его с эквивалентной реализацией на основе сокетов, представленной в примере 10.24, - цикл тот же самый, но здесь не требуется предварительно выполнять подключение к сокету (родительская программа читает обычный поток вывода) и нет необходимости вручную выталкивать выходной буфер (ключ -u, указанный при запуске дочерней программы, отключает буферизацию).
Пример 10.27. PP4E\Gui\Tools\pipe-nongui.py
# сценарий командной строки: действует как обычно, не требует выполнения
# дополнительных операций import time
while True: # реализация сценария командной строки
print(time.asctime()) # отправить процессу GUI time.sleep(2.0) # выталкивать буфер здесь не требуется
Запустите сценарий графического интерфейса из примера 10.26: он автоматически запустит сценарий командной строки, подключится к его потоку стандартного вывода и отобразит окно, как показано на рис. 10.15. Своим внешним видом оно напоминает окно сценария, реализованного на основе сокетов, изображенное на рис. 10.14, но в данном случае будут выводиться строки str, которые мы получаем при чтении каналов, а не строки байтов, как при чтении из сокетов.
Рис. 10.15. Сообщения от сценария командной строки, выводимые графическим интерфейсом (каналы)
Сценарии действуют, но реализация графического интерфейса выглядит несколько странно - в ней отсутствует явный вызов функции main-loop, и мы получаем дополнительное пустое окно верхнего уровня по умолчанию. Фактически этот графический интерфейс действует лишь благодаря вызову метода update внутри функции перенаправления, который на мгновение передает управление циклу событий Tk, чтобы обработать ожидающие события. Более удачное решение представлено в примере 10.28. Этот сценарий создает графический интерфейс и запускает цикл событий вручную до того, как будет запущена команда оболочки, - при ее запуске воспроизводится то же самое окно (рис. 10.15).
Пример 10.28. PP4E\Gui\Tools\pipe-gui2.py
# графический интерфейс: действует так же, как pipes-gui1, но явно создает
# главное окно и запускает цикл событий
from tkinter import *
from PP4E.Gui.Tools.guiStreams import redirectedGuiShellCmd def launch():
redirectedGuiShellCmd(‘python -u pipe-nongui.py’) window = Tk()
Button(window, text=’GO!’, command=launch).pack() window.mainloop()
Ключ -u, отключающий буферизацию, здесь также имеет большое значение - без него мы не увидели бы текст в окне вывода. Графический интерфейс оказался бы заблокирован в первой операции чтения из канала, потому что текст, выводимый дочерним сценарием, оставался бы в буфере потока стандартного вывода.
С другой стороны, наличие ключа -u, запрещающего буферизацию, не предотвращает блокирование графического интерфейса из предыдущего раздела, использующего сокеты, потому что в том примере после запуска дочерней программы поток вывода переключается на другой объект. Дополнительные сведения об этом приводятся в главе 12. Запомните также, что аргумент функции os.popen (и subprocess.Popen), определяющий параметры буферизации, управляет буферизацией только на стороне вызывающего процесса, но не в порожденной программе, тогда как ключ -u передается при запуске последней.
Призрак блокирования операций чтения
Однако при любом подходе графические интерфейсы в примерах 10.26 и 10.28 оказываются заблокированными на две секунды каждый раз, когда пытаются прочитать данные из канала с помощью os.popen. На практике интерфейсы становятся очень неповоротливыми - команды переместить окно, изменить его размер, перерисовать, поднять над другими окнами и так далее ожидают до двух секунд, пока сценарий командной строки не отправит данные графическому интерфейсу и тем самым не обеспечит возврат из функции чтения канала. Еще хуже то, что если щелкнуть на кнопке GO! дважды во второй версии графического интерфейса, только одно окно будет обновляться каждые две секунды, потому что графический интерфейс «застрянет» в обработчике события нажатия кнопки - он не сможет выйти из цикла чтения, пока дочерний сценарий командной строки не завершится. Завершение работы программы также выполняется не очень изящно (в окне консоли появится множество сообщений об ошибках).
Из-за этих ограничений - чтобы избежать заблокированных состояний - независимо запускаемый графический интерфейс не должен читать данные непосредственно, если могут возникать задержки в отображении. Например, в сценарии из предыдущего раздела (пример 10.25), использующем сокеты, цикл обработки событий от таймера after позволяет графическому интерфейсу проверять наличие данных вместо того, чтобы ждать их, и отображать по мере появления. Поскольку графический интерфейс не ждет, пока данные появятся, он остается активным между операциями вывода.
Конечно, истинная причина этих проблем заключается в том, что цикл чтения/записи в используемой здесь функции из модуля guiStreams слишком упрощен - ошибочное размещение операции чтения в графическом интерфейсе провоцирует блокировку. Существуют различные решения, позволяющие избежать этого.
Обновление графических интерфейсов
внутри потоков выполнения... и другие «решения»
Чтобы исправить эту проблему, можно было бы попробовать вызывать функцию перенаправления в дочернем потоке выполнения, например, изменив функцию launch в примере 10.28, как показано ниже (этот фрагмент взят из сценария pipe-gui2-thread.py, входящего в состав пакета с примерами для книги):
def launch():
import _thread
_thread.start_new_thread(redirectedGuiShellCmd,
(‘python -u pipe-nongui.py’,))
Но тогда графический интерфейс будет обновляться из дочернего потока выполнения, что, как мы уже знаем, заканчивается плохо. Параллельные попытки обновления графического интерфейса могут нанести ему вред.
Фактически, после внесения предложенных изменений на моем ноутбуке с Windows 7, графический интерфейс зависает сразу же после первого щелчка на кнопке GO!, становясь совершенно неотзывчивым, и его приходится закрывать принудительно. Это происходит до (или, может быть, во время) создания нового окна с текстовым виджетом. Когда этот пример запускался в Windows XP, во время работы над предыдущим изданием книги, он также иногда подвисал при первом щелчке на кнопке GO!, а несколько щелчков на кнопке гарантированно подвешивали его и в этой системе - процесс приходилось останавливать принудительно. Прямое обновление графического интерфейса из дочерних потоков выполнения не является приемлемым решением.
Как вариант, можно было бы попробовать использовать функцию Python select. select (описывается в главе 12) для реализации проверки наличия данных в канале - к сожалению, в настоящее время функция select в Windows работает только с сокетами (в Unix она также работает с каналами и с дескрипторами файлов).
В некоторых контекстах графический интерфейс, запускаемый отдельно, мог бы использовать сигналы для информирования программы командной строки о наступлении момента обмена данными, и наоборот (с помощью модуля signal и функции os.kill, представленных в главе 5). Недостаток такого решения состоит в том, что он требует добавлять обработку сигналов в реализацию программы командной строки.
Альтернативой сокетам в примерах с 10.23 по 10.25 могли бы в определенных ситуациях служить именованные каналы (файлы fifo, представленные в главе 5), но сокеты работают в стандартной версии Python для Windows, а именованные каналы - нет (функция os.mkfifo недоступна в версии Python 3.1 для Windows, хотя она имеется в версии Cygwin Python). Но даже там, где они работают, нам все еще необходимо использовать цикл обработки событий от таймера на основе метода after, чтобы избежать блокирования графического интерфейса.
Также мы могли бы использовать функцию createfilehandler из библиотеки tkinter, чтобы зарегистрировать обработчик, который будет вызываться при появлении данных в канале:
def callback(file, mask):
...чтение данных из файла...
import _tkinter, tkinter
_tkinter.createfilehandler(file, tkinter.READABLE, callback)
Операция регистрации обработчика доступна в виде функции в модуле tkinter и в виде метода экземпляра класса Tk. К сожалению, как уже отмечалось в конце главы 9, эта функция недоступна в Windows и может служить альтернативой только в Unix.
Предотвращение блокирования операций чтения с помощью потоков выполнения
Намного более универсальное решение проблемы блокирования операций чтения заключается в том, чтобы графический интерфейс порождал дочерний поток, который будет читать данные из сокета или канала и помещать их в очередь. Фактически прием, основанный на потоках выполнения, с которым мы встречались выше в этой главе, можно было бы напрямую использовать для решения данной проблемы. При таком подходе, пока поток выполнения ждет поступления данных, графический интерфейс не блокируется, а поток выполнения не пытается обновлять графический интерфейс. Кроме того, одновременно могут выполняться несколько потоков и производить продолжительные операции.
Пример 10.29 демонстрирует реализацию этого решения. Основная хитрость состоит в том, чтобы отделить операции ввода и вывода в оригинальной функции redirectedGuiShellCmd из модуля guiStreams, представленного в примере 10.12. В той версии операция ввода запускается в параллельном потоке выполнения и не блокирует графический интерфейс. Главный поток графического интерфейса использует цикл обработки событий от таймера after как обычно - чтобы проверять наличие данных в общей очереди, добавляемых потоком чтения. Так как главный поток сам не занимается чтением вывода дочерней программы, он не блокируется в ожидании поступления новых данных.
Пример 10.29. PP4E\Gui\Tools\pipe_gui3.py
читает данные из канала в отдельном потоке выполнения и помещает их в очередь, которая проверяется в цикле обработки событий от таймера; позволяет сценарию отображать вывод программы, не вызывая блокирование графического интерфейса между операциями вывода; со стороны дочерних программ не требуется выполнять подключение или выталкивать буферы, но данное решение сложнее, чем подход на основе сокетов
import _thread as thread, queue, os from tkinter import Tk
from PP4E.Gui.Tools.guiStreams import GuiOutput stdoutQueue = queue.Queue() # бесконечной длины
def producer(input): while True:
line = input.readline() # блокирование не страшно: дочерний поток
stdoutQueue.put(line) # пустая строка - конец файла
if not line: break
def consumer(output, root, term=’
line = stdoutQueue.get(block=False) # главный поток: проверять очередь except queue.Empty: # 4 раза в сек, это нормально,
pass # если очередь пуста
else:
if not line: # остановить цикл по достижении конца файла
output.write(term) # иначе отобразить следующую строку return
output.write(line)
root.after(250, lambda: consumer(output, root, term))
def redirectedGuiShellCmd(command, root):
input = os.popen(command, ‘r’) # запустить программу командной строки output = GuiOutput(root)
thread.start_new_thread(producer, (input,)) # запустить поток чтения consumer(output, root)
if __name__ == ‘__main__’: win = Tk()
redirectedGuiShellCmd(‘python -u pipe-nongui.py’, win) win.mainloop()
Здесь мы используем очередь, чтобы избежать необходимости обновления графического интерфейса в дочерних потоках. Обратите внимание, что в предыдущем разделе, в примере с сокетами, очереди и потоки выполнения не требовались лишь потому, что у нас была возможность проверить сокет на наличие данных без блокирования - цикла обработки событий от таймера after было вполне достаточно. Однако при организации обмена данными через канал потоки выполнения являются самым простым способом избежать блокирования графического интерфейса.
Если запустить этот сценарий, программный код самотестирования создаст окно с виджетом ScrolledText, в котором будут отображаться текущие дата и время, отправляемые сценарием pipes-nongui.py из примера 10.27. Фактически это окно идентично тем, что создают предыдущие версии (рис. 10.15). Каждые две секунды в окне будет появляться новая строка, потому что именно с такой частотой сценарий pipes-nongui выводит сообщения в stdout.
Обратите внимание, что поток-производитель загружает данные по одной строке с помощью метода readline(). Мы не можем использовать функции чтения, которые пытаются загрузить все данные из потока ввода целиком (такие как read(), readlines()), потому что они не возвращают управление, пока программа не завершится и не отправит признак конца файла. Для чтения фрагмента данных можно было бы использовать метод read(N), но в этом случае мы исходим из предположения, что в поток вывода передаются текстовые данные. Обратите также внимание, что здесь снова используется ключ -u, запрещающий буферизацию потоков ввода-вывода, чтобы обеспечить получение данных по мере их вывода. Без этого выводимые данные вообще не попали бы в графический интерфейс, потому что сохранялись бы в выходном буфере дочерней программы (попробуйте сами).
Сокеты и каналы: сходства и различия
Давайте посмотрим, что у нас получилось. Этот сценарий по своему духу напоминает сценарий в примере 10.28. Тем не менее, благодаря реструктуризации программного кода, сценарий в примере 10.29 имеет значительное преимущество: так как на этот раз операция чтения данных выполняется в дочернем потоке, графический интерфейс остается отзывчивым. Операции перемещения окна, изменения его размеров и так далее, выполняются немедленно, потому что графический интерфейс не блокируется в ожидании вывода очередной порции данных программой командной строки. Комбинация канала, потока выполнения и очереди в этом примере творит чудеса - графическому интерфейсу не приходится ждать дочернюю программу, а дочернему потоку не требуется обновлять графический интерфейс.
Несмотря на сложность реализации и необходимость использовать многопоточную модель выполнения, отсутствие блокировок в примере 10.29 делают его функцию redirectedGuiShellCmd намного более полезной, чем в оригинальной версии. Тем не менее, в сравнении с реализацией на основе сокетов из предыдущего раздела, данное решение выглядит как смесь разных приемов:
• Поскольку эта реализация графического интерфейса читает данные из стандартного потока вывода дочерней программы, отпадает необходимость вносить в нее какие-либо изменения. В отличие от примера из предыдущего раздела, основанного на применении сокетов, программе командной строки не требуется знать о существовании графического интерфейса, отображающего ее результаты, - ей не требуется выполнять подключение к сокету и выталкивать свои выходные буферы, как в предыдущем решении с сокетами.
• Несмотря на отсутствие необходимости вносить изменения в программу, вывод которой отображается, сложность реализации графического интерфейса начинает приближаться к сложности реализации варианта на основе сокетов, особенно если отбросить шаблонный программный код, необходимый в любой программе, использующей сокеты.
• Данное решение не поддерживает возможность выполнения графического интерфейса и программы командной строки независимо друг от друга или на разных компьютерах. Как мы увидим в главе 12, сокеты позволяют передавать данные между программами, работающими на одном и том же компьютере, или по сети.
• Сокеты могут применяться не только для отображения стандартного потока вывода программы. Если от графического интерфейса требуется нечто большее, чем отображение вывода другой программы, сокеты могут обеспечить более универсальное решение. Кроме того, как мы увидим далее, сокеты по своей природе являются двунаправленными потоками данных, поэтому они позволяют передавать данные между программами в обоих направлениях более произвольными способами.
Другие примеры использования многопоточных графических интерфейсов и каналов
Несмотря на некоторые незначительные недостатки, реализация графических интерфейсов на основе потоков/очередей/каналов имеет весьма широкую область применения. Для иллюстрации приведем еще один короткий пример использования. Ниже демонстрируется запуск простого сценария в окне консоли, который каждые две секунды выводит все более и более длинную строку:
C:\...\PP4E\Gui\Tools> type spams.py import time
for i in range(1, 10, 2):
time.sleep(2) # выводит текст в стандартный поток вывода
print(‘spam’ * i) # GUI ничего не знает об этом, ведь так?
C:\...\PP4E\Gui\Tools> python spams.py
spam
spamspamspam
spamspamspamspamspam
spamspamspamspamspamspamspam
spamspamspamspamspamspamspamspamspam
Попробуем завернуть этот сценарий в графический интерфейс, введя программный код в интерактивной оболочке, для разнообразия. Следующий фрагмент импортирует новую версию функции перенаправления потока вывода в графический интерфейс как библиотечный компонент и с ее помощью создает окно, в котором отображаются пять строк, выводимые сценарием каждые две секунды - так же, как в окне консоли, - за которыми следует строка
C:\...\PP4E\Gui\Tools> python
>>> from tkinter import Tk
>>> from pipe_gui3 import redirectedGuiShellCmd
>>> root = Tk()
>>> redirectedGuiShellCmd('python -u spams.py', root)
Рис. 10.16. Графический интерфейс, отображающий полученный по каналу вывод другой программы
Когда дочерняя программа завершится, поток-производитель в примере 10.29 определит признак конца файла и поместит в очередь заключительную пустую строку. В ответ на это цикл обработки событий от таймера выведет строку
Наконец, такой программный код, не использующий сокеты, не требующий вносить изменения в оригинальную программу и не блокирующий графический интерфейс, можно было бы использовать для отображения вывода программ командной строки в графическом интерфейсе. Конечно, во многих случаях может оказаться слишком сложным добавлять графический интерфейс таким способом, и для вас может оказаться проще превратить свой сценарий в традиционную программу с графическим интерфейсом, у которой есть главное окно и цикл событий. Кроме того, графические интерфейсы, которые мы реализовали в этом разделе, мы обязали просто отображать вывод другой программы, тогда как на практике от графического интерфейса может требоваться нечто большее. Однако для многих программ отделение представления от реализации, которое обеспечивает модель графического интерфейса, порождающего дочернюю программу командной строки, имеет свои преимущества - обе части приложения понять будет намного проще, если они не будут смешиваться.
Сокеты мы будем подробно рассматривать в следующей части книги, поэтому данное обсуждение следует рассматривать, как предварительное знакомство. Как мы увидим далее, все станет еще более интересным, как только мы начнем комбинировать графические интерфейсы, потоки выполнения и сокеты.
В следующей главе мы закончим обсуждение тем, касающихся исключительно графического интерфейса, где рассмотрим применение уже знакомых нам виджетов и приемов для реализации более практичных программ. Но перед этим в следующем разделе мы познакомимся с некоторыми крупными примерами графических интерфейсов, рассмотрев сценарии, которые запускают их автоматически и могут служить образцами, демонстрирующими возможности языка Python и библиотеки tkinter.
Запускающие программы PyDemos и PyGadgets
В завершение главы исследуем реализацию двух графических интерфейсов, с помощью которых запускаются основные примеры для этой книги. Следующие два графических интерфейса, PyDemos и PyGadgets, служат для запуска других программ с графическим интерфейсом. На самом деле мы подошли к концу истории о программах, запускающих демонстрационные примеры, - обе программы, представленные здесь, взаимодействуют с модулями, с которыми мы встречались ранее, во второй части книги:
launchmodes.py
Запускает независимые программы Python переносимым образом. Launcher.py
Отыскивает программы и в конечном итоге запускает обе программы, PyDemos и PyGadgets, при использовании самонастраивающихся сценариев верхнего уровня.
LaunchBrowser.py
Запускает веб-броузеры переносимым способом, открывая в них локальные или удаленные страницы.
Реализацию этих модулей вы найдете во второй части книги (особенно в главах 5 и 6). Представленные здесь программы добавляют компоненты графического интерфейса в систему запуска программ - они создают простые в использовании кнопки, нажатием которых можно запустить большинство крупных примеров, содержащихся в книге.
Кроме того, оба эти сценария предполагают, что при запуске текущим рабочим каталогом будет каталог, где они находятся (в них жестко определены пути к другим программам относительно этого каталога). Щелкните на их именах в проводнике по файловой системе или запустите из командной строки, выполнив команду cd для перехода в корневой каталог примеров PP4E. В этих сценариях можно было бы реализовать поддержку запуска и из других каталогов, путем использования значений переменных окружения для получения путей к сценариям, но в действительности они предназначены только для запуска из корневого каталога PP4E.
Поскольку эти сценарии запуска демонстрационных примеров являются достаточно длинными программами, в интересах экономии места на страницах книги будут приводиться только наиболее интересные их фрагменты. Полный программный код вы найдете в пакете с примерами.
Панель запуска PyDemos
Сценарий PyDemos создает панель с кнопками, которые запускают программы в демонстрационном режиме - не для повседневного применения. Я использую PyDemos, когда мне необходимо показать программы Python, - гораздо проще нажимать на кнопки, чем набирать командные строки или искать сценарии с помощью проводника по файловой системе.
Вы можете использовать PyDemos (и PyGadgets) для запуска и опробования примеров, представленных в этой книге, - все кнопки в этом графическом интерфейсе представляют примеры, с которыми мы познакомимся в последующих главах. Однако если вы соберетесь использовать сценарии Launch_PyDemos и Launch_PyGadgets_bar, находящиеся в корневом каталоге с примерами, не забудьте включить в переменную окружения PYTHONPATH путь к каталогу PP4E - они не предусматривают автоматическую настройку вашей системы или путей поиска модулей.
Чтобы пользоваться этой панелью запуска было еще легче, перетащите ее на рабочий стол Windows, создав ярлык, на котором можно щелкнуть мышью (нечто подобное можно проделать и на других системах). Так как в этом сценарии жестко определены команды для запуска программ, находящихся в других подкаталогах в дереве примеров, он также полезен как предметный указатель к главным примерам из книги. На рис. 10.17 показано, как выглядит интерфейс сценария PyDemos при выполнении в Windows, наряду с несколькими демонстрационными программами, которые он запускает; PyDemos - это вертикальная панель с кнопками. В Linux он выглядит несколько иначе, но действует так же.
Рис. 10.17. PyDemos c несколькими демонстрационными программами
Исходный программный код, с помощью которого создается такая картина, приводится в примере 10.30 (его начало может несколько отличаться от того, что изображено на рис. 10.17, из-за мелких изменений, которые разработчики так любят вносить в последний момент). Сценарий PyDemos не содержит ничего особенного с точки зрения программирования графических интерфейсов, поэтому большая его часть не вошла в листинг - полную реализацию вы найдете в пакете с примерами.
В двух словах, функция demoButton в нем просто прикрепляет к главному окну новую кнопку, готовую при нажатии запустить программу на языке Python. Для запуска программ сценарий PyDemos вызывает экземпляр объекта launchmodes.PortableLauncher, с которым мы познакомились в конце главы 5, - поскольку здесь он выступает в роли обработчика tkinter, для запуска программы используется операция вызова функции.
Как показано на рис. 10.17, сценарий PyDemos создает также два всплывающих окна, когда нажимаются кнопки в нижней части главного окна, - окно Info содержит краткое описание последней запущенной демонстрационной программы, а окно Links содержит переключатели, нажатие которых открывает связанные с книгой сайты в локальном веб-броузере:
• Всплывающее окно Info отображает простую строку сообщения и раз в секунду изменяет ее шрифт, чтобы привлечь к себе внимание. Поскольку это может раздражать, всплывающее окно сначала появляется в свернутом виде (щелкните на кнопке Info, чтобы увидеть его или спрятать).
• Переключатели всплывающего окна Links своим поведением напоминают гиперссылки на веб-странице, но этот графический интерфейс на является броузером: при щелчке на них, с помощью сценария LaunchBrowser, упоминавшегося во второй части книги, отыскивается и запускается веб-броузер, подключающийся к соответствующему сайту при наличии соединения с Интернетом. Этот модуль в свою очередь использует современный модуль webbrowser из стандартной библиотеки Python.
• Чтобы ко всем окнам этого сценария привязать ярлык с синими буквами «PY» вместо стандартных красных букв «Tk», используется модуль windows, написанный нами ранее в этой главе.
В графическом интерфейсе сценария PyDemos также присутствуют кнопки code, расположенные правее кнопок с именами демонстрационных программ. Щелчок на этих кнопках открывает файлы с исходными текстами соответствующих примеров. Файлы открываются в текстовом редакторе PyEdit, с которым мы встретимся в главе 11. На рис. 10.18 изображены некоторые из окон с исходными текстами с несколько измененными размерами.
Для примеров, демонстрирующих работу с Интернетом, которые запускаются последними двумя кнопками на панели, выполняется попытка запустить локальный веб-сервер, обеспечивающий работу демонстрационных программ, не показанных здесь (мы встретимся с сервером в главе 15). В этом издании веб-серверы запускаются, только когда впервые выполняется щелчок на кнопке того или иного примера, демонстрирующего работу с Интернетом (а не при запуске PyDemos). При запуске веб-сервера в Windows открывается окно консоли, в которое выводятся сообщения о состоянии сервера.
PyDemos работает в Windows, Mac и в Linux в основном благодаря присущей переносимости Python и tkinter. Дополнительные подробности можно найти в исходных текстах, частично представленных в примере 10.30.
Рис. 10.18. Сценарий PyDemos с окнами «code» для отображения исходных текстов
Пример 10.30. PP4E\PyDemos.pyw (external)
##############################################################################
PyDemos.pyw
Программирование на Python, 2, 3 и 4 издания (PP4E), 2001--2006--2010
Версия 2.1 (4E), апрель, 2010: добавлена возможность выполнения под управлением Python 3.X и запуск локальных веб-серверов при первой попытке запустить пример, демонстрирующий работу с Интернетом.
Версия 2.0 (3E), март, 2006: добавлены кнопки просмотра исходных текстов примеров; добавлены новые демонстрационные программы (PyPhoto, PyMailGUI); предусмотрен запуск локальных веб-серверов для демонстрационных примеров, использующих веб-броузер; добавлены ярлыки окон; и, наверное, еще что-то, о чем я забыл.
Запускает основные примеры графических интерфейсов Python+Tk из книги независимым от платформы способом. Этот файл может также служить предметным указателем к основным примерам программ, хотя многие примеры в книге не имеют графического интерфейса и потому здесь не перечислены. Смотрите также:
- PyGadgets.py, более простой сценарий запуска программ в недемонстрационном режиме, который можно использовать для повседневной работы
- PyGadgets_bar.pyw, создает панель с кнопками для запуска всех программ PyGadgets по отдельности, а не всех сразу
- Launcher.py позволяет запускать программы без настройки окружения -отыскивает Python, устанавливает PYTHONPATH и так далее.
- Launch_*.pyw, запускает PyDemos и PyGadgets с помощью Launcher.py -попробуйте запустить их для беглого знакомства
- LaunchBrowser.pyw, открывает веб-страницы примеров в веб-броузере, обнаруживаемом автоматически
- README-PP4E.txt, общая информация о примерах
ВНИМАНИЕ: эта программа пытается автоматически запускать локальный веб-сервер и веб-броузер для демонстрационных примеров работы с Интернетом, но не завершает работу сервера.
##############################################################################
...часть программного кода опущена: смотрите файлы в дереве примеров...
##############################################################################
# начало создания главных окон графического интерфейса ##############################################################################
from PP4E.Gui.Tools.windows import MainWindow # Tk с ярлыком, заголовком,
# кнопкой закрытия
from PP4E.Gui.Tools.windows import PopupWindow # То же, но Toplevel,
# отличается действием
# кнопки закрытия
Root = MainWindow(‘PP4E Demos 2.1’)
# создать окно сообщений
Stat = PopupWindow(‘PP4E demo info’)
Stat.protocol(‘WM_DELETE_WINDOW’, lambda:0) # игнорировать событие
Info = Label(Stat, text = ‘Select demo’,
font=(‘courier’, 20, ‘italic’), padx=12, pady=12, bg=’lightblue’) Info.pack(expand=YES, fill=BOTH)
##############################################################################
# добавить кнопки запуска с объектами обработчиков ##############################################################################
from PP4E.Gui.TextEditor.textEditor import TextEditorMainPopup
# класс механизма запуска демонстрационных программ
class Launcher(launchmodes.PortableLauncher): # использовать имеющийся класс
def announce(self, text): # настроить метку в интерфейсе
Info.config(text=text)
def viewer(sources):
for filename in sources:
TextEditorMainPopup(Root, filename, # как всплывающее окно
loadEncode=’utf-8’) # иначе PyEdit может выводить # запросы для каждого!
def demoButton(name, what, doit, code):
добавляет кнопки, которые выполняют команды doit и открывают все
файлы в списке code; кнопка doit сохраняет информацию в объекте, а кнопка
code - в объемлющей области видимости;
rowfrm = Frame(Root)
rowfrm.pack(side=TOP, expand=YES, fill=BOTH)
b = Button(rowfrm, bg=’navy’, fg=’white’, relief=RIDGE, border=4) b.config(text=name, width=20, command=Launcher(what, doit)) b.pack(side=LEFT, expand=YES, fill=BOTH)
b = Button(rowfrm, bg=’beige’, fg=’navy’) b.config(text=’code’, command=(lambda: viewer(code))) b.pack(side=LEFT, fill=BOTH)
##############################################################################
# демонстрационные программы с графическим интерфейсом tkinter - некоторые
# используют сетевые соединения
##############################################################################
demoButton(name=’PyEdit’,
what=’Text file editor’, # редактировать
doit=’Gui/TextEditor/textEditor.py PyDemos.pyw’, # предполагается code=[‘launchmodes.py’, # в тек. раб. кат.
‘Tools/find.py’,
‘Gui/Tour/scrolledlist.py’, # вывести в PyEdit ‘Gui/ShellGui/formrows.py’, # последний = верхний в стопке ‘Gui/Tools/guimaker.py’,
‘Gui/TextEditor/textConfig.py’,
‘Gui/TextEditor/textEditor.py’])
demoButton(name=’PyView’,
what=’Image slideshow, plus note editor’, doit=’Gui/SlideShow/slideShowPlus.py Gui/gifs’, code=[‘Gui/Texteditor/textEditor.py’,
‘Gui/SlideShow/slideShow.py’,
‘Gui/SlideShow/slideShowPlus.py’])
...часть программного кода опущена: смотрите файлы в дереве примеров...
##############################################################################
# переключение шрифта в окне Info раз в секунду ##############################################################################
def refreshMe(info, ncall):
slant = [‘normal’, ‘italic’, ‘bold’, ‘bold italic’][ncall % 4] info.config(font=(‘courier’, 20, slant))
Root.after(1000, (lambda: refreshMe(info, ncall+1)) )
##############################################################################
# показать/спрятать окно Info в случае щелчка на кнопке Info ##############################################################################
Stat.iconify() def onInfo():
if Stat.state() == ‘iconic’:
Stat.deiconify()
else:
Stat.iconify() # было ‘normal’
##############################################################################
# конец создания графического интерфейса, запуск цикла события ##############################################################################
def onLinks():
...часть программного кода опущена: смотрите файлы в дереве примеров...
Button(Root, text=’Info’, command=onInfo).pack(side=TOP, fill=X)
Button(Root, text=’Links’, command=onLinks).pack(side=TOP, fill=X)
Button(Root, text=’Quit’, command=Root.quit).pack(side=BOTTOM, fill=X) refreshMe(Info, 0) # запустить переключение шрифтов в окне Info Root.mainloop()
Панель запуска PyGadgets
Сценарий PyGadgets запускает часть тех же программ, что и PyDemos, но для практического использования, а не как кратковременные демонстрации. Оба сценария отображают панель с кнопками и запускают программы с помощью модуля launchmodes, но сценарий PyGadgets немного проще, потому что его задача более узкая. Кроме того, сценарий PyGadgets поддерживает два режима запуска - он может сразу запустить одновременно все программы из списка или вывести графический интерфейс для запуска каждой программы отдельно. На рис. 10.19 изображен графический интерфейс в виде панели с кнопками для запуска программ по отдельности. Сценарии PyGadgets и PyDemos могут выполняться одновременно, и оба позволяют изменять размеры окна (попробуйте сами, чтобы увидеть, как это делается).
Рис. 10.19. Панель запуска PyGadgets
Из-за этих различий построение графического интерфейса в сценарии PyGadgets в большей мере основывается на данных: он сохраняет имена программ в списке и просматривает его при необходимости, а не проходит по последовательности заранее запрограммированных вызовов функции demoButton. Например, набор кнопок в панели запуска на рис. 10.19 целиком зависит от содержимого списка программ.
Программный код этого графического интерфейса приводится в примере 10.31. Его объем невелик, потому что опирается на использование других модулей, которые мы написали ранее, и осуществляющих большую часть его действий: launchmodes - для запуска программ, LaunchBrowser - для запуска веб-броузера, windows - для переопределения ярлыков и реализации операции завершения. На рабочем столе моего компьютера я создал ярлык для PyGadgets, и его окно практически всегда открыто у меня. С его помощью я легко получаю доступ к повседневно используемым инструментам - текстовым редакторам, калькуляторам, электронной почте, средствам обработки изображений и так далее, которые все встретятся нам в будущих главах.
Для настройки PyGadgets под собственные нужды просто импортируйте и вызывайте его функции через свои списки команд, запускающих программы, или измените список mytools вызываемых программ, который находится ближе к концу файла. В конце концов, это Python.
Пример 10.31. PP4E\PyGadgets.py
##############################################################################
Запускает различные примеры; запускайте сценарий при загрузке системы, чтобы сделать их постоянно доступными.
Этот файл предназначен для запуска программ, действительно необходимых в работе; для запуска демонстрационных программ Python/Tk и получения дополнительных сведений о параметрах запуска программ обращайтесь к сценарию PyDemos. Замечание о работе в Windows: это файл с расширением ‘.py’, поэтому при его запуске щелчком мыши выводится окно консоли, которое используется для вывода начального сообщения (включая 10-секундную паузу, чтобы обеспечить его видимость, пока запускаются приложения). Чтобы избежать вывода окна консоли, запускайте сценарий с помощью программы ‘pythonw’ (а не ‘python’), используйте расширение ‘.pyw’, в свойствах ярлыка в Windows выберите значение ‘Свернутое в значок’ (‘run minimized’) в поле ‘Окно’ (‘Window’) или запускайте файл из другой программы (см. PyDemos). ##############################################################################
import sys, time, os, time from tkinter import *
from launchmodes import PortableLauncher # повторное использ. класса запуска from Gui.Tools.windows import MainWindow # повторное использ. оконных
# инструментов: ярлык, обработчик
# закрытия окна
def runImmediate(mytools):
немедленный запуск программ
print(‘Starting Python/Tk gadgets...’) # вывод в stdout (временный)
for (name, commandLine) in mytools:
PortableLauncher(name, commandLine)() # сразу вызвать для запуска print(‘One moment please...’)
if sys.platform[:3] == ‘win’: # windows: закрыть консоль через
for i in range(10): # 10 секунд
time.sleep(1); print(‘.’ * 5 * (i+1))
def runLauncher(mytools):
создать простую панель запуска для использования в дальнейшем
root = MainWindow(‘PyGadgets PP4E’) # или root = Tk()
for (name, commandLine) in mytools:
b = Button(root, text=name, fg=’black’, bg=’beige’, border=2, command=PortableLauncher(name, commandLine)) b.pack(side=LEFT, expand=YES, fill=BOTH) root.mainloop()
mytools = [
(‘PyEdit’, ‘Gui/TextEditor/textEditor.py’),
(‘PyCalc’, ‘Lang/Calculator/calculator.py’),
(‘PyPhoto’, ‘Gui/PIL/pyphoto1.py Gui/PIL/images’),
(‘PyMail’, ‘Internet/Email/PyMailGui/PyMailGui.py’),
(‘PyClock’, ‘Gui/Clock/clock.py -size 175 -bg white’
‘ -picture Gui/gifs/pythonPowered.gif’),
(‘PyToe’, ‘Ai/TicTacToe/tictactoe.py’
‘ -mode Minimax -fg white -bg navy’),
(‘PyWeb’, ‘LaunchBrowser.pyw’
‘ -live index.html learning-python.com’)]