tkinter и asyncio, цикл событий перетаскивания / изменения размеров блоков, один поток - PullRequest
1 голос
/ 02 апреля 2019

У Tkinter и asyncio есть некоторые проблемы, связанные с совместной работой: они оба представляют собой циклы событий, которые хотят блокировать на неопределенный срок, и если вы попытаетесь запустить их оба в одном потоке, один заблокирует выполнение другого вообще. Это означает, что если вы хотите запустить цикл событий tk (Tk.mainloop ()), ни одна из ваших асинхронных задач не будет выполняться; и если вы хотите запустить цикл событий asyncio, ваш графический интерфейс никогда не будет отображаться на экране. Чтобы обойти это, мы можем смоделировать цикл событий Tk, вызвав Tk.update () как асинхронную задачу (как показано в ui_update_task () ниже). Это хорошо работает для меня, за исключением одной проблемы: события менеджера окон блокируют цикл событий asyncio. К ним относятся операции перетаскивания / изменения размера окна. Мне не нужно изменять размер, поэтому я отключил его в своей программе (не отключен в MCVE ниже), но пользователю может понадобиться перетащить окно, и мне бы очень хотелось, чтобы мое приложение продолжало работать в течение этого времени .

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

Я также попытался overrideredirect(True) полностью удалить строку заголовка и заменить ее на просто tk.Frame, содержащий метку и кнопку X, и реализовал свои собственные методы перетаскивания. Это также имеет нежелательный побочный эффект удаления значка панели задач, который можно исправить , сделав невидимое корневое окно, которое претендует на то, чтобы быть реальным окном . Эта кроличья нора обходных путей могла бы быть хуже, но я бы на самом деле предпочел бы не переопределять и не взламывать так много основных оконных операций. Однако, если я не смогу найти решение этой проблемы, скорее всего, я выберу маршрут.

import asyncio
import tkinter as tk


class tk_async_window(tk.Tk):
    def __init__(self, loop, update_interval=1/20):
        super(tk_async_window, self).__init__()
        self.protocol('WM_DELETE_WINDOW', self.close)
        self.geometry('400x100')
        self.loop = loop
        self.tasks = []
        self.update_interval = update_interval

        self.status = 'working'
        self.status_label = tk.Label(self, text=self.status)
        self.status_label.pack(padx=10, pady=10)

        self.close_event = asyncio.Event()

    def close(self):
        self.close_event.set()

    async def ui_update_task(self, interval):
        while True:
            self.update()
            await asyncio.sleep(interval)

    async def status_label_task(self):
        """
        This keeps the Status label updated with an alternating number of dots so that you know the UI isn't
        frozen even when it's not doing anything.
        """
        dots = ''
        while True:
            self.status_label['text'] = 'Status: %s%s' % (self.status, dots)
            await asyncio.sleep(0.5)
            dots += '.'
            if len(dots) >= 4:
                dots = ''

    def initialize(self):
        coros = (
            self.ui_update_task(self.update_interval),
            self.status_label_task(),
            # additional network-bound tasks
        )
        for coro in coros:
            self.tasks.append(self.loop.create_task(coro))

async def main():
    gui = tk_async_window(asyncio.get_event_loop())
    gui.initialize()
    await gui.close_event.wait()
    gui.destroy()

if __name__ == '__main__':
    asyncio.run(main(), debug=True)

Если вы запустите приведенный выше пример кода, вы увидите окно с надписью: Status: working, за которыми следуют 0-3 точки. Если вы удерживаете строку заголовка, вы заметите, что точки перестают анимироваться, то есть цикл событий asyncio блокируется. Это связано с тем, что вызов self.update() блокируется в ui_update_task(). После освобождения строки заголовка вы должны получить сообщение в консоли от asyncio: Executing <Handle <TaskWakeupMethWrapper object at 0x041F4B70>(<Future finis...events.py:396>) created at C:\Program Files (x86)\Python37-32\lib\asyncio\futures.py:288> took 1.984 seconds с количеством секунд, сколько бы вы ни тянули окно. То, что я хотел бы, это какой-то способ обработки событий перетаскивания без блокировки asyncio или порождения новых потоков. Есть ли способ сделать это?

1 Ответ

2 голосов
/ 05 апреля 2019

Фактически вы выполняете отдельные обновления Tk в цикле событий asyncio и сталкиваетесь с местом, где update() блокируется.Другой вариант - инвертировать логику и вызвать один шаг цикла событий asyncio из таймера Tkinter - т.е. использовать Widget.after, чтобы продолжать вызывать run_once.

Вот ваш код с изменениями, описанными выше:

import asyncio
import tkinter as tk


class tk_async_window(tk.Tk):
    def __init__(self, loop, update_interval=1/20):
        super(tk_async_window, self).__init__()
        self.protocol('WM_DELETE_WINDOW', self.close)
        self.geometry('400x100')
        self.loop = loop
        self.tasks = []

        self.status = 'working'
        self.status_label = tk.Label(self, text=self.status)
        self.status_label.pack(padx=10, pady=10)

        self.after(0, self.__update_asyncio, update_interval)
        self.close_event = asyncio.Event()

    def close(self):
        self.close_event.set()

    def __update_asyncio(self, interval):
        self.loop.call_soon(self.loop.stop)
        self.loop.run_forever()
        if self.close_event.is_set():
            self.quit()
        self.after(int(interval * 1000), self.__update_asyncio, interval)

    async def status_label_task(self):
        """
        This keeps the Status label updated with an alternating number of dots so that you know the UI isn't
        frozen even when it's not doing anything.
        """
        dots = ''
        while True:
            self.status_label['text'] = 'Status: %s%s' % (self.status, dots)
            await asyncio.sleep(0.5)
            dots += '.'
            if len(dots) >= 4:
                dots = ''

    def initialize(self):
        coros = (
            self.status_label_task(),
            # additional network-bound tasks
        )
        for coro in coros:
            self.tasks.append(self.loop.create_task(coro))

if __name__ == '__main__':
    gui = tk_async_window(asyncio.get_event_loop())
    gui.initialize()
    gui.mainloop()
    gui.destroy()

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

...