好的,现在能够显示宏了,并且宏名称为ExposedMacro,请结合我的下述代码,更新一个流程,打开excel文件后,自动运行宏ExposedMacro
#软件著作人:杨晨 2025.07.11
import os
import subprocess
import shutil
import time
import tkinter as tk
from tkinter import filedialog, ttk, scrolledtext, messagebox, PhotoImage
import threading
import queue
import traceback
import webbrowser
import datetime
import configparser
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('View.TButton',
font=('Segoe UI', 10, 'bold'),
borderwidth=1,
foreground="#008000",
background="#4CAF50",
bordercolor="#388E3C",
relief="flat",
padding=8,
anchor="center")
self.style.map('View.TButton',
background=[('active', '#388E3C'), ('disabled', '#BDBDBD')],
foreground=[('active', '#004D00'), ('disabled', '#808080')])
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.create_folder_selector(file_frame, "原始文件夹:", "old_folder")
self.new_folder_entry, _ = self.create_folder_selector(file_frame, "修改后文件夹:", "new_folder")
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)
self.excel_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="12")
self.excel_frame.pack(fill=tk.X, pady=5)
ttk.Label(self.excel_frame, text="目标Excel文件:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
self.excel_file_entry = ttk.Entry(self.excel_frame, width=60)
self.excel_file_entry.grid(row=0, column=1, padx=5, pady=5)
ttk.Button(self.excel_frame, text="浏览...",
command=lambda: self.select_file(self.excel_file_entry, "excel_file",
[("Excel文件", "*.xlsx *.xlsm")])).grid(row=0, column=2, padx=5, pady=5)
winmerge_frame = ttk.Frame(self.excel_frame)
winmerge_frame.grid(row=1, column=0, columnspan=3, sticky=tk.W, padx=5, pady=5)
ttk.Label(winmerge_frame, text="WinMerge路径:").grid(row=0, column=0, sticky=tk.W)
self.winmerge_entry = ttk.Entry(winmerge_frame, width=60)
self.winmerge_entry.grid(row=0, column=1, padx=5)
self.winmerge_entry.insert(0, r"E:\App\WinMerge\WinMerge2.16.46.0\WinMergeU.exe")
ttk.Button(winmerge_frame, text="浏览...",
command=lambda: self.select_file(self.winmerge_entry, "winmerge_path",
[("WinMerge 可执行文件", "*.exe")])).grid(row=0, column=2)
delete_frame = ttk.Frame(self.excel_frame)
delete_frame.grid(row=2, column=0, columnspan=3, sticky=tk.W, padx=5, pady=5)
self.delete_temp_files_var = tk.BooleanVar(value=False)
delete_check = ttk.Checkbutton(
delete_frame,
text="完成后删除临时文件",
variable=self.delete_temp_files_var,
command=self.update_view_button_state
)
delete_check.grid(row=0, column=0, padx=5, sticky=tk.W)
self.delete_status_label = ttk.Label(
delete_frame,
text="(勾选后将删除报告文件,无法查看)",
foreground="#FF0000",
font=("Segoe UI", 9)
)
self.delete_status_label.grid(row=0, column=1, padx=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.view_folder_report_button = ttk.Button(button_frame, text="查看文件夹报告",
command=lambda: self.view_report("folder"),
width=15, state=tk.DISABLED, style='View.TButton')
self.view_folder_report_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.folder_report_path = None
self.files_dir = None
self.copied_html_files = []
self.config_file = "folder_compare_config.ini"
self.load_paths()
self.root.after(100, self.process_queue)
def update_view_button_state(self):
if self.delete_temp_files_var.get():
self.view_folder_report_button.config(state=tk.DISABLED)
self.style.configure('View.TButton', foreground='#808080', background='#BDBDBD')
else:
if self.folder_report_path and os.path.exists(self.folder_report_path):
self.view_folder_report_button.config(state=tk.NORMAL)
self.style.configure('View.TButton', foreground='#008000', background='#4CAF50')
else:
self.view_folder_report_button.config(state=tk.DISABLED)
self.style.configure('View.TButton', foreground='#808080', background='#BDBDBD')
self.view_folder_report_button.configure(style='View.TButton')
def create_folder_selector(self, parent, label_text, config_key):
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, config_key))
button.grid(row=0, column=2, padx=5, pady=5)
return entry, button
def select_folder(self, entry, config_key):
initial_dir = self.get_last_path(config_key)
if not initial_dir or not os.path.exists(initial_dir):
initial_dir = os.getcwd()
foldername = filedialog.askdirectory(initialdir=initial_dir)
if foldername:
entry.delete(0, tk.END)
entry.insert(0, foldername)
self.populate_folder_tree(foldername)
self.save_path(config_key, foldername)
def select_file(self, entry, config_key, filetypes=None):
if filetypes is None:
filetypes = [("所有文件", "*.*")]
initial_dir = self.get_last_path(config_key)
if not initial_dir or not os.path.exists(initial_dir):
initial_dir = os.getcwd()
if os.path.isfile(initial_dir):
initial_dir = os.path.dirname(initial_dir)
filename = filedialog.askopenfilename(filetypes=filetypes, initialdir=initial_dir)
if filename:
entry.delete(0, tk.END)
entry.insert(0, filename)
self.save_path(config_key, filename)
def get_last_path(self, config_key):
config = configparser.ConfigParser()
if os.path.exists(self.config_file):
config.read(self.config_file)
if config.has_option('Paths', config_key):
return config.get('Paths', config_key)
return None
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 view_report(self, report_type):
if report_type == "folder" and self.folder_report_path and os.path.exists(self.folder_report_path):
try:
webbrowser.open(self.folder_report_path)
except Exception as e:
messagebox.showerror("错误", f"无法打开文件夹报告: {str(e)}")
else:
messagebox.showwarning("警告", f"没有可用的{report_type}报告文件")
def process_folders(self, old_path, new_path, excel_file):
try:
report_dir = os.path.dirname(excel_file) or os.getcwd()
os.makedirs(report_dir, exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
self.folder_report_path = os.path.join(report_dir, f"folder_diff_report_{timestamp}.html")
self.update_status("生成比较报告...")
self.update_progress(30)
winmerge_path = self.winmerge_entry.get()
if not self.run_winmerge(winmerge_path, old_path, new_path):
self.update_status("WinMerge执行失败")
return
if not os.path.exists(self.folder_report_path) or os.path.getsize(self.folder_report_path) == 0:
self.log_message("警告: 文件夹报告为空或未生成")
else:
self.log_message(f"文件夹报告生成成功: {self.folder_report_path} ({os.path.getsize(self.folder_report_path)} bytes)")
self.copy_detail_files(report_dir)
self.update_status("打开Excel文件...")
self.update_progress(80)
if not self.open_excel_file(excel_file):
self.update_status("打开Excel失败")
return
if self.delete_temp_files_var.get():
self.delete_winmerge_reports()
self.log_message("已删除所有临时文件")
else:
self.log_message("保留临时文件,可查看报告")
self.root.after(100, self.update_view_button_state)
self.update_progress(100)
self.update_status("处理完成!")
self.log_message("文件夹比较流程执行完毕")
messagebox.showinfo("完成", "已生成报告并打开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()
def delete_winmerge_reports(self):
if not self.folder_report_path:
return
if os.path.exists(self.folder_report_path):
try:
os.remove(self.folder_report_path)
self.log_message(f"已删除报告文件: {self.folder_report_path}")
except Exception as e:
self.log_message(f"删除报告文件失败: {str(e)}")
base_path = os.path.splitext(self.folder_report_path)[0]
files_dir = base_path + ".files"
if os.path.exists(files_dir):
try:
shutil.rmtree(files_dir)
self.log_message(f"已删除.files目录: {files_dir}")
except Exception as e:
self.log_message(f"删除.files目录失败: {str(e)}")
report_dir = os.path.dirname(self.folder_report_path)
if os.path.exists(report_dir) and hasattr(self, 'copied_html_files') and self.copied_html_files:
deleted_count = 0
for file_path in self.copied_html_files:
if os.path.exists(file_path):
try:
os.remove(file_path)
deleted_count += 1
self.log_message(f"已删除复制的HTML文件: {file_path}")
except Exception as e:
self.log_message(f"删除HTML文件失败: {file_path} - {str(e)}")
self.log_message(f"已删除 {deleted_count}/{len(self.copied_html_files)} 个复制的HTML文件")
self.copied_html_files = []
self.folder_report_path = None
self.view_folder_report_button.config(state=tk.DISABLED)
self.root.after(100, self.update_view_button_state)
def copy_detail_files(self, report_dir):
base_path = os.path.splitext(self.folder_report_path)[0]
files_dir = base_path + ".files"
if not os.path.exists(files_dir):
self.log_message(f"警告: 详细文件目录不存在 {files_dir}")
return
html_files = [f for f in os.listdir(files_dir) if f.lower().endswith('.html')]
if not html_files:
self.log_message(f"警告: 详细文件目录中没有HTML文件 {files_dir}")
return
if not hasattr(self, 'copied_html_files') or not self.copied_html_files:
self.copied_html_files = []
copied_count = 0
for file_name in html_files:
src_path = os.path.join(files_dir, file_name)
dst_path = os.path.join(report_dir, file_name)
try:
shutil.copy2(src_path, dst_path)
copied_count += 1
self.copied_html_files.append(dst_path)
except Exception as e:
self.log_message(f"复制文件失败: {file_name} - {str(e)}")
self.log_message(f"已复制 {copied_count}/{len(html_files)} 个详细HTML文件到报告目录")
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格式")
winmerge_path = self.winmerge_entry.get()
if not winmerge_path or not os.path.exists(winmerge_path):
validation_errors.append("WinMerge路径无效或未设置")
if validation_errors:
self.log_message("错误: " + "; ".join(validation_errors))
messagebox.showerror("输入错误", "\n".join(validation_errors))
return
self.run_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.view_folder_report_button.config(state=tk.DISABLED)
self.processing = True
self.copied_html_files = []
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, winmerge_path, path1, path2):
if not os.path.exists(winmerge_path):
self.log_message(f"错误: WinMerge路径不存在 {winmerge_path}")
return False
os.makedirs(os.path.dirname(self.folder_report_path), exist_ok=True)
cmd = [
winmerge_path,
'/u',
'/nosplash',
'/dl', 'Base',
'/dr', 'Modified',
'/noninteractive',
'/minimize'
]
if self.recursive_var.get():
cmd.extend(['/r', '/s'])
else:
cmd.extend(['/r-', '/s-'])
file_filter = self.filter_var.get()
if file_filter and file_filter != "*.*":
cmd.extend(['-f', file_filter])
cmd.extend(['/or', self.folder_report_path])
cmd.extend([path1, path2])
self.update_status("正在生成比较报告...")
return self.execute_winmerge_command(cmd, "比较报告")
def execute_winmerge_command(self, cmd, report_type):
try:
self.log_message(f"开始生成{report_type}...")
self.log_message(f"执行命令: {' '.join(cmd)}")
start_time = time.time()
creation_flags = 0
if os.name == 'nt':
creation_flags = subprocess.CREATE_NO_WINDOW
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
errors='replace',
creationflags=creation_flags
)
timeout = 900
try:
stdout, stderr = process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
process.kill()
stdout, stderr = process.communicate()
self.log_message(f"{report_type}生成超时({timeout}秒),已终止进程")
return False
elapsed = time.time() - start_time
self.log_message(f"{report_type}生成完成,耗时: {elapsed:.2f}秒")
if stdout.strip():
self.log_message(f"WinMerge输出:\n{stdout[:2000]}")
if stderr.strip():
self.log_message(f"WinMerge错误:\n{stderr[:1000]}")
if process.returncode == 0:
self.log_message(f"{report_type}命令执行成功")
return True
elif process.returncode == 1:
self.log_message(f"{report_type}命令执行完成(发现差异)")
return True
elif process.returncode == 2:
self.log_message(f"{report_type}命令执行完成(发现错误)")
return False
else:
error_msg = f"{report_type}生成失败(退出码{process.returncode})"
self.log_message(error_msg)
return False
except Exception as e:
self.log_message(f"{report_type}生成错误: {str(e)}\n{traceback.format_exc()}")
return False
def open_excel_file(self, excel_path):
self.log_message("正在打开Excel文件...")
try:
if not os.path.exists(excel_path):
self.log_message(f"错误: Excel文件不存在 {excel_path}")
return False
os.startfile(excel_path)
self.log_message(f"Excel文件已打开: {excel_path}")
self.log_message("Excel已打开,请手动执行操作")
messagebox.showinfo("Excel已打开", "Excel文件已打开,请手动执行所需操作")
return True
except Exception as e:
self.log_message(f"打开Excel文件失败: {str(e)}\n{traceback.format_exc()}")
return False
def stop_processing(self):
self.processing = False
self.stop_button.config(state=tk.DISABLED)
self.run_button.config(state=tk.NORMAL)
self.root.after(100, self.update_view_button_state)
self.update_status("操作已停止")
def load_paths(self):
config = configparser.ConfigParser()
if os.path.exists(self.config_file):
try:
config.read(self.config_file)
if config.has_option('Paths', 'old_folder'):
old_path = config.get('Paths', 'old_folder')
self.old_folder_entry.delete(0, tk.END)
self.old_folder_entry.insert(0, old_path)
if os.path.isdir(old_path):
self.populate_folder_tree(old_path)
if config.has_option('Paths', 'new_folder'):
new_path = config.get('Paths', 'new_folder')
self.new_folder_entry.delete(0, tk.END)
self.new_folder_entry.insert(0, new_path)
if config.has_option('Paths', 'excel_file'):
excel_path = config.get('Paths', 'excel_file')
self.excel_file_entry.delete(0, tk.END)
self.excel_file_entry.insert(0, excel_path)
if config.has_option('Paths', 'winmerge_path'):
winmerge_path = config.get('Paths', 'winmerge_path')
self.winmerge_entry.delete(0, tk.END)
self.winmerge_entry.insert(0, winmerge_path)
self.log_message("已加载上次保存的路径")
except Exception as e:
self.log_message(f"加载配置文件失败: {str(e)}")
else:
self.log_message("未找到配置文件,将使用默认路径")
def save_path(self, key, path):
config = configparser.ConfigParser()
if os.path.exists(self.config_file):
config.read(self.config_file)
if not config.has_section('Paths'):
config.add_section('Paths')
config.set('Paths', key, path)
try:
with open(self.config_file, 'w') as configfile:
config.write(configfile)
self.log_message(f"已保存路径: {key} = {path}")
except Exception as e:
self.log_message(f"保存路径失败: {str(e)}")
def save_all_paths(self):
config = configparser.ConfigParser()
config.add_section('Paths')
config.set('Paths', 'old_folder', self.old_folder_entry.get())
config.set('Paths', 'new_folder', self.new_folder_entry.get())
config.set('Paths', 'excel_file', self.excel_file_entry.get())
config.set('Paths', 'winmerge_path', self.winmerge_entry.get())
try:
with open(self.config_file, 'w') as configfile:
config.write(configfile)
self.log_message("所有路径已保存")
except Exception as e:
self.log_message(f"保存所有路径失败: {str(e)}")
def on_closing(self):
self.save_all_paths()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = DiffProcessorApp(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()