Как отмечалось ранее, возможно также реализовать графический интерфейс приложения в виде отдельной программы. Это наиболее тернистый путь, но в некоторых ситуациях он может упростить интеграцию слабо связанных компонентов. Этот способ может, например, помочь решить проблемы, свойственные примеру 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’)() # запустить сценарий
Запустите сценарий из примера 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. Модель клиент-сервер на основе сокетов неплохо подходит для соединения графического интерфейса со сценариями командной строки, но существуют и другие альтернативы, которые мы рассмотрим в следующем разделе, прежде чем двинуться дальше.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011