在单独的线程中运行Tkinter表单

11

我编写了一个简短的模块,可以传递图像并创建一个 Tkinter 窗口并显示它。我遇到的问题是,即使我实例化并在单独的线程中调用显示图像的方法,主程序也不会继续执行,直到 Tkinter 窗口关闭。

这是我的模块:

import Image, ImageTk
import Tkinter


class Viewer(Tkinter.Tk):
    def __init__(self,parent):
        Tkinter.Tk.__init__(self,parent)
        self.parent = parent
        self.initialize()

    def initialize(self):
        self.grid()

    def show(self,img):
        self.to_display = ImageTk.PhotoImage(img)
        self.label_image = Tkinter.Label(self,image=self.to_display)
        self.label_image.grid(column = 0, row = 0, sticky = "NSEW")
        self.mainloop()

看起来它运行得很好,除了当我从我的测试程序像下面这样调用它时,即使在不同的线程中启动,它也似乎不允许我的测试程序继续。

import Image
from viewer import Viewer
import threading

def showimage(im):
    view = Viewer(None)
    view.show(im)

if __name__ == "__main__":
    im = Image.open("gaben.jpg")
    t = threading.Thread(showimage(im))
    t.start()
    print "Program keeps going..."

我认为也许我的问题是应该在模块本身内创建一个新的线程,但是我想尽量保持简单,因为我是 Python 的新手。

无论如何,提前感谢任何帮助。

编辑:为了明确,我只是想制作一个模块,在 Tkinter 窗口中显示图像,以便我可以随时使用此模块来显示图像。 我遇到的问题是每当程序使用此模块时,它都无法恢复,直到关闭 Tkinter 窗口。


2
提醒下一个访问者或者也许是六年后的 OP:除了 tkInter 和线程之外,主要问题在于 "threading.Thread(showimage(im))" 并没有在一个线程中执行 "showimage",而是调用了 "showimage(im)" 并将返回值(None)作为参数传递给 Thread 构造函数。正确的线程创建方式应该是 "Thread(target=partial(showimage, im))"。 - SleepProgger
4个回答

22

Tkinter并不是线程安全的,普遍认为Tkinter不能在非主线程中运行。如果您重写代码,使Tkinter在主线程中运行,那么您可以让您的工作线程在其他线程中运行。

但最大的限制是工作线程不能与Tkinter小部件进行交互。它们必须将数据写入队列,您的主GUI线程必须轮询该队列。

如果您只需要显示图像,则可能根本不需要线程。仅当您有一个长时间运行的进程会阻塞GUI时,线程才有用。 Tkinter可以轻松处理数百个图像和窗口而不会出现问题。


非常感谢您的建议。我注意到,如果我在查看器模块中运行self.mainloop(),就像上面的例子一样,直到窗口关闭后才会继续我的实际程序。如果我将此行放置在程序中(例如view.mainloop()),它也会做同样的事情。我曾认为将此行放置在单独的线程中可以解决问题。您说我应该能够在没有线程的情况下完成这个操作,您是指有一种方法可以显示窗口而不使用mainloop()吗? - derricw
@ballsdotballs:如果您打算创建交互式GUI,则需要调用mainloop。由于您正在运行事件循环,因此可能不需要线程。调用mainloop后,“它将永远不会继续执行[您的]实际程序”,但这就是它设计的工作方式。您建立循环,然后您的程序只响应事件。 mainloop几乎总是应用程序启动逻辑中的最后一行代码。 - Bryan Oakley
我想问题是,我只想编写一个显示图像的模块。我需要让程序在使用此模块后继续运行,而不是等待用户关闭图像。您的意思是说Tkinter没有办法做到这一点吗? - derricw
一般来说,没有GUI工具包能像那样工作。GUI工具包有事件循环并响应事件。话虽如此,在启动事件循环后仍然可以运行其他代码,但在该代码运行时,您的GUI将会冻结。如果不知道您真正想要实现什么,就很难更具体地说明。我不知道这是否有帮助,但请查找tkinter的“after”方法的示例。使用它,您可以要求事件循环在未来运行某些内容,因此您可以创建一个带有额外代码的函数,然后在调用“mainloop”后几毫秒运行它。 - Bryan Oakley
非常感谢您对此的帮助。我知道我看起来很迟钝,但我认为您忽略了我的根本问题。我不想让GUI做任何其他事情。它已经完全按照我想要的方式显示图像了。我希望创建查看器的程序(上面的第二个块)能够在查看器运行时继续运行(也就是说,我希望以某种方式到达打印语句行,而无需关闭GUI)。 - derricw
线程仅在您有一个长时间运行的进程会否阻塞GUI时才有用。我还使用它来定期运行代码,使用time.sleep在线程中等待而不阻塞整个程序。 - Artemis

5

从您的评论中可以看出,您根本不需要GUI界面。只需将图像写入磁盘并调用外部查看器即可。

在大多数系统上,可以使用以下方式启动默认查看器:

import subprocess 

subprocess.Popen("yourimage.png")

1
它在哪个系统上运行以将图像文件作为进程执行?这确实尝试直接执行文件,没有涉及shell,也没有查看文件类型或类似内容的代码。 - BlackJack
我完全同意@BlackJack的看法……这样做真的非常危险……恶意软件可以冒充该文件……您不应该直接运行它。 - TheTechRobo the Nerd

3
据我所知,Tkinter不喜欢在其他线程中运行。请参阅此帖子...我需要一点关于Python、Tkinter和线程的帮助 解决方法是在主线程中创建一个(可能是隐藏的)toplevel,生成一个单独的线程来打开图像等,并使用共享队列将消息发送回Tk线程。
您的项目是否必须使用Tkinter?我喜欢Tkinter。它很“快速简单”,但有(许多)情况下其他GUI工具包更适合。

3
你的回答的前半部分不错,但由于建议尝试其他工具包,我不能点赞。Tkinter非常适合这个问题需要的类型。 - Bryan Oakley

2

我曾尝试从单独的线程运行tkinter,但这并不是个好主意,会导致界面冻结。

有一种解决方法是在主线程中运行GUI,并向主GUI发送事件。下面是一个类似的示例,它只显示一个标签。

import Tkinter as t
global root;
root = t.Tk()
root.title("Control center")
root.mainloop()

def new_window(*args):
    global root
    print "new window"
    window = t.Toplevel(root)
    label = t.Label(window, text="my new window")
    label.pack(side="top", fill="both", padx=10, pady=10)
    window.mainloop()

root.bind("<<newwin>>",new_window)

#this can be run in another thread
root.event_generate("<<newwin>>",when="tail")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接