实现了基本的目录扫描、文件/目录大小统计功能
This commit is contained in:
		
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							@@ -1,2 +1,15 @@
 | 
			
		||||
# SpaceSniffer
 | 
			
		||||
 | 
			
		||||
### 1.软件说明
 | 
			
		||||
一款开源的文件/目录扫描工具,可以直观地展示目录的文件结构以及文件大小
 | 
			
		||||
 | 
			
		||||
### 2.使用方法
 | 
			
		||||
打开软件后选择要扫描的目录,等到扫描完成即可
 | 
			
		||||
更多的使用方法请参考软件自带的帮助文档
 | 
			
		||||
 | 
			
		||||
### 3.软件截图
 | 
			
		||||
待添加
 | 
			
		||||
 | 
			
		||||
### 4.开源相关
 | 
			
		||||
开源协议:GPLv3
 | 
			
		||||
开源地址:https://git.a6.wiki/ahdoawhfo/SpaceSniffer/
 | 
			
		||||
							
								
								
									
										434
									
								
								SpaceSniffer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										434
									
								
								SpaceSniffer.py
									
									
									
									
									
										Normal file
									
								
							@@ -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("<Button-3>", 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()
 | 
			
		||||
		Reference in New Issue
	
	Block a user