python tk编程出现: Tcl_AsyncDelete: async handler deleted by the wrong thread

发布时间 2023-09-09 22:32:27作者: 顺其自然,道法自然
  • 问题现象
    我有一个主TK界面, 同时又创建了一个新的独立的TK窗口. 这个新的TK窗口设置为topmost, 用于超时提醒的. 这个窗口虽然是topmost的, 但是可能没有输入焦点. 我想设置一个快捷键, 用于关闭此窗口. 也就是说, 在另外的线程中关闭tk窗口. 采用的方法是在另外线程中调用root.destroy(). 结果就是偶尔好使, 有时会出现错误: Tcl_AsyncDelete: async handler deleted by the wrong thread. 出现错误后, 整个python解释器会退出.
  • 解决方案1
    在另外的线程中使用代码:
    root.after(0,lambda:root.destroy())
    
    这样可以强制在root线程中运行代码, 但是有时候仍然出同样的错误, 只不过频率会降低一些.
    如果代码修改为如下(等待1秒钟之后再调用):
    root.after(1000,lambda:root.destroy())
    
    基本上不再出现问题, 不过副作用是关闭窗口时会延迟1秒.
    方案1, 我觉得不是特别完美.
  • 解决方案2
    完全避免跨线程调用. 思路是在tk窗口线程中轮询某个信号量, 当出现指定的信号执行相应的动作. 代码如下:
    def _loop_query(root:tk.Tk, callback:callable):
    	'''tk线程内轮询'''
    	callback()  # 调用回调函数
    	root.after(300, lambda: _loop_query(root, callback))
    
    def Alarm(msg:str='提醒!',showTime:float=None):
    	'''提醒用户, 计划采用的方式是弹出提醒窗口; msg为可选提醒信息
    	showTime: 显示时间, 单位秒, 超过显示时间自动销毁
    	'23090801_AlarmClose': 关闭消息, 当收到此消息时关闭窗口
    	'''
    	start_time = time.time()    # 窗口创建时间
    	def _check_close():
    		'''检查是否收到关闭通知, 收到后关闭窗口'''
    		notice = message.get_notice('23090901_CloseAlarm')
    		if notice!=None and notice.time>start_time: 
    			root.destroy()  # 销毁窗口
    
    	root = tk.Tk()
    	root.title('提醒框')
    	root.geometry("200x100+20+20")
    	tk.Label(root, text=msg).pack(side=tk.TOP,anchor=tk.W)
    	root.wm_attributes("-topmost", 1)
    	if showTime!=None: root.after(int(showTime*1000), lambda: root.destroy())  # 自动销毁窗口
    	_loop_query(root, _check_close) # 循环检查, 需要时关闭窗口
    	root.mainloop()
    
    经过实践验证, 完美解决了我的问题.
    测试代码如下:
    import TkExt
    import time
    import message
    import Common
    from TkExt import *
    
    @Common.run_in_thread
    def test_func():
    	Alarm('hzq test.')
    	print('hzq test2.')
    
    test_func()
    time.sleep(30)
    message.pub_notice('23090901_CloseAlarm')
    time.sleep(1000)