# -*- coding: utf-8 -*-"""文件筛选工具 v3.0功能: 1. 按关键字筛选文件(删除包含/不包含关键字的文件)-> 回收站 2. 将文件夹中所有文件的修改日期调整为今天 3. 支持递归/非递归模式无需额外依赖,删除直接走Windows原生回收站API"""import osimport sysimport ctypesimport tracebackfrom datetime import datetimeimport tkinter as tkfrom tkinter import ttk, messagebox, filedialog# ========== Windows原生回收站API(无需send2trash)==========def send_to_recycle_bin(path): """使用Windows shell32 API将文件/文件夹移至回收站""" abs_path = os.path.abspath(path) if len(abs_path) > 240: if not abs_path.startswith('\\\\?\\'): abs_path = '\\\\?\\' + abs_path class SHFILEOPSTRUCTW(ctypes.Structure): _fields_ = [ ("hwnd", ctypes.c_void_p), ("wFunc", ctypes.c_uint), ("pFrom", ctypes.c_wchar_p), ("pTo", ctypes.c_wchar_p), ("fFlags", ctypes.c_ushort), ("fAnyOperationsAborted", ctypes.c_bool), ("hNameMappings", ctypes.c_void_p), ("lpszProgressTitle", ctypes.c_wchar_p), ] FO_DELETE = 0x0003 FOF_ALLOWUNDO = 0x0040 FOF_NOCONFIRMATION = 0x0010 FOF_SILENT = 0x0004 op = SHFILEOPSTRUCTW() op.hwnd = None op.wFunc = FO_DELETE op.pFrom = abs_path + '\0' op.pTo = None op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT op.fAnyOperationsAborted = False op.hNameMappings = None op.lpszProgressTitle = None result = ctypes.windll.shell32.SHFileOperationW(ctypes.byref(op)) if result != 0: raise OSError(f"SHFileOperationW returned {result}") if op.fAnyOperationsAborted: raise OSError("Operation was aborted")def set_file_mtime(path, new_time=None): if new_time is None: new_time = datetime.now() timestamp = new_time.timestamp() os.utime(path, (timestamp, timestamp))def show_fatal_error(msg): try: ctypes.windll.user32.MessageBoxW(0, msg, "启动错误", 0x10) except: print(msg, file=sys.stderr)# ========== 配色主题 ==========COLORS = { 'bg_main': '#F8F9FA', 'bg_card': '#FFFFFF', 'primary': '#2563EB', 'primary_hover': '#1D4ED8', 'danger': '#DC2626', 'danger_hover': '#B91C1C', 'success': '#059669', 'success_hover': '#047857', 'text': '#1F2937', 'text_light': '#6B7280', 'border': '#E5E7EB', 'header': '#1E3A5F',}class FileFilterApp: def __init__(self, root): self.root = root self.root.title("文件筛选工具 v3.0") self.root.geometry("880x720") self.root.configure(bg=COLORS['bg_main']) self.root.resizable(False, False) self.current_dir = os.getcwd() self._create_widgets() self.refresh_list() def _create_widgets(self): # ----- 标题栏 ----- header = tk.Frame(self.root, bg=COLORS['header'], height=55) header.pack(fill='x') header.pack_propagate(False) tk.Label(header, text="文件筛选工具", font=("微软雅黑", 18, "bold"), fg='white', bg=COLORS['header']).pack(side='left', padx=20, pady=8) tk.Label(header, text="智能文件管理", font=("微软雅黑", 11), fg='#B0C4DE', bg=COLORS['header']).pack(side='left', pady=14) # ----- 主内容区 ----- main = tk.Frame(self.root, bg=COLORS['bg_main']) main.pack(fill='both', expand=True, padx=16, pady=10) # 左侧面板 left = tk.Frame(main, bg=COLORS['bg_main'], width=260) left.pack(side='left', fill='y', padx=(0, 10)) left.pack_propagate(False) self._create_info_panel(left) self._create_action_panel(left) # 右侧面板:文件列表 right = tk.Frame(main, bg=COLORS['bg_card'], bd=1, relief='solid') right.pack(side='right', fill='both', expand=True) tk.Label(right, text="文件列表", font=("微软雅黑", 12, "bold"), bg=COLORS['bg_card'], fg=COLORS['text']).pack(anchor='w', padx=12, pady=8) self.stats_label = tk.Label(right, text="共 0 个文件", fg=COLORS['text_light'], bg=COLORS['bg_card'], font=("微软雅黑", 9)) self.stats_label.pack(anchor='w', padx=12) list_frame = tk.Frame(right, bg=COLORS['bg_card']) list_frame.pack(fill='both', expand=True, padx=12, pady=5) scrollbar = tk.Scrollbar(list_frame, bg=COLORS['bg_card']) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.listbox = tk.Listbox(list_frame, selectmode=tk.EXTENDED, yscrollcommand=scrollbar.set, font=("Consolas", 10), bg='#FAFBFC', fg=COLORS['text'], selectbackground=COLORS['primary'], selectforeground='white', relief='flat', highlightthickness=1, highlightcolor=COLORS['border']) self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.config(command=self.listbox.yview) # 底部日志 log_frame = tk.LabelFrame(self.root, text="操作日志", font=("微软雅黑", 9), bg=COLORS['bg_main'], fg=COLORS['text_light']) log_frame.pack(fill='x', padx=16, pady=(0, 10)) self.log_text = tk.Text(log_frame, height=4, font=("Consolas", 9), wrap="word", bg='#FAFBFC', fg=COLORS['text'], relief='flat', highlightthickness=1, highlightcolor=COLORS['border']) self.log_text.pack(fill='x', padx=6, pady=6) self.log_text.config(state="disabled") def _create_info_panel(self, parent): # 目录信息卡 card = tk.Frame(parent, bg=COLORS['bg_card'], bd=1, relief='solid') card.pack(fill='x', pady=5, padx=2) tk.Label(card, text="当前目录", font=("微软雅黑", 10, "bold"), bg=COLORS['bg_card'], fg=COLORS['text']).pack(anchor='w', padx=10, pady=(8, 2)) self.dir_label = tk.Label(card, text=self.current_dir, bg=COLORS['bg_card'], fg=COLORS['text_light'], font=("Consolas", 8), wraplength=230, justify='left') self.dir_label.pack(anchor='w', padx=10, pady=(0, 8)) btn_frame = tk.Frame(card, bg=COLORS['bg_card']) btn_frame.pack(fill='x', padx=10, pady=(0, 10)) tk.Button(btn_frame, text="选择文件夹", command=self._select_dir, bg=COLORS['primary'], fg='white', font=("微软雅黑", 9), relief='flat', cursor='hand2').pack(side='left', padx=(0, 5)) tk.Button(btn_frame, text="刷新", command=self.refresh_list, bg='#E5E7EB', fg=COLORS['text'], font=("微软雅黑", 9), relief='flat', cursor='hand2').pack(side='left') # 选项卡 opt_card = tk.Frame(parent, bg=COLORS['bg_card'], bd=1, relief='solid') opt_card.pack(fill='x', pady=5, padx=2) tk.Label(opt_card, text="选项", font=("微软雅黑", 10, "bold"), bg=COLORS['bg_card'], fg=COLORS['text']).pack(anchor='w', padx=10, pady=8) self.recursive_var = tk.BooleanVar(value=True) tk.Checkbutton(opt_card, text="包含子文件夹(递归)", variable=self.recursive_var, command=self.refresh_list, font=("微软雅黑", 9), bg=COLORS['bg_card'], activebackground=COLORS['bg_card']).pack(anchor='w', padx=10, pady=2) self.show_detail_var = tk.BooleanVar(value=False) tk.Checkbutton(opt_card, text="显示完整路径", variable=self.show_detail_var, command=self.refresh_list, font=("微软雅黑", 9), bg=COLORS['bg_card'], activebackground=COLORS['bg_card']).pack(anchor='w', padx=10, pady=2) def _create_action_panel(self, parent): # Tab切换 self.notebook = ttk.Notebook(parent) self.notebook.pack(fill='x', pady=5, padx=2) style = ttk.Style() style.configure('TNotebook.Tab', font=("微软雅黑", 9)) # Tab 1: 筛选删除 tab1 = tk.Frame(self.notebook, bg=COLORS['bg_main']) self.notebook.add(tab1, text="筛选删除") card1 = tk.Frame(tab1, bg=COLORS['bg_card'], bd=1, relief='solid') card1.pack(fill='x', pady=5, padx=2) tk.Label(card1, text="关键字", font=("微软雅黑", 10, "bold"), bg=COLORS['bg_card'], fg=COLORS['text']).pack(anchor='w', padx=10, pady=(10, 4)) self.keyword_entry = tk.Entry(card1, width=24, font=("微软雅黑", 10), relief='solid', highlightthickness=1, highlightcolor=COLORS['primary']) self.keyword_entry.pack(anchor='w', padx=10, pady=(0, 8)) self.mode_var = tk.StringVar(value="delete_contain") tk.Radiobutton(card1, text="删除包含关键字的文件", variable=self.mode_var, value="delete_contain", font=("微软雅黑", 9), bg=COLORS['bg_card'], activebackground=COLORS['bg_card']).pack(anchor='w', padx=10, pady=2) tk.Radiobutton(card1, text="删除不包含关键字的文件", variable=self.mode_var, value="delete_not_contain", font=("微软雅黑", 9), bg=COLORS['bg_card'], activebackground=COLORS['bg_card']).pack(anchor='w', padx=10, pady=2) tk.Button(card1, text="执行删除 → 回收站", command=self._execute_delete, bg=COLORS['danger'], fg='white', font=("微软雅黑", 10, "bold"), relief='flat', cursor='hand2', width=20).pack(pady=12) # Tab 2: 修改日期 tab2 = tk.Frame(self.notebook, bg=COLORS['bg_main']) self.notebook.add(tab2, text="修改日期") card2 = tk.Frame(tab2, bg=COLORS['bg_card'], bd=1, relief='solid') card2.pack(fill='x', pady=5, padx=2) tk.Label(card2, text='修改日期', font=("微软雅黑", 10, "bold"), bg=COLORS['bg_card'], fg=COLORS['text']).pack(anchor='w', padx=10, pady=(10, 4)) tk.Label(card2, text='将文件"修改日期"调整为:', font=("微软雅黑", 9), bg=COLORS['bg_card'], fg=COLORS['text_light']).pack(anchor='w', padx=10) self.date_var = tk.StringVar(value=datetime.now().strftime("%Y-%m-%d %H:%M:%S")) tk.Entry(card2, textvariable=self.date_var, width=24, font=("Consolas", 10), relief='solid', highlightthickness=1, highlightcolor=COLORS['primary']).pack(anchor='w', padx=10, pady=5) tk.Label(card2, text="格式: YYYY-MM-DD HH:MM:SS", fg=COLORS['text_light'], font=("微软雅黑", 8), bg=COLORS['bg_card']).pack(anchor='w', padx=10) tk.Button(card2, text="一键修改为今天", command=self._execute_set_date, bg=COLORS['success'], fg='white', font=("微软雅黑", 10, "bold"), relief='flat', cursor='hand2', width=20).pack(pady=12) def log(self, message): self.log_text.config(state="normal") self.log_text.delete("1.0", "end") self.log_text.insert("1.0", message) self.log_text.config(state="disabled") self.root.update() def _select_dir(self): new_dir = filedialog.askdirectory(initialdir=self.current_dir) if new_dir: self.current_dir = new_dir self.dir_label.config(text=self.current_dir) self.refresh_list() def refresh_list(self): self.listbox.delete(0, tk.END) recursive = self.recursive_var.get() show_full = self.show_detail_var.get() count = 0 try: if recursive: for root_dir, dirs, files in os.walk(self.current_dir): rel_root = os.path.relpath(root_dir, self.current_dir) if rel_root == ".": rel_root = "" for file in files: count += 1 if show_full: display = os.path.join(root_dir, file) else: display = os.path.join(rel_root, file) if rel_root else file self.listbox.insert(tk.END, display) else: for item in sorted(os.listdir(self.current_dir)): full_path = os.path.join(self.current_dir, item) if os.path.isfile(full_path): count += 1 self.listbox.insert(tk.END, item) self.stats_label.config(text=f"共 {count} 个文件") except Exception as e: messagebox.showerror("错误", f"读取目录失败:{e}") def _execute_delete(self): keyword = self.keyword_entry.get().strip() if not keyword: messagebox.showwarning("警告", "请输入关键字") return file_list = list(self.listbox.get(0, tk.END)) if not file_list: messagebox.showinfo("提示", "没有文件可操作") return mode = self.mode_var.get() to_delete = [] for display_path in file_list: if os.path.isabs(display_path): full_path = display_path else: full_path = os.path.join(self.current_dir, display_path) basename = os.path.basename(full_path) if mode == "delete_contain": if keyword in basename: to_delete.append(full_path) else: if keyword not in basename: to_delete.append(full_path) if not to_delete: messagebox.showinfo("提示", "没有符合条件的文件") return msg = f"将删除 {len(to_delete)} 个文件(移至回收站)。\n\n确定继续吗?" if not messagebox.askyesno("确认操作", msg): return deleted = 0 errors = [] for full_path in to_delete: try: send_to_recycle_bin(full_path) deleted += 1 except Exception as e: errors.append(f"{os.path.basename(full_path)}: {e}") result = f"成功将 {deleted} 个文件移至回收站" if errors: result += f"\n失败 {len(errors)} 个:\n" + "\n".join(errors[:5]) self.log(result) messagebox.showinfo("完成", result) self.refresh_list() def _execute_set_date(self): file_list = list(self.listbox.get(0, tk.END)) if not file_list: messagebox.showinfo("提示", "没有文件可操作") return date_str = self.date_var.get().strip() try: if date_str: target_time = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") else: target_time = datetime.now() except ValueError: messagebox.showerror("错误", "日期格式错误,请使用 YYYY-MM-DD HH:MM:SS") return msg = (f"将把 {len(file_list)} 个文件的修改日期\n" f"调整为: {target_time.strftime('%Y-%m-%d %H:%M:%S')}\n\n" f"确定继续吗?") if not messagebox.askyesno("确认操作", msg): return success = 0 errors = [] for display_path in file_list: if os.path.isabs(display_path): full_path = display_path else: full_path = os.path.join(self.current_dir, display_path) try: set_file_mtime(full_path, target_time) success += 1 except Exception as e: errors.append(f"{os.path.basename(full_path)}: {e}") result = f"成功修改 {success} 个文件的修改日期" if errors: result += f"\n失败 {len(errors)} 个:\n" + "\n".join(errors[:5]) self.log(result) messagebox.showinfo("完成", result) self.refresh_list()if __name__ == "__main__": try: root = tk.Tk() app = FileFilterApp(root) root.mainloop() except Exception as e: show_fatal_error(f"程序启动失败:\n\n{str(e)}\n\n详细错误:\n{traceback.format_exc()}") raise