SpaceSniffer/SpaceSniffer.py

425 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) # 扫描结束
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,
command=self.update_folder_first_var,
)
self.folder_first_checkbutton.pack(side=tk.LEFT, fill=tk.X)
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.reselect_button = tk.Button(
self, text="重新选择目录", command=self.reselect_directory
)
self.reselect_button.pack(side=tk.LEFT, fill=tk.X)
self.tree.bind("<Double-1>", self.on_double_click)
self.tree.bind("<Return>", self.on_enter_press)
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 on_double_click(self, event): # V1.2 Update当双击的是文件时打开文件
item = self.tree.identify_row(event.y)
if item:
if os.path.isfile(item):
self.open_item(item)
def on_enter_press(self, event): # V1.2 Update当回车的是文件时打开文件
item = self.tree.selection()[0]
if os.path.isfile(item):
self.open_item(item)
def reselect_directory(self): # V1.2 Update重新选择目录
new_path = filedialog.askdirectory()
if new_path:
self.start_path = new_path
self.refresh_tree()
else:
messagebox.showinfo("提示", "没有选中任何目录,程序即将退出")
self.on_close()
def format_size(self, size): # 格式化文件/目录大小
units = ["B", "KB", "MB", "GB", "TB"]
index = 0
while size >= 1024 and index < len(units) - 1:
size /= 1024.0
index += 1
return f"{size:.2f} {units[index]}"
def populate_root(self): # 初始化根目录
size = self.scanner.get_size(self.start_path)
formatted_size = self.format_size(size)
self.tree.insert(
"",
"end",
iid=self.start_path,
text=f"{os.path.basename(self.start_path)} ({formatted_size})",
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
formatted_size = self.format_size(size)
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)} ({formatted_size})",
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): # 获取目录/文件的 iid
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, "Open File")
)
menu.add_command(
label="打开所在目录",
command=lambda: self.open_directory(item, "Open Containing Folder"),
)
menu.add_command(
label="删除文件/目录(放入回收站)",
command=lambda: self.delete_item(item),
)
menu.post(event.x_root, event.y_root)
def open_item(self, item, context_menu_action=None): # 打开文件/目录
name = self.tree.item(item, "text").split(" ")[0]
try:
if os.path.isdir(item):
if context_menu_action == "Open File":
subprocess.Popen(f'explorer "{os.path.realpath(item)}"')
elif context_menu_action == "Open Containing Folder":
parent_folder = os.path.dirname(item)
subprocess.Popen(f'explorer "{os.path.realpath(parent_folder)}"')
else:
os.startfile(item)
except Exception as e:
messagebox.showerror("错误", f"无法打开 {name}\n{e}")
def open_directory(self, item, context_menu_action=None): # 打开所在目录
name = self.tree.item(item, "text").split(" ")[0]
if item == self.start_path:
parent_path = item
else:
parent_path = os.path.dirname(item)
try:
subprocess.Popen(f'explorer "{os.path.realpath(parent_path)}"')
except Exception as e:
messagebox.showerror("错误", f"无法打开 {name}\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): # 刷新树状图
self.is_sorting = False # Reset sorting flag
self.queue.queue.clear()
self.progress_queue.queue.clear()
self.tree.delete(*self.tree.get_children())
self.populate_root()
self.scanner = FolderScanner(self.start_path, self.queue, self.progress_queue)
self.scanner.start()
self.update_tree()
self.update_progress()
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 # 禁止关闭进度窗口
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()