import os
import tkinter as tk
import subprocess
import multiprocessing
from multiprocessing import Queue
from tkinter import filedialog, messagebox, ttk
import json
import time
# 确保Windows系统支持多进程
if os.name == 'nt':
multiprocessing.set_start_method('spawn', force=True)
class FileTypeFinder:
def __init__(self, root):
self.root = root
self.root.title("非指定格式文件查找器")
self.root.geometry("950x650")
self.root.resizable(True, True)
# 设置中文字体支持
self.font = ('SimHei', 10)
# 默认要排除的文件格式(不区分大小写)
self.default_excluded_extensions = {
'.jpg', '.jpeg', '.png', '.mp4', '.mov', '.avi', '.mts', '.mkv', '.avif',
'.jpe', '.bmp', '.gif', '.m4v', '.webf', '.tiff', '.heic', '.wmv', '.cr2',
'.cr3', '.ts', '.jfif', '.arw', '.pdf', '.livp', '.flv', '.f4v'
}
self.excluded_extensions = set(self.default_excluded_extensions) # 初始化
# 配置文件路径
self.config_file = os.path.join(os.path.expanduser("~"), ".file_type_finder_config.json")
# 存储找到的文件列表和进程相关变量
self.found_files = []
self.search_process = None
self.queue = None
self.is_searching = False
# 存储复选框和变量
self.checkboxes = {}
self.extension_vars = {}
# 加载配置
self.load_config()
# 创建UI组件
self.create_widgets()
def create_widgets(self):
# 路径输入框架
path_frame = tk.Frame(self.root)
path_frame.pack(padx=10, pady=10, fill=tk.X)
# 路径输入框
self.path_var = tk.StringVar()
path_entry = tk.Entry(path_frame, textvariable=self.path_var, width=60, font=self.font)
path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
# 浏览按钮
browse_btn = tk.Button(path_frame, text="浏览...", command=self.browse_directory, font=self.font)
browse_btn.pack(side=tk.LEFT, padx=(0, 5))
# 粘贴按钮
paste_btn = tk.Button(path_frame, text="粘贴路径", command=self.paste_path, font=self.font)
paste_btn.pack(side=tk.LEFT, padx=(0, 5))
# 运行/停止按钮
self.run_btn = tk.Button(path_frame, text="运行查找", command=self.toggle_search, font=self.font)
self.run_btn.pack(side=tk.LEFT)
# 创建扩展名勾选区域
self.create_extension_checkboxes()
# 进度显示框架
progress_frame = tk.Frame(self.root)
progress_frame.pack(padx=10, fill=tk.X)
# 进度标签
self.progress_var = tk.StringVar()
self.progress_var.set("等待开始...")
progress_label = tk.Label(progress_frame, textvariable=self.progress_var, font=self.font, fg="blue")
progress_label.pack(anchor=tk.W)
# 结果显示区域
result_frame = tk.Frame(self.root)
result_frame.pack(padx=10, pady=(5, 10), fill=tk.BOTH, expand=True)
# 结果标签
result_label = tk.Label(result_frame, text="查找结果:", font=self.font)
result_label.pack(anchor=tk.W, pady=(0, 5))
# 创建带滚动条的树状视图来显示文件列表
columns = ("文件路径", "操作")
self.file_tree = ttk.Treeview(result_frame, columns=columns, show="headings")
# 设置列宽和标题
self.file_tree.heading("文件路径", text="文件路径")
self.file_tree.heading("操作", text="操作")
self.file_tree.column("文件路径", width=650, anchor=tk.W)
self.file_tree.column("操作", width=100, anchor=tk.CENTER)
# 添加滚动条
scrollbar = ttk.Scrollbar(result_frame, orient="vertical", command=self.file_tree.yview)
self.file_tree.configure(yscrollcommand=scrollbar.set)
# 布局树状视图和滚动条
self.file_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 绑定双击事件来打开文件位置
self.file_tree.bind("<Double-1>", self.open_file_location)
# 状态栏
self.status_var = tk.StringVar()
self.status_var.set("就绪")
status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W,
font=self.font)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def create_extension_checkboxes(self):
"""创建扩展名勾选框"""
frame = tk.LabelFrame(self.root, text="勾选要排除的文件类型(取消勾选表示要查找)", font=self.font)
frame.pack(padx=10, pady=5, fill=tk.X)
# 按扩展名排序,每行显示4个
extensions = sorted(self.default_excluded_extensions)
row = 0
col = 0
for ext in extensions:
var = tk.BooleanVar()
var.set(True) # 默认勾选
# 定义更新颜色的函数
def make_update_color(ext_var, button):
def update_color(*args):
if ext_var.get():
# 勾选状态:白色背景 + 红色文字
button.config(
bg="white", fg="red",
activebackground="lightpink",
activeforeground="darkred"
)
else:
# 未勾选状态:浅灰色背景 + 黑色文字
button.config(
bg="lightgray", fg="black",
activebackground="gray",
activeforeground="white"
)
return update_color
# 使用 indicatoron=0 变成按钮样式,设置宽高
cb = tk.Checkbutton(
frame,
text=ext,
variable=var,
font=self.font,
indicatoron=0,
width=10,
height=2,
anchor='w',
padx=10,
relief=tk.RAISED
)
cb.grid(row=row, column=col, sticky=tk.W, padx=5, pady=2)
# 绑定变量变化,自动更新颜色
var.trace_add("write", make_update_color(var, cb))
make_update_color(var, cb)() # 初始调用一次
self.extension_vars[ext] = var
self.checkboxes[ext] = cb
col += 1
if col > 6:
col = 0
row += 1
def update_excluded_extensions(self):
"""根据勾选状态更新要排除的文件格式"""
self.excluded_extensions.clear()
for ext, var in self.extension_vars.items():
if var.get():
self.excluded_extensions.add(ext)
def save_config(self):
"""保存当前勾选状态到配置文件"""
config = {ext: var.get() for ext, var in self.extension_vars.items()}
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
except Exception as e:
print("保存配置失败:", e)
def load_config(self):
"""加载配置文件中的勾选状态"""
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
for ext, value in config.items():
if ext in self.extension_vars:
self.extension_vars[ext].set(value)
except Exception as e:
print("读取配置失败:", e)
def browse_directory(self):
"""打开文件夹选择对话框"""
directory = filedialog.askdirectory()
if directory:
self.path_var.set(directory)
def paste_path(self):
"""将剪贴板内容粘贴到路径输入框"""
try:
clipboard = self.root.clipboard_get()
if os.path.isdir(clipboard):
self.path_var.set(clipboard)
else:
messagebox.showwarning("无效路径", "剪贴板中的内容不是一个有效的文件夹路径。")
except tk.TclError:
messagebox.showwarning("剪贴板为空", "剪贴板中没有内容。")
def toggle_search(self):
"""切换查找状态(开始/停止)"""
if self.is_searching:
self.stop_search()
else:
self.start_search()
def start_search(self):
"""开始查找文件(使用子进程)"""
self.update_excluded_extensions() # 更新排除列表
self.save_config() # 保存当前勾选状态
directory = self.path_var.get()
# 检查路径是否有效
if not directory:
messagebox.showerror("错误", "请选择一个文件夹")
return
if not os.path.isdir(directory):
messagebox.showerror("错误", f"路径不存在或不是一个文件夹:\n{directory}")
return
# 清空之前的结果
for item in self.file_tree.get_children():
self.file_tree.delete(item)
self.found_files = []
# 更新UI状态
self.is_searching = True
self.run_btn.config(text="停止查找")
self.status_var.set("正在查找...")
self.progress_var.set("已处理: 0 个文件 | 找到: 0 个目标文件")
# 创建队列用于进程间通信
self.queue = Queue()
# 启动子进程
self.search_process = multiprocessing.Process(
target=search_files,
args=(directory, self.excluded_extensions, self.queue)
)
self.search_process.daemon = True
self.search_process.start()
# 开始监听队列中的消息
self.listen_to_queue()
def stop_search(self):
"""停止查找文件"""
if self.search_process and self.search_process.is_alive():
self.search_process.terminate()
self.search_process.join()
# 更新UI状态
self.is_searching = False
self.run_btn.config(text="运行查找")
self.status_var.set("查找已停止")
def listen_to_queue(self):
"""监听子进程发送的消息并更新UI"""
if not self.is_searching:
return
# 检查队列中是否有消息
try:
while not self.queue.empty():
message = self.queue.get_nowait()
# 处理进度消息
if isinstance(message, tuple) and message[0] == 'progress':
total_processed, total_found = message[1], message[2]
self.progress_var.set(f"已处理: {total_processed} 个文件 | 找到: {total_found} 个目标文件")
# 处理找到的文件
elif isinstance(message, tuple) and message[0] == 'file':
file_path = message[1]
self.found_files.append(file_path)
self.file_tree.insert("", tk.END, values=(file_path, "打开位置"))
# 处理完成消息
elif message == 'done':
self.is_searching = False
self.run_btn.config(text="运行查找")
self.status_var.set(f"查找完成 - 共处理 {len(self.found_files)} 个文件")
return
except Exception as e:
print(f"处理队列消息时出错: {e}")
# 继续监听(每100毫秒检查一次)
self.root.after(100, self.listen_to_queue)
def open_file_location(self, event):
"""打开文件所在的文件夹并选中该文件"""
# 获取被点击的项目
try:
item = self.file_tree.selection()[0]
# 获取文件路径
file_path = self.file_tree.item(item, "values")[0]
if os.path.exists(file_path):
try:
if os.name == 'nt': # Windows系统
# 使用资源管理器打开并选中文件
subprocess.run(f'explorer /select,"{file_path}"', shell=True)
else: # macOS或Linux系统
# 打开文件所在目录
dir_path = os.path.dirname(file_path)
if os.name == 'posix': # macOS
subprocess.run(['open', dir_path])
else: # Linux
subprocess.run(['xdg-open', dir_path])
except Exception as e:
messagebox.showerror("错误", f"无法打开文件位置:\n{str(e)}")
else:
messagebox.showerror("错误", f"文件不存在:\n{file_path}")
except IndexError:
# 未选中任何项目时不做处理
pass
def search_files(directory, excluded_extensions, queue):
"""在子进程中查找文件"""
try:
total_processed = 0
total_found = 0
# 遍历目录
for root_dir, _, files in os.walk(directory):
for file in files:
total_processed += 1
file_path = os.path.join(root_dir, file)
# 获取文件扩展名(小写)
_, ext = os.path.splitext(file)
ext = ext.lower()
# 检查是否为非目标文件
if ext not in excluded_extensions:
total_found += 1
# 发送找到的文件路径
queue.put(('file', file_path))
# 每处理10个文件发送一次进度更新,避免消息过多
if total_processed % 10 == 0:
queue.put(('progress', total_processed, total_found))
# 发送最终进度
queue.put(('progress', total_processed, total_found))
# 发送完成消息
queue.put('done')
except Exception as e:
print(f"查找过程中发生错误:{e}")
queue.put('done')
if __name__ == "__main__":
root = tk.Tk()
app = FileTypeFinder(root)
root.mainloop()
保存选择项没有实现