线程与tkinter

21 投票
2 回答
17397 浏览
提问于 2025-05-01 04:41

我听说在Python中处理线程并不简单,尤其是和tkinter一起使用的时候,更容易出现问题。

我遇到了一个问题。我有两个类,一个是用来做图形界面的(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

我不知道为什么会出现这个错误,也不明白它是什么意思。

暂无标签

2 个回答

0

这里的解决办法很简单,但不容易发现:

在调用 mainwindow.mainloop() 之后,立刻调用 mainWindow.quit(),这样清理工作就会在创建tk界面的那个线程上进行,而不是在Python退出时的主线程上。

23

所有的Tcl命令都需要来自同一个线程。因为tkinter依赖于Tcl,所以通常需要确保所有tkinter的图形界面(GUI)指令都来自同一个线程。问题出在mainWindow是在tkinterGui线程中创建的,但由于mainWindowtkinterGui的一个属性,所以它不会被销毁,直到tkinterGui在主线程中被销毁。

要避免这个问题,可以不把mainWindow当作tkinterGui的属性,也就是说,把self.mainWindow改成mainWindow。这样一来,mainWindow就可以在tkinterGui线程的run方法结束时被销毁。不过,通常你可以通过使用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()

如果你想在一个类里面定义图形界面,还是可以做到的:

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

或者更简单的,直接用主线程来运行图形界面的主循环:

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

撰写回答