Какая магия предотвращает блокировку программ Tkinter в интерактивной оболочке?

Примечание. Это своего рода продолжение вопроса: Tkinter - когда мне нужно вызывать mainloop?

Обычно при использовании Tkinter вы вызываете Tk.mainloop, чтобы запустить цикл событий и убедиться, что события обрабатываются должным образом, а окна остаются интерактивными без блокировки .

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

>>> import tkinter
>>> t = tkinter.Tk()

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

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

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

>>> t = tkinter.Tk()
>>> print('Not pressing enter now.') # not executing this

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

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

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


person poke    schedule 02.01.2014    source источник
comment
Эта часть магии на самом деле очень проста - она ​​просто вплетает основной цикл Tcl в основной цикл Python. Тем не менее, специфичные для платформы вещи, которые делает Python, чтобы позволить ему ждать на стандартном вводе, а также запускать цикл графического интерфейса (в Windows и классическом Mac вы должны запрашивать это вручную, запустив pythonw, в OS X это то, что заставляет значок приложения внезапно появляется в вашем Доке, на большинстве систем * nix / X11 он более незаметен), это некрасиво.   -  person abarnert    schedule 03.01.2014
comment
Вы можете видеть, насколько проста специфичная для tkinter вещь: если вы просто запустите import tkinter; tk=tkinter.Tk(); input() (как скрипт, а не в интерактивном интерпретаторе), Python запустит комбинированный цикл GUI и stdin, а это все, что нужно для получения Tcl работает, пока ожидает вашего input.   -  person abarnert    schedule 03.01.2014


Ответы (1)


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

import tkinter
t = tkinter.Tk()
input()

(В Windows вам может потребоваться запустить сценарий в pythonw.exe вместо python.exe, но в противном случае вам не нужно делать ничего особенного.)


Итак, как это работает? В конечном итоге фокус сводится к PyOS_InputHook - так же, как работает модуль readline.

Если stdin является TTY, то каждый раз, когда он пытается получить строку с input(), различными битами модуля code, встроенным REPL и т. Д., Python вызывает любой установленный PyOS_InputHook вместо простого чтения из stdin.

Наверное, легче понять что делает readline: он пытается select на stdin или аналогично, цикл для каждого нового символа ввода, или каждые 0,1 секунды, или каждого сигнала.

То, что делает Tkinter, аналогично. Это сложнее, потому что он имеет дело с Windows, но в * nix он делает что-то очень похожее на readline. За исключением того, что он вызывает Tcl_DoOneEvent каждый раз в цикле.

И это ключ. Повторный вызов Tcl_DoOneEvent - это то же самое, что и mainloop.

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


Итак, пока ваш код Python тратит большую часть своего времени на ввод TTY (как обычно бывает в интерактивном интерпретаторе), интерпретатор Tcl продолжает работать, а ваш графический интерфейс отвечает. Если вы создаете блок интерпретатора Python для чего-то другого, кроме ввода TTY, интерпретатор Tcl не работает, и ваш графический интерфейс не отвечает.


Что, если вы хотите сделать то же самое вручную в чистом коде Python? Вам нужно это сделать, если вы хотите, например, интегрировать графический интерфейс Tkinter и сетевой клиент на основе select в однопоточное приложение, верно?

Это просто: отделите одну петлю от другой.

Вы можете select с таймаутом 0,02 с (тот же тайм-аут, который используется обработчиком ввода по умолчанию) и вызывать t.dooneevent(Tkinter.DONT_WAIT) каждый раз в цикле.

Или, в качестве альтернативы, вы можете позволить Tk управлять, позвонив mainloop, но используйте after и друзей, чтобы убедиться, что вы звоните select достаточно часто.

person abarnert    schedule 02.01.2014
comment
Большое спасибо за понимание и ссылки на источник! - person poke; 03.01.2014
comment
В более общем плане, что внутри Tcl_DoOneEvent позволяет ему выбирать события stdin или GUI? Я вижу в Tkinter, где он обрабатывает события Tcl, но, похоже, никогда не вернется к Python. Я установил PyOS_InputHook, но поскольку оператор select всегда ждет 100 мс, графический интерфейс плохо заикается. - person Walter Nissen; 30.05.2014
comment
Не могли бы вы, пожалуйста, привести больше примеров того, как решать такого рода проблемы (развернуть свой ответ), или хороший более полный справочник с примерами? - person nbro; 17.01.2015