我想在
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()
。不幸的是,这只会导致会话不再响应。
没关系。我发现一些额外的东西不是必需的,只是错误的来源。因此,在尝试修复以前的错误时,我引入了新的错误。我删除了
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()