tkinter和asyncio,窗口拖动/调整大小阻止事件循环,单线程

时间:2019-04-01 22:35:35

标签: python python-3.x tkinter python-asyncio

Tkinter和asyncio在一起工作时会遇到一些问题:它们都是事件循环,它们希望无限期地阻塞,并且如果您尝试在同一线程上同时运行它们,则它们将完全阻塞另一个线程。这意味着,如果您要运行tk事件循环(Tk.mainloop()),则不会运行任何异步任务。如果要运行asyncio事件循环,则GUI永远不会绘制到屏幕上。要解决此问题,我们可以通过调用Tk.update()作为asyncio Task(如下所示ui_update_task()中所示)来模拟Tk的事件循环。除了一个问题,这对我来说效果很好:窗口管理器事件阻止了asyncio事件循环。这些包括窗口拖动/调整大小操作。我不需要调整大小,因此我已经在程序中禁用了它(在下面的MCVE中未禁用),但是用户可能需要拖动窗口,我非常希望我的应用程序在此期间继续运行

这个问题的目的是看看是否可以在单个线程中解决。在这里和其他地方,有几个答案可以解决此问题,方法是在一个线程中运行tk的事件循环,在另一个线程中运行asyncio的事件循环,通常使用队列将数据从一个线程传递到另一个线程。我已经对此进行了测试,并出于多种原因确定这是解决我的问题的不受欢迎的解决方案。如果可能的话,我想在一个线程中完成。

我还尝试过overrideredirect(True)完全删除标题栏,仅用包含标签和X按钮的tk.Frame替换它,并实现了我自己的拖动方法。这也具有删除任务栏图标的不良副作用,可以修复by making an invisible root window that pretends to be your real window。解决方法的繁琐工作可能会更糟,但我真的只希望不必重新实现和修改那么多基本的窗口操作。但是,如果我找不到解决此问题的方法,那很可能就是我选择的路线。

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 不管秒数有多长,您都在拖动窗口。 我想要的是一种处理拖动事件而又不会阻塞异步或产生新线程的方法。有什么办法可以做到这一点?

1 个答案:

答案 0 :(得分:2)

有效地,您正在asyncio事件循环内执行各个Tk更新,并且正在update()被阻止的地方运行。另一个选择是反转逻辑并从Tkinter计时器内部调用asyncio事件循环的单个步骤-即使用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上,在Linux上,窗口的移动是由桌面的窗口管理器组件而不是桌面来完成的。程序本身。