在 tkinter 中使用进度条运行和中断循环

问题描述 投票:0回答:1

我想在

tkinter
中创建一个小的 GUI,以下载存储为 URL 的文件列表在
*.csv
表中。

但是,我找不到结合进度条和随时中断进程的选项的方法。如果我在主线程中运行下载循环,它会阻止其他任何东西,一些用户点击“取消”将没有任何效果。另一方面,我似乎无法从外部进程获取循环的当前状态并使用它来更新进度条值。

目前,这是我的代码:

#-----------------------------------------------------------------------------|
# Import modules
import os, requests
import pandas as pd
import threading as th
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog as fd

#-----------------------------------------------------------------------------|
# Functions

def download(remote_url, local_file):
    data = requests.get(remote_url)
    try:
        with open(local_file, "wb") as f:
            f.write(data.content)
        exit_status = 0
    except:
        exit_status = 1
    return exit_status

#-----------------------------------------------------------------------------|
# GUI

class BulkDownloader(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)
        self.pack(padx = 0, pady = 0)
        self.create_widgets()
        self.master.minsize(300, 50)
        self._continue_loop = True
        self._current_progress = 0
        self._downloading = False
    
    def create_widgets(self):
        # Variables
        ## Input file
        self.file_name = tk.StringVar()
        self.file_name.set("")
        
        ## Output folder
        self.output_folder = tk.StringVar()
        self.output_folder.set("")
        
        ## Progress bar status
        self.progress_var = tk.DoubleVar()
        self.progress_var.set(0)
        
        # Title
        self.winfo_toplevel().title("Bulk Downloader")
        
        # Display
        self.display_current_in = tk.Entry(self)
        self.display_current_in.grid(row = 0, column = 4, columnspan = 4,
                                  sticky = "EW")
        self.display_current_in["textvariable"] = self.file_name
        
        self.display_current_out = tk.Entry(self)
        self.display_current_out.grid(row = 1, column = 4, columnspan = 4,
                                  sticky = "EW")
        self.display_current_out["textvariable"] = self.output_folder
        
        # Buttons
        ## Select input file
        self.select_in_button = tk.Button(self)
        self.select_in_button["text"] = "Select input file"
        self.select_in_button["command"] = self.select_in_file
        self.select_in_button.grid(row = 0, column = 0, columnspan = 3,
                                   sticky = "EW")
        
        ## Select output directory
        self.select_out_button = tk.Button(self)
        self.select_out_button["text"] = "Select output directory"
        self.select_out_button["command"] = self.select_out_folder
        self.select_out_button.grid(row = 1, column = 0, columnspan = 3,
                                 sticky = "EW")
        
        ## Close main window
        self.cancel_button = tk.Button(self)
        self.cancel_button["text"] = "Cancel"
        self.cancel_button["command"] = self.cancel_all
        self.cancel_button.grid(row = 2, column = 0, columnspan = 3,
                                sticky = "EW")
        
        ## Download files
        self.download_button = tk.Button(self)
        self.download_button["text"] = "Download"
        self.download_button["command"] = self.start_download
        self.download_button.grid(row = 2, column = 4, columnspan = 4,
                                  sticky = "E")
        
        ## Progress bar
        self.progress = ttk.Progressbar(self, variable = self.progress_var,
                                        mode = "determinate")
        self.progress.grid(row = 3, column = 0, columnspan = 38, sticky = "EW")
    
    def select_in_file(self):
        current_selection = self.file_name.get()
        set_dir_to = current_selection if current_selection != "" else "/"
        file_types = (("csv file", "*.csv"), ("all files", "*.*"))
        selection = fd.askopenfilename(title = "Open file",
                                       initialdir = set_dir_to,
                                       filetypes = file_types)
        self.file_name.set(selection)
    
    def select_out_folder(self):
        current_selection = self.output_folder.get()
        set_dir_to = current_selection if current_selection != "" else "/"
        selection = fd.askdirectory(title = "Select destination directory",
                                       initialdir = set_dir_to)
        self.output_folder.set(selection)
    
    def download_files(self):
        local_folder = self.output_folder.get()
        csv_table = self.file_name.get()
        
        table = pd.read_csv(csv_table)
        url_list = table[table.columns[0]].to_list()
        
        for enumerator, item in enumerate(url_list):
            if not self._continue_loop:
                break
            local_file = os.path.join(local_folder, os.path.split(item)[1])
            exit_status = download(item, local_file)
            
            self._current_progress = ((enumerator + 1) * 100) // len(url_list)
        
        self.finished(exit_status)
    
    def start_download(self):
        self._continue_loop = True
        if not self._downloading:
            self._current_progress = 0
            t = th.Thread(target = self.download_files, daemon = True)
            t.start()
            self.check_progress()
    
    def check_progress(self):
            self.progress_var.set(self._current_progress)
            #self.master.update()
            self.after(1000, self.check_progress)
    
    def finished(self, exit_status):
        self._downloading = False
        
        messageWindow = tk.Toplevel(self)
        window_title = "Bulk Downloader finished" if exit_status == 0 else \
            "Bulk download failed"
        messageWindow.title(window_title)
        tk.Label(messageWindow, text = window_title).pack()
        tk.Button(messageWindow, text = "Close window",
                  command = self.master.destroy).pack()
    
    def cancel_all(self):
        self._continue_loop = False
        self._downloading = False
        self.master.destroy

#-----------------------------------------------------------------------------|
# Run Bulk Downloader
root = tk.Tk()
app = BulkDownloader(root)
app.mainloop()

我还尝试将进度设置为全局变量(而不是类的属性)但没有成功。 我还尝试通过添加

从外部线程更新进度
            self.progress_var.set(self._current_progress)
            self.master.update()

self.download_files()
内的循环;然而,在这种情况下,Python 失败了,因为 [一些消息不在主循环中],我的 IDE 在我能读出问题所在之前就崩溃了。但我想我根本无法从外部线程更新
tkinter.DoubleVar

我也尝试使用

Queue
作为 this answer to a somewhat similar problem.在这里,我对脚本进行了如下调整:

    def download_files(self, Q):
        local_folder = self.output_folder.get()
        csv_table = self.file_name.get()
        
        table = pd.read_csv(csv_table)
        url_list = table[table.columns[0]].to_list()
        
        for enumerator, item in enumerate(url_list):
            if not self._continue_loop:
                break
            local_file = os.path.join(local_folder, os.path.split(item)[1])
            exit_status = download(item, local_file)
            
            #self._current_progress = ((enumerator + 1) * 100) // len(url_list)
            prog = ((enumerator + 1) * 100) // len(url_list)
            Q.put(prog)
        
        self.finished(exit_status)
    
    def start_download(self):
        self._continue_loop = True
        if not self._downloading:
            self.Q = qu.Queue()
            self._current_progress = 0
            t = th.Thread(target = self.download_files, daemon = True,
                          args = (self.Q,))
            t.start()
            self.check_progress()
    
    def check_progress(self):
            #self.progress_var.set(self._current_progress)
            #self.master.update()
            try:
                value = int(self.Q)
            except:
                value = 0
            self.progress_var.set(value)
            self.after(1000, self.check_progress)

仍然没有任何变化。

也许代码中有一些不同的错误?

编辑

正如 Michael Butscher 所指出的,

queue
不能转换为
int
。我在尝试此转换的地方添加了
.get()
。不幸的是,这只会导致会话不再响应。

python tkinter python-multithreading progress
1个回答
0
投票

没关系。我发现一些额外的东西不是必需的,只是错误的来源。因此,在尝试修复以前的错误时,我引入了新的错误。我删除了

queue
after()
方法,得到了一个更小但更好的版本。

这个版本对我有用:

#-----------------------------------------------------------------------------|
# Import modules
import os, requests
import pandas as pd
import threading as th
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog as fd

#-----------------------------------------------------------------------------|
# Functions

def download(remote_url, local_file):
    data = requests.get(remote_url)
    try:
        with open(local_file, "wb") as f:
            f.write(data.content)
        exit_status = 0
    except:
        exit_status = 1
    return exit_status

#-----------------------------------------------------------------------------|
# GUI

class BulkDownloader(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)
        self.pack(padx = 0, pady = 0)
        self.create_widgets()
        self.master.minsize(300, 50)
        self._current_progress = 0
        self._downloading = False
    
    def create_widgets(self):
        # Variables
        ## Input file
        self.file_name = tk.StringVar()
        self.file_name.set("")
        
        ## Output folder
        self.output_folder = tk.StringVar()
        self.output_folder.set("")
        
        ## Progress bar status
        self.progress_var = tk.DoubleVar()
        self.progress_var.set(0)
        
        # Title
        self.winfo_toplevel().title("Bulk Downloader")
        
        # Display
        self.display_current_in = tk.Entry(self)
        self.display_current_in.grid(row = 0, column = 4, columnspan = 4,
                                  sticky = "EW")
        self.display_current_in["textvariable"] = self.file_name
        
        self.display_current_out = tk.Entry(self)
        self.display_current_out.grid(row = 1, column = 4, columnspan = 4,
                                  sticky = "EW")
        self.display_current_out["textvariable"] = self.output_folder
        
        # Buttons
        ## Select input file
        self.select_in_button = tk.Button(self)
        self.select_in_button["text"] = "Select input file"
        self.select_in_button["command"] = self.select_in_file
        self.select_in_button.grid(row = 0, column = 0, columnspan = 3,
                                   sticky = "EW")
        
        ## Select output directory
        self.select_out_button = tk.Button(self)
        self.select_out_button["text"] = "Select output directory"
        self.select_out_button["command"] = self.select_out_folder
        self.select_out_button.grid(row = 1, column = 0, columnspan = 3,
                                 sticky = "EW")
        
        ## Close main window
        self.cancel_button = tk.Button(self)
        self.cancel_button["text"] = "Cancel"
        self.cancel_button["command"] = self.cancel_all
        self.cancel_button.grid(row = 2, column = 0, columnspan = 3,
                                sticky = "EW")
        
        ## Download files
        self.download_button = tk.Button(self)
        self.download_button["text"] = "Download"
        self.download_button["command"] = self.start_download
        self.download_button.grid(row = 2, column = 4, columnspan = 4,
                                  sticky = "E")
        
        ## Progress bar
        self.progress = ttk.Progressbar(self, variable = self.progress_var,
                                        mode = "determinate")
        self.progress.grid(row = 3, column = 0, columnspan = 38, sticky = "EW")
    
    def select_in_file(self):
        current_selection = self.file_name.get()
        set_dir_to = current_selection if current_selection != "" else "/"
        file_types = (("csv file", "*.csv"), ("all files", "*.*"))
        selection = fd.askopenfilename(title = "Open file",
                                       initialdir = set_dir_to,
                                       filetypes = file_types)
        self.file_name.set(selection)
    
    def select_out_folder(self):
        current_selection = self.output_folder.get()
        set_dir_to = current_selection if current_selection != "" else "/"
        selection = fd.askdirectory(title = "Select destination directory",
                                       initialdir = set_dir_to)
        self.output_folder.set(selection)
    
    def download_files(self):
        local_folder = self.output_folder.get()
        csv_table = self.file_name.get()
        
        table = pd.read_csv(csv_table)
        url_list = table[table.columns[0]].to_list()
        
        for enumerator, item in enumerate(url_list):
            if not self._continue_loop:
                break
            local_file = os.path.join(local_folder, os.path.split(item)[1])
            exit_status = download(item, local_file)
            
            self._current_progress = ((enumerator + 1) * 100) // len(url_list)
            self.progress_var.set(self._current_progress)
        self.finished(exit_status)
    
    def start_download(self):
        self._continue_loop = True
        if not self._downloading:
            self._current_progress = 0
            t = th.Thread(target = self.download_files, daemon = True)
            t.start()
    
    def finished(self, exit_status):
        self._downloading = False
        
        messageWindow = tk.Toplevel(self)
        window_title = "Bulk Downloader finished" if exit_status == 0 else \
            "Bulk download failed"
        messageWindow.title(window_title)
        tk.Label(messageWindow, text = window_title).pack()
        tk.Button(messageWindow, text = "Close window",
                  command = self.master.destroy).pack()
    
    def cancel_all(self):
        self._continue_loop = False
        self._downloading = False
        self.master.destroy()

#-----------------------------------------------------------------------------|
# Run Bulk Downloader
root = tk.Tk()
app = BulkDownloader(root)
app.mainloop()
© www.soinside.com 2019 - 2024. All rights reserved.