A-A+

高级文件查重工具 (Pro Version) – python代码

2026年01月06日 10:54 学习笔记 暂无评论 共21230字 (阅读223 views次)

【注意:此文章为博主原创文章!转载需注意,请带原文链接,至少也要是txt格式!】

网上有很多电脑文件查重工具,有些没问题,有些怕有病毒,自己识别起来很麻烦,就想着快速利用AI造轮子,虽然有些重复造轮子,但是最起码也放心。分别用了GPT-5.2-Thinking、Gemini-3-Pro,对于GPT的输入内容如下:

##我的电脑有很多文件,有的很大比如几个GB,有的相对比较小。请用python的tkinter帮我写一个界面化的操作工具,它的主要功能:
1、可以添加多个指定的文件夹或盘符;
2、有一个检索按钮、一个停止按钮,如果点击检索按钮,程序会检索所有指定地址里面所有的文件,注意只要有文件与其它文件是一样的就列出这些重复文件;当点击停止按钮,程序就会停止检索;
3、有一个最小文件设定,有一个最大文件设定,可以自由设定检索的最小及最大文件的大小;
4、有一个框,这个框里面列出了所有的重复文件。第一列是选择框、第二列是文件名、第三列是每个文件的创建日期、第四类是文件的修改日期,最后一列是文件的路径地址;
5、当鼠标右键选中文件,则可以弹出多个选项,如"打开",则打开文件所在的目录;"选择",则这个文件前面的选择框变为选中状态;"删除",则从磁盘中删除这个文件;"属性",则弹出此文件的属性。
6、有一个"选择"按钮,当鼠标放在上面后,会提示"如果点击此按钮,则默认会选择所有日志较远的文件",当点击此按钮时,则所有的重复文件,仅保留日期最新的,其它文件的选择框都变为选中状态。
7、可点击取消勾选,当点击后,所有是勾选状态的文件都取消勾选
8、有一个删除按钮,当点击后,弹出"确认","取消"按钮,并提示"当您点击确认则会删除所有选中文件",如果当用户点击确认则删除所有选中文件,不进入回收站直接删除。
9、同时并在关键位置加入了较完整注释,方便你理解与二次修改。
10、创建时间严格按 Windows“创建时间”:用 ctypes + GetFileTime 获取(比 os.path.getctime 更明确)。
11、打开目录并选中文件:右键“打开(定位并选中)”调用 explorer /select,"path"。
12、更快的重复判断(三段式):
先按文件大小分组
对大文件先算采样哈希(首 1MB + 尾 1MB + size)
采样一致才算全量 SHA-256(避免对大量大文件都全量读)
13、可停止:扫描与哈希阶段都会频繁检查 stop_event,停止更及时。
14、右键菜单增强:打开文件、打开(定位并选中)、复制路径、属性、删除等
15、导出 CSV:方便留档与人工复核
16、删除安全性:删除前二次确认;删除后自动从列表移除;失败会汇总报错
17、可以自定义多个排除文件类型,如后缀是.dll等等,当有定义后,这些文件将不进行扫描
18、可以自定义选择后缀文件类型,只搜索这些类型的文件是否重复,此功能大于排除文件类型

##基于以上需求,请你自行分析整理,希望你帮我按你的想法优化、增强,再给出完整的代码。

 

结果运行它们俩给的代码,发现差距真心蛮大的。

 

 

GPT的外观如下:

images

Gemini运行后的软件外观如下:

images

外关其实也还好,相对来说还好,大差不差,主要是软件运行的速度,差距可不是一星半点。同样的设定搜索,Gemini的速度非常的快,GPT的时间可以说是Gemini的2倍。

 

不废话了,给出Gemini的代码:

 

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import hashlib
import threading
import datetime
import subprocess
import ctypes
from ctypes import wintypes
import csv
import queue

# =============================================================================
# 第一部分:Windows API 封装 (Ctypes)
# =============================================================================

try:
    kernel32 = ctypes.windll.kernel32
    shell32 = ctypes.windll.shell32


    class FILETIME(ctypes.Structure):
        _fields_ = [("dwLowDateTime", wintypes.DWORD),
                    ("dwHighDateTime", wintypes.DWORD)]


    def get_file_handle(path):
        GENERIC_READ = 0x80000000
        FILE_SHARE_READ = 0x00000001
        OPEN_EXISTING = 3
        FILE_ATTRIBUTE_NORMAL = 128

        return kernel32.CreateFileW(
            wintypes.LPCWSTR(path),
            GENERIC_READ,
            FILE_SHARE_READ,
            None,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            None
        )


    def close_handle(handle):
        kernel32.CloseHandle(handle)


    def filetime_to_dt(ft):
        quad = (ft.dwHighDateTime << 32) | ft.dwLowDateTime
        ts = (quad - 116444736000000000) / 10000000
        try:
            return datetime.datetime.fromtimestamp(ts)
        except OSError:
            return datetime.datetime(1970, 1, 1)


    def get_win_creation_time(path):
        h_file = get_file_handle(path)
        if h_file == -1:
            return datetime.datetime.fromtimestamp(os.path.getctime(path))

        ctime = FILETIME()
        mtime = FILETIME()
        atime = FILETIME()

        success = kernel32.GetFileTime(h_file, ctypes.byref(ctime), ctypes.byref(atime), ctypes.byref(mtime))
        close_handle(h_file)

        if success:
            return filetime_to_dt(ctime)
        else:
            return datetime.datetime.fromtimestamp(os.path.getctime(path))


    def show_properties_dialog(path):
        class SHELLEXECUTEINFO(ctypes.Structure):
            _fields_ = [
                ("cbSize", wintypes.DWORD),
                ("fMask", wintypes.ULONG),
                ("hwnd", wintypes.HWND),
                ("lpVerb", wintypes.LPCWSTR),
                ("lpFile", wintypes.LPCWSTR),
                ("lpParameters", wintypes.LPCWSTR),
                ("lpDirectory", wintypes.LPCWSTR),
                ("nShow", wintypes.INT),
                ("hInstApp", wintypes.HINSTANCE),
                ("lpIDList", ctypes.c_void_p),
                ("lpClass", wintypes.LPCWSTR),
                ("hkeyClass", wintypes.HKEY),
                ("dwHotKey", wintypes.DWORD),
                ("hIcon", wintypes.HANDLE),
                ("hProcess", wintypes.HANDLE),
            ]

        sei = SHELLEXECUTEINFO()
        sei.cbSize = ctypes.sizeof(sei)
        sei.fMask = 0x0000000C
        sei.lpVerb = "properties"
        sei.lpFile = path
        sei.nShow = 1
        shell32.ShellExecuteExW(ctypes.byref(sei))

except Exception as e:
    print(f"Windows API 加载告警: {e}")
    get_win_creation_time = lambda p: datetime.datetime.fromtimestamp(os.path.getctime(p))
    show_properties_dialog = lambda p: None


# =============================================================================
# 第二部分:UI 辅助类
# =============================================================================

class Tooltip:
    def __init__(self, widget, text):
        self.widget = widget
        self.text = text
        self.tooltip = None
        self.widget.bind("<Enter>", self.enter)
        self.widget.bind("<Leave>", self.leave)

    def enter(self, event=None):
        x, y, _, _ = self.widget.bbox("insert")
        x += self.widget.winfo_rootx() + 25
        y += self.widget.winfo_rooty() + 25

        self.tooltip = tk.Toplevel(self.widget)
        self.tooltip.wm_overrideredirect(True)
        self.tooltip.wm_geometry(f"+{x}+{y}")

        label = tk.Label(self.tooltip, text=self.text, background="#ffffe0", relief="solid", borderwidth=1,
                         justify="left")
        label.pack()

    def leave(self, event=None):
        if self.tooltip:
            self.tooltip.destroy()
            self.tooltip = None


# =============================================================================
# 第三部分:核心逻辑 (后端处理)
# =============================================================================

class DuplicateFinder:
    def __init__(self):
        self.stop_event = threading.Event()
        self.progress_queue = queue.Queue()

    def get_chunk_hash(self, filepath, size):
        """采样哈希:头4KB + 尾4KB + 大小"""
        try:
            hasher = hashlib.md5()
            hasher.update(str(size).encode('utf-8'))

            with open(filepath, 'rb') as f:
                chunk_head = f.read(4096)
                hasher.update(chunk_head)
                if size > 8192:
                    f.seek(-4096, 2)
                    chunk_tail = f.read(4096)
                    hasher.update(chunk_tail)
            return hasher.hexdigest()
        except Exception:
            return None

    def get_full_hash(self, filepath):
        """全量哈希"""
        hasher = hashlib.sha256()
        try:
            with open(filepath, 'rb') as f:
                while True:
                    if self.stop_event.is_set(): return None
                    buf = f.read(65536)
                    if not buf: break
                    hasher.update(buf)
            return hasher.hexdigest()
        except Exception:
            return None

    def scan(self, paths, min_size, max_size, exclude_exts, include_exts):
        """
        新增参数 include_exts: 包含后缀列表(白名单)
        """
        try:
            self.stop_event.clear()

            # --- 阶段 1: 扫描文件 ---
            size_groups = {}
            scanned_count = 0
            self.progress_queue.put(("status", "阶段 1/3: 正在扫描文件结构..."))

            for path in paths:
                for root, dirs, files in os.walk(path):
                    if self.stop_event.is_set(): return None

                    for file in files:
                        _, ext = os.path.splitext(file)
                        ext_lower = ext.lower()

                        # --- 核心修改:筛选逻辑 ---
                        # 1. 优先判断白名单 (include_exts)
                        if include_exts:
                            # 如果定义了白名单,且当前后缀不在白名单中,跳过
                            if ext_lower not in include_exts:
                                continue
                        # 2. 如果白名单为空,则判断黑名单 (exclude_exts)
                        elif ext_lower in exclude_exts:
                            continue
                        # -------------------------

                        filepath = os.path.join(root, file)
                        try:
                            if os.path.islink(filepath): continue
                            size = os.path.getsize(filepath)

                            if min_size <= size <= max_size:
                                if size not in size_groups: size_groups[size] = []
                                size_groups[size].append(filepath)
                                scanned_count += 1
                                if scanned_count % 1000 == 0:
                                    self.progress_queue.put(("status", f"已扫描 {scanned_count} 个文件..."))
                        except (OSError, PermissionError):
                            continue

            candidates = {s: p for s, p in size_groups.items() if len(p) > 1}
            total_groups = len(candidates)

            # --- 阶段 2: 采样哈希 ---
            self.progress_queue.put(("status", f"阶段 2/3: 快速采样比对 {total_groups} 组文件..."))
            partial_hash_groups = {}
            processed_groups = 0

            for size, file_paths in candidates.items():
                if self.stop_event.is_set(): return None

                temp_map = {}
                for fp in file_paths:
                    p_hash = self.get_chunk_hash(fp, size)
                    if p_hash:
                        if p_hash not in temp_map: temp_map[p_hash] = []
                        temp_map[p_hash].append(fp)

                for h, paths in temp_map.items():
                    if len(paths) > 1: partial_hash_groups[h] = paths

                processed_groups += 1
                if processed_groups % 50 == 0:
                    self.progress_queue.put(("status", f"快速比对进度: {processed_groups}/{total_groups} 组"))

            # --- 阶段 3: 全量哈希 ---
            final_groups = {}
            total_checks = len(partial_hash_groups)
            current_check = 0
            self.progress_queue.put(("status", f"阶段 3/3: 正在进行全量数据校验 ({total_checks} 组)..."))

            for _, paths in partial_hash_groups.items():
                if self.stop_event.is_set(): return None

                temp_map = {}
                for fp in paths:
                    if self.stop_event.is_set(): return None
                    full_h = self.get_full_hash(fp)
                    if full_h:
                        if full_h not in temp_map: temp_map[full_h] = []
                        try:
                            stats = os.stat(fp)
                            ctime = get_win_creation_time(fp)
                            mtime = datetime.datetime.fromtimestamp(stats.st_mtime)

                            temp_map[full_h].append({
                                'path': fp,
                                'filename': os.path.basename(fp),
                                'size': stats.st_size,
                                'ctime': ctime,
                                'mtime': mtime
                            })
                        except OSError:
                            pass

                for h, details in temp_map.items():
                    if len(details) > 1: final_groups[h] = details

                current_check += 1
                if current_check % 10 == 0:
                    self.progress_queue.put(("progress", (current_check / total_checks) * 100))

            return final_groups

        except Exception as e:
            self.progress_queue.put(("error", str(e)))
            return None


# =============================================================================
# 第四部分:GUI 主界面
# =============================================================================

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("高级文件查重工具 (Pro Version)")
        self.geometry("1200x850")

        self.style = ttk.Style()
        self.style.theme_use('clam')
        self.style.configure("Treeview", rowheight=28, font=('Segoe UI', 9))
        self.style.configure("Treeview.Heading", font=('Segoe UI', 9, 'bold'))

        self.logic = DuplicateFinder()
        self.search_paths = []
        self.exclude_exts = set()
        self.include_exts = set()  # 新增:白名单集合
        self.results = {}

        self.setup_ui()
        self.check_queue()

    def setup_ui(self):
        # --- 区域1: 顶部路径设置 ---
        top_frame = tk.Frame(self, pady=5)
        top_frame.pack(fill="x", padx=10)

        lf_path = tk.LabelFrame(top_frame, text="第一步:选择扫描位置")
        lf_path.pack(side="left", fill="both", expand=True, padx=5)

        self.lb_paths = tk.Listbox(lf_path, height=4, width=50)
        self.lb_paths.pack(side="left", fill="both", expand=True, padx=5, pady=5)

        btn_box = tk.Frame(lf_path)
        btn_box.pack(side="right", fill="y", padx=5)
        tk.Button(btn_box, text="添加文件夹", command=self.add_dir).pack(fill="x", pady=2)
        tk.Button(btn_box, text="添加磁盘", command=self.add_drive).pack(fill="x", pady=2)
        tk.Button(btn_box, text="清空列表", command=self.clear_dirs).pack(fill="x", pady=2)

        # --- 区域2: 过滤设置 ---
        lf_settings = tk.LabelFrame(top_frame, text="第二步:过滤规则")
        lf_settings.pack(side="left", fill="both", padx=5)

        # 大小设置
        f_size = tk.Frame(lf_settings)
        f_size.pack(fill="x", padx=5, pady=2)
        tk.Label(f_size, text="最小(KB):").pack(side="left")
        self.ent_min = tk.Entry(f_size, width=8)
        self.ent_min.insert(0, "0")
        self.ent_min.pack(side="left", padx=5)

        tk.Label(f_size, text="最大(MB):").pack(side="left")
        self.ent_max = tk.Entry(f_size, width=8)
        self.ent_max.insert(0, "0")
        self.ent_max.pack(side="left", padx=5)

        # 排除后缀
        f_ext = tk.Frame(lf_settings)
        f_ext.pack(fill="x", padx=5, pady=2)
        tk.Label(f_ext, text="排除后缀:").pack(side="left")
        self.ent_exclude = tk.Entry(f_ext, width=20)
        self.ent_exclude.insert(0, ".dll;.sys;.tmp")
        self.ent_exclude.pack(side="left", padx=5)
        Tooltip(self.ent_exclude, "黑名单:这些后缀将直接跳过。\n例如: .dll;.exe\n(如果下方'只搜后缀'有值,则此项失效)")

        # [新增] 只搜后缀
        f_inc = tk.Frame(lf_settings)
        f_inc.pack(fill="x", padx=5, pady=2)
        tk.Label(f_inc, text="只搜后缀:").pack(side="left")
        self.ent_include = tk.Entry(f_inc, width=20)
        self.ent_include.pack(side="left", padx=5)
        Tooltip(self.ent_include,
                "白名单:优先级高于排除后缀。\n如果有值(如 .jpg;.png),程序将仅扫描这些类型,忽略其他所有文件。")

        # --- 区域3: 核心操作栏 ---
        action_frame = tk.Frame(self, pady=5)
        action_frame.pack(fill="x", padx=10)

        self.btn_search = tk.Button(action_frame, text=" 开始检索", command=self.start_search, bg="#e1f5fe",
                                    font=('Segoe UI', 10, 'bold'), width=15)
        self.btn_search.pack(side="left", padx=5)

        self.btn_stop = tk.Button(action_frame, text="⛔ 停止", command=self.stop_search, state="disabled", bg="#ffcdd2",
                                  width=10)
        self.btn_stop.pack(side="left", padx=5)

        self.lbl_status = tk.Label(action_frame, text="准备就绪", fg="gray")
        self.lbl_status.pack(side="left", padx=20)

        self.progress = ttk.Progressbar(action_frame, orient="horizontal", length=300, mode="determinate")
        self.progress.pack(side="right", padx=10)

        # --- 区域4: 结果列表 ---
        result_frame = tk.LabelFrame(self, text="检索结果")
        result_frame.pack(fill="both", expand=True, padx=10, pady=5)

        columns = ("check", "filename", "created", "modified", "size", "path")
        self.tree = ttk.Treeview(result_frame, columns=columns, show="headings", selectmode="extended")

        self.tree.heading("check", text="选择", command=lambda: self.sort_column("check", False))
        self.tree.column("check", width=50, anchor="center", stretch=False)
        self.tree.heading("filename", text="文件名", command=lambda: self.sort_column("filename", False))
        self.tree.column("filename", width=200)
        self.tree.heading("created", text="创建时间 (Windows)", command=lambda: self.sort_column("created", False))
        self.tree.column("created", width=150, anchor="center")
        self.tree.heading("modified", text="修改时间", command=lambda: self.sort_column("modified", False))
        self.tree.column("modified", width=150, anchor="center")
        self.tree.heading("size", text="大小", command=lambda: self.sort_column("size", False))
        self.tree.column("size", width=80, anchor="e")
        self.tree.heading("path", text="完整路径", command=lambda: self.sort_column("path", False))
        self.tree.column("path", width=400)

        vsb = ttk.Scrollbar(result_frame, orient="vertical", command=self.tree.yview)
        hsb = ttk.Scrollbar(result_frame, orient="horizontal", command=self.tree.xview)
        self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)

        self.tree.grid(row=0, column=0, sticky='nsew')
        vsb.grid(row=0, column=1, sticky='ns')
        hsb.grid(row=1, column=0, sticky='ew')
        result_frame.grid_columnconfigure(0, weight=1)
        result_frame.grid_rowconfigure(0, weight=1)

        self.tree.bind("<Button-1>", self.on_click)
        self.tree.bind("<Button-3>", self.show_context_menu)

        # --- 区域5: 底部功能区 ---
        bottom_frame = tk.Frame(self, pady=10)
        bottom_frame.pack(fill="x", padx=10)

        self.btn_smart = tk.Button(bottom_frame, text="✨ 智能选择 (保留最新)", command=self.smart_select)
        self.btn_smart.pack(side="left", padx=5)
        Tooltip(self.btn_smart,
                "程序将自动分析每一组重复文件。\n逻辑:保留【修改时间】最新的那一个,勾选其他所有旧文件以便删除。")

        self.btn_clear = tk.Button(bottom_frame, text="❌ 清空选择", command=self.clear_selection)
        self.btn_clear.pack(side="left", padx=5)
        Tooltip(self.btn_clear, "取消勾选所有文件")

        tk.Button(bottom_frame, text=" 导出 CSV", command=self.export_csv).pack(side="left", padx=5)

        self.btn_delete = tk.Button(bottom_frame, text="️ 删除选中文件", command=self.delete_files, bg="#d32f2f",
                                    fg="white", font=('Segoe UI', 10, 'bold'))
        self.btn_delete.pack(side="right", padx=5)
        Tooltip(self.btn_delete, "物理删除所有打钩的文件,不进回收站!\n删除前会进行二次确认。")

        self.context_menu = tk.Menu(self, tearoff=0)
        self.context_menu.add_command(label="打开文件", command=self.ctx_open_file)
        self.context_menu.add_command(label="打开所在目录 (定位)", command=self.ctx_open_folder)
        self.context_menu.add_separator()
        self.context_menu.add_command(label="选中 / 取消选中", command=self.ctx_toggle_check)
        self.context_menu.add_command(label="复制完整路径", command=self.ctx_copy_path)
        self.context_menu.add_separator()
        self.context_menu.add_command(label="属性", command=self.ctx_properties)
        self.context_menu.add_command(label="立即从磁盘删除", command=self.ctx_delete_single)

    # =========================================================================
    # 事件处理逻辑
    # =========================================================================

    def add_dir(self):
        d = filedialog.askdirectory()
        if d and d not in self.search_paths:
            self.search_paths.append(d)
            self.lb_paths.insert(tk.END, d)

    def add_drive(self):
        drives = [f"{d}:\\" for d in "DEFGHIJKLMNOPQRSTUVWXYZ" if os.path.exists(f"{d}:")]
        if not drives:
            messagebox.showinfo("提示", "未检测到额外的数据盘符")
            return

        def select_drive(d):
            if d not in self.search_paths:
                self.search_paths.append(d)
                self.lb_paths.insert(tk.END, d)
            top.destroy()

        top = tk.Toplevel(self)
        top.title("选择盘符")
        top.geometry("300x200")
        for d in drives:
            tk.Button(top, text=d, command=lambda x=d: select_drive(x)).pack(fill="x", pady=2)

    def clear_dirs(self):
        self.search_paths = []
        self.lb_paths.delete(0, tk.END)

    def start_search(self):
        if not self.search_paths:
            messagebox.showwarning("警告", "请至少添加一个扫描目录")
            return

        try:
            min_kb = float(self.ent_min.get())
            max_mb = float(self.ent_max.get())
            min_size = min_kb * 1024
            max_size = max_mb * 1024 * 1024 if max_mb > 0 else float('inf')

            # 解析排除后缀
            raw_ex = self.ent_exclude.get().split(';')
            self.exclude_exts = {e.strip().lower() for e in raw_ex if e.strip()}

            # [新增] 解析只搜后缀
            raw_in = self.ent_include.get().split(';')
            self.include_exts = {e.strip().lower() for e in raw_in if e.strip()}

        except ValueError:
            messagebox.showerror("错误", "文件大小必须是数字")
            return

        self.btn_search.config(state="disabled")
        self.btn_stop.config(state="normal")
        self.tree.delete(*self.tree.get_children())
        self.progress['value'] = 0
        self.lbl_status.config(text="正在初始化...")

        # 传递 include_exts 参数
        threading.Thread(target=self.run_logic, args=(min_size, max_size), daemon=True).start()

    def run_logic(self, min_s, max_s):
        # 传递 include_exts 参数
        results = self.logic.scan(self.search_paths, min_s, max_s, self.exclude_exts, self.include_exts)
        self.logic.progress_queue.put(("done", results))

    def stop_search(self):
        self.logic.stop_event.set()
        self.lbl_status.config(text="正在停止,请稍候...")

    def check_queue(self):
        try:
            while True:
                msg_type, data = self.logic.progress_queue.get_nowait()

                if msg_type == "status":
                    self.lbl_status.config(text=data)
                    self.progress['mode'] = 'indeterminate'
                    self.progress.start(10)

                elif msg_type == "progress":
                    self.progress['mode'] = 'determinate'
                    self.progress.stop()
                    self.progress['value'] = data

                elif msg_type == "error":
                    messagebox.showerror("错误", data)
                    self.reset_ui()

                elif msg_type == "done":
                    self.progress.stop()
                    self.progress['value'] = 100
                    if data is None:
                        self.lbl_status.config(text="已停止")
                    else:
                        self.populate_tree(data)
                        count = sum(len(v) for v in data.values())
                        self.lbl_status.config(text=f"完成!发现 {len(data)} 组重复,共 {count} 个文件")
                    self.reset_ui()

        except queue.Empty:
            pass
        finally:
            self.after(100, self.check_queue)

    def reset_ui(self):
        self.btn_search.config(state="normal")
        self.btn_stop.config(state="disabled")

    def format_size(self, size):
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size < 1024: return f"{size:.2f} {unit}"
            size /= 1024
        return f"{size:.2f} TB"

    def populate_tree(self, data):
        self.results = data
        self.tree.tag_configure('group1', background='#ffffff')
        self.tree.tag_configure('group2', background='#f0f8ff')
        self.tree.tag_configure('selected_row', foreground='red')

        idx = 0
        for _, files in data.items():
            files.sort(key=lambda x: x['path'])
            tag = 'group1' if idx % 2 == 0 else 'group2'

            for f in files:
                values = (
                    "☐",
                    f['filename'],
                    f['ctime'].strftime('%Y-%m-%d %H:%M:%S'),
                    f['mtime'].strftime('%Y-%m-%d %H:%M:%S'),
                    self.format_size(f['size']),
                    f['path']
                )
                self.tree.insert("", tk.END, values=values, tags=(tag,))
            idx += 1

    def on_click(self, event):
        region = self.tree.identify("region", event.x, event.y)
        if region == "cell" and self.tree.identify_column(event.x) == "#1":
            item = self.tree.identify_row(event.y)
            if item: self.toggle_check(item)

    def toggle_check(self, item):
        vals = list(self.tree.item(item, "values"))
        if not vals: return
        tags = list(self.tree.item(item, "tags"))

        if vals[0] == "☐":
            vals[0] = "☑"
            if 'selected_row' not in tags: tags.append('selected_row')
        else:
            vals[0] = "☐"
            if 'selected_row' in tags: tags.remove('selected_row')

        self.tree.item(item, values=tuple(vals), tags=tuple(tags))

    def show_context_menu(self, event):
        item = self.tree.identify_row(event.y)
        if item:
            if item not in self.tree.selection():
                self.tree.selection_set(item)
            self.context_menu.post(event.x_root, event.y_root)

    def ctx_open_file(self):
        item = self.tree.selection()[0]
        try:
            os.startfile(self.tree.item(item, "values")[5])
        except Exception as e:
            messagebox.showerror("错误", str(e))

    def ctx_open_folder(self):
        path = os.path.normpath(self.tree.item(self.tree.selection()[0], "values")[5])
        subprocess.run(f'explorer /select,"{path}"')

    def ctx_toggle_check(self):
        for item in self.tree.selection(): self.toggle_check(item)

    def ctx_copy_path(self):
        self.clipboard_clear()
        self.clipboard_append(self.tree.item(self.tree.selection()[0], "values")[5])

    def ctx_properties(self):
        show_properties_dialog(self.tree.item(self.tree.selection()[0], "values")[5])

    def ctx_delete_single(self):
        item = self.tree.selection()[0]
        path = self.tree.item(item, "values")[5]
        if messagebox.askyesno("确认", f"确定要永久删除此文件吗?\n{path}"):
            try:
                os.remove(path)
                self.tree.delete(item)
            except Exception as e:
                messagebox.showerror("错误", str(e))

    def smart_select(self):
        if not self.results: return
        path_map = {self.tree.item(item, "values")[5]: item for item in self.tree.get_children()}
        count = 0
        for _, files in self.results.items():
            if len(files) < 2: continue
            sorted_files = sorted(files, key=lambda x: x['mtime'], reverse=True)
            nid = path_map.get(sorted_files[0]['path'])
            if nid and self.tree.item(nid, "values")[0] == "☑":
                self.toggle_check(nid)
            for f in sorted_files[1:]:
                oid = path_map.get(f['path'])
                if oid and self.tree.item(oid, "values")[0] == "☐":
                    self.toggle_check(oid)
                    count += 1
        messagebox.showinfo("完成", f"已智能选中 {count} 个较旧的文件。")

    def clear_selection(self):
        for item in self.tree.get_children():
            vals = list(self.tree.item(item, "values"))
            if vals[0] == "☑":
                vals[0] = "☐"
                tags = list(self.tree.item(item, "tags"))
                if 'selected_row' in tags: tags.remove('selected_row')
                self.tree.item(item, values=tuple(vals), tags=tuple(tags))

    def delete_files(self):
        targets = []
        for item in self.tree.get_children():
            vals = self.tree.item(item, "values")
            if vals[0] == "☑": targets.append((item, vals[5]))

        if not targets:
            messagebox.showinfo("提示", "未选择任何文件")
            return

        msg = f"您即将删除 {len(targets)} 个文件!\n\n警告:这些文件将【直接从磁盘移除】,不进入回收站。\n确定要继续吗?"
        if not messagebox.askyesno("高危操作确认", msg, icon='warning'): return

        success = 0
        errors = []
        for item, path in targets:
            try:
                os.remove(path)
                self.tree.delete(item)
                success += 1
            except Exception as e:
                errors.append(f"{os.path.basename(path)}: {e}")

        res_msg = f"成功删除 {success} 个文件。"
        if errors: res_msg += f"\n\n有 {len(errors)} 个文件删除失败:\n" + "\n".join(errors[:5])
        messagebox.showinfo("操作结果", res_msg)

    def export_csv(self):
        if not self.results: return
        f = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV Files", "*.csv")])
        if f:
            try:
                with open(f, 'w', newline='', encoding='utf-8-sig') as c:
                    w = csv.writer(c)
                    w.writerow(['Filename', 'Path', 'Size', 'Created', 'Modified'])
                    for item in self.tree.get_children():
                        v = self.tree.item(item, "values")
                        w.writerow([v[1], v[5], v[4], v[2], v[3]])
                messagebox.showinfo("成功", "导出完成")
            except Exception as e:
                messagebox.showerror("错误", str(e))

    def sort_column(self, col, reverse):
        l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')]
        l.sort(reverse=reverse)
        for index, (val, k) in enumerate(l): self.tree.move(k, '', index)
        self.tree.heading(col, command=lambda: self.sort_column(col, not reverse))


if __name__ == "__main__":
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(1)
    except Exception:
        pass
    app = App()
    app.mainloop()

布施恩德可便相知重

微信扫一扫打赏

支付宝扫一扫打赏

×

给我留言