SpaceSniffer/SpaceSniffer.py

435 lines
14 KiB
Python

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