我的目标是将大量文件从我的文件服务器复制到 USB 磁盘或其他磁盘以进行备份。对于这个任务,我想编写一个程序,使用线程来复制文件,并让用户知道幕后发生了什么。
这是我写的程序:
import tkinter as tk
from tkinter import filedialog
import datetime
import os
import shutil
import threading
from concurrent.futures import ThreadPoolExecutor
count = 0 # Counts copied files
Ex_dirs_list = [] # Excluded directories - those directories won't be copied.
task_counter = 0 # Counter for tracking the number of tasks
counter_lock = threading.Lock() # Lock for updating the counter safely
def copy_file(srcfile, dstfile):
# function to copy file from source to destination
os.makedirs(os.path.dirname(dstfile), exist_ok=True)
shutil.copy2(srcfile, dstfile)
def copy_files_parallel(srcfile, dstfile):
global count
global task_counter
try:
copy_file(srcfile, dstfile)
return True
except Exception as e:
StatusText.insert(tk.INSERT, f"Error copying {srcfile} to {dstfile}: {e}")
StatusText.see("end")
return False
def copy_directory(src_dir, dst_dir):
global count
global task_counter
excluded_dirs_set = set(os.path.basename(x) for x in Ex_dirs_list)
dirs = '\n'.join(Ex_dirs_list)
StatusText.insert(tk.INSERT, f"Coping files from {src_dir} to {dst_dir}.\nIgnores following directories:\n{dirs}\n\n")
StatusText.see("end")
start = datetime.datetime.now()
with ThreadPoolExecutor() as executor:
futures = []
for root, dirs, files in os.walk(src_dir):
base_dir = os.path.basename(root)
if base_dir in excluded_dirs_set:
# Skip excluded directories
StatusText.insert(tk.INSERT, f"Skipping directory: {base_dir}\n")
StatusText.see("end")
continue
dstpath = os.path.join(dst_dir, root[len(src_dir)+1:])
for file in files:
srcfile = os.path.join(root, file)
dstfile = os.path.join(dstpath, file)
if All_files.get() == 1: # If all files checked, copy all the files regardless of file changes
StatusText.insert(tk.INSERT, f"{srcfile} => {dstfile}: All files\n")
StatusText.see("end")
futures.append(executor.submit(copy_files_parallel, srcfile, dstfile))
else:
if os.path.exists(dstfile):
if os.path.getsize(srcfile) != os.path.getsize(dstfile):
StatusText.insert(tk.INSERT, f"{srcfile} => {dstfile}: changed\n")
status_text.see("end")
futures.append(executor.submit(copy_files_parallel, srcfile, dstfile))
else:
StatusText.insert(tk.INSERT, f"{srcfile} => {dstfile}: added\n")
StatusText.see("end")
futures.append(executor.submit(copy_files_parallel, srcfile, dstfile))
finish = datetime.datetime.now()
StatusText.insert(tk.INSERT, f"\nSummary:\n=======\nCopied {count} files\nFinished in {(finish - start).total_seconds()} seconds\n\n")
StatusText.see("end")
def WinBackUp():
t = threading.Thread(target=copy_directory, args=(FromDir.get(), ToDir.get()))
t.start()
def ChooseDir(dir):
global Excluded_dirs
if dir == "src":
FromDir.set(filedialog.askdirectory())
if dir == "dst":
ToDir.set(filedialog.askdirectory())
if dir == "exclude":
directory = filedialog.askdirectory()
if directory in Ex_dirs_list:
StatusText.insert(tk.INSERT, f"Error! Can't choose {directory} more than once\n")
else:
Ex_dirs_list.append(directory)
ExcludeText.delete('1.0',"end")
ExcludeText.insert(tk.END, "\n".join(Ex_dirs_list))
Excluded_dirs = [os.path.basename(x) for x in Ex_dirs_list]
if dir == "delete":
if len(Ex_dirs_list)>=1:
directory = filedialog.askdirectory()
if directory in Ex_dirs_list:
Ex_dirs_list.remove(directory)
ExcludeText.delete('1.0',"end")
ExcludeText.insert(tk.END, "\n".join(Ex_dirs_list))
Excluded_dirs = [os.path.basename(x) for x in Ex_dirs_list]
else:
StatusText.insert(tk.INSERT, f"Error! directory {directory} is not in list\n")
else:
StatusText.insert(tk.INSERT, f"Error! no directory chosen\n")
win = tk.Tk()
win.geometry("910x350")
ToDir = tk.StringVar()
FromDir = tk.StringVar()
All_files = tk.IntVar()
All_files.set(0) # Default: copy only the changed files
TilteLabel = tk.Label(win, text="Smart Backup", font=("Arial", 14)).pack()
FromButton = tk.Button(win, text="From", command=lambda:ChooseDir("src")).place(x=10, y=50)
FromEntry = tk.Entry(win, textvariable=FromDir, width=10)
FromEntry.place(x=60, y=50, height=25)
ToButton = tk.Button(win, text="To", command=lambda: ChooseDir("dst")).place(x=150, y=50)
ToEntry = tk.Entry(win, textvariable=ToDir, width=10)
ToEntry.place(x=180, y=50, height=25)
ExcludeButton = tk.Button(win, text="Excluded folders / files", command=lambda: ChooseDir("exclude")).place(x=10, y=90)
ExcludeText = tk.Text(win)
ExcludeText.place(x=10, y=120, height=155, width=250)
DelDirButton = tk.Button(win, text="Del dir", command=lambda: ChooseDir("delete")).place(x=200, y=90)
StatusLabel = tk.Label(win, text="Status")
StatusText = tk.Text(win)
scroll_bar = tk.Scrollbar(win)
scroll_bar.pack(side=tk.RIGHT)
StatusText.place(x=290, y=70, height=205, width=600)
StatusLabel.place(x=290, y=45)
IsAllFiles = tk.Checkbutton(win, text="All files", variable=All_files)
IsAllFiles.place(x=100, y=300)
BackupButton = tk.Button(win, text="Backup", command=WinBackUp).place(x=200, y=300)
win.mainloop()
因此,程序会复制文件,但更新不是实时的,例如,我在文件复制的文本小部件中看到更新,但该文件在目标中尚不存在。
据我了解,操作系统将管理线程,因此我无法知道会同时复制多少个文件。如果比逐一复制快的话对我有好处(我可以检查一下吗?)
如何将实时更新与当前发生的操作同步,以便当前正在处理的文件将在发生的同时进行更新?
文件复制应用程序中的实时更新意味着将 UI 更新与文件复制任务的完成同步。
+---------------------------------------+
| tkinter GUI |
| +-------------------+ +------------+ |
| | File Selection | | Status Box | |
| +-------------------+ +------------+ |
| |
| [FromDir] --> [copy_directory] |
| | |
| v |
| [ThreadPoolExecutor] |
| | |
| v |
| [copy_files_parallel] |
| | |
| [Real-time status updates in GUI] |
+---------------------------------------+
您当前的实现是异步的:文件复制操作被提交到线程池,并且无需等待这些操作完成即可更新UI。因此,文件复制操作和 UI 更新之间的时间存在差异。
不要直接从
copy_files_parallel
函数将文本插入到状态框中,而是使用 回调机制 仅在文件成功复制后更新 UI。
由于 Tkinter 不是线程安全的,因此您应该在主线程中安排 UI 更新。这可以使用
tkinter.Tk.after
方法 来完成,如此处所示。
将
update_status
函数更改为一个简单的包装器,使用 tk.after
安排实际的 UI 更新。文件复制操作完成后,使用 tk.after
安排 Tkinter 主线程中的更新。
def copy_files_parallel(srcfile, dstfile):
try:
copy_file(srcfile, dstfile)
win.after(0, update_status, f"Successfully copied {srcfile} to {dstfile}")
return True
except Exception as e:
win.after(0, update_status, f"Error copying {srcfile} to {dstfile}: {e}")
return False
def update_status(message):
StatusText.insert(tk.INSERT, message + '\n')
StatusText.see("end")
copy_directory
函数变为:
# Other parts of your function
with ThreadPoolExecutor() as executor:
futures = []
for root, dirs, files in os.walk(src_dir):
# Directory and file processing logic
for file in files:
# File handling logic
futures.append(executor.submit(copy_files_parallel, srcfile, dstfile))
# Rest of your function
提交到线程池的每个文件复制任务将在复制操作完成后通过
tk.after
独立调度自己的 UI 更新。这应该确保所有 UI 更新都发生在主线程中,从而维护 Tkinter 的线程安全。
尝试复制大小文件的混合并观察状态更新。
注意:您还可以实时显示进度,例如进度条或复制的文件总数。