From 7aa83c0e83a5a99958bc6088b149a0724c242dd5 Mon Sep 17 00:00:00 2001 From: ahdoawhfo Date: Sun, 23 Jun 2024 22:46:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=86=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=E7=9B=AE=E5=BD=95=E6=89=AB=E6=8F=8F=E3=80=81=E6=96=87?= =?UTF-8?q?=E4=BB=B6/=E7=9B=AE=E5=BD=95=E5=A4=A7=E5=B0=8F=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++ SpaceSniffer.py | 434 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100644 SpaceSniffer.py diff --git a/README.md b/README.md index 04db9ea..aecf827 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ # SpaceSniffer +### 1.软件说明 +一款开源的文件/目录扫描工具,可以直观地展示目录的文件结构以及文件大小 + +### 2.使用方法 +打开软件后选择要扫描的目录,等到扫描完成即可 +更多的使用方法请参考软件自带的帮助文档 + +### 3.软件截图 +待添加 + +### 4.开源相关 +开源协议:GPLv3 +开源地址:https://git.a6.wiki/ahdoawhfo/SpaceSniffer/ \ No newline at end of file diff --git a/SpaceSniffer.py b/SpaceSniffer.py new file mode 100644 index 0000000..f6633ec --- /dev/null +++ b/SpaceSniffer.py @@ -0,0 +1,434 @@ +import os +import threading +import tkinter as tk +from tkinter import ttk, messagebox +import tkinter.filedialog +from queue import Queue +import subprocess +import send2trash + + +class FolderScanner(threading.Thread): + def __init__(self, start_path, queue): + """ + 初始化函数,创建一个线程并设置起始路径和队列。 + + Args: + start_path (str): 起始路径,表示要遍历的文件夹的根目录。 + queue (Queue): 队列对象,用于线程间的通信,存放待处理或已处理过的文件或文件夹路径。 + + Returns: + None + """ + # 使用super()调用父类threading.Thread的__init__方法 + 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) + size = self.get_size(folder_path) + self.queue.put((folder_path, size, dirpath, "folder")) + for filename in filenames: + file_path = os.path.join(dirpath, filename) + size = os.path.getsize(file_path) + self.queue.put((file_path, size, dirpath, "file")) + + def get_size(self, path): + # 初始化总大小为0 + total_size = 0 + # 遍历指定路径下的所有文件和目录 + for dirpath, dirnames, filenames in os.walk(path): + # 遍历当前目录下的所有文件 + for f in filenames: + # 拼接文件的完整路径 + fp = os.path.join(dirpath, f) + # 将文件大小累加到总大小中 + total_size += os.path.getsize(fp) + return total_size + + +class App(tk.Tk): + def __init__(self, start_path): + """ + 初始化目录扫描器窗口 + + Args: + start_path (str): 开始扫描的目录路径 + + Returns: + None + """ + super().__init__() + self.title("目录扫描器 V1.0") + self.start_path = start_path + self.queue = Queue() + self.scanner = FolderScanner(start_path, self.queue) + self.scanner.start() + + self.style = ttk.Style(self) + self.style.configure( + "Treeview", font=("Helvetica", 10), rowheight=25 + ) # 设置行高 + self.style.configure("Treeview.Heading", font=("Helvetica", 12, "bold")) + + self.tree = ttk.Treeview(self, style="Treeview") + self.tree.heading("#0", text="目录/文件名(目录/文件大小)", anchor="w") + self.tree.tag_configure("folder", foreground="orange") + self.tree.tag_configure("file", foreground="black") + self.tree.pack(fill=tk.BOTH, expand=True) + + self.folder_first_var = tk.BooleanVar() + self.folder_first_checkbutton = tk.Checkbutton( + self, text="目录优先显示", variable=self.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("", self.show_context_menu) + + self.size_sort_order = True + self.name_sort_order = True + self.is_sorting = False + + self.populate_root() + self.update_tree() + + self.protocol("WM_DELETE_WINDOW", self.on_close) + self.set_window_size() + + def populate_root(self): + """ + 在树形控件中填充根节点。 + + Args: + 无参数。 + + Returns: + 无返回值。 + + """ + size = self.scanner.get_size(self.start_path) / 1024 / 1024 + self.tree.insert( + "", + "end", + iid=self.start_path, + text=f"{os.path.basename(self.start_path)} ({size:.2f} MB)", + tags=("folder",), + open=True, + ) + + def update_tree(self): + """ + 更新树形结构,将磁盘文件信息更新到树形视图中。 + + Args: + 无 + + Returns: + 无 + + """ + if not self.is_sorting: + while not self.queue.empty(): + path, size, parent, tag = self.queue.get() + size_mb = size / 1024 / 1024 + parent_iid = self.get_iid(parent) + if parent == self.start_path: + parent_iid = self.start_path + if not self.tree.exists(self.get_iid(path)): + self.tree.insert( + parent_iid, + "end", + iid=self.get_iid(path), + text=f"{os.path.basename(path)} ({size_mb:.2f} MB)", + tags=(tag,), + open=False, + ) + self.after(100, self.update_tree) + + def get_iid(self, path): + """ + 获取指定路径的iid值 + + Args: + path (str): 文件或目录的路径 + + Returns: + str: 路径本身作为iid值返回 + """ + return path + + def sort_by_size(self): + """ + 根据文件或文件夹的大小对文件列表进行排序。 + + Args: + 无参数。 + + Returns: + 无返回值,直接修改当前实例的文件列表。 + + """ + self.sort_items( + lambda x: ( + 0 if x[1] == "folder" and self.folder_first_var.get() else 1, + ( + -float(x[0].split()[-2].replace("(", "")) + if self.size_sort_order + else float(x[0].split()[-2].replace("(", "")) + ), + ), + False, + ) + self.size_sort_order = not self.size_sort_order + + def sort_by_name(self): + """ + 根据名称对列表进行排序 + + Args: + 无 + + Returns: + 无返回值,但会改变对象的内部状态 + + """ + self.sort_items( + lambda x: ( + 0 if x[1] == "folder" and self.folder_first_var.get() else 1, + x[0].lower(), + ), + self.name_sort_order, + ) + self.name_sort_order = not self.name_sort_order + + def sort_items(self, key, reverse): + """ + 对树形控件中的指定目录进行排序。 + + Args: + key (callable): 用于排序的key函数。 + reverse (bool): 是否降序排序。 + + Returns: + None + + """ + if self.folder_first_var.get(): + selected = self.tree.selection() + if not selected: + messagebox.showinfo("提示", "请选择你要排序的目录") + return + + self.scanner.join() + self.is_sorting = True + + for parent in selected: + children = self.tree.get_children(parent) + folder_items = [] + file_items = [] + + for child in children: + item = ( + self.tree.item(child)["text"], + self.tree.item(child)["tags"][0], + child, + ) + if item[1] == "folder": + folder_items.append(item) + else: + file_items.append(item) + + sorted_items = sorted(folder_items, key=key, reverse=reverse) + sorted( + file_items, key=key, reverse=reverse + ) + + for item in sorted_items: + self.tree.detach(item[2]) + for index, item in enumerate(sorted_items): + self.tree.move(item[2], parent, index) + else: + selected = self.tree.selection() + if not selected: + messagebox.showinfo("提示", "请选择你要排序的目录") + return + + self.scanner.join() + self.is_sorting = True + + for parent in selected: + children = self.tree.get_children(parent) + items = [ + ( + self.tree.item(child)["text"], + self.tree.item(child)["tags"][0], + child, + ) + for child in children + ] + sorted_items = sorted(items, key=key, reverse=reverse) + + for item in sorted_items: + self.tree.detach(item[2]) + for index, item in enumerate(sorted_items): + self.tree.move(item[2], parent, index) + + def show_context_menu(self, event): + """ + 显示上下文菜单 + + Args: + event (tkinter.Event): 鼠标事件对象 + + Returns: + None + + """ + item = self.tree.identify_row(event.y) + if item: + self.tree.selection_set(item) + menu = tk.Menu(self, tearoff=0) + menu.add_command(label="打开文件", command=lambda: self.open_item(item)) + menu.add_command( + label="打开所在目录", command=lambda: self.open_directory(item) + ) + menu.add_command( + label="删除文件/目录(放入回收站)", + command=lambda: self.delete_item(item), + ) + menu.post(event.x_root, event.y_root) + + def open_item(self, item): + path = self.tree.item(item, "text").split(" ")[0] + full_path = os.path.join(self.start_path, path) + try: + if os.path.isdir(full_path): + subprocess.Popen(f'explorer "{os.path.realpath(full_path)}"') + else: + os.startfile(full_path) + except Exception as e: + messagebox.showerror("错误", f"无法打开 {full_path}\n{e}") + + def open_directory(self, item): + path = self.tree.item(item, "text").split(" ")[0] + full_path = os.path.join(self.start_path, path) + if not os.path.isdir(full_path): + full_path = os.path.dirname(full_path) + try: + subprocess.Popen(f'explorer "{os.path.realpath(full_path)}"') + except Exception as e: + messagebox.showerror("错误", f"无法打开 {full_path}\n{e}") + + def delete_item(self, item): + path = self.tree.item(item, "text").split(" ")[0] + full_path = os.path.normpath( + os.path.join(self.start_path, path) + ) # 使用os.path.normpath来规范化路径 + confirm = messagebox.askyesno("提示", f"你确定要删除 {full_path} 吗?") + if confirm: + try: + send2trash.send2trash(full_path) + self.tree.delete(item) + except Exception as e: + messagebox.showerror("错误", f"无法删除 {full_path}\n{e}") + self.refresh_tree() + + def refresh_tree(self): + for item in self.tree.get_children(): + self.tree.delete(item) + self.populate_root() + self.scanner = FolderScanner(self.start_path, self.queue) + self.scanner.start() + + def on_close(self): + self.scanner.join() + self.destroy() + + def set_window_size(self): + screen_width = self.winfo_screenwidth() + screen_height = self.winfo_screenheight() + window_width = int(screen_width * 0.6) + window_height = int(screen_height * 0.6) + self.geometry( + f"{window_width}x{window_height}+{screen_width//2 - window_width//2}+{screen_height//2 - window_height//2}" + ) + self.update_font_size() + + def update_font_size(self): + current_font = self.style.lookup("Treeview", "font") + font_size = int(current_font.split()[1]) + screen_width = self.winfo_screenwidth() + new_font_size = int(screen_width / 100) + if new_font_size != font_size: + new_font = (current_font.split()[0], new_font_size) + self.style.configure("Treeview", font=new_font) + self.style.configure( + "Treeview.Heading", + font=(current_font.split()[0], new_font_size + 2, "bold"), + ) + + def sort_root_items(self, by="size"): + if by == "size": + self.sort_by_size() + elif by == "name": + self.sort_by_name() + + def update_folder_first_var(self): + if self.folder_first_var.get(): + self.folder_first_var.set(False) + else: + self.folder_first_var.set(True) + + +if __name__ == "__main__": + root = tk.Tk() + root.withdraw() + start_path = tkinter.filedialog.askdirectory() + if start_path: + app = App(start_path) + app.mainloop() + else: + messagebox.showinfo("提示", "没有选中任何目录,程序即将退出") + root.destroy()