tkinter和异步,窗口拖动/调整大小块事件循环,单线程
问题描述
tkinter和asyncio在一起工作时遇到了一些问题:它们都是希望无限期挡路的事件循环,如果您尝试在同一线程上运行它们,其中一个将挡路另一个永远不会执行。这意味着如果要运行tk事件循环(Tk.mainloop()),则不会运行任何异步任务;如果要运行异步事件循环,则GUI永远不会显示在屏幕上。要解决这个问题,我们可以通过调用Tk.update()作为异步任务(如下面的UI_UPDATE_TASK()所示)来模拟Tk的事件循环。这对我来说工作得很好,除了一个问题:窗口管理器事件、挡路、异步事件循环。这些操作包括窗口拖动/调整大小操作。我不需要调整大小,所以我在我的程序中禁用了它(在下面的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个点。如果您按住标题栏,您会注意到点将停止动画,这意味着异步事件循环被阻止。这是因为对self.update()
的调用在ui_update_task()
中被阻止。释放标题栏后,您应该会在控制台中收到一条来自Asyncio的消息:
Executing <Handle <TaskWakeupMethWrapper object at 0x041F4B70>(<Future finis...events.py:396>) created at C:Program Files (x86)Python37-32libasynciofutures.py:288> took 1.984 seconds
无论您拖动窗口的时间有多长,秒数都是一样的。
我想要的是在不阻塞异步或产生新线程的情况下处理拖动事件的方法。有什么办法可以做到这一点吗?
解决方案
您在Asyncio事件循环内有效地执行单个Tk更新,并运行到update()
块所在的位置。另一种选择是反转逻辑并从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上,在Linux上,窗口的移动是由桌面的窗口管理器组件而不是程序本身来处理的。
相关文章