让对话框对UPDATE_COMMAND_UI生效

博客指出UPDATE_COMMAND_UI在基于对话框的菜单中修改状态无效,原因是对话框程序无OnInitMenuPopup函数,不会调用UPDATE_COMMAND_UI响应函数。给出解决方法,包括在对话框类的.cpp文件添加消息映射入口、在.h文件添加消息函数声明、在.cpp文件添加函数代码,并提供参考链接。

问题:一般情况下我们用UPDATE_COMMAND_UI来修改菜单的状态(enable/disable, check/uncheck, change text),但这个方法在一个基于对话框上的菜单却没有效果。
void CTestDlg::OnUpdateFileExit(CCmdUI* pCmdUI)
{
pCmdUI->Enable(FALSE);
pCmdUI->SetCheck(TRUE);
pCmdUI->SetRadio(TRUE);
pCmdUI->SetText("Close");
//以上方法在MDI、SDI程序中都能起作用,在对话框中却没有效果,根本没有调用这个函数。
}

原因分析:当显示一个下拉的菜单的时候,在显示菜单前会发送WM_INITMENUPOPUP消息。而CFrameWnd::OnInitMenuPopup函数会刷新这个菜单项,同时如果有UPDATE_COMMAND_UI响应函数,则调用它。通过它来更新反应每个菜单的外观效果(enabled/disabled, checked/unchecked).
在一个基于对话框的程序中,因为没有OnInitMenuPopup函数,所以不会调用UPDATE_COMMAND_UI响应函数,而是使用了CWnd类的默认处理, 这种处理没有调用UPDATE_COMMAND_UI响应函数。

解决方法如下:
第一步:
在对话框类的.cpp文件,添加一个ON_WM_INITMENUPOPUP入口到消息映射里面
BEGIN_MESSAGE_MAP(CTestDlg, CDialog)
//}}AFX_MSG_MAP
ON_WM_INITMENUPOPUP()
END_MESSAGE_MAP()
第二步:
在对话框类的.h文件添加消息函数声明。
// Generated message map functions
//{{AFX_MSG(CDisableDlgMenuDlg)
afx_msg void OnInitMenuPopup(CMenu *pPopupMenu, UINT nIndex,BOOL bSysMenu);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
第三步:
在对话框类的.cpp文件添加如下函数代码(大部分代码取自WinFrm.cpp文件的函数CFrameWnd::OnInitMenuPopup):
void C******Dlg::OnInitMenuPopup(CMenu *pPopupMenu, UINT nIndex,BOOL bSysMenu)
{
ASSERT(pPopupMenu != NULL);
// Check the enabled state of various menu items.

CCmdUI state;
state.m_pMenu = pPopupMenu;
ASSERT(state.m_pOther == NULL);
ASSERT(state.m_pParentMenu == NULL);

// Determine if menu is popup in top-level menu and set m_pOther to
// it if so (m_pParentMenu == NULL indicates that it is secondary popup).
HMENU hParentMenu;
if (AfxGetThreadState()->m_hTrackingMenu == pPopupMenu->m_hMenu)
state.m_pParentMenu = pPopupMenu;??? // Parent == child for tracking popup.
else if ((hParentMenu = ::GetMenu(m_hWnd)) != NULL)
{
CWnd* pParent = this;
// Child windows don't have menus--need to go to the top!
if (pParent != NULL &&
(hParentMenu = ::GetMenu(pParent->m_hWnd)) != NULL)
{
int nIndexMax = ::GetMenuItemCount(hParentMenu);
for (int nIndex = 0; nIndex < nIndexMax; nIndex++)
{
if (::GetSubMenu(hParentMenu, nIndex) == pPopupMenu->m_hMenu)
{
// When popup is found, m_pParentMenu is containing menu.
state.m_pParentMenu = CMenu::FromHandle(hParentMenu);
break;
}
}
}
}

state.m_nIndexMax = pPopupMenu->GetMenuItemCount();
for (state.m_nIndex = 0; state.m_nIndex < state.m_nIndexMax;
state.m_nIndex++)
{
state.m_nID = pPopupMenu->GetMenuItemID(state.m_nIndex);
if (state.m_nID == 0)
continue; // Menu separator or invalid cmd - ignore it.

ASSERT(state.m_pOther == NULL);
ASSERT(state.m_pMenu != NULL);
if (state.m_nID == (UINT)-1)
{
// Possibly a popup menu, route to first item of that popup.
state.m_pSubMenu = pPopupMenu->GetSubMenu(state.m_nIndex);
if (state.m_pSubMenu == NULL ||
(state.m_nID = state.m_pSubMenu->GetMenuItemID(0)) == 0 ||
state.m_nID == (UINT)-1)
{
continue; // First item of popup can't be routed to.
}
state.DoUpdate(this, TRUE);?? // Popups are never auto disabled.
}
else
{
// Normal menu item.
// Auto enable/disable if frame window has m_bAutoMenuEnable
// set and command is _not_ a system command.
state.m_pSubMenu = NULL;
state.DoUpdate(this, FALSE);
}

// Adjust for menu deletions and additions.
UINT nCount = pPopupMenu->GetMenuItemCount();
if (nCount < state.m_nIndexMax)
{
state.m_nIndex -= (state.m_nIndexMax - nCount);
while (state.m_nIndex < nCount &&
pPopupMenu->GetMenuItemID(state.m_nIndex) == state.m_nID)
{
state.m_nIndex++;
}
}
state.m_nIndexMax = nCount;
}
}

更详细的信息可参考
http://support.microsoft.com/default.aspx?scid=kb;en-us;242577

修改,生成完整代码::2025-08-28 19:35:01 - DLT_Analysis - INFO - UI模块已初始化 Traceback (most recent call last): File "C:\Users\Administrator\Desktop\project\modules\follow_analysis_ui.py", line 269, in <module> module_ui = FollowAnalysisModule(master=app_root) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Administrator\Desktop\project\modules\follow_analysis_ui.py", line 70, in __init__ self._setup_precise_result_handler() File "C:\Users\Administrator\Desktop\project\modules\follow_analysis_ui.py", line 159, in _setup_precise_result_handler event_center.subscribe(EventType.MODULE_ERROR.value, handle_error, self.module_name) ^^^^^^^^^^^^^^^^^^^^^^ AttributeError: type object 'EventType' has no attribute 'MODULE_ERROR'. Did you mean: 'MODULE_RUN'? 进程已结束,退出代码为 1 重复的代码段(6 行长),,从外部作用域隐藏名称 'e',从外部作用域隐藏名称 'e'代码#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 大乐透智能分析平台 - 跟随分析模块 (精准点对点通信版) """ import os import sys import json import time import threading import logging import tkinter as tk from tkinter import ttk, scrolledtext, messagebox from typing import Dict, Any, List, Union from collections import defaultdict from enum import Enum # 添加core包路径 CORE_DIR = os.path.join(os.path.dirname(__file__), '..', 'core') if CORE_DIR not in sys.path: sys.path.insert(0, CORE_DIR) # 从core包导入必要的模块 try: from global_config import GlobalConfig from event_center import Event, event_center # 导入事件类型 from event_types import EventType except ImportError as e: print(f"导入core包模块失败: {e}") print("请确保core包路径正确") # 如果导入失败,使用本地定义的事件类型 class EventType(Enum): MODULE_RUN = "module_run" ANALYSIS_RESULT = "analysis_result" MODULE_ERROR = "module_error" MODULE_READY = "module_ready" # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger('DLT_Analysis') # ====== 模块配置 ====== class FollowConfig: UI_MODULE_ID = 'follow_analysis' # UI模块ID ENGINE_MODULE_ID = 'follow_analysis_engine' # 分析引擎ID # ==================== 精准跟随分析模块 ==================== class FollowAnalysisModule: FIXED_LABELS = ["前推荐多", "前推荐少", "后推荐多", "后推荐少"] def __init__(self, master=None): self.module_name = FollowConfig.UI_MODULE_ID self.master = master or self._create_root_window() self.value_vars = {} self.data_lock = threading.Lock() self.analysis_start_time = 0 self.button_frame = None self.run_button = None self.dynamic_data = {label: "" for label in self.FIXED_LABELS} logger.info("UI模块已初始化") self._create_ui() self._setup_precise_result_handler() self._setup_timeout_detection() @staticmethod def _create_root_window(): """创建主窗口""" main_window = tk.Tk() main_window.title("跟随分析模块") main_window.geometry("800x600") return main_window def _create_ui(self): """创建用户界面""" main_frame = ttk.Frame(self.master) main_frame.pack(fill='both', expand=True, padx=10, pady=5) title_frame = ttk.Frame(main_frame) title_frame.pack(fill='x', pady=5) ttk.Label(title_frame, text="跟随分析", font=("楷体", 14, "bold")).pack(side='left') ttk.Label(title_frame, text=f"模块ID: {self.module_name}", font=("宋体", 10)).pack(side='right', padx=10) self._create_dynamic_section(main_frame) self._create_result_section(main_frame) self._create_button_section(main_frame) def _create_dynamic_section(self, parent): """创建动态数据显示区域""" dynamic_frame = ttk.LabelFrame(parent, text="动态区") dynamic_frame.pack(fill='x', padx=10, pady=5, ipadx=5, ipady=5) for label in self.FIXED_LABELS: frame = ttk.Frame(dynamic_frame) frame.pack(fill='x', padx=5, pady=2) ttk.Label(frame, text=f"{label}:", width=10, anchor='e').pack(side='left', padx=(0, 5)) self.value_vars[label] = tk.StringVar() ttk.Entry(frame, textvariable=self.value_vars[label], state='readonly', width=30).pack( side='left', fill='x', expand=True) def _create_result_section(self, parent): """创建结果展示区域""" result_frame = ttk.LabelFrame(parent, text="分析结果") result_frame.pack(fill='both', expand=True, padx=10, pady=5) self.result_text = scrolledtext.ScrolledText(result_frame, wrap=tk.WORD, height=10) self.result_text.pack(fill='both', expand=True, padx=5, pady=5) self._update_result_text("点击'运行'按钮开始分析…") def _create_button_section(self, parent): """创建按钮区域""" self.button_frame = ttk.Frame(parent) self.button_frame.pack(fill='x', padx=10, pady=10) btn_container = ttk.Frame(self.button_frame) btn_container.pack(side='right', padx=5) buttons_info = [ ("清除", self._clear_data), ("保存", self.save_data), ("刷新", self._refresh_data) ] for btn_text, command in buttons_info: ttk.Button(btn_container, text=btn_text, command=command).pack(side='left', padx=5) self.run_button = ttk.Button(btn_container, text="运行", command=self.run_analysis) self.run_button.pack(side='left', padx=5) def _setup_precise_result_handler(self): """设置精准结果事件处理器""" def handle_analysis_result(event: Event): # 精准验证:必须来自分析引擎且token匹配 if (event.source == FollowConfig.ENGINE_MODULE_ID and event.token == self.module_name): logger.info("接收到精准分析结果") with self.data_lock: self._update_dynamic_data(event.data) self._update_ui() self._complete_analysis() else: logger.debug(f"忽略非匹配事件: source={event.source}, token={event.token}") def handle_error(event: Event): if (event.source == FollowConfig.ENGINE_MODULE_ID and event.token == self.module_name): logger.error(f"接收到错误: {event.data.get('error', '未知错误')}") self._update_result_text(f"错误: {event.data.get('error', '未知错误')}") self._complete_analysis() # 精准订阅:只接收来自分析引擎且token匹配的事件 event_center.subscribe(EventType.ANALYSIS_RESULT.value, handle_analysis_result, self.module_name) event_center.subscribe(EventType.MODULE_ERROR.value, handle_error, self.module_name) def _setup_timeout_detection(self): """设置超时检测机制""" def check_timeout(): if self.analysis_start_time > 0: elapsed = time.time() - self.analysis_start_time if elapsed > 15: self._handle_analysis_timeout() self.master.after(1000, check_timeout) self.master.after(1000, check_timeout) def _handle_analysis_timeout(self): """处理分析超时""" self.analysis_start_time = 0 self.run_button.config(state='normal') self._update_result_text("分析超时,请检查分析引擎状态") logger.error("分析超时") def _complete_analysis(self): """完成分析后的通用操作""" self.run_button.config(state='normal') self.analysis_start_time = 0 def run_analysis(self): """运行分析 - 精准发送指令""" try: self.run_button.config(state=tk.DISABLED) self.analysis_start_time = time.time() self._update_result_text("分析中,请稍候…") self._publish_precise_run_event() logger.info("已精准发送运行指令") except Exception as e: logger.error(f"运行分析时出错: {str(e)}") self._update_result_text(f"错误: {str(e)}") self._complete_analysis() def _publish_precise_run_event(self): """精准发布运行事件""" run_event = Event( event_id=int(time.time()), event_type=EventType.MODULE_RUN.value, source=self.module_name, target=FollowConfig.ENGINE_MODULE_ID, data={"command": "start_analysis"}, token=FollowConfig.ENGINE_MODULE_ID ) event_center.publish(run_event) logger.info(f"精准发送: target={run_event.target}, token={run_event.token}") def _update_dynamic_data(self, result: Dict): """更新动态数据 - 精准匹配标签""" for label in self.FIXED_LABELS: if label in result: value = result[label] display_value = ', '.join(map(str, value)) if isinstance(value, list) else str(value) self.dynamic_data[label] = display_value self.value_vars[label].set(display_value) def _update_ui(self): """更新结果界面""" result_str = "=== 精准分析结果 ===\n\n" for label in self.FIXED_LABELS: value = self.dynamic_data.get(label, "") result_str += f"{label}: {value}\n" self._update_result_text(result_str) def _update_result_text(self, message): """更新结果文本框""" self.result_text.config(state=tk.NORMAL) self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, message) self.result_text.config(state=tk.DISABLED) def _clear_data(self): """清除数据""" for label in self.FIXED_LABELS: self.value_vars[label].set("") self.dynamic_data[label] = "" self._update_result_text("数据已清除") logger.info("动态区数据已清除") def save_data(self): """保存数据""" try: filename = f"跟随分析{time.strftime('%Y%m%d%H%M%S')}.json" with open(filename, 'w', encoding='utf-8') as f: json.dump(self.dynamic_data, f, indent=2, ensure_ascii=False) self._update_result_text(f"数据已保存到: {filename}\n") logger.info(f"数据已保存到: {filename}") except Exception as e: logger.error(f"保存失败: {e}") messagebox.showerror("保存错误", f"保存数据时出错: {str(e)}") def _refresh_data(self): """刷新数据""" self._update_ui() self._update_result_text("数据已刷新\n") logger.info("数据已刷新") # ====== 测试代码 ====== if __name__ == "__main__": app_root = tk.Tk() app_root.title("跟随分析模块") app_root.geometry("800x600") module_ui = FollowAnalysisModule(master=app_root) app_root.mainloop()
08-29
1、路径选择对话框是excel的弹窗,目前的弹窗的路径自动填充无法生效 2、处理确认对话框的名称为Winmerge差分ファイルまとめるツール,路径选择对话框的名称为一覧ファイル(*.html)を選択,能否只处理查找和处理这两个弹出的框,查找到其余的框不处理。将这两个框的处理的优先级排高一点 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 import win32com.client import pythoncom import win32gui import win32con import win32api 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 send_enter_key(): # 查找活动窗口(通常是弹出的对话框) hwnd = win32gui.GetForegroundWindow() if hwnd: # 发送Enter键 win32api.PostMessage(hwnd, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0) win32api.PostMessage(hwnd, win32con.WM_KEYUP, win32con.VK_RETURN, 0) def open_excel_file(self, excel_path): self.log_message("正在后台自动化处理Excel文件...") # 初始化COM库 pythoncom.CoInitialize() try: if not os.path.exists(excel_path): self.log_message(f"错误: Excel文件不存在 {excel_path}") return False excel_app = None try: # 创建Excel应用程序对象 excel_app = win32com.client.Dispatch("Excel.Application") excel_app.Visible = True excel_app.WindowState = -4140 # win32com.client.constants.xlMinimized excel_app.DisplayAlerts = False excel_app.ScreenUpdating = False excel_app.EnableEvents = False # 打开工作簿 wb = excel_app.Workbooks.Open(os.path.abspath(excel_path)) self.log_message(f"Excel文件已打开: {excel_path}") # 自动执行宏并处理后续交互 success = self.execute_macro_with_automation(excel_app, wb) # 保存并关闭工作簿 if success: wb.Close(SaveChanges=True) self.log_message("Excel文件处理完成并保存") else: wb.Close(SaveChanges=False) self.log_message("宏执行失败,已放弃更改") return success except Exception as e: error_msg = f"Excel自动化失败: {str(e)}" self.log_message(error_msg) self.log_message(traceback.format_exc()) return False finally: # 确保释放Excel对象 if excel_app is not None: try: excel_app.Quit() # 确保Excel进程完全退出 time.sleep(1) # 额外的清理操作 del excel_app except: pass finally: # 确保取消初始化COM库 pythoncom.CoUninitialize() def execute_macro_with_automation(self, excel_app, workbook): """异步执行宏并监控对话框 - 增强版""" try: # 尝试多次获取窗口句柄 hwnd = None for i in range(5): # 增加尝试次数 hwnd = self.find_excel_window(excel_app) if hwnd: break self.log_message(f"尝试 {i+1}/5 获取Excel窗口句柄") time.sleep(1) # 等待1秒再试 if not hwnd: self.log_message("错误: 多次尝试后仍无法获取Excel窗口句柄!") return False # 创建事件标志用于线程通信 self.macro_finished = threading.Event() # 启动宏执行监控线程 monitor_thread = threading.Thread( target=self.monitor_macro_execution, args=(excel_app, workbook, hwnd) ) monitor_thread.daemon = True monitor_thread.start() self.log_message("宏监控线程已启动") # 执行宏 try: self.log_message("开始执行宏...") excel_app.Run("Sheet1.ExposedMacro") self.log_message("宏执行完成") except Exception as e: self.log_message(f"宏执行异常: {str(e)}") finally: # 标记宏执行完成 self.macro_finished.set() self.log_message("已设置宏完成标志") # 等待监控线程完成 monitor_thread.join(timeout=60) # 最多等待60秒 if monitor_thread.is_alive(): self.log_message("警告: 宏监控线程超时") return True except Exception as e: self.log_message(f"宏执行框架错误: {str(e)}\n{traceback.format_exc()}") return False def monitor_macro_execution(self, excel_app, workbook, hwnd): """后台监控宏执行状态并处理对话框 - 增强版""" self.log_message("启动宏执行监控线程...") # 等待宏开始执行 start_time = time.time() macro_started = False while not macro_started and time.time() - start_time < 10: if self.is_macro_running(excel_app): macro_started = True self.log_message("检测到宏已开始执行") time.sleep(0.5) if not macro_started: self.log_message("警告: 未检测到宏执行启动") return # 主监控循环 start_time = time.time() dialog_handled = {} # 跟踪已处理的对话框 while not self.macro_finished.is_set() and time.time() - start_time < 180: # 延长超时时间 try: # 检查对话框是否存在 dialog_hwnd = self.find_dialog_window(hwnd) if dialog_hwnd: dialog_title = win32gui.GetWindowText(dialog_hwnd) dialog_class = win32gui.GetClassName(dialog_hwnd) # 检查是否已处理过此对话框 dialog_id = f"{dialog_hwnd}-{dialog_title}" if dialog_id in dialog_handled: time.sleep(0.5) continue self.log_message(f"检测到对话框: 类={dialog_class}, 标题='{dialog_title}'") # 根据对话框类型采取不同操作 if "Winmerge差分ファイルまとめるツール" in dialog_title or "Winmerge差分ファイルまとめるツール" in dialog_title: self.log_message("处理确认对话框...") if self.click_dialog_button(dialog_hwnd): dialog_handled[dialog_id] = True time.sleep(1) # 等待对话框关闭 else: self.force_confirm_dialog(dialog_hwnd) dialog_handled[dialog_id] = True # 路径选择对话框处理逻辑 elif "一覧ファイル(*.html)を選択" in dialog_title or "Select" in dialog_title or "Browse" in dialog_title: self.log_message("检测到路径选择对话框,跳过自动化处理") # 标记已处理但不执行操作,等待用户输入 dialog_handled[dialog_id] = True # 在这里可以添加路径自动填充逻辑(如果需要) self.fill_path_dialog(dialog_hwnd) # 未知对话框处理 else: self.log_message("未知对话框类型,尝试默认处理...") if self.click_dialog_button(dialog_hwnd): dialog_handled[dialog_id] = True time.sleep(1) else: self.force_confirm_dialog(dialog_hwnd) dialog_handled[dialog_id] = True # 检查是否有新的对话框出现 elif dialog_handled: # 清空处理记录,允许重新处理对话框 dialog_handled.clear() except Exception as e: self.log_message(f"对话框监控错误: {str(e)}") # 短暂休眠避免CPU占用过高 time.sleep(0.5) if self.macro_finished.is_set(): self.log_message("宏执行监控正常结束") else: self.log_message("警告: 宏执行监控超时") def fill_path_dialog(self, dialog_hwnd): """自动填充路径选择对话框(示例)""" try: # 查找编辑框控件 edit_hwnd = None buttons = [] def enum_controls(hwnd, results): cls_name = win32gui.GetClassName(hwnd) text = win32gui.GetWindowText(hwnd) if "Edit" in cls_name: results["edit"] = hwnd elif "Button" in cls_name: buttons.append((hwnd, text)) controls = {} win32gui.EnumChildWindows(dialog_hwnd, enum_controls, controls) # 如果找到编辑框,填充报告路径 if "edit" in controls: edit_hwnd = controls["edit"] report_dir = os.path.dirname(self.folder_report_path) # 设置编辑框文本 win32gui.SendMessage(edit_hwnd, win32con.WM_SETTEXT, 0, report_dir) self.log_message(f"已填充路径: {report_dir}") # 查找并点击"确定"按钮 for btn_hwnd, btn_text in buttons: if "确定" in btn_text or "OK" in btn_text: win32gui.PostMessage(btn_hwnd, win32con.BM_CLICK, 0, 0) self.log_message("点击确定按钮") return True return False except Exception as e: self.log_message(f"填充路径对话框失败: {str(e)}") return False def find_excel_window(self, excel_app): """获取Excel应用程序窗口句柄""" try: # 方法1:通过窗口标题匹配 titles = [] def enum_windows_callback(hwnd, results): if win32gui.IsWindowVisible(hwnd): title = win32gui.GetWindowText(hwnd) # 匹配Excel主窗口标题的模式(通常包含"Excel") if "Excel" in title and "Book" in title: results.append(hwnd) win32gui.EnumWindows(enum_windows_callback, titles) if titles: return titles[0] # 返回第一个匹配的窗口 # 方法2:如果没有找到,尝试通过类名 class_names = [] def enum_class_callback(hwnd, results): if win32gui.IsWindowVisible(hwnd): class_name = win32gui.GetClassName(hwnd) if class_name == "XLMAIN": results.append(hwnd) win32gui.EnumWindows(enum_class_callback, class_names) if class_names: return class_names[0] # 方法3:作为备选,尝试获取活动窗口 hwnd = win32gui.GetForegroundWindow() if hwnd: title = win32gui.GetWindowText(hwnd) if "Excel" in title: return hwnd self.log_message("警告: 未找到Excel窗口") return None except Exception as e: self.log_message(f"查找Excel窗口失败: {str(e)}") return None def is_macro_running(self, excel_app): """ 检测宏是否正在运行 改进:通过检测Excel是否处于交互模式(有对话框打开) """ try: # 尝试访问Excel的Ready属性 - 如果宏正在运行,Excel通常不处于就绪状态 if excel_app.Ready: # 检查是否有打开的对话框 if excel_app.Dialogs.Count > 0: return True return False return True except: # 如果访问失败,假设宏仍在运行 return True def find_dialog_window(self, parent_hwnd): """查找Excel的对话框窗口 - 增强版""" try: dialogs = [] def enum_child_windows(hwnd, results): if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd): class_name = win32gui.GetClassName(hwnd) title = win32gui.GetWindowText(hwnd) # 常见对话框类名和标题模式 if (class_name in ["#32770", "bosa_sdm_XL", "EXCEL7"] or "Winmerge差分ファイルまとめるツール" in title or "一覧ファイル(*.html)を選択" in title): results.append((hwnd, class_name, title)) # 枚举子窗口 win32gui.EnumChildWindows(parent_hwnd, enum_child_windows, dialogs) # 按Z序排序(最顶层对话框优先) dialogs.sort(key=lambda d: win32gui.GetWindow(d[0], win32con.GW_HWNDPREV), reverse=True) if dialogs: # 返回最顶层的可见对话框 return dialogs[0][0] # 如果未找到,尝试在所有顶层窗口中查找 all_dialogs = [] win32gui.EnumWindows( lambda hwnd, results: results.append((hwnd, win32gui.GetClassName(hwnd), win32gui.GetWindowText(hwnd))) if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd) else None, all_dialogs ) # 筛选并排序对话框 valid_dialogs = [(h, c, t) for h, c, t in all_dialogs if c in ["#32770", "bosa_sdm_XL", "EXCEL7"] or "Winmerge差分ファイルまとめるツール" in t or "一覧ファイル(*.html)を選択" in t] valid_dialogs.sort(key=lambda d: win32gui.GetWindow(d[0], win32con.GW_HWNDPREV), reverse=True) return valid_dialogs[0][0] if valid_dialogs else None except Exception as e: self.log_message(f"查找对话框失败: {str(e)}") return None def click_dialog_button(self, dialog_hwnd): """点击对话框的确定按钮""" try: # 查找按钮控件 button_hwnd = None # 枚举对话框的子窗口查找按钮 buttons = [] win32gui.EnumChildWindows( dialog_hwnd, lambda hwnd, results: results.append(hwnd) if "Button" in win32gui.GetClassName(hwnd) else None, buttons ) # 查找文本为"确定"或"OK"的按钮 for btn in buttons: text = win32gui.GetWindowText(btn) if "确定" in text or "OK" in text: button_hwnd = btn break if button_hwnd: # 发送点击消息 win32gui.PostMessage(button_hwnd, win32con.BM_CLICK, 0, 0) self.log_message("已发送按钮点击消息") return True # 如果未找到特定按钮,尝试点击第一个按钮 if buttons: win32gui.PostMessage(buttons[0], win32con.BM_CLICK, 0, 0) self.log_message("已点击第一个按钮") return True return False except Exception as e: self.log_message(f"按钮点击失败: {str(e)}") return False def force_confirm_dialog(self, hwnd): """强制确认对话框的最后手段""" try: # 激活窗口 win32gui.SetForegroundWindow(hwnd) time.sleep(0.5) # 方法1:发送回车键 win32api.keybd_event(win32con.VK_RETURN, 0, 0, 0) win32api.keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0) self.log_message("已发送回车键") time.sleep(0.5) return True except Exception as e: self.log_message(f"强制确认失败: {str(e)}") 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()
最新发布
09-10
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值