【问题标题】:Blocking Tkinter interface until thread finishes its task阻塞 Tkinter 接口直到线程完成其任务
【发布时间】:2015-07-06 08:31:48
【问题描述】:

我正在用 python 编写一个程序,它将处理从某个 excel 文件中读取的大量数据。我已经使用 Tkinter 为这个程序构建了一个 GUI。我知道 Tkinter 是单线程的,因此为了打开文件并进行一些进程,我使用了一个线程来不阻塞 GUI。线程任务之一是填充一个列表(在我的代码中称为 columnList)并将其元素用作选项菜单中的选项,因此在线程完成之前选项菜单为空,因此我使用 join () 让主线程等待工作线程。而且,问题来了,只要工作线程在执行 GUI 就不会响应(大约 7 秒),但之后它会正常工作。

我想使用一些图形指示器来指示正在加载的内容,同时阻止 GUI 窗口,使用户无法单击它。线程停止后,指示器应消失,GUI 应再次启用。我搜索了这样一个概念,但是在网上没有找到这样的东西,这里的这个问题,Python Tkinter: loading screen和我的情况很相似,但是没有答案。

这是我需要应用该概念的代码的一部分:

__author__ = 'Dania'
import threading


from Tkinter import *
from tkFileDialog import askopenfilename
import numpy as np
import xlrd
global x
global v

x = np.ones(5)
v= np.ones(5)
global columnList
columnList=""


def open_file (file_name):


    try:
        workbook = xlrd.open_workbook(file_name)
        sheet=workbook.sheet_by_index(0)
        global columns
        columns = [] #this is a list, in each index we will store a numpy array of a column values.
        for i in range (0,sheet.ncols-1):
           columns.append(np.array (sheet.col_values(i,1))) # make a list, each index has a numpy array that represnts a column. 1 means start from row 1 (leave the label)
           if (i!=0):
               columns[i]= columns[i].astype(np.float)
        #Preprocessing columns[0]:
        m= columns [0]
        for i in range (m.shape[0]):
            m[i]= m[i]*2 +1

        m=m.astype(np.int)
        columns[0]=m

        global columnList
        columnList= np.array(sheet.row_values(0)) #I was using sheet.row(0), but this is better since it doesn't return a 'u'
        columnList=columnList.astype(np.str)


        # removing nans:
        index=input("enter the column index to interpolate: ") #this should be user input

        n= columns [index]
        for i in range (n.shape[0]-1, -1, -1):
            if (np.isnan(n[i])):
                n=np.delete(n,i)
                columns[0]=np.delete(columns[0],i)
                columns [index]= np.delete(columns[index],i)


    except IOError:
        print ("The specified file was not found")

    global x
    np.resize(x, m.shape[0])
    x=columns[0]

    global v
    np.resize(v,n.shape[0])
    v=columns[index]

    #return columns [0], columns [index]




class Interface:
    def __init__(self, master):

        self.title= Label(master,text="Kriging Missing data Imputation", fg="blue", font=("Helvetica", 18))
        self.select_file= Label (master, text="Select the file that contains the data (must be an excel file): ", font=("Helvetica", 12))


        self.title.grid (row=1, column=5, columnspan= 4, pady= (20,0))
        self.select_file.grid (row=3, column=1, sticky=W, pady=(20,0), padx=(5,2))
        self.browse_button= Button (master, text="Browse", command=self.browser, font=("Helvetica", 12), width=12)
        self.browse_button.grid (row=3, column=3, pady=(20,0))


        self.varLoc= StringVar(master)
        self.varLoc.set("status")

        self.varColumn= StringVar(master)
        self.varColumn.set("")

        self.locationColumn= Label(master,text="Select a column as a location indicator", font=("Helvetica", 12))
        self.columnLabel= Label(master,text="Select a column to process", font=("Helvetica", 12))

        global locationOption
        global columnOption
        columnOption= OptionMenu (master, self.varColumn,"",*columnList)
        locationOption= OptionMenu (master, self.varLoc,"",*columnList)

        self.locationColumn.grid (row=5, column=1, pady=(20,0), sticky=W, padx=(5,0))
        locationOption.grid (row=5, column=3, pady=(20,0))

        self.columnLabel.grid (row=7, column=1, pady=(20,0), sticky=W, padx=(5,0))
        columnOption.grid(row=7, column= 3, pady= (20,0))


        self.missing_label= Label(master, text="Select missing data indicator: ", font=("Helvetica", 12))
        self.var = StringVar (master)
        self.var.set("nan")
        self.menu= OptionMenu (master, self.var,"nan", "?", "*")

        self.missing_label.grid (row=9, column=1, padx=(5,2), pady= (20,0), sticky=W)
        self.menu.grid(row=9, column=3, pady= (20,0))

        self.extrapolate= Label (master, text="Select a range for extrapolation (max=800): ", font=("Helvetica", 12))
        self.max_extra= Entry (master)

        self.extrapolate.grid (row=11, column=1, padx=(5,2), pady= (20,0),  sticky=W)
        self.max_extra.grid (row=11, column=3, pady=(20,0))

        self.a_label= Label (master, text="enter the value of a (range): ", font=("Helvetica", 12))
        self.a_value= Entry (master)

        self.a_label.grid (row=13, column=1, padx=(5,2), pady=(20,0),  sticky=W)
        self.a_value.grid (row=13, column=3,  pady=(20,0))


        self.start_button= Button (master, text="Start", font=("Helvetica", 12), width=12)
        self.pause_button= Button (master, text= "Pause", font=("Helvetica", 12),width=12)
        self.stop_button= Button (master, text="stop", font=("Helvetica", 12),width=12)

        self.start_button.grid (row=15, column=1, pady=(30,0) )
        self.pause_button.grid (row=15, column=2, pady=(30,0))
        self.stop_button.grid (row=15, column=3, pady=(30,0))



    def browser (self):
            filename = askopenfilename()
            #indicator should start here.
            t=threading.Thread (target=open_file, args=(filename, ))

            t.start()
            t.join() #I use join because if I didn't,next lines will execute before  open_file is completed, this will make columnList empty and the code will not execute.
            #indicator should end here. 
            opt=columnOption.children ['menu']
            optLoc= locationOption.children ['menu']
            optLoc.entryconfig (0,label= columnList [0], command=self.justamethod)
            opt.entryconfig (0, label= columnList [0], command=self.justamethod)
            for i in range(1,len (columnList)):
                opt.add_command (label=columnList[i], command=self.justamethod)
                optLoc.add_command (label=columnList[i], command=self.justamethod)

    def justamethod (self):
        print("method is called")
        print(self.varLoc.get())




window= Tk () #main window.
starter= Interface (window)


window.mainloop() #keep the window open until the user decides to close it.

我尝试在方法浏览器中添加一些进度条,像这样,

 def browser (self):
            filename = askopenfilename()
            progressbar = ttk.Progressbar(orient=HORIZONTAL, length=200, mode='determinate')
            progressbar.pack(side="bottom")
            progressbar.start()
            t=threading.Thread (target=open_file, args=(filename, ))
            t.start()
            t.join() #I use join because if I didn't,next lines will execute before  open_file is completed, this will make columnList empty and the code will not execute.
            progressbar.stop()
            opt=columnOption.children ['menu']
            opt.entryconfig (0, label= columnList [0], command=self.justamethod)

            for i in range(1,len (columnList)):
                opt.add_command (label=columnList[i], command=self.justamethod)
                optLoc.add_command (label=columnList[i], command=self.justamethod)

 def justamethod (self):
        print("method is called")



window= Tk () #main window.
starter= Interface (window)


window.mainloop() #keep the window open until the user decides to close it.

但是,上面的代码甚至没有显示进度条,这不是我真正需要的。

编辑:编辑代码时固定空格。一个工作代码示例是第一个代码 sn-p 中的示例。

谁能告诉我怎么做?

感谢任何帮助。

谢谢。

【问题讨论】:

  • 能否请您修复缩进,因为 Python 中的缩进很重要?
  • @Christopher Wallace 是的,实际上我没有放所有代码,因此缩进组织得不好,我将编辑问题。谢谢
  • @Christopher Wallace 我编辑了问题
  • 嗨,达尼亚!我希望能够提供帮助,但我不确定我能不能。无论如何,不​​是要破坏你,但你应该提供一个工作示例,这意味着我们可以运行和测试一些代码。所以你应该包括imports等。例如,我在运行你的代码时遇到了这个错误:NameError: name 'columnList' is not defined。如果你这样做,我会开一个赏金,以便其他人可以更多地关注这个帖子;)
  • 谢谢 :),第一个代码 sn-p 是一个 woking 示例,请检查它。

标签: python multithreading user-interface tkinter progress-bar


【解决方案1】:

使用后台线程读取文件的优点之一是当前线程不会阻塞并且可以继续运行。通过在t.start 之后直接调用t.join(),您将阻止GUI,与您只是在当前线程中进行读取没有什么不同。

相反,您在执行操作之前将光标更改为wait 光标如何?我已经简化了你的代码,但是是这样的:

from tkinter import *
import time

class Interface:
    def __init__(self, master):
        self.master = master
        self.browse_button= Button (master, text="Browse", command=self.browser)
        self.browse_button.pack()

    def browser (self):
        self.master.config(cursor="wait")
        self.master.update()
        self.read_file("filename")
        self.master.config(cursor="")

    def read_file (self, filename):
        time.sleep(5)  # actually do the read file operation here

window = Tk()
starter = Interface(window)
window.mainloop()

编辑:好的,我想我更了解问题所在。我的操作系统没有说没有响应,因此无法真正测试问题,但可以使用 ThreadProgressbar 尝试此操作。

from tkinter import *
from tkinter.ttk import *
import time
import threading

class Interface:
    def __init__(self, master):
        self.master = master
        self.browse_button= Button (master, text="Browse", command=self.browser)
        self.browse_button.pack()
        self.progressbar = Progressbar(mode="determinate", maximum=75)

    def browser (self):
        t = threading.Thread(target=self.read_file, args=("filename",))
        self.progressbar.pack()
        self.browse_button.config(state="disabled")
        self.master.config(cursor="wait")
        self.master.update()

        t.start()
        while t.is_alive():
            self.progressbar.step(1)
            self.master.update_idletasks()  # or try self.master.update()
            t.join(0.1)

        self.progressbar.config(value="0")
        self.progressbar.pack_forget()
        self.browse_button.config(state="enabled")
        self.master.config(cursor="")

    def read_file (self, filename):
        time.sleep(7)  # actually do the read here

window = Tk()
starter = Interface(window)
window.mainloop()

注意:我没有做过太多的 GUI 编码,这可能不是最好的解决方案,只是通过并尝试提供帮助! :)

编辑 2:多考虑一下。由于您不确定读取究竟需要多长时间,您可以使用这种方法,让指示器在进度条的两端来回弹跳。

from tkinter import *
from tkinter.ttk import *
import time
import threading

class Interface:
    def __init__(self, master):
        self.master = master
        self.browse_button= Button (master, text="Browse", command=self.browser)
        self.browse_button.pack()
        # Create an indeterminate progressbar here but don't pack it.
        # Change the maximum to change speed. Smaller == faster.
        self.progressbar = Progressbar(mode="indeterminate", maximum=20)

    def browser (self):
        # set up thread to do work in
        self.thread = threading.Thread(target=self.read_file, args=("filename",))
        # disable the button
        self.browse_button.config(state="disabled")
        # show the progress bar
        self.progressbar.pack()
        # change the cursor
        self.master.config(cursor="wait")
        # force Tk to update
        self.master.update()

        # start the thread and progress bar
        self.thread.start()
        self.progressbar.start()
        # check in 50 milliseconds if the thread has finished
        self.master.after(50, self.check_completed)

    def check_completed(self):
        if self.thread.is_alive():
            # if the thread is still alive check again in 50 milliseconds
            self.master.after(50, self.check_completed)
        else:
            # if thread has finished stop and reset everything
            self.progressbar.stop()
            self.progressbar.pack_forget()
            self.browse_button.config(state="enabled")
            self.master.config(cursor="")
            self.master.update()

            # Call method to do rest of work, like displaying the info.
            self.display_file()

    def read_file (self, filename):
        time.sleep(7)  # actually do the read here

    def display_file(self):
        pass  # actually display the info here

window = Tk()
starter = Interface(window)
window.mainloop()

【讨论】:

  • @Dania 我理解你为什么使用join(),但是因为你实际上并没有在启动线程和调用join()之间做任何事情,所以不需要创建一个单独的线程,您可以在当前线程中执行读取操作。您是否确定在Interface__init__ 方法中包含self.master = master 行?我发布的代码在 Python 3.4.2 的 IDLE 中为我工作。
  • @Dania 用另外两个解决方案为您编辑了我的答案。最后一个可能最适合您的需求。
  • @Dania 好的,所以我编辑了我的代码,因为它有一些错误。您是否尝试过只运行我的代码而不进行任何修改?我刚刚将它复制到我的工作计算机上,它按预期工作。 check_completed 方法是停止进度条和光标的方法。它每 50 毫秒检查一次线程是否已完成,如果它调用 self.progressbar.stop() 停止进度条,self.progressbar.pack_forget() 停止显示进度条,self.browse_button.config(state="enabled") 再次启用按钮,self.master.config(cursor="") 重置光标.
  • 好的,感谢您的注意,我会更新我的答案。是的,我刚刚使用了pack(),因为它既快速又简单。我认为其他人可以在他们认为合适的时候编辑他们的布局。不用担心,我很高兴能帮上忙。 :)
  • @Dania: re “我使用网格来放置小部件,将两者放在同一个文件中通常会在布局中产生一些问题。” - 以防其他人阅读该评论并得到错误的印象,在同一个文件中使用它们是很常见的。唯一不能混合使用的情况是两个小部件共享同一个父级,但一个使用grid,一个使用pack
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-16
  • 1970-01-01
相关资源
最近更新 更多