如何在使用Tkinter时

时间:2018-05-24 13:27:19

标签: python tkinter concurrency

如果其中一个方法正在使用Tkinter对象,如何同时运行两个方法?

问题设置:方法A在给定时间过后停止方法B.在此之前,方法A在Tkinter标签中显示剩余时间。

问题:方法不会同时运行。

Python版本: 2.7

操作系统:Windows 7

我使用Threads来实现并发。我已经读过Python有一个名为Global Interpreter Lock的东西,它使线程串行运行。我认为这会导致问题。

解决方法是使用进程。这是不可能的,因为Tkinter对象不能转换为字符流(“pickle”)。当我尝试使用进程时出现此错误: PicklingError:无法挑选'tkapp'对象。

下面的可运行示例模仿了更大的真实程序。因此,它使用了模型 - 视图 - 控制器设计模式。我复制了Timeout on a function call的一些代码。

使用案例:用户点击按钮。这会启动一个可能需要很长时间的后台任务。当后台任务正在运行时,前端会不断通知用户在程序取消后台任务之前还剩多少时间。

在后台运行的线程未实现,因此可以停止。但这不是我想知道的。

from Tkinter import *
from time import sleep
from threading import Thread, Timer

class Frontend(Tk):

    def __init__(self):

        Tk.__init__(self)

        self.label = Label(self, text = "", font = ("Courier", 12))
        self.button = Button(self, text = "Run thread in background.", font = ("Courier", 12))
        self.label.grid()
        self.button.grid(sticky = "nsew")

class Backend:

    def background_task(self):

        print "Background task is executing."
        sleep(6)
        print "Finished."

class Controller:

    def __init__(self):

        self.INTERRUPT_AFTER = 4
        self.done = True
        self.backend = Backend()
        self.frontend = Frontend()
        self.frontend.button.configure(command = self.run_something_in_background)

    class Decorator(object):

        def __init__(self, instance, time):

            self.instance = instance
            self.time = time

        def exit_after(self):
            def outer(fn):
                def inner():

                    timer = Timer(self.time, self.quit_function)
                    timer.start()
                    fn()

                    return timer
                return inner
            return outer

        def quit_function(self):

            if not self.instance.done:
                self.instance.display_message("Interrupted background task.")
                self.instance.set_done(True)

    def run_something_in_background(self):

        backendThread = Thread(target = self.backend.background_task)
        decorator = self.Decorator(self, self.INTERRUPT_AFTER)
        countdown = decorator.exit_after()(self.countdown)    # exit_after returns the function with which we decorate.

        self.set_done(False)
        countdown() 
        backendThread.start()
        backendThread.join()
        self.set_done(True)


    def countdown(self):

        seconds = self.INTERRUPT_AFTER
        while seconds > 0 and not self.done:
            message = "Interrupting background task in {} seconds\nif not finished.".format(str(seconds))
            self.display_message(message)
            seconds -= 1
            sleep(1)

    def set_done(self, val):

        self.done = val

    def display_message(self, message):

        self.frontend.label.config(text = message)
        self.frontend.update_idletasks()

    def run(self):

        self.frontend.mainloop()


app = Controller()
app.run()

1 个答案:

答案 0 :(得分:1)

您尝试使用线程或多处理的挑战是tkinter事件循环似乎受到后端线程/进程的影响。你能做的就是我在这里所做的。关键是使用subprocess.Popen()。这迫使解释器打开另一个没有加载tkinter并且没有运行主循环的解释器(确保你没有)。

这是frontend.py程序:

from Tkinter import *
from subprocess import Popen

class Frontend(Tk):

    def __init__(self):

        Tk.__init__(self)
        self.label = Label(self, text = "", font = ("Courier", 12), justify='left')
        self.button = Button(self, text = "Run thread in background.", font = ("Courier", 12))
        self.label.grid()
        self.button.grid(sticky = "nsew")

class Controller:

    def __init__(self):

        self.INTERRUPT_AFTER = 4
        self.done = False
        self.frontend = Frontend()
        self.frontend.button.configure(command = self.run_something_in_background)

    def run_something_in_background(self, *args):

        self.set_done(False)
        seconds = 4
        self.frontend.after(int(seconds) * 1000, self.stopBackend)
        self.countdown(seconds) 

        self.backendProcess = Popen(['python', 'backend.py'])

    def stopBackend(self):
        self.backendProcess.terminate()
        self.done = True
        print 'Backend process terminated by frontend.'
        self.display_message('Backend process terminated')

    def countdown(self, remaining):
        print 'countdown', remaining
        if remaining > 0:
            message = "Interrupting background task in"
            message += " {} seconds\nif not finished.".format(str(remaining))
        elif self.done:
            message = 'Backend process completed'
        self.display_message(message)
        remaining -= 1
        if remaining > 0 and not self.done:
            self.frontend.after(1000, lambda s=remaining: self.countdown(s))
        else:
            message = 'Interrupting backend process.'
            self.display_message(message)

    def set_done(self, val):
        self.done = val

    def display_message(self, message):
        self.frontend.label.config(text = message)
        self.frontend.update_idletasks()

    def run(self):
        self.frontend.mainloop()

app = Controller()
app.run()

backend.py代码:

from time import sleep


def background_task():

    print "Background task is executing."
    for i in range(8):
        sleep(1)
        print 'Background process completed', i+1, 'iteration(s)'
    print "Finished."

background_task()