SpaceSniffer/SpaceSniffer.py

435 lines
14 KiB
Python
Raw Normal View History

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()