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

Python tkinter和asyncio,窗口拖动/调整大小块事件循环,单线程,python,python-3.x,tkinter,python-asyncio,Python,Python 3.x,Tkinter,Python Asyncio,Tkinter和asyncio在一起工作时存在一些问题:它们都是想要无限期阻塞的事件循环,如果您尝试在同一个线程上运行它们,其中一个将阻止另一个执行。这意味着,如果要运行tk事件循环(tk.mainloop()),则不会运行任何异步IO任务;如果您想运行asyncio事件循环,您的GUI将永远不会出现在屏幕上。为了解决这个问题,我们可以通过将Tk.update()作为异步IO任务调用来模拟Tk的事件循环(如下面的ui_update_Task()所示)。这对我来说非常有效,除了一个问题:窗口管理器

Tkinter和asyncio在一起工作时存在一些问题:它们都是想要无限期阻塞的事件循环,如果您尝试在同一个线程上运行它们,其中一个将阻止另一个执行。这意味着,如果要运行tk事件循环(tk.mainloop()),则不会运行任何异步IO任务;如果您想运行asyncio事件循环,您的GUI将永远不会出现在屏幕上。为了解决这个问题,我们可以通过将Tk.update()作为异步IO任务调用来模拟Tk的事件循环(如下面的ui_update_Task()所示)。这对我来说非常有效,除了一个问题:窗口管理器事件阻塞了异步IO事件循环。这些操作包括窗口拖动/调整大小操作。我不需要调整大小,所以我在我的程序中禁用了它(在下面的MCVE中没有禁用),但是用户可能需要拖动窗口,我非常希望我的应用程序在这段时间内继续运行

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

我还尝试了
overrideredirect(True)
完全删除标题栏,并将其替换为仅包含标签和X按钮的tk.Frame,并实现了我自己的拖动方法。这还有一个不良的副作用,即删除任务栏图标,这是可以纠正的。这个兔子洞的工作环境可能会更糟,但我真的宁愿不必重新实现和破解这么多基本的窗口操作。然而,如果我找不到解决这个问题的方法,这很可能就是我选择的路线

导入异步IO
将tkinter作为tk导入
类tk\u异步窗口(tk.tk):
定义初始化(自我、循环、更新间隔=1/20):
super(tk_异步窗口,self)。\uu初始化
self.protocol('WM\u DELETE\u WINDOW',self.close)
自几何体('400x100')
self.loop=循环
self.tasks=[]
self.update\u interval=更新\u interval
self.status='working'
self.status\u label=tk.label(self,text=self.status)
自我状态标签包(padx=10,pady=10)
self.close_event=asyncio.event()
def关闭(自我):
self.close_事件集()
异步定义ui_更新_任务(自身,间隔):
尽管如此:
self.update()
等待异步睡眠(间隔)
异步定义状态标签任务(自):
"""
这会使状态标签以交替的点数进行更新,以便您知道UI没有更新
即使它什么都不做,也会被冻住。
"""
圆点=“”
尽管如此:
self.status_标签['text']='状态:%s%s'(self.status,点)
等待异步睡眠(0.5)
点+='。'
如果len(点)>=4:
圆点=“”
def初始化(自):
科罗斯=(
self.ui\u更新任务(self.update\u间隔),
self.status_label_task(),
#附加的网络绑定任务
)
对于coro中的coro:
self.tasks.append(self.loop.create_任务(coro))
异步def main():
gui=tk\u async\u窗口(asyncio.get\u event\u loop())
gui.initialize()
wait gui.close_事件.wait()
gui.destroy()
如果uuuu name uuuuuu='\uuuuuuu main\uuuuuuu':
asyncio.run(main(),debug=True)
如果运行上面的示例代码,您将看到一个带有标签的窗口,上面写着:
状态:工作
后接0-3点。如果按住标题栏,您会注意到点将停止动画,这意味着asyncio事件循环被阻止。这是因为对
self.update()
的调用在
ui\u update\u task()
中被阻止。释放标题栏后,您应在控制台中从asyncio获得一条消息:
执行耗时1.984秒
秒数为拖动窗口的时间长度。
我想要的是在不阻止asyncio或生成新线程的情况下处理拖动事件的方法。有什么方法可以做到这一点吗?

有效地执行asyncio事件循环中的各个Tk更新,并且运行到
update()
阻塞的地方。另一个选项是反转逻辑并从Tkinter计时器内部调用asyncio事件循环的单个步骤,即使用以保持调用

以下是您的代码以及上面概述的更改:

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()

不幸的是,我无法在我的机器上测试它,因为在Linux上似乎没有出现阻止
update()
的问题,在Linux上,窗口的移动是由桌面的窗口管理器组件而不是程序本身来处理的。

实际上,您是在异步IO事件循环中执行单个Tk更新,并且正在运行到一个
update()
阻塞的地方。也许换一种方式效果更好:从
Tkinter
计时器内部调用Asyncio事件循环的单个步骤-即使用以保持调用。这是我的意思的一个例子,虽然我无法测试它是否解决了您的问题,因为我无法在Linux上复制它。这似乎是可行的。我很好奇
mainloop()
是如何避免被
update()
阻止的;这让我相信有一种方法可以实现与异步IO驱动相同的行为。这是一个很好的答案,但它迫使我决定asyncio的事件循环应该多长时间运行一次。在(1,self.\uu update\u asyncio)之后,如何最大限度地提高我处理asyncio任务的频率?很高兴知道这个行为在Linux上没有表现出来,我是否应该更新这个问题以澄清这个问题只影响Wi