线程和 tkinter

Posted

技术标签:

【中文标题】线程和 tkinter【英文标题】:Threads and tkinter 【发布时间】:2014-12-29 11:24:28 【问题描述】:

听说Python中的线程不好处理,和tkinter比较纠结。

我有以下问题。我有两个类,一个用于 GUI,另一个用于无限进程。首先,我启动 GUI 类,然后启动无限进程类。我希望当你关闭 GUI 时,它也完成了无限过程并且程序结束。

代码的简化版本如下:

import time, threading
from tkinter import *
from tkinter import messagebox

finish = False

class tkinterGUI(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):  
        global finish
        #Main Window
        self.mainWindow = Tk()
        self.mainWindow.geometry("200x200")
        self.mainWindow.title("My GUI Title")
        #Label
        lbCommand = Label(self.mainWindow, text="Hello world", font=("Courier New", 16)).place(x=20, y=20)
        #Start
        self.mainWindow.mainloop()
        #When the GUI is closed we set finish to "True"
        finish = True

class InfiniteProcess(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        global finish
        while not finish:
            print("Infinite Loop")
            time.sleep(3)

GUI = tkinterGUI()
GUI.start()
Process = InfiniteProcess()
Process.start()

当我点击关闭按钮(在右上角)时,控制台中出现以下错误:

Tcl_AsyncDelete: async handler deleted by the wrong thread

我不知道它为什么会发生或它意味着什么。

【问题讨论】:

您的简化版本对我来说没问题...一定是您忘记添加导致问题的原因 @mguijarr 我在 google 中读到这个错误在 Window 中更常见,你的 SO 是什么?我的是 Windows 7 x64。也许窗户是问题:/ 【参考方案1】:

All Tcl commands need to originate from the same thread。由于tkinter的 对Tcl的依赖,一般需要全部tkintergui语句 来自同一个线程。出现问题是因为 mainWindowtkinterGui 线程中被实例化,但是——因为mainWindowtkinterGui 的一个属性——直到tkinterGui 在主线程中被销毁时才会被销毁。

可以通过不将mainWindow 设为tkinterGui 的属性来避免该问题 -- 即将self.mainWindow 更改为mainWindow。这允许mainWindowrun 方法在tkinterGui 线程中结束时被销毁。但是,您通常可以通过使用 mainWindow.after 调用来完全避免线程:

import time, threading
from tkinter import *
from tkinter import messagebox

def infinite_process():
    print("Infinite Loop")
    mainWindow.after(3000, infinite_process)


mainWindow = Tk()
mainWindow.geometry("200x200")
mainWindow.title("My GUI Title")
lbCommand = Label(mainWindow, text="Hello world", font=("Courier New", 16)).place(x=20, y=20)
mainWindow.after(3000, infinite_process)
mainWindow.mainloop()

如果你想在一个类中定义 GUI,你仍然可以这样做:

import time, threading
from tkinter import *
from tkinter import messagebox

class App(object):
    def __init__(self, master):
        master.geometry("200x200")
        master.title("My GUI Title")
        lbCommand = Label(master, text="Hello world", 
                          font=("Courier New", 16)).place(x=20, y=20)

def tkinterGui():  
    global finish
    mainWindow = Tk()
    app = App(mainWindow)
    mainWindow.mainloop()
    #When the GUI is closed we set finish to "True"
    finish = True

def InfiniteProcess():
    while not finish:
        print("Infinite Loop")
        time.sleep(3)

finish = False
GUI = threading.Thread(target=tkinterGui)
GUI.start()
Process = threading.Thread(target=InfiniteProcess)
Process.start()
GUI.join()
Process.join()

或者更简单,只需使用主线程来运行 GUI 主循环:

import time, threading
from tkinter import *
from tkinter import messagebox

class App(object):
    def __init__(self, master):
        master.geometry("200x200")
        master.title("My GUI Title")
        lbCommand = Label(master, text="Hello world", 
                          font=("Courier New", 16)).place(x=20, y=20)

def InfiniteProcess():
    while not finish:
        print("Infinite Loop")
        time.sleep(3)

finish = False
Process = threading.Thread(target=InfiniteProcess)
Process.start()

mainWindow = Tk()
app = App(mainWindow)
mainWindow.mainloop()
#When the GUI is closed we set finish to "True"
finish = True
Process.join()

【讨论】:

非常感谢!你是大师! 但有时你需要将mainWindow作为一个属性,例如如果你想使用: self.mainWindow.protocol("WM_DELETE_WINDOW", self.quit) 当你定义函数quit()你写: self.mainWindow.destroy() self.mainWindow.quit() mainWindow 必须是一个属性,否则函数退出将无法识别 mainWindow。 你仍然可以使用类;只是不要使实例成为threading.Thread 的属性。我在上面添加了一些代码来建议如何做。 严格来说,Tcl 可以做多线程,Tk 也可以(前提是它们是用正确的选项构建的),但是你必须为每个线程保留一个完整的上下文。线程模型完全与 Python 不同——它更像是拥有一个单独的 OS 进程——尽管这确实意味着 Tcl 使用全局锁要少得多。【参考方案2】:

这里的解决方法很简单,但很难发现:

mainwindow.mainloop() 之后立即调用mainWindow.quit(),以便清理在与创建 tk UI 的线程相同的线程上进行,而不是在 python 退出时在主线程上进行。 p>

【讨论】:

这似乎不是一个通用的解决方案。我的代码出现了同样的问题(Tcl_AsyncDelete 错误),在mainWindow.mainloop() 之后添加mainWindow.quit() 无效。 @BryanOakley 是的,同样的问题:在试图避免 Tcl_AsyncDelete 时,我发现.quit() 在 Python 2.7.14 上是不必要的,在 Python 3.6.3 上无效。还没找到解决办法。

以上是关于线程和 tkinter的主要内容,如果未能解决你的问题,请参考以下文章

python --Tkinter详解

python写图形界面

学习笔记Python - tkinter

Python3 GUI编程: 自带图形库 tkinter 学习教程

Python如何完成一个上课点名系统!

用python写了一个上课点名系统