6 Commits
1.0 ... 1.3

View File

@@ -1,100 +1,74 @@
import os import os
import threading import threading
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, filedialog, messagebox
import tkinter.filedialog
from queue import Queue from queue import Queue
from send2trash import send2trash
import subprocess import subprocess
import send2trash
class FolderScanner(threading.Thread): class FolderScanner(threading.Thread):
def __init__(self, start_path, queue): def __init__(self, start_path, queue, progress_queue):
""" threading.Thread.__init__(self)
初始化函数,创建一个线程并设置起始路径和队列。 self.start_path = start_path # 扫描路径
self.queue = queue # 用于存储扫描结果
self.progress_queue = progress_queue # 用于存储扫描进度
self.total_items = 0 # 总共扫描的文件/目录数量
Args: def run(self): # 扫描文件/目录
start_path (str): 起始路径,表示要遍历的文件夹的根目录。 self.total_items = sum(
queue (Queue): 队列对象,用于线程间的通信,存放待处理或已处理过的文件或文件夹路径。 [len(files) + len(dirs) for r, dirs, files in os.walk(self.start_path)]
) # 计算总共扫描的文件/目录数量
self.scan_folder(self.start_path) # 扫描文件/目录
self.queue.put(None) # 扫描结束
Returns: def scan_folder(self, folder): # 扫描文件/目录
None scanned_items = 0 # 已扫描的文件/目录数量
""" for dirpath, dirnames, filenames in os.walk(folder): # 遍历文件/目录
# 使用super()调用父类threading.Thread的__init__方法 for dirname in dirnames: # 遍历目录
super().__init__()
self.start_path = start_path
self.queue = queue
def run(self):
"""
运行扫描文件夹并获取文件信息的操作。
Args:
无参数。
Returns:
无返回值。
"""
self.scan_folder(self.start_path)
def scan_folder(self, folder):
"""
遍历指定文件夹下的所有子文件夹和文件,并将它们的路径、大小、父文件夹路径和类型(文件夹或文件)放入队列中。
Args:
folder (str): 需要遍历的文件夹路径。
Returns:
None.
"""
for dirpath, dirnames, filenames in os.walk(folder):
for dirname in dirnames:
folder_path = os.path.join(dirpath, dirname) folder_path = os.path.join(dirpath, dirname)
size = self.get_size(folder_path) size = self.get_size(folder_path)
self.queue.put((folder_path, size, dirpath, "folder")) self.queue.put((folder_path, size, dirpath, "folder"))
for filename in filenames: scanned_items += 1
self.update_progress(scanned_items, folder_path)
for filename in filenames: # 遍历文件
file_path = os.path.join(dirpath, filename) file_path = os.path.join(dirpath, filename)
size = os.path.getsize(file_path) size = os.path.getsize(file_path)
self.queue.put((file_path, size, dirpath, "file")) self.queue.put((file_path, size, dirpath, "file"))
scanned_items += 1
self.update_progress(scanned_items, file_path)
def get_size(self, path): def get_size(self, path): # 获取文件/目录大小
# 初始化总大小为0
total_size = 0 total_size = 0
# 遍历指定路径下的所有文件和目录
for dirpath, dirnames, filenames in os.walk(path): for dirpath, dirnames, filenames in os.walk(path):
# 遍历当前目录下的所有文件
for f in filenames: for f in filenames:
# 拼接文件的完整路径
fp = os.path.join(dirpath, f) fp = os.path.join(dirpath, f)
# 将文件大小累加到总大小中
total_size += os.path.getsize(fp) total_size += os.path.getsize(fp)
return total_size return total_size
def update_progress(self, scanned_items, current_path): # 更新扫描进度
progress = (scanned_items / self.total_items) * 100
self.progress_queue.put(
(progress, scanned_items, self.total_items, current_path)
)
class App(tk.Tk): class App(tk.Tk):
def __init__(self, start_path): def __init__(self, start_path): # 初始化
"""
初始化目录扫描器窗口
Args:
start_path (str): 开始扫描的目录路径
Returns:
None
"""
super().__init__() super().__init__()
self.title("目录扫描器 V1.0") self.withdraw()
self.title("目录扫描器 V1.3")
self.start_path = start_path self.start_path = start_path
self.queue = Queue() self.queue = Queue()
self.scanner = FolderScanner(start_path, self.queue) self.progress_queue = Queue()
self.progress_window = ProgressWindow(self)
self.scanner = FolderScanner(start_path, self.queue, self.progress_queue)
self.scanner.start() self.scanner.start()
self.style = ttk.Style(self) self.style = ttk.Style(self)
self.style.configure( self.style.configure("Treeview", font=("Helvetica", 10), rowheight=25)
"Treeview", font=("Helvetica", 10), rowheight=25
) # 设置行高
self.style.configure("Treeview.Heading", font=("Helvetica", 12, "bold")) self.style.configure("Treeview.Heading", font=("Helvetica", 12, "bold"))
self.tree = ttk.Treeview(self, style="Treeview") self.tree = ttk.Treeview(self, style="Treeview")
@@ -105,27 +79,14 @@ class App(tk.Tk):
self.folder_first_var = tk.BooleanVar() self.folder_first_var = tk.BooleanVar()
self.folder_first_checkbutton = tk.Checkbutton( self.folder_first_checkbutton = tk.Checkbutton(
self, text="目录优先显示", variable=self.folder_first_var self,
text="目录优先显示",
variable=self.folder_first_var,
command=self.update_folder_first_var,
) )
self.folder_first_checkbutton.pack(side=tk.LEFT, fill=tk.X)
self.folder_first_checkbutton.config(command=self.update_folder_first_var)
self.sort_by_size_button = tk.Button(
self, text="根据目录/文件大小排序", command=self.sort_by_size
)
self.sort_by_size_button.pack(side=tk.LEFT, fill=tk.X)
self.sort_by_name_button = tk.Button(
self, text="根据目录/文件名排序", command=self.sort_by_name
)
self.sort_by_name_button.pack(side=tk.LEFT, fill=tk.X)
self.refresh_button = tk.Button(
self, text="刷新列表", command=self.refresh_tree
)
self.refresh_button.pack(side=tk.LEFT, fill=tk.X)
self.tree.bind("<Double-1>", self.on_double_click)
self.tree.bind("<Return>", self.on_enter_press)
self.tree.bind("<Button-3>", self.show_context_menu) self.tree.bind("<Button-3>", self.show_context_menu)
self.size_sort_order = True self.size_sort_order = True
@@ -134,46 +95,121 @@ class App(tk.Tk):
self.populate_root() self.populate_root()
self.update_tree() self.update_tree()
self.update_progress()
self.protocol("WM_DELETE_WINDOW", self.on_close) self.protocol("WM_DELETE_WINDOW", self.on_close)
self.set_window_size() self.set_window_size()
self.create_menu()
def populate_root(self): def create_menu(self):
""" menu_bar = tk.Menu(self)
在树形控件中填充根节点。 self.config(menu=menu_bar)
Args: file_menu = tk.Menu(menu_bar, tearoff=0)
无参数。 menu_bar.add_cascade(label="文件", menu=file_menu)
file_menu.add_command(label="选择目录", command=self.reselect_directory)
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.on_close)
Returns: view_menu = tk.Menu(menu_bar, tearoff=0)
无返回值。 menu_bar.add_cascade(label="视图", menu=view_menu)
view_menu.add_checkbutton(
label="目录优先显示",
variable=self.folder_first_var,
command=self.update_folder_first_var,
)
view_menu.add_command(
label="根据目录/文件大小排序",
command=self.sort_by_size,
)
view_menu.add_command(
label="根据目录/文件名排序",
command=self.sort_by_size,
)
view_menu.add_command(
label="刷新列表",
command=self.refresh_tree,
)
""" help_menu = tk.Menu(menu_bar, tearoff=0)
size = self.scanner.get_size(self.start_path) / 1024 / 1024 menu_bar.add_cascade(label="帮助", menu=help_menu)
help_menu.add_command(label="帮助", command=self.show_help)
help_menu.add_command(label="更新日志", command=self.show_update_log)
help_menu.add_command(label="关于", command=self.show_about)
def show_help(self):
messagebox.showinfo(
"帮助",
"1. 双击文件夹可以展开/折叠\n",
"2. 双击文件可以直接打开\n",
"3. 右键单击文件/文件夹可以打开、删除文件/文件夹、进入所在目录\n",
"4. 点击左上角文件菜单可以重新选择目录进行扫描\n",
"5. 点击左上角视图菜单可以根据目录/文件大小、目录/文件名排序、刷新列表\n",
)
def show_update_log(self):
messagebox.showinfo(
"更新日志",
"V1.0 实现了目录扫描功能,文件/目录大小统计功能\n",
"\n",
"V1.1 实现了重新选择目录扫描功能,新增了进度条显示\n",
"\n",
"V1.2 实现了双击打开目录、文件的功能,实现了右键菜单\n",
"\n",
"V1.3 优化了界面UI\n",
)
def show_about(self):
messagebox.showinfo("关于", "目录扫描器 V1.3\n作者: ahdoawhfo")
def on_double_click(self, event): # V1.2 Update当双击的是文件时打开文件
item = self.tree.identify_row(event.y)
if item:
if os.path.isfile(item):
self.open_item(item)
def on_enter_press(self, event): # V1.2 Update当回车的是文件时打开文件
item = self.tree.selection()[0]
if os.path.isfile(item):
self.open_item(item)
def reselect_directory(self): # V1.2 Update重新选择目录
new_path = filedialog.askdirectory()
if new_path:
self.start_path = new_path
self.refresh_tree()
def format_size(self, size): # 格式化文件/目录大小
units = ["B", "KB", "MB", "GB", "TB"]
index = 0
while size >= 1024 and index < len(units) - 1:
size /= 1024.0
index += 1
return f"{size:.2f} {units[index]}"
def populate_root(self): # 初始化根目录
size = self.scanner.get_size(self.start_path)
formatted_size = self.format_size(size)
self.tree.insert( self.tree.insert(
"", "",
"end", "end",
iid=self.start_path, iid=self.start_path,
text=f"{os.path.basename(self.start_path)} ({size:.2f} MB)", text=f"{os.path.basename(self.start_path)} ({formatted_size})",
tags=("folder",), tags=("folder",),
open=True, open=True,
) )
def update_tree(self): def update_tree(self): # 更新树状图
"""
更新树形结构,将磁盘文件信息更新到树形视图中。
Args:
Returns:
"""
if not self.is_sorting: if not self.is_sorting:
while not self.queue.empty(): while not self.queue.empty():
path, size, parent, tag = self.queue.get() item = self.queue.get()
size_mb = size / 1024 / 1024 if item is None: # Scanning is done
self.progress_window.destroy()
self.deiconify()
return
path, size, parent, tag = item
formatted_size = self.format_size(size)
parent_iid = self.get_iid(parent) parent_iid = self.get_iid(parent)
if parent == self.start_path: if parent == self.start_path:
parent_iid = self.start_path parent_iid = self.start_path
@@ -182,35 +218,32 @@ class App(tk.Tk):
parent_iid, parent_iid,
"end", "end",
iid=self.get_iid(path), iid=self.get_iid(path),
text=f"{os.path.basename(path)} ({size_mb:.2f} MB)", text=f"{os.path.basename(path)} ({formatted_size})",
tags=(tag,), tags=(tag,),
open=False, open=False,
) )
self.after(100, self.update_tree) self.after(100, self.update_tree)
def get_iid(self, path): def update_progress(self): # 更新扫描进度
""" if not self.progress_window.winfo_exists(): # Check if the window still exists
获取指定路径的iid值 return
while not self.progress_queue.empty():
progress, scanned_items, total_items, current_path = (
self.progress_queue.get()
)
self.progress_window.progress_var.set(progress)
self.progress_window.progress_bar["value"] = progress
self.progress_window.progress_label.config(
text=f"扫描进度: {scanned_items} / {total_items}"
)
self.progress_window.path_label.config(text=f"正在扫描: {current_path}")
Args: self.after(100, self.update_progress)
path (str): 文件或目录的路径
Returns: def get_iid(self, path): # 获取目录/文件的 iid
str: 路径本身作为iid值返回
"""
return path return path
def sort_by_size(self): def sort_by_size(self): # 根据目录/文件大小排序
"""
根据文件或文件夹的大小对文件列表进行排序。
Args:
无参数。
Returns:
无返回值,直接修改当前实例的文件列表。
"""
self.sort_items( self.sort_items(
lambda x: ( lambda x: (
0 if x[1] == "folder" and self.folder_first_var.get() else 1, 0 if x[1] == "folder" and self.folder_first_var.get() else 1,
@@ -224,17 +257,7 @@ class App(tk.Tk):
) )
self.size_sort_order = not self.size_sort_order self.size_sort_order = not self.size_sort_order
def sort_by_name(self): def sort_by_name(self): # 根据目录/文件名排序
"""
根据名称对列表进行排序
Args:
Returns:
无返回值,但会改变对象的内部状态
"""
self.sort_items( self.sort_items(
lambda x: ( lambda x: (
0 if x[1] == "folder" and self.folder_first_var.get() else 1, 0 if x[1] == "folder" and self.folder_first_var.get() else 1,
@@ -244,18 +267,7 @@ class App(tk.Tk):
) )
self.name_sort_order = not self.name_sort_order self.name_sort_order = not self.name_sort_order
def sort_items(self, key, reverse): def sort_items(self, key, reverse): # 排序
"""
对树形控件中的指定目录进行排序。
Args:
key (callable): 用于排序的key函数。
reverse (bool): 是否降序排序。
Returns:
None
"""
if self.folder_first_var.get(): if self.folder_first_var.get():
selected = self.tree.selection() selected = self.tree.selection()
if not selected: if not selected:
@@ -315,24 +327,17 @@ class App(tk.Tk):
for index, item in enumerate(sorted_items): for index, item in enumerate(sorted_items):
self.tree.move(item[2], parent, index) self.tree.move(item[2], parent, index)
def show_context_menu(self, event): def show_context_menu(self, event): # 显示右键菜单
"""
显示上下文菜单
Args:
event (tkinter.Event): 鼠标事件对象
Returns:
None
"""
item = self.tree.identify_row(event.y) item = self.tree.identify_row(event.y)
if item: if item:
self.tree.selection_set(item) self.tree.selection_set(item)
menu = tk.Menu(self, tearoff=0) menu = tk.Menu(self, tearoff=0)
menu.add_command(label="打开文件", command=lambda: self.open_item(item))
menu.add_command( menu.add_command(
label="打开所在目录", command=lambda: self.open_directory(item) label="打开文件", command=lambda: self.open_item(item, "Open File")
)
menu.add_command(
label="打开所在目录",
command=lambda: self.open_directory(item, "Open Containing Folder"),
) )
menu.add_command( menu.add_command(
label="删除文件/目录(放入回收站)", label="删除文件/目录(放入回收站)",
@@ -340,53 +345,63 @@ class App(tk.Tk):
) )
menu.post(event.x_root, event.y_root) menu.post(event.x_root, event.y_root)
def open_item(self, item): def open_item(self, item, context_menu_action=None): # 打开文件/目录
path = self.tree.item(item, "text").split(" ")[0] name = self.tree.item(item, "text").split(" ")[0]
full_path = os.path.join(self.start_path, path)
try: try:
if os.path.isdir(full_path): if os.path.isdir(item):
subprocess.Popen(f'explorer "{os.path.realpath(full_path)}"') if context_menu_action == "Open File":
subprocess.Popen(f'explorer "{os.path.realpath(item)}"')
elif context_menu_action == "Open Containing Folder":
parent_folder = os.path.dirname(item)
subprocess.Popen(f'explorer "{os.path.realpath(parent_folder)}"')
else: else:
os.startfile(full_path) os.startfile(item)
except Exception as e: except Exception as e:
messagebox.showerror("错误", f"无法打开 {full_path}\n{e}") messagebox.showerror("错误", f"无法打开 {name}\n{e}")
def open_directory(self, item): def open_directory(self, item, context_menu_action=None): # 打开所在目录
path = self.tree.item(item, "text").split(" ")[0] name = self.tree.item(item, "text").split(" ")[0]
full_path = os.path.join(self.start_path, path) if item == self.start_path:
if not os.path.isdir(full_path): parent_path = item
full_path = os.path.dirname(full_path) else:
parent_path = os.path.dirname(item)
try: try:
subprocess.Popen(f'explorer "{os.path.realpath(full_path)}"') subprocess.Popen(f'explorer "{os.path.realpath(parent_path)}"')
except Exception as e: except Exception as e:
messagebox.showerror("错误", f"无法打开 {full_path}\n{e}") messagebox.showerror("错误", f"无法打开 {name}\n{e}")
def delete_item(self, item): def delete_item(self, item): # 删除文件/目录
path = self.tree.item(item, "text").split(" ")[0] path = self.tree.item(item, "text").split(" ")[0]
full_path = os.path.normpath( full_path = os.path.normpath(os.path.join(self.start_path, path))
os.path.join(self.start_path, path)
) # 使用os.path.normpath来规范化路径
confirm = messagebox.askyesno("提示", f"你确定要删除 {full_path} 吗?") confirm = messagebox.askyesno("提示", f"你确定要删除 {full_path} 吗?")
if confirm: if confirm:
try: try:
send2trash.send2trash(full_path) send2trash(full_path)
self.tree.delete(item) self.tree.delete(item)
except Exception as e: except Exception as e:
messagebox.showerror("错误", f"无法删除 {full_path}\n{e}") messagebox.showerror("错误", f"无法删除 {full_path}\n{e}")
self.refresh_tree() self.refresh_tree()
def refresh_tree(self): def refresh_tree(self): # 刷新树状图
for item in self.tree.get_children(): self.is_sorting = False
self.tree.delete(item) self.queue.queue.clear()
self.progress_queue.queue.clear()
self.tree.delete(*self.tree.get_children())
self.populate_root() self.populate_root()
self.scanner = FolderScanner(self.start_path, self.queue) self.scanner = FolderScanner(self.start_path, self.queue, self.progress_queue)
self.progress_window = ProgressWindow(self)
self.withdraw()
self.scanner.start() self.scanner.start()
self.update_tree()
self.update_progress()
def on_close(self): def on_close(self): # 关闭程序
if self.scanner.is_alive():
self.scanner.join() self.scanner.join()
self.destroy() self.destroy()
root.destroy()
def set_window_size(self): def set_window_size(self): # 设置窗口大小
screen_width = self.winfo_screenwidth() screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight() screen_height = self.winfo_screenheight()
window_width = int(screen_width * 0.6) window_width = int(screen_width * 0.6)
@@ -396,7 +411,7 @@ class App(tk.Tk):
) )
self.update_font_size() self.update_font_size()
def update_font_size(self): def update_font_size(self): # 更新字体大小
current_font = self.style.lookup("Treeview", "font") current_font = self.style.lookup("Treeview", "font")
font_size = int(current_font.split()[1]) font_size = int(current_font.split()[1])
screen_width = self.winfo_screenwidth() screen_width = self.winfo_screenwidth()
@@ -409,23 +424,38 @@ class App(tk.Tk):
font=(current_font.split()[0], new_font_size + 2, "bold"), font=(current_font.split()[0], new_font_size + 2, "bold"),
) )
def sort_root_items(self, by="size"): def update_folder_first_var(self): # 更新目录优先显示变量
if by == "size": self.folder_first_var.set(not self.folder_first_var.get())
self.sort_by_size()
elif by == "name":
self.sort_by_name()
def update_folder_first_var(self):
if self.folder_first_var.get(): class ProgressWindow(tk.Toplevel): # 进度窗口
self.folder_first_var.set(False) def __init__(self, parent): # 初始化
else: super().__init__(parent)
self.folder_first_var.set(True) self.title("扫描进度")
self.geometry("500x200")
self.label = tk.Label(
self, text="扫描中,请稍等", fg="deepskyblue", font=("Helvetica", 16)
)
self.label.pack(pady=20)
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(
self, variable=self.progress_var, maximum=100
)
self.progress_bar.pack(fill=tk.X, padx=20, pady=10)
self.progress_label = tk.Label(self, text="扫描进度: 0 / 0")
self.progress_label.pack(pady=5)
self.path_label = tk.Label(self, text="正在扫描: ")
self.path_label.pack(pady=5)
self.protocol("WM_DELETE_WINDOW", self.on_close)
def on_close(self):
pass # 禁止关闭进度窗口
if __name__ == "__main__": if __name__ == "__main__":
root = tk.Tk() root = tk.Tk()
root.withdraw() root.withdraw()
start_path = tkinter.filedialog.askdirectory() start_path = filedialog.askdirectory()
if start_path: if start_path:
app = App(start_path) app = App(start_path)
app.mainloop() app.mainloop()