SpaceSniffer/SpaceSniffer.py

381 lines
14 KiB
Python
Raw Normal View History

import os
import threading
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from queue import Queue
from send2trash import send2trash
import subprocess
class FolderScanner(threading.Thread):
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
def run(self):
self.total_items = sum(
[len(files) + len(dirs) for r, dirs, files in os.walk(self.start_path)]
)
self.scan_folder(self.start_path)
self.queue.put(None) # Indicate that scanning is done
def scan_folder(self, folder):
scanned_items = 0
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"))
scanned_items += 1
self.update_progress(scanned_items, folder_path)
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"))
scanned_items += 1
self.update_progress(scanned_items, file_path)
def get_size(self, path):
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
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):
def __init__(self, start_path):
super().__init__()
self.withdraw()
self.title("目录扫描器 V1.0")
self.start_path = start_path
self.queue = Queue()
self.progress_queue = Queue()
self.progress_window = ProgressWindow(self)
self.scanner = FolderScanner(start_path, self.queue, self.progress_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.update_progress()
self.protocol("WM_DELETE_WINDOW", self.on_close)
self.set_window_size()
def populate_root(self):
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):
if not self.is_sorting:
while not self.queue.empty():
item = self.queue.get()
if item is None: # Scanning is done
self.progress_window.destroy()
self.deiconify()
return
path, size, parent, tag = item
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 update_progress(self):
if not self.progress_window.winfo_exists(): # Check if the window still exists
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}")
self.after(100, self.update_progress)
def get_iid(self, path):
return path
def sort_by_size(self):
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):
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):
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):
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))
confirm = messagebox.askyesno("提示", f"你确定要删除 {full_path} 吗?")
if confirm:
try:
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.progress_queue)
self.progress_window = ProgressWindow(self) # Create a new progress window
self.withdraw() # Hide the main window
self.scanner.start()
self.update_tree() # Start updating the tree again
self.update_progress() # Start updating the progress again
def on_close(self):
if self.scanner.is_alive():
self.scanner.join()
self.destroy()
root.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 update_folder_first_var(self):
self.folder_first_var.set(not self.folder_first_var.get())
class ProgressWindow(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
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 # Prevent the user from closing the progress window
if __name__ == "__main__":
root = tk.Tk()
root.withdraw()
start_path = filedialog.askdirectory()
if start_path:
app = App(start_path)
app.mainloop()
else:
messagebox.showinfo("提示", "没有选中任何目录,程序即将退出")
root.destroy()