import os
import subprocess
import shutil
import time
import tkinter as tk
from tkinter import filedialog, ttk, scrolledtext, messagebox, PhotoImage
import win32com.client as win32
import threading
import tempfile
import queue
import traceback
class DiffProcessorApp:
def __init__(self, root):
self.root = root
root.title("高级文件夹比较工具")
root.geometry("1000x700")
root.configure(bg="#f5f5f5")
# 创建现代风格主题
self.style = ttk.Style()
self.style.theme_use('clam')
# 自定义主题颜色
self.style.configure('TButton',
font=('Segoe UI', 10, 'bold'),
borderwidth=1,
foreground="#333",
background="#4CAF50",
bordercolor="#388E3C",
relief="flat",
padding=8,
anchor="center")
self.style.map('TButton',
background=[('active', '#388E3C'), ('disabled', '#BDBDBD')],
foreground=[('disabled', '#9E9E9E')])
self.style.configure('TLabel', font=('Segoe UI', 9), background="#f5f5f5")
self.style.configure('TLabelframe', font=('Segoe UI', 10, 'bold'),
background="#f5f5f5", relief="flat", borderwidth=2)
self.style.configure('TLabelframe.Label', font=('Segoe UI', 10, 'bold'),
background="#f5f5f5", foreground="#2E7D32")
self.style.configure('Treeview', font=('Segoe UI', 9), rowheight=25)
self.style.configure('Treeview.Heading', font=('Segoe UI', 9, 'bold'))
# 创建主框架
main_frame = ttk.Frame(root, padding="15")
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 标题区域
header_frame = ttk.Frame(main_frame)
header_frame.pack(fill=tk.X, pady=(0, 15))
# 添加标题图标
try:
icon = PhotoImage(file="folder_icon.png")
self.icon_label = ttk.Label(header_frame, image=icon)
self.icon_label.image = icon
self.icon_label.pack(side=tk.LEFT, padx=(0, 10))
except:
self.icon_label = ttk.Label(header_frame, text="📁", font=("Arial", 24))
self.icon_label.pack(side=tk.LEFT, padx=(0, 10))
title_label = ttk.Label(header_frame, text="高级文件夹比较工具",
font=("Segoe UI", 18, "bold"), foreground="#2E7D32")
title_label.pack(side=tk.LEFT)
# 文件选择区域
file_frame = ttk.LabelFrame(main_frame, text="文件夹选择", padding="12")
file_frame.pack(fill=tk.X, pady=5)
# 文件夹选择
self.old_folder_entry, self.new_folder_entry = self.create_folder_selector(file_frame, "原始文件夹:")
self.new_folder_entry = self.create_folder_selector(file_frame, "修改后文件夹:")[0]
# 比较选项区域
options_frame = ttk.LabelFrame(main_frame, text="比较选项", padding="12")
options_frame.pack(fill=tk.X, pady=5)
# 递归比较选项
self.recursive_var = tk.BooleanVar(value=True)
recursive_check = ttk.Checkbutton(options_frame, text="递归比较子文件夹",
variable=self.recursive_var)
recursive_check.grid(row=0, column=0, padx=10, pady=5, sticky=tk.W)
# 文件过滤
filter_frame = ttk.Frame(options_frame)
filter_frame.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W)
ttk.Label(filter_frame, text="文件过滤:").pack(side=tk.LEFT, padx=(0, 5))
self.filter_var = tk.StringVar(value="*.*")
filter_entry = ttk.Entry(filter_frame, textvariable=self.filter_var, width=15)
filter_entry.pack(side=tk.LEFT)
# 目标Excel选择
excel_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="12")
excel_frame.pack(fill=tk.X, pady=5)
ttk.Label(excel_frame, text="目标Excel文件:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
self.excel_file_entry = ttk.Entry(excel_frame, width=60)
self.excel_file_entry.grid(row=0, column=1, padx=5, pady=5)
ttk.Button(excel_frame, text="浏览...",
command=lambda: self.select_file(self.excel_file_entry,
[("Excel文件", "*.xlsx *.xlsm")])).grid(row=0, column=2, padx=5, pady=5)
# 执行按钮区域
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=10)
self.run_button = ttk.Button(button_frame, text="执行比较",
command=self.start_processing, width=20, style='TButton')
self.run_button.pack(side=tk.LEFT)
# 停止按钮
self.stop_button = ttk.Button(button_frame, text="停止",
command=self.stop_processing, width=10, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=10)
# 进度条
self.progress = ttk.Progressbar(main_frame, orient=tk.HORIZONTAL, length=700, mode='determinate')
self.progress.pack(fill=tk.X, pady=5)
# 状态信息
status_frame = ttk.Frame(main_frame)
status_frame.pack(fill=tk.X, pady=5)
self.status_var = tk.StringVar(value="准备就绪")
status_label = ttk.Label(status_frame, textvariable=self.status_var,
font=("Segoe UI", 9), foreground="#2E7D32")
status_label.pack(side=tk.LEFT)
# 日志和预览区域
notebook = ttk.Notebook(main_frame)
notebook.pack(fill=tk.BOTH, expand=True, pady=5)
# 文件夹结构标签
tree_frame = ttk.Frame(notebook, padding="5")
notebook.add(tree_frame, text="文件夹结构")
# 创建树形视图
self.tree = ttk.Treeview(tree_frame, columns=("Status"), show="tree")
self.tree.heading("#0", text="文件夹结构", anchor=tk.W)
self.tree.heading("Status", text="状态", anchor=tk.W)
self.tree.column("#0", width=400)
self.tree.column("Status", width=100)
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
hsb = ttk.Scrollbar(tree_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")
# 日志标签
log_frame = ttk.Frame(notebook, padding="5")
notebook.add(log_frame, text="执行日志")
self.log_text = scrolledtext.ScrolledText(log_frame, height=10, wrap=tk.WORD,
font=("Consolas", 9))
self.log_text.pack(fill=tk.BOTH, expand=True)
self.log_text.config(state=tk.DISABLED)
# 设置网格权重
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
# 线程控制
self.processing = False
self.queue = queue.Queue()
# 启动队列处理
self.root.after(100, self.process_queue)
def create_folder_selector(self, parent, label_text):
"""创建文件夹选择器组件"""
frame = ttk.Frame(parent)
frame.pack(fill=tk.X, pady=5)
ttk.Label(frame, text=label_text).grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
entry = ttk.Entry(frame, width=70)
entry.grid(row=0, column=1, padx=5, pady=5)
button = ttk.Button(frame, text="浏览文件夹...",
command=lambda: self.select_folder(entry))
button.grid(row=0, column=2, padx=5, pady=5)
return entry, button
def select_folder(self, entry):
"""选择文件夹"""
foldername = filedialog.askdirectory()
if foldername:
entry.delete(0, tk.END)
entry.insert(0, foldername)
# 自动填充文件夹结构
self.populate_folder_tree(foldername)
def select_file(self, entry, filetypes=None):
"""选择文件"""
if filetypes is None:
filetypes = [("所有文件", "*.*")]
filename = filedialog.askopenfilename(filetypes=filetypes)
if filename:
entry.delete(0, tk.END)
entry.insert(0, filename)
def populate_folder_tree(self, path):
"""填充文件夹结构树"""
self.tree.delete(*self.tree.get_children())
if not os.path.isdir(path):
return
# 添加根节点
root_node = self.tree.insert("", "end", text=os.path.basename(path),
values=("文件夹",), open=True)
self.add_tree_nodes(root_node, path)
def add_tree_nodes(self, parent, path):
"""递归添加树节点"""
try:
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isdir(item_path):
node = self.tree.insert(parent, "end", text=item, values=("文件夹",))
self.add_tree_nodes(node, item_path)
else:
self.tree.insert(parent, "end", text=item, values=("文件",))
except PermissionError:
self.log_message(f"权限错误: 无法访问 {path}")
def log_message(self, message):
"""记录日志消息"""
self.queue.put(("log", message))
def update_progress(self, value):
"""更新进度条"""
self.queue.put(("progress", value))
def update_status(self, message):
"""更新状态信息"""
self.queue.put(("status", message))
def process_queue(self):
"""处理线程队列中的消息"""
try:
while not self.queue.empty():
msg_type, data = self.queue.get_nowait()
if msg_type == "log":
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, data + "\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
elif msg_type == "progress":
self.progress['value'] = data
elif msg_type == "status":
self.status_var.set(data)
except queue.Empty:
pass
self.root.after(100, self.process_queue)
def process_folders(self, old_path, new_path, excel_file):
"""处理文件夹比较的线程函数 - 简化流程:生成HTML后直接触发按钮"""
output_html = None
try:
# 步骤1: 生成HTML差异文件
self.update_status("生成HTML差异文件...")
self.update_progress(30)
# 使用临时文件存储HTML报告
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as temp_file:
output_html = temp_file.name
if not self.run_winmerge(old_path, new_path, output_html):
self.update_status("WinMerge执行失败")
return
# 步骤2: 将HTML文件与Excel放在同一目录
self.update_status("复制HTML报告到目标目录...")
self.update_progress(60)
excel_dir = os.path.dirname(excel_file)
if excel_dir:
target_html = os.path.join(excel_dir, "diff_report.html")
try:
shutil.copy(output_html, target_html)
self.log_message(f"已将HTML文件复制到: {target_html}")
except Exception as e:
self.log_message(f"文件复制失败: {str(e)}")
# 即使复制失败,我们仍然尝试触发按钮
# 步骤3: 直接触发目标Excel中的"作成"按钮
self.update_status("触发Excel按钮...")
self.update_progress(80)
if not self.trigger_excel_button(excel_file):
self.update_status("触发按钮失败")
return
# 完成
self.update_progress(100)
self.update_status("处理完成!")
self.log_message("文件夹比较流程执行完毕")
messagebox.showinfo("完成", "已生成HTML报告并触发Excel处理")
except Exception as e:
error_msg = f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}"
self.log_message(error_msg)
self.update_status("执行失败")
messagebox.showerror("错误", f"处理失败: {str(e)}")
finally:
# 重新启用执行按钮
if self.processing:
self.stop_processing()
# 清理临时文件
if output_html and os.path.exists(output_html):
try:
os.remove(output_html)
except:
pass
def start_processing(self):
"""启动处理线程 - 修复无响应问题"""
if self.processing:
self.log_message("警告: 处理正在进行中")
return
# 获取路径
old_path = self.old_folder_entry.get()
new_path = self.new_folder_entry.get()
excel_file = self.excel_file_entry.get()
# 详细路径验证
validation_errors = []
if not old_path:
validation_errors.append("原始文件夹路径为空")
elif not os.path.isdir(old_path):
validation_errors.append(f"原始文件夹路径无效: {old_path}")
if not new_path:
validation_errors.append("新文件夹路径为空")
elif not os.path.isdir(new_path):
validation_errors.append(f"新文件夹路径无效: {new_path}")
if not excel_file:
validation_errors.append("Excel文件路径为空")
elif not excel_file.lower().endswith(('.xlsx', '.xlsm')):
validation_errors.append("Excel文件必须是.xlsx或.xlsm格式")
if validation_errors:
self.log_message("错误: " + "; ".join(validation_errors))
messagebox.showerror("输入错误", "\n".join(validation_errors))
return
# 检查WinMerge安装
winmerge_path = r"E:\App\WinMerge\WinMerge2.16.12.0\WinMergeU.exe"
if not os.path.exists(winmerge_path):
self.log_message(f"错误: WinMerge未安装在默认位置 {winmerge_path}")
messagebox.showwarning("WinMerge未安装",
"请确保WinMerge已安装或更新路径配置")
return
# 禁用执行按钮,启用停止按钮
self.run_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.processing = True
# 启动处理线程
thread = threading.Thread(target=self.process_folders, args=(old_path, new_path, excel_file))
thread.daemon = True
thread.start()
self.log_message("处理线程已启动")
def run_winmerge(self, path1, path2, output_html):
"""调用WinMerge生成HTML差异文件"""
winmerge_path = r"E:\App\WinMerge\WinMerge2.16.12.0\WinMergeU.exe"
# 验证WinMerge可执行文件
if not os.path.exists(winmerge_path):
self.log_message(f"错误: WinMerge路径不存在 {winmerge_path}")
return False
winmerge_cmd = [
winmerge_path,
'/u',
'/dl', 'Base',
'/dr', 'Modified',
'/or', output_html,
path1, path2
]
# 添加递归选项
if self.recursive_var.get():
winmerge_cmd.insert(1, '/r')
# 添加文件过滤
file_filter = self.filter_var.get()
if file_filter and file_filter != "*.*":
winmerge_cmd.extend(['-f', file_filter])
self.log_message(f"执行WinMerge命令: {' '.join(winmerge_cmd)}")
try:
result = subprocess.run(
winmerge_cmd,
capture_output=True,
text=True,
timeout=120,
creationflags=subprocess.CREATE_NO_WINDOW
)
if result.returncode == 0:
self.log_message(f"HTML差异报告生成完成: {output_html}")
return True
else:
error_msg = f"WinMerge执行失败(退出码{result.returncode}): {result.stderr}"
self.log_message(error_msg)
return False
except subprocess.TimeoutExpired:
self.log_message("WinMerge执行超时(120秒),请检查输入文件大小")
return False
except Exception as e:
self.log_message(f"WinMerge执行错误: {str(e)}")
return False
def trigger_excel_button(self, excel_path):
"""触发目标Excel文件中的按钮"""
self.log_message("正在打开Excel并触发按钮...")
excel = None
workbook = None
try:
# 验证Excel文件存在
if not os.path.exists(excel_path):
self.log_message(f"错误: Excel文件不存在 {excel_path}")
return False
# 使用win32com打开Excel
excel = win32.gencache.EnsureDispatch('Excel.Application')
excel.Visible = True
excel.DisplayAlerts = False
# 打开工作簿
try:
workbook = excel.Workbooks.Open(os.path.abspath(excel_path))
except Exception as e:
self.log_message(f"打开Excel文件失败: {str(e)}")
return False
# 检查工作表是否存在
sheet_names = [sheet.Name for sheet in workbook.Sheets]
if "一覧" not in sheet_names:
self.log_message("错误: Excel文件中缺少'一覧'工作表")
return False
sheet = workbook.Sheets("一覧")
# 触发"作成"按钮
self.log_message("正在触发'作成'按钮...")
try:
# 查找按钮并点击
button = sheet.Buttons("作成")
button.OnAction = "作成按钮的处理"
button.Click()
self.log_message("已触发'作成'按钮")
# 等待处理完成
self.update_status("处理中...请等待")
wait_time = 0
max_wait = 60 # 最大等待60秒
while self.processing and wait_time < max_wait:
if excel.CalculationState == 0: # 0 = xlDone
break
time.sleep(1)
wait_time += 1
self.log_message(f"处理中...({wait_time}秒)")
if wait_time >= max_wait:
self.log_message("警告: 处理超时")
else:
self.log_message("处理完成")
return True
except Exception as e:
self.log_message(f"按钮操作失败: {str(e)}. 请手动点击'作成'按钮")
return False
except Exception as e:
self.log_message(f"Excel操作失败: {str(e)}\n{traceback.format_exc()}")
return False
finally:
# 确保正确关闭Excel
try:
if workbook:
workbook.Close(SaveChanges=True) # 保存更改
if excel:
excel.Quit()
except Exception as e:
self.log_message(f"关闭Excel时出错: {str(e)}")
def stop_processing(self):
"""停止处理"""
self.processing = False
self.stop_button.config(state=tk.DISABLED)
self.run_button.config(state=tk.NORMAL)
self.update_status("操作已停止")
if __name__ == "__main__":
root = tk.Tk()
app = DiffProcessorApp(root)
root.mainloop()
这是我的代码,我希望完成的操作是
1、用WinMerge生成文件夹比较报告及文件比较报告
2、将生成的报告与目标Excel文件放在同一目录下
3、点击目标Excel文件“一覧”工作表上的“作成”按钮
4、等待处理完成
最新发布