功能测试说明:
1.基础文件搜索:输入关键字(如:“.txt”,或文件名),选择有效目录,点击"开始搜索"查看结果。
2.正则表达式搜索:勾选正则表达式选项,输入正则表达式(如:^report_\d{4}),验证匹配结果和高亮显示。
3.高亮显示测试:普通模式下搜索包含大小写字母的关键字,正则模式下测试边界匹配,观察文件名中的黄色高亮部分。
4.中断搜索测试:在大目录搜索过程中点击"停止搜索",验证搜索是否能正确终止。
5.错误处理测试:输入无效正则表达式,选择无效目录,验证错误提示是否正常显示。
6.该版本完整实现了以下功能:多线程文件搜索,正则表达式支持,实时进度更新,搜索关键词高亮,搜索中断功能,友好的错误提示,状态栏反馈。
# -*- coding: utf-8 -*-
# @Author : 小红牛
# 微信公众号:WdPython
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import re
import threading
import queue
class FileSearchApp:
def __init__(self, master):
self.master = master
master.title("文件搜索工具 v2.0")
# 多线程相关变量
self.search_thread = None
self.stop_event = threading.Event()
self.result_queue = queue.Queue()
self.current_keyword = ""
self.current_use_regex = False
# 初始化界面组件
self.create_widgets()
self.setup_layout()
self.setup_bindings()
# 配置文本高亮样式
self.result_text.tag_configure("highlight", background="yellow")
# 启动队列检查
self.check_queue()
def create_widgets(self):
"""创建界面组件"""
# 搜索选项部分
self.keyword_label = ttk.Label(self.master, text="搜索内容:")
self.keyword_entry = ttk.Entry(self.master, width=40)
self.regex_var = tk.BooleanVar()
self.regex_check = ttk.Checkbutton(
self.master, text="正则表达式", variable=self.regex_var)
# 目录选择部分
self.dir_label = ttk.Label(self.master, text="搜索目录:")
self.dir_entry = ttk.Entry(self.master, width=35)
self.browse_btn = ttk.Button(self.master, text="浏览", command=self.browse_directory)
# 控制按钮
self.search_btn = ttk.Button(self.master, text="开始搜索", command=self.start_search)
self.stop_btn = ttk.Button(
self.master, text="停止搜索",
state=tk.DISABLED,
command=self.stop_search
)
# 搜索结果展示
self.result_text = tk.Text(self.master, wrap=tk.WORD, height=15)
self.scrollbar = ttk.Scrollbar(self.master, command=self.result_text.yview)
self.result_text.configure(yscrollcommand=self.scrollbar.set)
# 状态栏
self.status_var = tk.StringVar()
self.status_bar = ttk.Label(
self.master,
textvariable=self.status_var,
relief=tk.SUNKEN,
anchor=tk.W
)
def setup_layout(self):
"""布局管理"""
# 第一行:搜索内容
self.keyword_label.grid(row=0, column=0, padx=5, pady=5, sticky='e')
self.keyword_entry.grid(row=0, column=1, padx=5, pady=5, sticky='we')
self.regex_check.grid(row=0, column=2, padx=5, pady=5)
# 第二行:目录选择
self.dir_label.grid(row=1, column=0, padx=5, pady=5, sticky='e')
self.dir_entry.grid(row=1, column=1, padx=5, pady=5, sticky='we')
self.browse_btn.grid(row=1, column=2, padx=5, pady=5)
# 第三行:控制按钮
self.search_btn.grid(row=2, column=0, padx=5, pady=5)
self.stop_btn.grid(row=2, column=2, padx=5, pady=5)
# 第四行:搜索结果
self.result_text.grid(row=3, column=0, columnspan=2, padx=5, pady=5, sticky='nsew')
self.scrollbar.grid(row=3, column=2, sticky='ns')
# 第五行:状态栏
self.status_bar.grid(row=4, column=0, columnspan=3, sticky='ew')
# 配置网格自适应
self.master.grid_rowconfigure(3, weight=1)
self.master.grid_columnconfigure(1, weight=1)
def setup_bindings(self):
"""绑定事件"""
self.master.protocol("WM_DELETE_WINDOW", self.on_close)
def browse_directory(self):
"""选择目录"""
selected_dir = filedialog.askdirectory()
if selected_dir:
self.dir_entry.delete(0, tk.END)
self.dir_entry.insert(0, selected_dir)
def start_search(self):
"""启动搜索线程"""
# 获取输入参数
self.current_keyword = self.keyword_entry.get().strip()
directory = self.dir_entry.get().strip()
self.current_use_regex = self.regex_var.get()
# 输入验证
if not self.validate_input(self.current_keyword, directory):
return
# 初始化搜索状态
self.stop_event.clear()
self.result_text.delete(1.0, tk.END)
self.status_var.set("准备搜索...")
self.toggle_buttons(False)
# 创建并启动搜索线程
self.search_thread = threading.Thread(
target=self.file_search_worker,
args=(directory, self.current_keyword, self.current_use_regex),
daemon=True
)
self.search_thread.start()
def stop_search(self):
"""停止搜索"""
self.stop_event.set()
self.status_var.set("正在停止搜索...")
def on_close(self):
"""窗口关闭事件处理"""
self.stop_search()
if self.search_thread and self.search_thread.is_alive():
self.search_thread.join(timeout=1)
self.master.destroy()
def file_search_worker(self, directory, keyword, use_regex):
"""多线程搜索工作函数"""
try:
# 预处理搜索模式
if use_regex:
try:
pattern = re.compile(keyword)
except re.error as e:
self.result_queue.put(("error", f"正则表达式错误: {str(e)}"))
return
else:
keyword = keyword.lower()
found_files = []
for root, dirs, files in os.walk(directory):
if self.stop_event.is_set():
break
for file in files:
if self.stop_event.is_set():
break
try:
# 执行匹配判断
if use_regex:
match = pattern.search(file)
else:
match = keyword in file.lower()
if match:
full_path = os.path.join(root, file)
found_files.append(full_path)
# 每找到10个文件更新一次界面
if len(found_files) % 10 == 0:
self.result_queue.put(("progress", found_files[-10:]))
except Exception as e:
continue
# 发送最终结果
self.result_queue.put(("result", found_files))
except Exception as e:
self.result_queue.put(("error", str(e)))
def check_queue(self):
"""定期检查结果队列"""
try:
while True:
msg_type, content = self.result_queue.get_nowait()
if msg_type == "progress":
self.display_progress(content)
elif msg_type == "result":
self.display_result(content)
elif msg_type == "error":
self.display_error(content)
except queue.Empty:
pass
finally:
self.master.after(100, self.check_queue)
def display_progress(self, files):
"""显示进度更新"""
for path in files:
# 插入文件路径
self.result_text.insert(tk.END, f"{path}\n")
# 高亮匹配部分
line_start = self.result_text.index("end-2c linestart")
line_end = self.result_text.index("end-1c lineend")
self.highlight_line(line_start, line_end, path)
self.result_text.see(tk.END)
self.status_var.set(f"已找到 {self.result_text.index('end-1c').split('.')[0]} 个文件")
def highlight_line(self, line_start, line_end, path):
"""高亮指定行中的匹配部分"""
# 获取文件名部分
filename = os.path.basename(path)
dir_part = os.path.dirname(path)
# 计算文件名在路径中的起始位置
filename_start = len(dir_part) + 1 if dir_part else 0
# 匹配模式处理
if self.current_use_regex:
try:
pattern = re.compile(self.current_keyword)
matches = list(pattern.finditer(filename))
except re.error:
return
else:
keyword = self.current_keyword.lower()
filename_lower = filename.lower()
matches = []
start = 0
while True:
idx = filename_lower.find(keyword, start)
if idx == -1:
break
matches.append((idx, idx + len(keyword)))
start = idx + len(keyword)
# 转换为完整路径中的位置
for match in matches:
if isinstance(match, re.Match):
start, end = match.start(), match.end()
else:
start, end = match
full_start = filename_start + start
full_end = filename_start + end
# 转换为文本组件坐标
start_index = f"{line_start}+{full_start}c"
end_index = f"{line_start}+{full_end}c"
try:
self.result_text.tag_add("highlight", start_index, end_index)
except tk.TclError:
pass
def display_result(self, files):
"""显示最终结果"""
self.toggle_buttons(True)
count = len(files)
if count > 0:
self.result_text.insert(tk.END, f"\n\n搜索完成,共找到 {count} 个文件\n")
for path in files:
self.result_text.insert(tk.END, f"{path}\n")
line_start = self.result_text.index("end-2c linestart")
line_end = self.result_text.index("end-1c lineend")
self.highlight_line(line_start, line_end, path)
else:
self.result_text.insert(tk.END, "\n\n未找到匹配文件")
self.status_var.set("搜索完成" if count > 0 else "未找到匹配文件")
def display_error(self, error_msg):
"""显示错误信息"""
self.toggle_buttons(True)
messagebox.showerror("发生错误", error_msg)
self.status_var.set("搜索出错")
def validate_input(self, keyword, directory):
"""验证输入有效性"""
if not keyword:
messagebox.showerror("错误", "请输入搜索内容")
return False
if not directory:
messagebox.showerror("错误", "请选择搜索目录")
return False
if not os.path.isdir(directory):
messagebox.showerror("错误", "目录不存在或无效")
return False
return True
def toggle_buttons(self, enable):
"""切换按钮状态"""
state = tk.NORMAL if enable else tk.DISABLED
self.search_btn.config(state=state)
self.stop_btn.config(state=tk.DISABLED if enable else tk.NORMAL)
if __name__ == "__main__":
root = tk.Tk()
app = FileSearchApp(root)
root.mainloop()
完毕!!感谢您的收看
----------★★跳转到历史博文集合★★----------