【问题标题】:Tkinter gui freezes until function finishesTkinter gui 冻结直到函数完成
【发布时间】:2021-09-02 05:42:49
【问题描述】:

我有一个处理视频上传到谷歌驱动器的脚本,所以我想用 Tkinter 为我的 python 脚本制作一个 gui。在用户界面上,我希望用户从他/她的计算机中选择要上传的视频。在上传功能上,我还会刷新一个文本区域以向用户提供有关上传过程的信息。

问题是,当用户选择文件并启动上传功能时,gui 冻结并且文本区域没有显示任何信息。

上传功能完成后,一切恢复正常,它只是刷新整个窗口,显示我想看到的文本信息上传完成后

我知道我必须为我的上传功能使用线程,因为它是一项长时间运行的任务,但即使在使用线程之后它仍然冻结直到上传功能完成。

任何帮助将不胜感激:)

from tkinter import *
from tkinter import filedialog
import threading

root = Tk()
root.title("Drive Video Uploader")
root.geometry("400x500")

def main():
    creds = None
    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.

    tokenPath='token.json'
    credPath='credentials.json'

    if os.path.exists(tokenPath):
        creds = Credentials.from_authorized_user_file(tokenPath, SCOPES)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(credPath, SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open(tokenPath, 'w') as token:
            token.write(creds.to_json())

    service = build('drive', 'v3', credentials=creds)
    root.label_0 =  Label(root,name="label_0", text="\n").pack()
    root.label_1 = Label(root, name="label_1", text="Please select videos to upload drive!").pack()
    root.button_1 = Button(root, name="button_1",text="Select Videos", command= lambda: threading.Thread(target=videoSelection(service)).start().pack()
    root.label_2 =  Label(root,name="label_2", text="\n").pack()
    root.text_1 = Text(root, width=50, height=25, name="text_1", state=DISABLED,bg="dark blue", fg="white", bd=4).pack()
    root.mainloop()


def videoSelection(service):
    selectedVideoPaths = filedialog.askopenfilenames(initialdir= os.getcwd(),
                                    title= "Please select a file:",
                                    filetypes=[("MP4 files","*.mp4")])
    textArea = root.nametowidget('text_1')
    textArea.configure(state=NORMAL)
    textArea.delete("1.0",END)
    textArea.configure(state=DISABLED)
    
    for video in selectedVideoPaths:
        textArea.configure(state=NORMAL)
        textArea.insert(END,"Upload started for: " + video + "\n")
        textArea.configure(state=DISABLED)
        uploadVideo(service=service,videoFilepath=video)

if __name__ == '__main__':
    main()

【问题讨论】:

  • 目前videoSelection函数包含GUI和上传代码。您为其创建线程的函数不应包含任何 GUI 代码,而应完全专注于上传视频。
  • 我已经将 for 循环从 videoSelection 函数中分离出来,并使其成为另一个函数。还将uploadVideo函数的行改为threading.Thread(target=uploadVideo(service=service,videoFilepath=video)).start()。它没有帮助@scotty3785
  • 您应该在自己的线程中运行整个循环。将视频文件路径列表传递给线程并启动线程。我看看能不能一起做个demo。

标签: python multithreading tkinter


【解决方案1】:

我试图在 cmets 中解释的一个非常粗略的例子。这应该将“长任务”分离到它们自己的线程中,但仍允许更新 GUI 以显示进度。

from tkinter import *
from tkinter import filedialog
import threading
import time
import os

root = Tk()
root.title("Drive Video Uploader")
root.geometry("400x500")

def main():
    service = {'test':'test'}
    root.label_0 =  Label(root,name="label_0", text="\n")
    root.label_0.pack()
    root.label_1 = Label(root, name="label_1", text="Please select videos to upload drive!")
    root.label_1.pack()
    root.button_1 = Button(root, name="button_1",text="Select Videos", command=lambda: videoSelection(service,root))
    root.button_1.pack()
    root.label_2 =  Label(root,name="label_2", text="\n")
    root.label_2.pack()
    root.text_1 = Text(root, width=50, height=25, name="text_1", state=DISABLED,bg="dark blue", fg="white", bd=4)
    root.text_1.pack()
    root.mainloop()


def videoSelection(service,root):
    selectedVideoPaths = filedialog.askopenfilenames(initialdir= os.getcwd(),
                                    title= "Please select a file:",
                                    filetypes=[("MP4 files","*.mp4")])
    uploadThread = threading.Thread(target=threadVideoUpload,args=(service,selectedVideoPaths,root))
    uploadThread.start()
    

def uploadVideo(service,videoFilepath,root):
    print(service,videoFilepath,root.text_1)
    print(f"Uploading: {videoFilepath}")
    root.text_1.config(state=NORMAL)
    root.text_1.insert("end",f"Uploading: {videoFilepath}\n")
    root.text_1.config(state=DISABLED)
    time.sleep(10)
                           

def threadVideoUpload(service,selectedVideoPaths,root):
    for video in selectedVideoPaths:
        uploadVideo(service,video,root)                           

if __name__ == '__main__':
    main()

TheLizzard 编辑:如果您想要更安全的方法,请不要从新线程调用任何 tkinter 函数。您可以像这样使用全局变量:

import tkinter as tk
from tkinter.filedialog import askopenfilenames
from threading import Thread, Lock
import time
import os


def main():
    global service, root, text_1
    service = {"test": "test"}

    root = tk.Tk()
    root.title("Drive Video Uploader")
    root.geometry("400x500")

    text_1 = tk.Text(root, width=50, height=25, state="disabled",
                     bg="dark blue", fg="white", bd=4)
    label_0 = tk.Label(root)
    label_1 = tk.Label(root, text="Please select videos to upload drive!")
    button_1 = tk.Button(root, text="Select Videos", command=video_selection)
    label_2 = tk.Label(root)

    label_0.pack()
    label_1.pack()
    button_1.pack()
    label_2.pack()
    text_1.pack()

    root.mainloop()


def video_selection():
    # Get ready for the new thread:
    global text_to_insert, text_lock
    text_to_insert = ""
    text_lock = Lock()

    # Start a tkinter loop
    text_widget_loop()

    selectedVideoPaths = askopenfilenames(initialdir=os.getcwd(),
                                          title="Please select a file:",
                                          filetypes=[("MP4 files", "*.mp4")])
    upload_thread = Thread(target=thread_video_upload,
                           args=(selectedVideoPaths, ))
    upload_thread.start()


def text_widget_loop():
    global text_to_insert

    # Stop the loop when `text_to_insert` becomes `None`
    if text_to_insert is None:
        print("Stopping the loop.")
        return None

    # If there is text that we need to insert into the text widget:
    if text_to_insert != "":
        text_1.config(state="normal")
        text_1.insert("end", text_to_insert)
        text_1.config(state="disabled")

        with text_lock:
            text_to_insert = ""

    # After 100 ms run this function again:
    text_1.after(100, text_widget_loop)


def upload_video(video_path):
    global text_to_insert
    print(f"Uploading: {video_path}")
    with text_lock:
        text_to_insert += f"Uploading: {video_path}\n"
    time.sleep(3)


def thread_video_upload(selected_video_paths):
    global text_to_insert

    for video in selected_video_paths:
        upload_video(video)

    # Stop the tkinter loop:
    text_to_insert = None


if __name__ == "__main__":
    main()

【讨论】:

  • 你不应该从另一个线程调用tkinter 函数。只有tkinter 的某些部分是线程安全的。有时tkinter 会引发错误甚至崩溃。
  • @TheLizzard 正如我所说,快速/粗略的示例。更好的沟通方式是通过队列,但这很快就会成为一个很长的例子。我不相信我所击中的内容在另一个线程上是不安全的。
  • 工作就像一个魅力,谢谢:) @scotty3785
  • @scotty3785 除非您在答案中使用队列,否则我真的认为 OP 不会考虑实施它们。我还没有看到明确定义的不安全tkinter 方法列表。我很确定大多数方法在某些情况下会变得不安全。
  • @TheLizzard 随意扩展我的答案。
猜你喜欢
  • 2013-11-02
  • 2015-04-08
  • 2019-05-10
  • 1970-01-01
  • 1970-01-01
  • 2018-05-03
  • 2015-11-11
  • 2018-10-11
相关资源
最近更新 更多