513. Find Bottom Left Tree Value

题目

Given a binary tree, find the leftmost value in the last row of the tree.

Example 1:

Input:

    2
   / \
  1   3

Output:
1

Example 2: 

Input:

        1
       / \
      2   3
     /   / \
    4   5   6
       /
      7

Output:
7

Note: You may assume the tree (i.e., the given root node) is not NULL.

分析

寻找一个二叉树的深度最左节点,可以通过计算二叉树深度的递归方法进行遍历,从左子树开始保证先找到最左节点,由于要找最深的最左节点,因此需要保存最大深度值,与当前深度作比较。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        int maxDepth=0;
        int leftmost=root->val;
        deepsearch(root,leftmost,maxDepth,0);
        return leftmost;
    }
    void deepsearch(TreeNode* root,int& leftmost,int& maxDepth,int depth)
    {
        if(root==NULL)
            return;
        deepsearch(root->left,leftmost,maxDepth,depth+1);
        deepsearch(root->right,leftmost,maxDepth,depth+1);
        if(depth>maxDepth)
        {
            maxDepth=depth;
            leftmost=root->val;
        }
    }
};


# -*- coding: utf-8 -*- import pandas as pd from datetime import datetime import tkinter as tk from tkinter import ttk, filedialog, messagebox import os import traceback import re from openpyxl import load_workbook from concurrent.futures import ThreadPoolExecutor from tkinter import font as tkfont class EnhancedVersionUpdaterApp: def __init__(self, root): self.root = root self.root.title("Excel批量修改工具") self.root.geometry("1300x900") # 设置全局字体 default_font = tkfont.nametofont("TkDefaultFont") default_font.configure(size=10) self.root.option_add("*Font", default_font) # 使用线程池提高性能 self.executor = ThreadPoolExecutor(max_workers=4) self.running_tasks = 0 # 初始化变量 self.file_path = "" self.old_project = "" self.new_project = "" self.old_date = "" self.new_date = datetime.now().strftime("%Y-%m-%d") self.old_responsible = "" self.new_responsible = "" self.project_updates = [] self.date_updates = [] self.responsible_updates = [] # 特殊sheet配置 self.special_sheets = { '変更履歴': {'process': False}, 'history': {'process': False}, 'log': {'process': False}, '封面': {'process': True, 'update_time': False}, '表紙': {'process': True, 'update_time': False}, 'cover': {'process': True, 'update_time': False} } # 时间格式正则表达式 self.date_patterns = [ r'\d{4}-\d{2}-\d{2}', # YYYY-MM-DD r'\d{4}/\d{2}/\d{2}', # YYYY/MM/DD r'\d{4}年\d{2}月\d{2}日', # 中文日期 r'\d{2}-\d{2}-\d{4}', # MM-DD-YYYY r'\d{2}/\d{2}/\d{4}' # MM/DD/YYYY ] self.create_enhanced_ui() self.setup_style() def setup_style(self): """设置界面样式""" style = ttk.Style() style.configure('TFrame', background='white') style.configure('TLabel', background='white') style.configure('Treeview', rowheight=25) style.configure('Treeview.Heading', font=('Arial', 10, 'bold')) style.configure('TNotebook.Tab', padding=[10, 5]) style.configure('TButton', padding=5) style.map('TButton', background=[('active', '#e6e6e6'), ('!active', '#f0f0f0')], foreground=[('active', 'black'), ('!active', 'black')]) def create_enhanced_ui(self): """创建增强版用户界面""" # 主框架 main_frame = ttk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 顶部工具栏 toolbar_frame = ttk.Frame(main_frame) toolbar_frame.pack(fill=tk.X, pady=(0, 10)) # 文件选择区域 file_frame = ttk.LabelFrame(toolbar_frame, text="文件选择", padding=10) file_frame.pack(side=tk.LEFT, fill=tk.X, expand=True) ttk.Label(file_frame, text="Excel文件路径:").pack(side=tk.LEFT) self.file_entry = ttk.Entry(file_frame, width=60) self.file_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) ttk.Button(file_frame, text="浏览", command=self.browse_file).pack(side=tk.LEFT) # 进度条 self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar(toolbar_frame, variable=self.progress_var, maximum=100) self.progress_bar.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(10, 0)) self.progress_label = ttk.Label(toolbar_frame, text="就绪") self.progress_label.pack(side=tk.RIGHT, padx=5) # 主内容区域 - 使用Notebook实现标签页 notebook = ttk.Notebook(main_frame) notebook.pack(fill=tk.BOTH, expand=True) # 项目变更标签页 project_tab = ttk.Frame(notebook) notebook.add(project_tab, text="项目编号变更") self.create_project_tab(project_tab) # 时间变更标签页 date_tab = ttk.Frame(notebook) notebook.add(date_tab, text="时间变更") self.create_date_tab(date_tab) # 担当变更标签页 responsible_tab = ttk.Frame(notebook) notebook.add(responsible_tab, text="担当变更") self.create_responsible_tab(responsible_tab) # 底部状态栏 self.status_var = tk.StringVar() self.status_var.set("就绪") status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN) status_bar.pack(fill=tk.X, pady=(5, 0)) def create_project_tab(self, parent): """创建项目变更标签页 - 仅表格可滚动""" # 主框架 main_frame = ttk.Frame(parent) main_frame.pack(fill=tk.BOTH, expand=True) # 输入区域 input_frame = ttk.Frame(main_frame) input_frame.pack(fill=tk.X, pady=5) ttk.Label(input_frame, text="原项目编号:").grid(row=0, column=0, sticky="w", padx=5) self.old_project_entry = ttk.Entry(input_frame, width=30) self.old_project_entry.grid(row=0, column=1, sticky="w", padx=5) ttk.Label(input_frame, text="新项目编号:").grid(row=1, column=0, sticky="w", padx=5) self.new_project_entry = ttk.Entry(input_frame, width=30) self.new_project_entry.grid(row=1, column=1, sticky="w", padx=5) ttk.Button(input_frame, text="查找项目", command=lambda: self.executor.submit(self.load_project_changes)).grid(row=0, column=2, rowspan=2, padx=10) # 表格容器框架 table_container = ttk.Frame(main_frame) table_container.pack(fill=tk.BOTH, expand=True, pady=5) # 创建Treeview和滚动条 tree_scroll_y = ttk.Scrollbar(table_container) tree_scroll_y.pack(side=tk.RIGHT, fill=tk.Y) tree_scroll_x = ttk.Scrollbar(table_container, orient=tk.HORIZONTAL) tree_scroll_x.pack(side=tk.BOTTOM, fill=tk.X) self.project_tree = ttk.Treeview( table_container, columns=("Sheet", "位置", "原项目", "新项目", "状态"), show="headings", height=15, yscrollcommand=tree_scroll_y.set, xscrollcommand=tree_scroll_x.set ) self.project_tree.pack(fill=tk.BOTH, expand=True) # 配置滚动条 tree_scroll_y.config(command=self.project_tree.yview) tree_scroll_x.config(command=self.project_tree.xview) # 配置列 for col, width in [("Sheet", 200), ("位置", 100), ("原项目", 200), ("新项目", 200), ("状态", 100)]: self.project_tree.column(col, width=width, anchor="w") self.project_tree.heading(col, text=col) # 操作按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X, pady=5) ttk.Button(btn_frame, text="标记更新", command=lambda: self.update_project_status("待更新")).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="标记不更新", command=lambda: self.update_project_status("不更新")).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="执行更新", command=lambda: self.executor.submit(self.apply_project_updates)).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="导出报告", command=self.export_project_report).pack(side=tk.RIGHT, padx=5) def create_date_tab(self, parent): """创建时间变更标签页 - 仅表格可滚动""" # 主框架 main_frame = ttk.Frame(parent) main_frame.pack(fill=tk.BOTH, expand=True) # 输入区域 input_frame = ttk.Frame(main_frame) input_frame.pack(fill=tk.X, pady=5) ttk.Label(input_frame, text="原时间:").grid(row=0, column=0, sticky="w", padx=5) self.old_date_entry = ttk.Entry(input_frame, width=30) self.old_date_entry.grid(row=0, column=1, sticky="w", padx=5) ttk.Label(input_frame, text="新时间:").grid(row=1, column=0, sticky="w", padx=5) self.new_date_entry = ttk.Entry(input_frame, width=30) self.new_date_entry.insert(0, self.new_date) self.new_date_entry.grid(row=1, column=1, sticky="w", padx=5) ttk.Button(input_frame, text="查找时间", command=lambda: self.executor.submit(self.load_date_changes)).grid(row=0, column=2, rowspan=2, padx=10) # 表格容器框架 table_container = ttk.Frame(main_frame) table_container.pack(fill=tk.BOTH, expand=True, pady=5) # 创建Treeview和滚动条 tree_scroll_y = ttk.Scrollbar(table_container) tree_scroll_y.pack(side=tk.RIGHT, fill=tk.Y) tree_scroll_x = ttk.Scrollbar(table_container, orient=tk.HORIZONTAL) tree_scroll_x.pack(side=tk.BOTTOM, fill=tk.X) self.date_tree = ttk.Treeview( table_container, columns=("Sheet", "位置", "原时间", "新时间", "状态"), show="headings", height=15, yscrollcommand=tree_scroll_y.set, xscrollcommand=tree_scroll_x.set ) self.date_tree.pack(fill=tk.BOTH, expand=True) # 配置滚动条 tree_scroll_y.config(command=self.date_tree.yview) tree_scroll_x.config(command=self.date_tree.xview) # 配置列 for col, width in [("Sheet", 200), ("位置", 100), ("原时间", 200), ("新时间", 200), ("状态", 100)]: self.date_tree.column(col, width=width, anchor="w") self.date_tree.heading(col, text=col) # 操作按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X, pady=5) ttk.Button(btn_frame, text="标记更新", command=lambda: self.update_date_status("待更新")).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="标记不更新", command=lambda: self.update_date_status("不更新")).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="执行更新", command=lambda: self.executor.submit(self.apply_date_updates)).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="导出报告", command=self.export_date_report).pack(side=tk.RIGHT, padx=5) def create_responsible_tab(self, parent): """创建担当变更标签页 - 仅表格可滚动""" # 主框架 main_frame = ttk.Frame(parent) main_frame.pack(fill=tk.BOTH, expand=True) # 输入区域 input_frame = ttk.Frame(main_frame) input_frame.pack(fill=tk.X, pady=5) ttk.Label(input_frame, text="原担当:").grid(row=0, column=0, sticky="w", padx=5) self.old_responsible_entry = ttk.Entry(input_frame, width=30) self.old_responsible_entry.grid(row=0, column=1, sticky="w", padx=5) ttk.Label(input_frame, text="新担当:").grid(row=1, column=0, sticky="w", padx=5) self.new_responsible_entry = ttk.Entry(input_frame, width=30) self.new_responsible_entry.grid(row=1, column=1, sticky="w", padx=5) ttk.Button(input_frame, text="查找担当", command=lambda: self.executor.submit(self.load_responsible_changes)).grid(row=0, column=2, rowspan=2, padx=10) # 表格容器框架 table_container = ttk.Frame(main_frame) table_container.pack(fill=tk.BOTH, expand=True, pady=5) # 创建Treeview和滚动条 tree_scroll_y = ttk.Scrollbar(table_container) tree_scroll_y.pack(side=tk.RIGHT, fill=tk.Y) tree_scroll_x = ttk.Scrollbar(table_container, orient=tk.HORIZONTAL) tree_scroll_x.pack(side=tk.BOTTOM, fill=tk.X) self.responsible_tree = ttk.Treeview( table_container, columns=("Sheet", "位置", "原担当", "新担当", "状态"), show="headings", height=15, yscrollcommand=tree_scroll_y.set, xscrollcommand=tree_scroll_x.set ) self.responsible_tree.pack(fill=tk.BOTH, expand=True) # 配置滚动条 tree_scroll_y.config(command=self.responsible_tree.yview) tree_scroll_x.config(command=self.responsible_tree.xview) # 配置列 for col, width in [("Sheet", 200), ("位置", 100), ("原担当", 200), ("新担当", 200), ("状态", 100)]: self.responsible_tree.column(col, width=width, anchor="w") self.responsible_tree.heading(col, text=col) # 操作按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X, pady=5) ttk.Button(btn_frame, text="标记更新", command=lambda: self.update_responsible_status("待更新")).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="标记不更新", command=lambda: self.update_responsible_status("不更新")).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="执行更新", command=lambda: self.executor.submit(self.apply_responsible_updates)).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="导出报告", command=self.export_responsible_report).pack(side=tk.RIGHT, padx=5) def _display_data(self, treeview, data): """显示数据到表格 - 添加自动滚动到顶部功能""" treeview.delete(*treeview.get_children()) for update in data: tags = ("to_update",) if update["status"] == "待更新" else ("no_update",) if update["status"] == "不更新" else () treeview.insert("", "end", values=(update["sheet"], update["cell"], update["old_value"], update["new_value"], update["status"]), tags=tags) treeview.tag_configure("to_update", background="lightyellow") treeview.tag_configure("no_update", background="lightgray") # 自动滚动到顶部 treeview.yview_moveto(0) def browse_file(self): """浏览文件""" file_path = filedialog.askopenfilename( filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")], title="选择Excel文件" ) if file_path: self.file_entry.delete(0, tk.END) self.file_entry.insert(0, file_path) self.file_path = file_path self.update_status(f"已选择文件: {os.path.basename(file_path)}") def update_status(self, message): """更新状态栏""" self.status_var.set(message) self.root.update_idletasks() def update_progress(self, value, message=None): """更新进度条""" self.progress_var.set(value) if message: self.progress_label.config(text=message) self.root.update_idletasks() def load_project_changes(self): """加载项目编号变更""" try: self.running_tasks += 1 self.update_progress(0, "正在查找项目编号...") self.file_path = self.file_entry.get() self.old_project = self.old_project_entry.get().strip() self.new_project = self.new_project_entry.get().strip() if not all([self.file_path, self.old_project, self.new_project]): messagebox.showerror("错误", "请填写所有必填字段") return if not os.path.exists(self.file_path): messagebox.showerror("错误", "文件不存在!") return self.project_updates = self._detect_changes( target_value=self.old_project, new_value=self.new_project, value_type="project" ) if not self.project_updates: messagebox.showinfo("提示", "未找到匹配的项目编号") return self._display_data(self.project_tree, self.project_updates) self.update_status(f"找到 {len(self.project_updates)} 处项目编号需要更新") self.update_progress(100, "查找完成") except Exception as e: messagebox.showerror("错误", f"加载失败: {str(e)}") print(traceback.format_exc()) finally: self.running_tasks -= 1 def load_date_changes(self): """加载时间变更""" try: self.running_tasks += 1 self.update_progress(0, "正在查找时间...") self.file_path = self.file_entry.get() self.old_date = self.old_date_entry.get().strip() self.new_date = self.new_date_entry.get().strip() if not all([self.file_path, self.old_date, self.new_date]): messagebox.showerror("错误", "请填写所有必填字段") return if not os.path.exists(self.file_path): messagebox.showerror("错误", "文件不存在!") return self.date_updates = self._detect_changes( target_value=self.old_date, new_value=self.new_date, value_type="date" ) if not self.date_updates: messagebox.showinfo("提示", "未找到匹配的时间") return self._display_data(self.date_tree, self.date_updates) self.update_status(f"找到 {len(self.date_updates)} 处时间需要更新") self.update_progress(100, "查找完成") except Exception as e: messagebox.showerror("错误", f"加载失败: {str(e)}") print(traceback.format_exc()) finally: self.running_tasks -= 1 def load_responsible_changes(self): """加载担当变更""" try: self.running_tasks += 1 self.update_progress(0, "正在查找担当...") self.file_path = self.file_entry.get() self.old_responsible = self.old_responsible_entry.get().strip() self.new_responsible = self.new_responsible_entry.get().strip() if not all([self.file_path, self.old_responsible, self.new_responsible]): messagebox.showerror("错误", "请填写所有必填字段") return if not os.path.exists(self.file_path): messagebox.showerror("错误", "文件不存在!") return self.responsible_updates = self._detect_changes( target_value=self.old_responsible, new_value=self.new_responsible, value_type="responsible" ) if not self.responsible_updates: messagebox.showinfo("提示", "未找到匹配的担当") return self._display_data(self.responsible_tree, self.responsible_updates) self.update_status(f"找到 {len(self.responsible_updates)} 处担当需要更新") self.update_progress(100, "查找完成") except Exception as e: messagebox.showerror("错误", f"加载失败: {str(e)}") print(traceback.format_exc()) finally: self.running_tasks -= 1 def _detect_changes(self, target_value, new_value, value_type): """通用变更检测方法""" updates = [] try: wb = load_workbook(self.file_path) total_sheets = len(wb.sheetnames) for i, sheet_name in enumerate(wb.sheetnames): self.update_progress((i+1)/total_sheets*100, f"正在处理工作表: {sheet_name}") sheet_config = self._get_sheet_config(sheet_name) if not sheet_config['process']: continue if value_type == "date" and not sheet_config.get('update_time', True): continue sheet = wb[sheet_name] if value_type == "date": cells = self._find_date_cells(sheet, target_value) else: cells = self._find_cells_with_value(sheet, target_value) for cell in cells: updates.append({ "sheet": sheet_name, "cell": cell.coordinate, "old_value": cell.value, "new_value": new_value, "status": "待审核" }) except Exception as e: print(f"DEBUG - 读取错误: {str(e)}") return updates def _display_data(self, treeview, data): """显示数据到表格""" treeview.delete(*treeview.get_children()) for update in data: tags = ("to_update",) if update["status"] == "待更新" else ("no_update",) if update["status"] == "不更新" else () treeview.insert("", "end", values=(update["sheet"], update["cell"], update["old_value"], update["new_value"], update["status"]), tags=tags) treeview.tag_configure("to_update", background="lightyellow") treeview.tag_configure("no_update", background="lightgray") def update_project_status(self, status): """更新项目变更状态""" self._update_status(self.project_tree, self.project_updates, status) def update_date_status(self, status): """更新时间变更状态""" self._update_status(self.date_tree, self.date_updates, status) def update_responsible_status(self, status): """更新担当变更状态""" self._update_status(self.responsible_tree, self.responsible_updates, status) def _update_status(self, treeview, data, status): """通用状态更新方法""" selected = treeview.selection() if not selected: messagebox.showwarning("警告", "请先选择记录") return for item in selected: index = treeview.index(item) data[index]["status"] = status self._display_data(treeview, data) def apply_project_updates(self): """执行项目编号变更""" self._apply_updates( updates=self.project_updates, success_message="项目编号更新完成!", treeview=self.project_tree ) def apply_date_updates(self): """执行时间变更""" self._apply_updates( updates=self.date_updates, success_message="时间更新完成!", treeview=self.date_tree ) def apply_responsible_updates(self): """执行担当变更""" self._apply_updates( updates=self.responsible_updates, success_message="担当更新完成!", treeview=self.responsible_tree ) def _apply_updates(self, updates, success_message, treeview): """通用更新应用方法""" if not any(u["status"] == "待更新" for u in updates): messagebox.showwarning("警告", "没有标记为'待更新'的记录") return try: self.running_tasks += 1 self.update_progress(0, "正在更新...") wb = load_workbook(self.file_path) total_updates = len([u for u in updates if u["status"] == "待更新"]) processed = 0 for update in [u for u in updates if u["status"] == "待更新"]: sheet = wb[update["sheet"]] sheet[update["cell"]] = update["new_value"] processed += 1 self.update_progress(processed/total_updates*100, f"正在更新 {update['sheet']} {update['cell']}") wb.save(self.file_path) messagebox.showinfo("成功", success_message) # 重新加载数据 if treeview == self.project_tree: self.load_project_changes() elif treeview == self.date_tree: self.load_date_changes() else: self.load_responsible_changes() except Exception as e: messagebox.showerror("错误", f"更新失败: {str(e)}") print(traceback.format_exc()) finally: self.running_tasks -= 1 self.update_progress(100, "更新完成") def export_project_report(self): """导出项目变更报告""" self._export_report(self.project_updates, "项目变更报告") def export_date_report(self): """导出时间变更报告""" self._export_report(self.date_updates, "时间变更报告") def export_responsible_report(self): """导出担当变更报告""" self._export_report(self.responsible_updates, "担当变更报告") def _export_report(self, data, report_name): """通用报告导出方法""" if not data: messagebox.showwarning("警告", f"没有可导出的{report_name}数据") return try: file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel文件", "*.xlsx")], title=f"保存{report_name}" ) if not file_path: return df = pd.DataFrame([{ "工作表": item["sheet"], "单元格位置": item["cell"], "原内容": item["old_value"], "新内容": item["new_value"], "状态": item["status"] } for item in data]) df.to_excel(file_path, index=False) messagebox.showinfo("成功", f"{report_name}已导出到: {file_path}") except Exception as e: messagebox.showerror("错误", f"导出失败: {str(e)}") def _get_sheet_config(self, sheet_name): """获取sheet配置""" sheet_lower = sheet_name.lower() for kw in self.special_sheets: if kw.lower() in sheet_lower: config = self.special_sheets[kw].copy() config['is_special'] = True return config return {'process': True, 'update_time': True, 'is_special': False} def _find_cells_with_value(self, sheet, target_value): """查找包含目标值的所有单元格""" found_cells = [] pattern = re.compile(rf'.*{re.escape(str(target_value))}.*', re.IGNORECASE) for row in sheet.iter_rows(): for cell in row: if cell.value and pattern.search(str(cell.value)): found_cells.append(cell) return found_cells def _find_date_cells(self, sheet, target_date=None): """查找所有包含日期的单元格""" date_cells = [] for row in sheet.iter_rows(): for cell in row: if cell.value and self._is_date(cell.value): if target_date: try: cell_date = pd.to_datetime(cell.value).strftime('%Y-%m-%d') if target_date in str(cell_date): date_cells.append(cell) except: if target_date in str(cell.value): date_cells.append(cell) else: date_cells.append(cell) return date_cells def _is_date(self, value): """判断值是否为日期""" try: pd.to_datetime(value) return True except: # 检查是否是字符串形式的日期 if isinstance(value, str): for pattern in self.date_patterns: if re.fullmatch(pattern, value.strip()): return True return False def on_closing(self): """关闭窗口时的处理""" if self.running_tasks > 0: if messagebox.askokcancel("警告", "有任务正在运行,确定要退出吗?"): self.executor.shutdown(wait=False) self.root.destroy() else: self.root.destroy() if __name__ == "__main__": root = tk.Tk() app = EnhancedVersionUpdaterApp(root) root.protocol("WM_DELETE_WINDOW", app.on_closing) root.mainloop() 1、请增加一些关于文档检查的功能 2、请美化一下界面 3、请加快一下运行速度,并减少对内存的占用
08-14
# -*- coding: utf-8 -*- import pandas as pd from datetime import datetime import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import os import traceback import re from openpyxl import load_workbook from concurrent.futures import ThreadPoolExecutor from tkinter import font as tkfont import psutil import time import threading import math from PIL import Image, ImageTk class EnhancedVersionUpdaterApp: def __init__(self, root): self.root = root self.root.title("Excel批量修改工具 - 专业版") self.root.geometry("1400x950") self.root.configure(bg="#f0f2f5") # 设置主题颜色 self.primary_color = "#4e73df" self.secondary_color = "#858796" self.success_color = "#1cc88a" self.warning_color = "#f6c23e" self.danger_color = "#e74a3b" self.light_color = "#f8f9fc" self.dark_color = "#5a5c69" # 设置全局字体 default_font = tkfont.nametofont("TkDefaultFont") default_font.configure(family="Segoe UI", size=10) self.root.option_add("*Font", default_font) # 使用线程池提高性能 self.executor = ThreadPoolExecutor(max_workers=4) self.running_tasks = 0 # 初始化变量 self.file_path = "" self.old_project = "" self.new_project = "" self.old_date = "" self.new_date = datetime.now().strftime("%Y-%m-%d") self.old_responsible = "" self.new_responsible = "" self.project_updates = [] self.date_updates = [] self.responsible_updates = [] self.doc_checks = [] # 特殊sheet配置 self.special_sheets = { '変更履歴': {'process': False}, 'history': {'process': False}, 'log': {'process': False}, '封面': {'process': True, 'update_time': False}, '表紙': {'process': True, 'update_time': False}, 'cover': {'process': True, 'update_time': False} } # 时间格式正则表达式 self.date_patterns = [ re.compile(r'\d{4}-\d{2}-\d{2}'), # YYYY-MM-DD re.compile(r'\d{4}/\d{2}/\d{2}'), # YYYY/MM/DD re.compile(r'\d{4}年\d{2}月\d{2}日'), # 中文日期 re.compile(r'\d{2}-\d{2}-\d{4}'), # MM-DD-YYYY re.compile(r'\d{2}/\d{2}/\d{4}') # MM/DD/YYYY ] self.create_enhanced_ui() self.setup_style() # 启动内存监控线程 self.memory_usage = 0 self.memory_thread = threading.Thread(target=self.monitor_memory, daemon=True) self.memory_thread.start() def monitor_memory(self): """监控内存使用情况""" while True: process = psutil.Process(os.getpid()) self.memory_usage = process.memory_info().rss / (1024 * 1024) # MB time.sleep(2) def setup_style(self): """设置界面样式""" style = ttk.Style() # 配置主题 style.theme_use('clam') # 通用样式 style.configure('TFrame', background=self.light_color) style.configure('TLabel', background=self.light_color, foreground=self.dark_color) style.configure('TLabelframe', background=self.light_color, relief="flat", borderwidth=0) style.configure('TLabelframe.Label', background=self.light_color, foreground=self.primary_color, font=('Segoe UI', 10, 'bold')) # 按钮样式 style.configure('TButton', background="#f8f9fc", foreground=self.dark_color, borderwidth=1, relief="solid", padding=6, font=('Segoe UI', 9)) style.map('TButton', background=[('active', '#e6e6e6'), ('!active', '#f8f9fc')], foreground=[('active', self.dark_color), ('!active', self.dark_color)]) # 特殊按钮样式 style.configure('Primary.TButton', background=self.primary_color, foreground="white") style.map('Primary.TButton', background=[('active', '#2e59d9'), ('!active', self.primary_color)], foreground=[('active', 'white'), ('!active', 'white')]) style.configure('Success.TButton', background=self.success_color, foreground="white") style.map('Success.TButton', background=[('active', '#17a673'), ('!active', self.success_color)], foreground=[('active', 'white'), ('!active', 'white')]) style.configure('Warning.TButton', background=self.warning_color, foreground="white") style.map('Warning.TButton', background=[('active', '#dda20a'), ('!active', self.warning_color)], foreground=[('active', 'white'), ('!active', 'white')]) # 进度条样式 style.configure("Custom.Horizontal.TProgressbar", thickness=15, troughcolor=self.light_color, background=self.primary_color, lightcolor=self.primary_color, darkcolor=self.primary_color) # Treeview样式 style.configure("Treeview", background="white", foreground=self.dark_color, rowheight=28, fieldbackground="white", font=('Segoe UI', 9)) style.configure("Treeview.Heading", background=self.primary_color, foreground="white", font=('Segoe UI', 10, 'bold'), padding=(5, 5)) style.map("Treeview.Heading", background=[('active', '#2e59d9')]) # Notebook样式 style.configure("TNotebook", background=self.light_color) style.configure("TNotebook.Tab", background=self.light_color, foreground=self.dark_color, padding=(12, 6), font=('Segoe UI', 10, 'bold')) style.map("TNotebook.Tab", background=[('selected', 'white'), ('!selected', self.light_color)], foreground=[('selected', self.primary_color), ('!selected', self.secondary_color)]) def create_enhanced_ui(self): """创建增强版用户界面""" # 主框架 main_frame = ttk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=15) # 标题栏 title_frame = ttk.Frame(main_frame) title_frame.pack(fill=tk.X, pady=(0, 15)) ttk.Label(title_frame, text="Excel批量修改工具", font=('Segoe UI', 18, 'bold'), foreground=self.primary_color).pack(side=tk.LEFT) # 内存指示器 self.memory_label = ttk.Label(title_frame, text="内存使用: 0 MB", font=('Segoe UI', 9)) self.memory_label.pack(side=tk.RIGHT, padx=10) # 文件选择区域 file_frame = ttk.LabelFrame(main_frame, text="文件操作", padding=10) file_frame.pack(fill=tk.X, pady=(0, 15)) file_input_frame = ttk.Frame(file_frame) file_input_frame.pack(fill=tk.X, pady=5) ttk.Label(file_input_frame, text="Excel文件路径:").pack(side=tk.LEFT) self.file_entry = ttk.Entry(file_input_frame, width=70) self.file_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) ttk.Button(file_input_frame, text="浏览文件", command=self.browse_file, style='Primary.TButton').pack(side=tk.LEFT) # 状态和进度区域 status_frame = ttk.Frame(file_frame) status_frame.pack(fill=tk.X, pady=(10, 0)) # 进度条 self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar(status_frame, variable=self.progress_var, maximum=100, style="Custom.Horizontal.TProgressbar") self.progress_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10)) # 进度标签 self.progress_label = ttk.Label(status_frame, text="就绪", width=15) self.progress_label.pack(side=tk.RIGHT) # 主内容区域 - 使用Notebook实现标签页 notebook = ttk.Notebook(main_frame) notebook.pack(fill=tk.BOTH, expand=True) # 项目变更标签页 project_tab = ttk.Frame(notebook) notebook.add(project_tab, text="项目编号变更") self.create_project_tab(project_tab) # 时间变更标签页 date_tab = ttk.Frame(notebook) notebook.add(date_tab, text="时间变更") self.create_date_tab(date_tab) # 担当变更标签页 responsible_tab = ttk.Frame(notebook) notebook.add(responsible_tab, text="担当变更") self.create_responsible_tab(responsible_tab) # 文档检查标签页 doc_check_tab = ttk.Frame(notebook) notebook.add(doc_check_tab, text="文档检查") self.create_doc_check_tab(doc_check_tab) # 底部状态栏 status_bar = ttk.Frame(main_frame, relief=tk.SUNKEN, padding=(5, 3)) status_bar.pack(fill=tk.X, pady=(10, 0)) self.status_var = tk.StringVar() self.status_var.set("就绪 - 等待操作") ttk.Label(status_bar, textvariable=self.status_var, anchor=tk.W, font=('Segoe UI', 9)).pack(fill=tk.X) # 定期更新内存使用情况 self.update_memory_display() def update_memory_display(self): """更新内存显示""" self.memory_label.config(text=f"内存使用: {self.memory_usage:.2f} MB") self.root.after(2000, self.update_memory_display) def create_project_tab(self, parent): """创建项目变更标签页""" # 主框架 main_frame = ttk.Frame(parent) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 输入区域 input_frame = ttk.Frame(main_frame) input_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(input_frame, text="原项目编号:").grid(row=0, column=0, sticky="w", padx=5, pady=5) self.old_project_entry = ttk.Entry(input_frame, width=30) self.old_project_entry.grid(row=0, column=1, sticky="w", padx=5, pady=5) ttk.Label(input_frame, text="新项目编号:").grid(row=1, column=0, sticky="w", padx=5, pady=5) self.new_project_entry = ttk.Entry(input_frame, width=30) self.new_project_entry.grid(row=1, column=1, sticky="w", padx=5, pady=5) ttk.Button(input_frame, text="查找项目", command=lambda: self.executor.submit(self.load_project_changes), style='Primary.TButton').grid(row=0, column=2, rowspan=2, padx=10, pady=5) # 表格容器框架 table_frame = ttk.LabelFrame(main_frame, text="变更列表") table_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # 创建Treeview和滚动条 tree_container = ttk.Frame(table_frame) tree_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) tree_scroll_y = ttk.Scrollbar(tree_container) tree_scroll_y.pack(side=tk.RIGHT, fill=tk.Y) tree_scroll_x = ttk.Scrollbar(tree_container, orient=tk.HORIZONTAL) tree_scroll_x.pack(side=tk.BOTTOM, fill=tk.X) columns = ("Sheet", "位置", "原项目", "新项目", "状态") self.project_tree = ttk.Treeview( tree_container, columns=columns, show="headings", height=15, yscrollcommand=tree_scroll_y.set, xscrollcommand=tree_scroll_x.set, selectmode="extended" ) self.project_tree.pack(fill=tk.BOTH, expand=True) # 配置滚动条 tree_scroll_y.config(command=self.project_tree.yview) tree_scroll_x.config(command=self.project_tree.xview) # 配置列 col_widths = {"Sheet": 200, "位置": 100, "原项目": 200, "新项目": 200, "状态": 100} for col in columns: self.project_tree.heading(col, text=col) self.project_tree.column(col, width=col_widths.get(col, 120), anchor="w") # 操作按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(btn_frame, text="标记更新", command=lambda: self.update_project_status("待更新"), style='Success.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="标记不更新", command=lambda: self.update_project_status("不更新"), style='Warning.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="执行更新", command=lambda: self.executor.submit(self.apply_project_updates), style='Primary.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="导出报告", command=self.export_project_report, style='Primary.TButton').pack(side=tk.RIGHT, padx=5) def create_date_tab(self, parent): """创建时间变更标签页""" # 主框架 main_frame = ttk.Frame(parent) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 输入区域 input_frame = ttk.Frame(main_frame) input_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(input_frame, text="原时间:").grid(row=0, column=0, sticky="w", padx=5, pady=5) self.old_date_entry = ttk.Entry(input_frame, width=30) self.old_date_entry.grid(row=0, column=1, sticky="w", padx=5, pady=5) ttk.Label(input_frame, text="新时间:").grid(row=1, column=0, sticky="w", padx=5, pady=5) self.new_date_entry = ttk.Entry(input_frame, width=30) self.new_date_entry.insert(0, self.new_date) self.new_date_entry.grid(row=1, column=1, sticky="w", padx=5, pady=5) ttk.Button(input_frame, text="查找时间", command=lambda: self.executor.submit(self.load_date_changes), style='Primary.TButton').grid(row=0, column=2, rowspan=2, padx=10, pady=5) # 表格容器框架 table_frame = ttk.LabelFrame(main_frame, text="变更列表") table_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # 创建Treeview和滚动条 tree_container = ttk.Frame(table_frame) tree_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) tree_scroll_y = ttk.Scrollbar(tree_container) tree_scroll_y.pack(side=tk.RIGHT, fill=tk.Y) tree_scroll_x = ttk.Scrollbar(tree_container, orient=tk.HORIZONTAL) tree_scroll_x.pack(side=tk.BOTTOM, fill=tk.X) columns = ("Sheet", "位置", "原时间", "新时间", "状态") self.date_tree = ttk.Treeview( tree_container, columns=columns, show="headings", height=15, yscrollcommand=tree_scroll_y.set, xscrollcommand=tree_scroll_x.set, selectmode="extended" ) self.date_tree.pack(fill=tk.BOTH, expand=True) # 配置滚动条 tree_scroll_y.config(command=self.date_tree.yview) tree_scroll_x.config(command=self.date_tree.xview) # 配置列 col_widths = {"Sheet": 200, "位置": 100, "原时间": 200, "新时间": 200, "状态": 100} for col in columns: self.date_tree.heading(col, text=col) self.date_tree.column(col, width=col_widths.get(col, 120), anchor="w") # 操作按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(btn_frame, text="标记更新", command=lambda: self.update_date_status("待更新"), style='Success.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="标记不更新", command=lambda: self.update_date_status("不更新"), style='Warning.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="执行更新", command=lambda: self.executor.submit(self.apply_date_updates), style='Primary.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="导出报告", command=self.export_date_report, style='Primary.TButton').pack(side=tk.RIGHT, padx=5) def create_responsible_tab(self, parent): """创建担当变更标签页""" # 主框架 main_frame = ttk.Frame(parent) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 输入区域 input_frame = ttk.Frame(main_frame) input_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(input_frame, text="原担当:").grid(row=0, column=0, sticky="w", padx=5, pady=5) self.old_responsible_entry = ttk.Entry(input_frame, width=30) self.old_responsible_entry.grid(row=0, column=1, sticky="w", padx=5, pady=5) ttk.Label(input_frame, text="新担当:").grid(row=1, column=0, sticky="w", padx=5, pady=5) self.new_responsible_entry = ttk.Entry(input_frame, width=30) self.new_responsible_entry.grid(row=1, column=1, sticky="w", padx=5, pady=5) ttk.Button(input_frame, text="查找担当", command=lambda: self.executor.submit(self.load_responsible_changes), style='Primary.TButton').grid(row=0, column=2, rowspan=2, padx=10, pady=5) # 表格容器框架 table_frame = ttk.LabelFrame(main_frame, text="变更列表") table_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # 创建Treeview和滚动条 tree_container = ttk.Frame(table_frame) tree_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) tree_scroll_y = ttk.Scrollbar(tree_container) tree_scroll_y.pack(side=tk.RIGHT, fill=tk.Y) tree_scroll_x = ttk.Scrollbar(tree_container, orient=tk.HORIZONTAL) tree_scroll_x.pack(side=tk.BOTTOM, fill=tk.X) columns = ("Sheet", "位置", "原担当", "新担当", "状态") self.responsible_tree = ttk.Treeview( tree_container, columns=columns, show="headings", height=15, yscrollcommand=tree_scroll_y.set, xscrollcommand=tree_scroll_x.set, selectmode="extended" ) self.responsible_tree.pack(fill=tk.BOTH, expand=True) # 配置滚动条 tree_scroll_y.config(command=self.responsible_tree.yview) tree_scroll_x.config(command=self.responsible_tree.xview) # 配置列 col_widths = {"Sheet": 200, "位置": 100, "原担当": 200, "新担当": 200, "状态": 100} for col in columns: self.responsible_tree.heading(col, text=col) self.responsible_tree.column(col, width=col_widths.get(col, 120), anchor="w") # 操作按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(btn_frame, text="标记更新", command=lambda: self.update_responsible_status("待更新"), style='Success.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="标记不更新", command=lambda: self.update_responsible_status("不更新"), style='Warning.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="执行更新", command=lambda: self.executor.submit(self.apply_responsible_updates), style='Primary.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="导出报告", command=self.export_responsible_report, style='Primary.TButton').pack(side=tk.RIGHT, padx=5) def create_doc_check_tab(self, parent): """创建文档检查标签页""" # 主框架 main_frame = ttk.Frame(parent) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(btn_frame, text="执行文档检查", command=lambda: self.executor.submit(self.perform_document_checks), style='Primary.TButton').pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="导出检查报告", command=self.export_doc_check_report, style='Primary.TButton').pack(side=tk.LEFT, padx=5) # 检查结果区域 result_frame = ttk.LabelFrame(main_frame, text="文档检查结果") result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # 创建Treeview和滚动条 tree_container = ttk.Frame(result_frame) tree_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) tree_scroll_y = ttk.Scrollbar(tree_container) tree_scroll_y.pack(side=tk.RIGHT, fill=tk.Y) tree_scroll_x = ttk.Scrollbar(tree_container, orient=tk.HORIZONTAL) tree_scroll_x.pack(side=tk.BOTTOM, fill=tk.X) columns = ("检查项", "状态", "详情", "建议") self.doc_tree = ttk.Treeview( tree_container, columns=columns, show="headings", height=15, yscrollcommand=tree_scroll_y.set, xscrollcommand=tree_scroll_x.set ) self.doc_tree.pack(fill=tk.BOTH, expand=True) # 配置滚动条 tree_scroll_y.config(command=self.doc_tree.yview) tree_scroll_x.config(command=self.doc_tree.xview) # 配置列 col_widths = {"检查项": 200, "状态": 80, "详情": 300, "建议": 300} for col in columns: self.doc_tree.heading(col, text=col) self.doc_tree.column(col, width=col_widths.get(col, 150), anchor="w") # 配置标签样式 self.doc_tree.tag_configure("pass", background="#e8f5e9") self.doc_tree.tag_configure("warning", background="#fff8e1") self.doc_tree.tag_configure("error", background="#ffebee") self.doc_tree.tag_configure("info", background="#e3f2fd") def perform_document_checks(self): """执行文档检查""" try: self.running_tasks += 1 self.update_progress(0, "正在检查文档...") self.update_status("正在执行文档检查...") self.file_path = self.file_entry.get() if not self.file_path: messagebox.showerror("错误", "请先选择Excel文件") return if not os.path.exists(self.file_path): messagebox.showerror("错误", "文件不存在!") return self.doc_checks = [] # 1. 文件基本信息检查 file_size = os.path.getsize(self.file_path) / (1024 * 1024) # MB file_ext = os.path.splitext(self.file_path)[1].lower() self.doc_checks.append({ "检查项": "文件格式", "状态": "通过" if file_ext in ['.xlsx', '.xls'] else "错误", "详情": f"文件格式: {file_ext}", "建议": "使用.xlsx格式以获得最佳兼容性" if file_ext != '.xlsx' else "" }) self.doc_checks.append({ "检查项": "文件大小", "状态": "通过" if file_size < 10 else "警告", "详情": f"{file_size:.2f} MB", "建议": "文件过大可能影响性能,建议拆分" if file_size >= 10 else "" }) # 2. 工作簿结构检查 wb = load_workbook(self.file_path, read_only=True) # 工作表数量 sheet_count = len(wb.sheetnames) self.doc_checks.append({ "检查项": "工作表数量", "状态": "通过" if 1 <= sheet_count <= 20 else "警告", "详情": f"{sheet_count} 个工作表", "建议": "工作表数量过多可能导致性能问题" if sheet_count > 20 else "" }) # 隐藏工作表 hidden_sheets = [name for name in wb.sheetnames if wb[name].sheet_state == 'hidden'] self.doc_checks.append({ "检查项": "隐藏工作表", "状态": "警告" if hidden_sheets else "通过", "详情": f"{len(hidden_sheets)} 个隐藏工作表" if hidden_sheets else "无隐藏工作表", "建议": "检查隐藏工作表内容是否必要" if hidden_sheets else "" }) # 工作表保护 protected_sheets = [name for name in wb.sheetnames if wb[name].protection.sheet] self.doc_checks.append({ "检查项": "受保护工作表", "状态": "信息", "详情": f"{len(protected_sheets)} 个受保护工作表" if protected_sheets else "无受保护工作表", "建议": "确保有必要的访问权限" if protected_sheets else "" }) # 3. 内容检查 # 公式错误 formula_errors = {} for sheet_name in wb.sheetnames: sheet = wb[sheet_name] for row in sheet.iter_rows(): for cell in row: if cell.data_type == 'e': # 错误类型 if sheet_name not in formula_errors: formula_errors[sheet_name] = 0 formula_errors[sheet_name] += 1 error_details = ", ".join([f"{k}:{v}" for k, v in formula_errors.items()]) if formula_errors else "无" self.doc_checks.append({ "检查项": "公式错误", "状态": "错误" if formula_errors else "通过", "详情": f"共发现 {sum(formula_errors.values())} 处错误" if formula_errors else "无公式错误", "建议": "修复公式错误以确保数据准确性" if formula_errors else "" }) # 特殊字符 non_printable_chars = [] non_printable_pattern = re.compile(r'[\x00-\x1F\x7F-\x9F]') # 控制字符 for sheet_name in wb.sheetnames: sheet = wb[sheet_name] for row in sheet.iter_rows(): for cell in row: if cell.value and isinstance(cell.value, str) and non_printable_pattern.search(cell.value): non_printable_chars.append(f"{sheet_name}!{cell.coordinate}") self.doc_checks.append({ "检查项": "特殊字符", "状态": "警告" if non_printable_chars else "通过", "详情": f"发现 {len(non_printable_chars)} 个特殊字符" if non_printable_chars else "无特殊字符", "建议": "移除不可打印字符" if non_printable_chars else "" }) # 超链接检查 hyperlinks = {} for sheet_name in wb.sheetnames: sheet = wb[sheet_name] if sheet.hyperlinks: hyperlinks[sheet_name] = len(sheet.hyperlinks) self.doc_checks.append({ "检查项": "超链接", "状态": "信息", "详情": f"共 {sum(hyperlinks.values())} 个超链接" if hyperlinks else "无超链接", "建议": "验证超链接有效性" if hyperlinks else "" }) # 4. 元数据检查 # 文件属性 created_time = datetime.fromtimestamp(os.path.getctime(self.file_path)) modified_time = datetime.fromtimestamp(os.path.getmtime(self.file_path)) self.doc_checks.append({ "检查项": "文件属性", "状态": "信息", "详情": f"创建: {created_time.strftime('%Y-%m-%d')}, 修改: {modified_time.strftime('%Y-%m-%d')}", "建议": "" }) # 显示结果 self._display_doc_checks() self.update_progress(100, "检查完成") self.update_status(f"文档检查完成,共 {len(self.doc_checks)} 项检查") except Exception as e: messagebox.showerror("错误", f"文档检查失败: {str(e)}") print(traceback.format_exc()) finally: self.running_tasks -= 1 def _display_doc_checks(self): """显示文档检查结果""" self.doc_tree.delete(*self.doc_tree.get_children()) for check in self.doc_checks: status = check["状态"] tags = ("pass",) if status == "通过" else ("warning",) if status == "警告" else ("error",) if status == "错误" else ("info",) self.doc_tree.insert("", "end", values=(check["检查项"], check["状态"], check["详情"], check["建议"]), tags=tags) def browse_file(self): """浏览文件""" file_path = filedialog.askopenfilename( filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")], title="选择Excel文件" ) if file_path: self.file_entry.delete(0, tk.END) self.file_entry.insert(0, file_path) self.file_path = file_path self.update_status(f"已选择文件: {os.path.basename(file_path)}") def update_status(self, message): """更新状态栏""" self.status_var.set(message) self.root.update_idletasks() def update_progress(self, value, message=None): """更新进度条""" self.progress_var.set(value) if message: self.progress_label.config(text=message) self.root.update_idletasks() def load_project_changes(self): """加载项目编号变更""" try: self.running_tasks += 1 self.update_progress(0, "正在查找项目编号...") self.update_status("正在查找项目编号变更...") self.file_path = self.file_entry.get() self.old_project = self.old_project_entry.get().strip() self.new_project = self.new_project_entry.get().strip() if not all([self.file_path, self.old_project, self.new_project]): messagebox.showerror("错误", "请填写所有必填字段") return if not os.path.exists(self.file_path): messagebox.showerror("错误", "文件不存在!") return # 使用只读模式加载工作簿以节省内存 wb = load_workbook(self.file_path, read_only=True) total_sheets = len(wb.sheetnames) self.project_updates = [] for i, sheet_name in enumerate(wb.sheetnames): self.update_progress((i+1)/total_sheets*100, f"处理: {sheet_name}") sheet_config = self._get_sheet_config(sheet_name) if not sheet_config['process']: continue sheet = wb[sheet_name] # 分块处理大型工作表 for row_chunk in self._chunked_rows(sheet, chunk_size=100): for cell in row_chunk: if cell.value and self.old_project in str(cell.value): self.project_updates.append({ "sheet": sheet_name, "cell": cell.coordinate, "old_value": cell.value, "new_value": cell.value.replace(self.old_project, self.new_project), "status": "待审核" }) if not self.project_updates: messagebox.showinfo("提示", "未找到匹配的项目编号") return self._display_data(self.project_tree, self.project_updates) self.update_status(f"找到 {len(self.project_updates)} 处项目编号需要更新") self.update_progress(100, "查找完成") except Exception as e: messagebox.showerror("错误", f"加载失败: {str(e)}") print(traceback.format_exc()) finally: self.running_tasks -= 1 def load_date_changes(self): """加载时间变更""" try: self.running_tasks += 1 self.update_progress(0, "正在查找时间...") self.update_status("正在查找时间变更...") self.file_path = self.file_entry.get() self.old_date = self.old_date_entry.get().strip() self.new_date = self.new_date_entry.get().strip() if not all([self.file_path, self.old_date, self.new_date]): messagebox.showerror("错误", "请填写所有必填字段") return if not os.path.exists(self.file_path): messagebox.showerror("错误", "文件不存在!") return # 使用只读模式加载工作簿以节省内存 wb = load_workbook(self.file_path, read_only=True) total_sheets = len(wb.sheetnames) self.project_updates = [] for i, sheet_name in enumerate(wb.sheetnames): self.update_progress((i+1)/total_sheets*100, f"处理: {sheet_name}") sheet_config = self._get_sheet_config(sheet_name) if not sheet_config['process']: continue sheet = wb[sheet_name] # 分块处理大型工作表 for row_chunk in self._chunked_rows(sheet, chunk_size=100): for cell in row_chunk: if cell.value and self.old_project in str(cell.value): self.project_updates.append({ "sheet": sheet_name, "cell": cell.coordinate, "old_value": cell.value, "new_value": cell.value.replace(self.old_project, self.new_project), "status": "待审核" }) if not self.date_updates: messagebox.showinfo("提示", "未找到匹配的时间") return self._display_data(self.date_tree, self.date_updates) self.update_status(f"找到 {len(self.date_updates)} 处时间需要更新") self.update_progress(100, "查找完成") except Exception as e: messagebox.showerror("错误", f"加载失败: {str(e)}") print(traceback.format_exc()) finally: self.running_tasks -= 1 def load_responsible_changes(self): """加载担当变更""" try: self.running_tasks += 1 self.update_progress(0, "正在查找担当...") self.update_status("正在查找担当变更...") self.file_path = self.file_entry.get() self.old_responsible = self.old_responsible_entry.get().strip() self.new_responsible = self.new_responsible_entry.get().strip() if not all([self.file_path, self.old_responsible, self.new_responsible]): messagebox.showerror("错误", "请填写所有必填字段") return if not os.path.exists(self.file_path): messagebox.showerror("错误", "文件不存在!") return # 使用只读模式加载工作簿以节省内存 wb = load_workbook(self.file_path, read_only=True) total_sheets = len(wb.sheetnames) self.project_updates = [] for i, sheet_name in enumerate(wb.sheetnames): self.update_progress((i+1)/total_sheets*100, f"处理: {sheet_name}") sheet_config = self._get_sheet_config(sheet_name) if not sheet_config['process']: continue sheet = wb[sheet_name] # 分块处理大型工作表 for row_chunk in self._chunked_rows(sheet, chunk_size=100): for cell in row_chunk: if cell.value and self.old_project in str(cell.value): self.project_updates.append({ "sheet": sheet_name, "cell": cell.coordinate, "old_value": cell.value, "new_value": cell.value.replace(self.old_project, self.new_project), "status": "待审核" }) if not self.responsible_updates: messagebox.showinfo("提示", "未找到匹配的担当") return self._display_data(self.responsible_tree, self.responsible_updates) self.update_status(f"找到 {len(self.responsible_updates)} 处担当需要更新") self.update_progress(100, "查找完成") except Exception as e: messagebox.showerror("错误", f"加载失败: {str(e)}") print(traceback.format_exc()) finally: self.running_tasks -= 1 def _display_data(self, treeview, data): """显示数据到表格""" treeview.delete(*treeview.get_children()) for update in data: tags = ("to_update",) if update["status"] == "待更新" else ("no_update",) if update["status"] == "不更新" else () treeview.insert("", "end", values=(update["sheet"], update["cell"], update["old_value"], update["new_value"], update["status"]), tags=tags) treeview.tag_configure("to_update", background="lightyellow") treeview.tag_configure("no_update", background="lightgray") # 自动滚动到顶部 treeview.yview_moveto(0) def _detect_changes(self, target_value, new_value, value_type): """通用变更检测方法""" updates = [] try: wb = load_workbook(self.file_path) total_sheets = len(wb.sheetnames) for i, sheet_name in enumerate(wb.sheetnames): self.update_progress((i+1)/total_sheets*100, f"正在处理工作表: {sheet_name}") sheet_config = self._get_sheet_config(sheet_name) if not sheet_config['process']: continue if value_type == "date" and not sheet_config.get('update_time', True): continue sheet = wb[sheet_name] if value_type == "date": cells = self._find_date_cells(sheet, target_value) else: cells = self._find_cells_with_value(sheet, target_value) for cell in cells: updates.append({ "sheet": sheet_name, "cell": cell.coordinate, "old_value": cell.value, "new_value": new_value, "status": "待审核" }) except Exception as e: print(f"DEBUG - 读取错误: {str(e)}") return updates def update_project_status(self, status): """更新项目变更状态""" self._update_status(self.project_tree, self.project_updates, status) def update_date_status(self, status): """更新时间变更状态""" self._update_status(self.date_tree, self.date_updates, status) def update_responsible_status(self, status): """更新担当变更状态""" self._update_status(self.responsible_tree, self.responsible_updates, status) def _update_status(self, treeview, data, status): """通用状态更新方法""" selected = treeview.selection() if not selected: messagebox.showwarning("警告", "请先选择记录") return for item in selected: index = treeview.index(item) data[index]["status"] = status self._display_data(treeview, data) def apply_project_updates(self): """执行项目编号变更""" self._apply_updates( updates=self.project_updates, success_message="项目编号更新完成!", treeview=self.project_tree ) def apply_date_updates(self): """执行时间变更""" self._apply_updates( updates=self.date_updates, success_message="时间更新完成!", treeview=self.date_tree ) def apply_responsible_updates(self): """执行担当变更""" self._apply_updates( updates=self.responsible_updates, success_message="担当更新完成!", treeview=self.responsible_tree ) def _apply_updates(self, updates, success_message, treeview): """通用更新应用方法""" if not any(u["status"] == "待更新" for u in updates): messagebox.showwarning("警告", "没有标记为'待更新'的记录") return try: self.running_tasks += 1 self.update_progress(0, "正在更新...") wb = load_workbook(self.file_path) total_updates = len([u for u in updates if u["status"] == "待更新"]) processed = 0 for update in [u for u in updates if u["status"] == "待更新"]: sheet = wb[update["sheet"]] sheet[update["cell"]] = update["new_value"] processed += 1 self.update_progress(processed/total_updates*100, f"正在更新 {update['sheet']} {update['cell']}") wb.save(self.file_path) messagebox.showinfo("成功", success_message) # 重新加载数据 if treeview == self.project_tree: self.load_project_changes() elif treeview == self.date_tree: self.load_date_changes() else: self.load_responsible_changes() except Exception as e: messagebox.showerror("错误", f"更新失败: {str(e)}") print(traceback.format_exc()) finally: self.running_tasks -= 1 self.update_progress(100, "更新完成") def export_project_report(self): """导出项目变更报告""" self._export_report(self.project_updates, "项目变更报告") def export_date_report(self): """导出时间变更报告""" self._export_report(self.date_updates, "时间变更报告") def export_responsible_report(self): """导出担当变更报告""" self._export_report(self.responsible_updates, "担当变更报告") def _export_report(self, data, report_name): """通用报告导出方法""" if not data: messagebox.showwarning("警告", f"没有可导出的{report_name}数据") return try: file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel文件", "*.xlsx")], title=f"保存{report_name}" ) if not file_path: return df = pd.DataFrame([{ "工作表": item["sheet"], "单元格位置": item["cell"], "原内容": item["old_value"], "新内容": item["new_value"], "状态": item["status"] } for item in data]) df.to_excel(file_path, index=False) messagebox.showinfo("成功", f"{report_name}已导出到: {file_path}") except Exception as e: messagebox.showerror("错误", f"导出失败: {str(e)}") def export_doc_check_report(self): """导出文档检查报告""" if not self.doc_checks: messagebox.showwarning("警告", "没有可导出的文档检查数据") return try: file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel文件", "*.xlsx")], title="保存文档检查报告" ) if not file_path: return df = pd.DataFrame(self.doc_checks) df.to_excel(file_path, index=False) messagebox.showinfo("成功", f"文档检查报告已导出到: {file_path}") except Exception as e: messagebox.showerror("错误", f"导出失败: {str(e)}") def on_closing(self): """关闭窗口时的处理""" if self.running_tasks > 0: if messagebox.askokcancel("警告", "有任务正在运行,确定要退出吗?"): self.executor.shutdown(wait=False) self.root.destroy() else: self.root.destroy() def _get_sheet_config(self, sheet_name): """获取sheet配置""" sheet_lower = sheet_name.lower() for kw in self.special_sheets: if kw.lower() in sheet_lower: config = self.special_sheets[kw].copy() config['is_special'] = True return config return {'process': True, 'update_time': True, 'is_special': False} def _find_cells_with_value(self, sheet, target_value): """查找包含目标值的所有单元格""" found_cells = [] pattern = re.compile(rf'.*{re.escape(str(target_value))}.*', re.IGNORECASE) for row in sheet.iter_rows(): for cell in row: if cell.value and pattern.search(str(cell.value)): found_cells.append(cell) return found_cells def _find_date_cells(self, sheet, target_date=None): """查找所有包含日期的单元格""" date_cells = [] for row in sheet.iter_rows(): for cell in row: if cell.value and self._is_date(cell.value): if target_date: try: cell_date = pd.to_datetime(cell.value).strftime('%Y-%m-%d') if target_date in str(cell_date): date_cells.append(cell) except: if target_date in str(cell.value): date_cells.append(cell) else: date_cells.append(cell) return date_cells def _is_date(self, value): """判断值是否为日期""" try: pd.to_datetime(value) return True except: # 检查是否是字符串形式的日期 if isinstance(value, str): for pattern in self.date_patterns: if re.fullmatch(pattern, value.strip()): return True return False def on_closing(self): """关闭窗口时的处理""" if self.running_tasks > 0: if messagebox.askokcancel("警告", "有任务正在运行,确定要退出吗?"): self.executor.shutdown(wait=False) self.root.destroy() else: self.root.destroy() if __name__ == "__main__": root = tk.Tk() app = EnhancedVersionUpdaterApp(root) root.protocol("WM_DELETE_WINDOW", app.on_closing) root.mainloop() 这是我按照你的要求更改后的代码,但是其中有一些错误,例如AttributeError: 'EnhancedVersionUpdaterApp' object has no attribute 'memory_usage'. Did you mean: 'memory_label'?,请检查类似的问题,并修改完成给我
08-14
import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext #import xml.etree.ElementTree as ET from lxml import etree as ET import os import re from xml.dom import minidom from collections import OrderedDict, defaultdict class ConfigUpdaterApp: def __init__(self, root): self.root = root self.root.title("ID统一配置文件自动更新工具") self.root.geometry("1920x1080") self.root.configure(bg="#f0f0f0") # 创建样式 self.style = ttk.Style() self.style.configure("TButton", padding=6, relief="flat", background="#4a7a8c", foreground="blue") self.style.configure("Treeview", font=("Consolas", 10), rowheight=25) self.style.configure("Treeview.Heading", font=("Arial", 11, "bold")) self.style.configure("TNotebook", background="#f0f0f0") self.style.configure("TNotebook.Tab", padding=(10, 5), font=("Arial", 10, "bold")) self.style.configure("XMLText", font=("Consolas", 10), background="#f8f8f8") # 创建主框架 self.create_widgets() # 初始化数据结构 self.rawdata_template = [] self.rawdata_source = None self.event_template = [] self.event_source = None self.current_filter = "" self.variable_mapping = {} self.module_mapping = {} self.init_module() self.event_categories = [] self.special_mapping_id = {} self.special_mapping_name = {} self.init_special_mapping() def init_module(self): """ 初始化moduleID与SVEC模板中一致 :return: """ self.module_mapping["System"] = "00" self.module_mapping["CryoPump"] = "00" self.module_mapping["HeatExchanger"] = "00" self.module_mapping["EFEM"] = "01" self.module_mapping["LP1"] = "01" self.module_mapping["LP2"] = "01" self.module_mapping["LP3"] = "01" self.module_mapping["Transfer"] = "02" self.module_mapping["TC"] = "02" self.module_mapping["Buffer"] = "03" self.module_mapping["BF"] = "03" self.module_mapping["Bufferr"] = "03" self.module_mapping["LA"] = "11" self.module_mapping["LB"] = "12" self.module_mapping["LC"] = "13" self.module_mapping["LD"] = "14" self.module_mapping["LAB"] = "15" self.module_mapping["LCD"] = "16" self.module_mapping["Ch1"] = "21" self.module_mapping["Ch2"] = "22" self.module_mapping["Ch3"] = "23" self.module_mapping["Ch4"] = "24" self.module_mapping["Ch5"] = "25" self.module_mapping["Ch6"] = "26" self.module_mapping["ChA"] = "27" self.module_mapping["ChB"] = "28" self.module_mapping["ChC"] = "29" self.module_mapping["ChD"] = "30" self.module_mapping["ChE"] = "31" self.module_mapping["ChF"] = "32" def get_module_id(self, module_name): """ 根据module_name和映射表获取id :param module_name: :return: """ if module_name not in self.module_mapping: return None else: return self.module_mapping[module_name] def create_widgets(self): """创建界面组件""" # 创建选项卡 self.notebook = ttk.Notebook(self.root) self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) self.template_frame = ttk.Frame(self.notebook) self.notebook.add(self.template_frame, text="TemplateConfig") self.create_template_tab(self.template_frame) # RawdataConfig 标签页 self.rawdata_frame = ttk.Frame(self.notebook) self.notebook.add(self.rawdata_frame, text="RawdataConfig") self.create_rawdata_tab(self.rawdata_frame) # EventConfig 标签页 self.event_frame = ttk.Frame(self.notebook) self.notebook.add(self.event_frame, text="EventConfig") self.create_event_tab(self.event_frame) # 状态栏 self.status_var = tk.StringVar(value="就绪") status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W, padding=5) status_bar.pack(side=tk.BOTTOM, fill=tk.X) def create_template_tab(self, parent): btn_frame = ttk.Frame(parent) btn_frame.pack(fill=tk.X, padx=10, pady=10) ttk.Button(btn_frame, text="加载模板源文件", command=self.load_rawdata_template_txt).pack(side=tk.LEFT, padx=5) paned_window = ttk.PanedWindow(parent, orient=tk.VERTICAL) paned_window.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) template_frame = ttk.LabelFrame(paned_window, text="模板显示") paned_window.add(template_frame, weight=1) source_frame = ttk.LabelFrame(paned_window, text="源文件显示(XML原始格式)") paned_window.add(source_frame, weight=1) def create_rawdata_tab(self, parent): """创建RawdataConfig标签页""" # 顶部按钮区域 btn_frame = ttk.Frame(parent) btn_frame.pack(fill=tk.X, padx=10, pady=10) # RawdataConfig 按钮 ttk.Button(btn_frame, text="1.加载模板文件", command=self.load_rawdata_template).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="2.加载源文件", command=self.load_rawdata_source).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="3.新增ID属性", command=self.add_id_to_rawdata).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="4.筛选模板Variable", command=self.filter_template_vars).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="5.筛选源文件Variable", command=self.filter_source_vars).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="6.对比", command=self.compare_vars).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="7.导出标红内容", command=self.export_highlighted).pack(side=tk.LEFT, padx=5) #ttk.Button(btn_frame, text="8.加载映射表", command=self.fill_ids).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="8.填充ID属性", command=self.fill_ids).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="9.删除空ID节点", command=self.filter_empty_id_nodes).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="10.导出文件", command=self.export_rawdata).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="11.拆分文件并导出", command=self.split_export).pack(side=tk.LEFT, padx=5) # 创建分割面板 paned_window = ttk.PanedWindow(parent, orient=tk.VERTICAL) paned_window.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) # 上半部分 - 模板显示 template_frame = ttk.LabelFrame(paned_window, text="模板显示") paned_window.add(template_frame, weight=1) # 模板表格 self.create_template_table(template_frame) # 下半部分 - 源文件显示(XML原始格式) source_frame = ttk.LabelFrame(paned_window, text="源文件显示(XML原始格式)") paned_window.add(source_frame, weight=1) # XML文本显示区域 self.rawdata_xml_text = scrolledtext.ScrolledText( source_frame, wrap=tk.WORD, font=("Consolas", 10), bg="#f8f8f8", padx=10, pady=10 ) self.rawdata_xml_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.rawdata_xml_text.config(state=tk.DISABLED) # 初始为只读 # 底部筛选区域 filter_frame = ttk.Frame(parent) filter_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) # 模板Variable筛选 ttk.Label(filter_frame, text="模板Variable筛选:").pack(side=tk.LEFT, padx=(0, 5)) self.template_filter_var = tk.StringVar() self.template_filter_box = tk.Listbox(filter_frame, listvariable=self.template_filter_var, height=6, width=30, selectmode=tk.EXTENDED) self.template_filter_box.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.BOTH, expand=True) # 源文件Variable筛选 ttk.Label(filter_frame, text="源文件Variable筛选:").pack(side=tk.LEFT, padx=(20, 5)) self.source_filter_var = tk.StringVar() self.source_filter_box = tk.Listbox(filter_frame, listvariable=self.source_filter_var, height=6, width=30, selectmode=tk.EXTENDED) self.source_filter_box.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.BOTH, expand=True) # 对比结果区域 self.compare_result = tk.Text(filter_frame, height=6, width=30, state=tk.DISABLED) self.compare_result.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.BOTH, expand=True) def create_event_tab(self, parent): """创建EventConfig标签页""" # 顶部按钮区域 btn_frame = ttk.Frame(parent) btn_frame.pack(fill=tk.X, padx=10, pady=10) # EventConfig 按钮 ttk.Button(btn_frame, text="1.加载模板文件", command=self.load_event_template).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="2.加载源文件", command=self.load_event_source).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="3.新增ID属性", command=self.add_id_to_event).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="4.填充ID属性", command=self.fill_event_attributes).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="5.导出文件", command=self.export_event).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="6.拆分文件并导出", command=self.split_export_event).pack(side=tk.LEFT, padx=5) # 创建分割面板 paned_window = ttk.PanedWindow(parent, orient=tk.VERTICAL) paned_window.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) # 上半部分 - 模板显示 template_frame = ttk.LabelFrame(paned_window, text="模板显示") paned_window.add(template_frame, weight=1) # 模板表格 self.create_event_template_table(template_frame) # 下半部分 - 源文件显示(XML原始格式) source_frame = ttk.LabelFrame(paned_window, text="源文件显示(XML原始格式)") paned_window.add(source_frame, weight=1) # XML文本显示区域 self.event_xml_text = scrolledtext.ScrolledText( source_frame, wrap=tk.WORD, font=("Consolas", 10), bg="#f8f8f8", padx=10, pady=10 ) self.event_xml_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.event_xml_text.config(state=tk.DISABLED) # 初始为只读 def create_template_table(self, parent): """创建Rawdata模板表格""" # 创建滚动条 scroll_y = ttk.Scrollbar(parent, orient=tk.VERTICAL) scroll_x = ttk.Scrollbar(parent, orient=tk.HORIZONTAL) # 创建Treeview表格 columns = ("variable", "id", "mapping") self.template_table = ttk.Treeview( parent, columns=columns, show="headings", yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set, selectmode="extended" ) # 配置列标题 # self.template_table.heading("name", text="Name", anchor=tk.W) # self.template_table.heading("type", text="Type", anchor=tk.W) self.template_table.heading("variable", text="Variable", anchor=tk.W) self.template_table.heading("id", text="ID", anchor=tk.W) self.template_table.heading("mapping", text="Mapping", anchor=tk.W) # 配置列宽度 #self.template_table.column("name", width=150, anchor=tk.W) #self.template_table.column("type", width=120, anchor=tk.W) self.template_table.column("variable", width=200, anchor=tk.W) self.template_table.column("id", width=100, anchor=tk.W) self.template_table.column("mapping", width=300, anchor=tk.W) # 配置滚动条 scroll_y.config(command=self.template_table.yview) scroll_x.config(command=self.template_table.xview) # 布局 self.template_table.grid(row=0, column=0, sticky="nsew") scroll_y.grid(row=0, column=1, sticky="ns") scroll_x.grid(row=1, column=0, sticky="ew") # 配置网格权重 parent.grid_rowconfigure(0, weight=1) parent.grid_columnconfigure(0, weight=1) def create_event_template_table(self, parent): """创建Event模板表格""" # 创建滚动条 scroll_y = ttk.Scrollbar(parent, orient=tk.VERTICAL) scroll_x = ttk.Scrollbar(parent, orient=tk.HORIZONTAL) # 创建Treeview表格 columns = ("name", "id", "type", "mapping") self.event_template_table = ttk.Treeview( parent, columns=columns, show="headings", yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set, selectmode="extended" ) # 配置列标题 self.event_template_table.heading("name", text="Name", anchor=tk.W) self.event_template_table.heading("id", text="ID", anchor=tk.W) self.event_template_table.heading("type", text="Type", anchor=tk.W) self.event_template_table.heading("mapping", text="Maping", anchor=tk.W) # 配置列宽度 self.event_template_table.column("name", width=100, anchor=tk.W) self.event_template_table.column("id", width=100, anchor=tk.W) self.event_template_table.column("type", width=100, anchor=tk.W) self.event_template_table.column("mapping", width=100, anchor=tk.W) # 配置滚动条 scroll_y.config(command=self.event_template_table.yview) scroll_x.config(command=self.event_template_table.xview) # 布局 self.event_template_table.grid(row=0, column=0, sticky="nsew") scroll_y.grid(row=0, column=1, sticky="ns") scroll_x.grid(row=1, column=0, sticky="ew") # 配置网格权重 parent.grid_rowconfigure(0, weight=1) parent.grid_columnconfigure(0, weight=1) def display_xml_content(self, xml_tree, text_widget): """在文本框中显示格式化的XML内容""" try: # 将XML树转换为字符串 xml_str = ET.tostring(xml_tree.getroot(), encoding='utf-8').decode('utf-8') # 使用minidom进行格式化 dom = minidom.parseString(xml_str) pretty_xml = dom.toprettyxml(indent=" ") # 移除多余的换行 pretty_xml = "\n".join([line for line in pretty_xml.split("\n") if line.strip()]) # 更新文本框 text_widget.config(state=tk.NORMAL) text_widget.delete(1.0, tk.END) text_widget.insert(tk.END, pretty_xml) text_widget.config(state=tk.DISABLED) # 高亮空ID节点(可选) self.highlight_empty_id_nodes(text_widget, pretty_xml) except Exception as e: messagebox.showerror("显示错误", f"格式化XML显示失败: {str(e)}") def highlight_empty_id_nodes(self, text_widget, xml_content): """高亮显示id为空的data节点""" if not xml_content: return # 查找所有id属性为空的data节点 pattern = r'(<data\b[^>]*\bid="")' matches = re.finditer(pattern, xml_content) # 设置高亮样式 text_widget.tag_configure("empty_id", background="#ffe6e6") # 应用高亮 for match in matches: start_index = f"1.0 + {match.start()} chars" end_index = f"1.0 + {match.end()} chars" text_widget.tag_add("empty_id", start_index, end_index) # ================ TemplateConfig 功能 ================ def load_rawdata_template_txt(self): file_path = filedialog.askopenfilename( title="选择Template模板文件", filetypes=[("Text files", "*.txt"), ("All files", "*.*")] ) if not file_path: return try: with open(file_path, 'r', encoding='utf-8') as file: lines = file.readlines() column_names = [ 'path', 'type', 'variable', 'id', 'data_type_id', 'subsystem_id', 'parts', 'parts_id', 'attribute', 'attribute_id', 'mapping' ] except Exception as e: messagebox.showerror("加载错误", f"加载Template模板文件失败: {str(e)}") # ================ RawdataConfig 功能 ================ def load_rawdata_template(self): """加载Rawdata模板文件(.txt)""" file_path = filedialog.askopenfilename( title="选择Rawdata模板文件", filetypes=[("Text files", "*.txt"), ("All files", "*.*")] ) if not file_path: return try: with open(file_path, 'r', encoding='utf-8') as file: lines = file.readlines() self.rawdata_template = [] self.variable_mapping = {} # 清空表格 for item in self.template_table.get_children(): self.template_table.delete(item) # 解析模板文件 for line in lines: line = line.strip() if not line: continue # 分割行数据 parts = line.split() if len(parts) < 2: continue # 提取前四列 #name = parts[0] #var_type = parts[1] variable = parts[0] id = parts[1] if len(parts) > 1 else "" mapping_info = parts[2] if len(parts) > 2 else None self.rawdata_template.append({ "variable": variable, "id": id, "mapping": mapping_info }) if mapping_info: mapping_parts = mapping_info.split(';') for map in mapping_parts: if map in self.variable_mapping: messagebox.showwarning("信息有误","请确认模板文件中mapping列是否有重复信息"+map) return else: self.variable_mapping[map] = variable # 添加到表格 self.template_table.insert("", tk.END, values=(variable, id, mapping_info)) self.status_var.set(f"已加载Rawdata模板: {os.path.basename(file_path)} - {len(self.rawdata_template)} 条记录") except Exception as e: messagebox.showerror("加载错误", f"加载Rawdata模板文件失败: {str(e)}") def load_rawdata_source(self): """加载Rawdata源文件(.xml)""" file_path = filedialog.askopenfilename( title="选择Rawdata源文件", filetypes=[("XML files", "*.xml"), ("All files", "*.*")] ) if not file_path: return try: self.rawdata_tree = ET.parse(file_path) self.rawdata_root = self.rawdata_tree.getroot() self.rawdata_source = self.rawdata_tree # 在文本框中显示XML内容 self.display_xml_content(self.rawdata_tree, self.rawdata_xml_text) self.status_var.set(f"已加载Rawdata源文件: {os.path.basename(file_path)}") except Exception as e: messagebox.showerror("加载错误", f"加载Rawdata源文件失败: {str(e)}") def add_id_to_rawdata(self): """为Rawdata源文件添加ID属性""" if not self.rawdata_source: messagebox.showwarning("无数据", "请先加载Rawdata源文件") return # 添加ID属性 count = 0 for data in self.rawdata_root.findall('.//data'): if 'id' not in data.attrib: # 在属性开头位置添加id属性 new_attrib = OrderedDict() new_attrib['id'] = '' for key, value in data.attrib.items(): new_attrib[key] = value #data.attrib = new_attrib data.attrib.clear() for key, value in new_attrib.items(): data.set(key, value) count += 1 # 更新XML显示 self.display_xml_content(self.rawdata_tree, self.rawdata_xml_text) self.status_var.set(f"已为{count}个data标签添加id属性") def filter_template_vars(self): """筛选模板Variable""" if not self.rawdata_template: messagebox.showwarning("无数据", "请先加载Rawdata模板文件") return # 提取Variable后半部分并去重 var_parts = set() for item in self.rawdata_template: variable = item["variable"] parts = variable.split('_', 1) # 只分割第一个下划线 if len(parts) > 1: var_parts.add(parts[1]) # 更新列表 self.template_filter_var.set(tuple(sorted(var_parts))) self.status_var.set(f"已筛选出 {len(var_parts)} 个模板Variable") def filter_source_vars(self): """筛选源文件Variable""" if not self.rawdata_source: messagebox.showwarning("无数据", "请先加载Rawdata源文件") return # 提取Variable后半部分并去重 var_parts = set() for data in self.rawdata_root.findall('.//data'): variable = data.get('variable', '') if variable: parts = variable.split('_', 1) # 只分割第一个下划线 if len(parts) > 1: var_parts.add(parts[1]) # 更新列表 self.source_filter_var.set(tuple(sorted(var_parts))) self.status_var.set(f"已筛选出 {len(var_parts)} 个源文件Variable") def compare_vars(self): """对比模板和源文件Variable""" template_vars = self.template_filter_box.get(0, tk.END) source_vars = self.source_filter_box.get(0, tk.END) template_mapping_vars = self.variable_mapping.values(); if not template_vars or not source_vars: messagebox.showwarning("无数据", "请先筛选模板和源文件Variable") return # 清空对比结果 self.compare_result.config(state=tk.NORMAL) self.compare_result.delete(1.0, tk.END) # 查找源文件中有但模板中没有的Variable missing_in_template = set(source_vars) - set(template_vars) - set(template_mapping_vars) # 更新源文件列表,标红缺失项 self.source_filter_box.delete(0, tk.END) for var in sorted(source_vars): self.source_filter_box.insert(tk.END, var) if var in missing_in_template: self.source_filter_box.itemconfig(tk.END, fg='red') # 显示对比结果 self.compare_result.insert(tk.END, "对比结果:\n") self.compare_result.insert(tk.END, f"模板Variable数量: {len(template_vars)}\n") self.compare_result.insert(tk.END, f"源文件Variable数量: {len(source_vars)}\n") self.compare_result.insert(tk.END, f"源文件中有但模板中缺失的数量: {len(missing_in_template)}\n\n") if missing_in_template: self.compare_result.insert(tk.END, "缺失的Variable:\n") for var in sorted(missing_in_template): self.compare_result.insert(tk.END, f" - {var}\n") self.compare_result.config(state=tk.DISABLED) self.status_var.set(f"对比完成: 发现 {len(missing_in_template)} 个缺失项") def export_highlighted(self): """导出标红内容""" # 获取标红的项 highlighted_items = [] for i in range(self.source_filter_box.size()): if self.source_filter_box.itemcget(i, "fg") == "red": highlighted_items.append(self.source_filter_box.get(i)) if not highlighted_items: messagebox.showinfo("无数据", "没有标红的项可导出") return # 选择保存位置 file_path = filedialog.asksaveasfilename( title="保存标红内容", defaultextension=".txt", filetypes=[("Text files", "*.txt"), ("All files", "*.*")] ) if not file_path: return # 写入文件 try: with open(file_path, 'w', encoding='utf-8') as file: file.write("标红的Variable项:\n") for item in highlighted_items: file.write(f"{item}\n") self.status_var.set(f"已导出 {len(highlighted_items)} 个标红项到 {os.path.basename(file_path)}") messagebox.showinfo("导出成功", "标红项已成功导出") except Exception as e: messagebox.showerror("导出错误", f"导出文件失败: {str(e)}") def fill_ids(self): """填充ID属性(占位)""" if not self.rawdata_source: messagebox.showwarning("无数据", "请先加载Rawdata源文件") return if not self.rawdata_root: messagebox.showwarning("无数据", "请先加载Rawdata源文件") return if not self.rawdata_template: messagebox.showwarning("无数据", "请先加载模板文件") return # 自动加载映射表,如果不存在就不加载 #self.init_variable_mapping() fill_count = 0 not_exist = [] not_exist_all = [] for data_elem in self.rawdata_tree.findall('.//data'): variable = data_elem.get('variable') parts = variable.split('_', 1) if len(parts) < 2 or variable in self.special_mapping_id: if variable in self.special_mapping_id and variable in self.special_mapping_name: temp_var = self.special_mapping_name[variable] temp_id = self.special_mapping_id[variable] else: continue else: module = parts[0] var = parts[1] temp_var, temp_id = self.get_id(module, var) if temp_var and temp_id: data_elem.set('variable', temp_var) data_elem.set('id', temp_id) fill_count += 1 else: if temp_id is None and var not in not_exist: not_exist.append(var) if temp_id is None: not_exist_all.append(variable) # 更新XML显示 self.display_xml_content(self.rawdata_tree, self.rawdata_xml_text) self.status_var.set(f"已为{fill_count}个data标签添加id属性,剩余{len(not_exist)}无对应值。总共剩余{len(not_exist_all)}个未设置") #messagebox.showinfo("功能待实现", "填充ID属性功能将在后续版本实现") def filter_empty_id_nodes(self): """删除id属性为空的data节点并刷新显示""" if not self.rawdata_source: messagebox.showwarning("无数据", "请先加载Rawdata源文件") return # 获取XML根节点 root = self.rawdata_tree.getroot() # 确保获取根节点[^5] # 查找所有id属性为空的data节点 empty_id_nodes = [] for data_node in root.iter('data'): if data_node.get('id', '') == '': empty_id_nodes.append(data_node) if not empty_id_nodes: messagebox.showinfo("无操作", "未找到id为空的data节点") return # 删除这些节点(使用替代方法获取父节点) for node in empty_id_nodes: # 替代 getparent() 的方法 for parent in root.iter(): if node in list(parent): # 检查节点是否是当前父节点的直接子节点 parent.remove(node) break # 刷新XML显示 self.display_xml_content(self.rawdata_tree, self.rawdata_xml_text) # 更新状态 self.status_var.set(f"已删除 {len(empty_id_nodes)} 个id为空的data节点") def export_rawdata(self): """导出Rawdata源文件""" if not self.rawdata_source: messagebox.showwarning("无数据", "没有可导出的数据") return file_path = filedialog.asksaveasfilename( title="保存Rawdata文件", defaultextension=".xml", filetypes=[("XML files", "*.xml"), ("All files", "*.*")] ) if not file_path: return try: # 使用minidom美化输出 ''' xml_str = ET.tostring(self.rawdata_root, encoding='utf-8').decode('utf-8') parsed = minidom.parseString(xml_str) pretty_xml = parsed.toprettyxml(indent=" ") # 写入文件 with open(file_path, 'w', encoding='utf-8') as file: file.write(pretty_xml) ''' self.rawdata_tree.write(file_path, encoding='utf-8', xml_declaration=True) self.status_var.set(f"Rawdata文件已成功导出到: {os.path.basename(file_path)}") messagebox.showinfo("导出成功", "Rawdata配置文件已成功导出") except Exception as e: messagebox.showerror("导出错误", f"导出文件失败: {str(e)}") def init_variable_mapping(self): with open('SV_mapping.txt', 'r') as file: for line in file.readlines(): parts = line.split() if len(parts) == 2: self.variable_mapping[parts[0]] = parts[1] def init_special_mapping(self): self.special_mapping_id['LP1_BypassReadID'] = '201210000' self.special_mapping_name['LP1_BypassReadID'] = 'EFEM_LP1BypassReadID' self.special_mapping_id['LP1_LoadPortTout'] = '201210001' self.special_mapping_name['LP1_LoadPortTout'] = 'EFEM_LP1LoadPortTout' self.special_mapping_id['LP2_BypassReadID'] = '201211000' self.special_mapping_name['LP2_BypassReadID'] = 'EFEM_LP2BypassReadID' self.special_mapping_id['LP2_LoadPortTout'] = '201211001' self.special_mapping_name['LP2_LoadPortTout'] = 'EFEM_LP2LoadPortTout' self.special_mapping_id['LP3_BypassReadID'] = '201212000' self.special_mapping_name['LP3_BypassReadID'] = 'EFEM_LP3BypassReadID' self.special_mapping_id['LP3_LoadPortTout'] = '201212001' self.special_mapping_name['LP3_LoadPortTout'] = 'EFEM_LP3LoadPortTout' self.special_mapping_id['CJobSubstProcStatusList'] = '100000004' self.special_mapping_name['CJobSubstProcStatusList'] = 'CJobSubstProcStatusList' self.special_mapping_id['ToolState'] = '100000000' self.special_mapping_name['ToolState'] = 'ToolState' self.special_mapping_id['Aligner_AngleLA'] = '201209000' self.special_mapping_name['Aligner_AngleLA'] = 'EFEM_AlignerAngleLA' self.special_mapping_id['Aligner_AngleLB'] = '201209001' self.special_mapping_name['Aligner_AngleLB'] = 'EFEM_AlignerAngleLB' self.special_mapping_id['Aligner_AngleX'] = '201209002' self.special_mapping_name['Aligner_AngleX'] = 'EFEM_AlignerAngleX' self.special_mapping_id['Aligner_NotchSupport'] = '201209003' self.special_mapping_name['Aligner_NotchSupport'] = 'EFEM_AlignerNotchSupport' self.special_mapping_id['Aligner_Bypass'] = '201209004' self.special_mapping_name['Aligner_Bypass'] = 'EFEM_AlignerBypass' self.special_mapping_id['CryoPump_CompressorPressure'] = '101122000' self.special_mapping_name['CryoPump_CompressorPressure'] = 'System_CryoPumpCompressorPressure' self.special_mapping_id['CryoPump_CompressorDiffPressure'] = '101122001' self.special_mapping_name['CryoPump_CompressorDiffPressure'] = 'System_CryoPumpCompressorDiffPressure' self.special_mapping_id['CryoPump_CompressorSupplyPressure'] = '101122002' self.special_mapping_name['CryoPump_CompressorSupplyPressure'] = 'System_CryoPumpCompressorSupplyPressure' self.special_mapping_id['HeatExchanger_RS'] = '100409000' self.special_mapping_name['HeatExchanger_RS'] = 'System_HeatExchangerRS' def get_id(self, module_name, var): if var in self.variable_mapping: #来自mapping,含有前缀CHXXX temp_var = self.variable_mapping[var] variable = temp_var.replace('CHXXX', module_name) # Ch1_LotID else: temp_var = "CHXXX_" + var #来自配置,不含有前缀CHXXX variable = module_name + '_' + var # Ch1_LotID module_id = self.get_module_id(module_name) temp_id = self.get_id_by_variable(temp_var) if temp_id and module_id: if 'XX' in temp_id: id = temp_id.replace('XX', str(module_id)) return variable.replace('CHXXX', module_name), id else: return None, None def get_id_by_variable(self, variable): for item in self.rawdata_template: if item['variable'] == variable: return item['id'] def split_export(self): if not self.rawdata_source: messagebox.showwarning("无数据", "没有可拆分的数据") return # 创建分组字典,前缀为data节点列表 groups = defaultdict(list) # 遍历所有data节点 for data in self.rawdata_root.findall('.//data'): variable = data.get('variable','') if '_' in variable: prefix = variable.split('_', 1)[0] groups[prefix].append(data) # 为每个分组创建新的xml文件 for prefix, data in groups.items(): # 创建新根节点(复制原始根节点标签和属性) new_root = ET.Element(self.rawdata_root.tag, attrib=self.rawdata_root.attrib) # 复制原始根节点的命名空间 # 复制原始根节点的命名空间 for ns, uri in self.rawdata_root.nsmap.items(): if ns: ET.register_namespace(ns, uri) # 添加原始非 data 节点 for child in self.rawdata_root: if child.tag != 'data': new_root.append(child) # 添加分组中的 data 节点 for node in data: new_root.append(node) # 创建 XML 树并写入文件 new_tree = ET.ElementTree(new_root) filename = f"RawData_{prefix}.xml" new_tree.write(filename, encoding='utf-8', xml_declaration=True) return # ================ EventConfig 功能 ================ def load_event_template(self): """加载Event模板文件(.txt)""" file_path = filedialog.askopenfilename( title="选择Event模板文件", filetypes=[("Text files", "*.txt"), ("All files", "*.*")] ) if not file_path: return try: with open(file_path, 'r', encoding='utf-8') as file: lines = file.readlines() self.event_template = [] self.event_mapping = {} # 清空表格 for item in self.event_template_table.get_children(): self.event_template_table.delete(item) # 解析模板文件 for line in lines: line = line.strip() if not line: continue # 分割行数据 parts = line.split() if len(parts) < 2: continue # 提取前两列 name = parts[0] id = parts[1] if len(parts) > 1 else "" mapping = parts[2] if len(parts) > 2 else None # 提取type name_parts = name.split('_',1) if len(name_parts) < 2: continue type = name_parts[1] self.event_categories.append(type) if mapping: mapping_parts = mapping.split(';') for map in mapping_parts: if map in self.event_mapping: messagebox.showwarning("数据有误","请确认Event模板数据中mapping列是否有重复数据") else: self.event_mapping[map] = name self.event_template.append({ "name": name, "id": id, "type": type, "mapping": mapping }) # 添加到表格 self.event_template_table.insert("", tk.END, values=(name, id, type, mapping)) self.event_categories = sorted(self.event_categories, key=len, reverse=True) self.status_var.set(f"已加载Event模板: {os.path.basename(file_path)} - {len(self.event_template)} 条记录") except Exception as e: messagebox.showerror("加载错误", f"加载Event模板文件失败: {str(e)}") def load_event_source(self): """加载Event源文件(.xml)""" file_path = filedialog.askopenfilename( title="选择Event源文件", filetypes=[("XML files", "*.xml"), ("All files", "*.*")] ) if not file_path: return try: self.event_tree = ET.parse(file_path) self.event_root = self.event_tree.getroot() self.event_source = self.event_tree # 在文本框中显示XML内容 self.display_xml_content(self.event_tree, self.event_xml_text) self.status_var.set(f"已加载Event源文件: {os.path.basename(file_path)}") except Exception as e: messagebox.showerror("加载错误", f"加载Event源文件失败: {str(e)}") def add_id_to_event(self): """为Event源文件添加ID属性""" if not self.event_source: messagebox.showwarning("无数据", "请先加载Event源文件") return # 添加ID属性 count = 0 for event in self.event_root.findall('.//event'): if 'id' not in event.attrib: # 在属性开头位置添加id属性 new_attrib = OrderedDict() new_attrib['id'] = '' for key, value in event.attrib.items(): new_attrib[key] = value # data.attrib = new_attrib event.attrib.clear() for key, value in new_attrib.items(): event.set(key, value) count += 1 # 更新XML显示 self.display_xml_content(self.event_tree, self.event_xml_text) self.status_var.set(f"已为{count}个event元素添加ID属性") def get_id_from_event_template(self, event): for item in self.event_template: if item['name'] == event: return item['id'] def get_id_event(self, module, cat): if cat in self.event_mapping: temp_name = self.event_mapping[cat] else: temp_name = "CHXXX_" + cat module_id = self.get_module_id(module) temp_id = self.get_id_from_event_template(temp_name) name = module + '_' + cat if temp_id and module_id: id = temp_id.replace('XX', str(module_id)) return name, id else: return None, None def split_by_type(self, event_name): if not self.event_categories: return for cat in self.event_categories: if event_name.endswith(cat): print('split_by_type:' + cat +'orign:' + event_name) module = event_name[:-len(cat)] return module, cat def fill_event_attributes(self): """填充Event属性(占位)""" # messagebox.showinfo("功能待实现", "填充属性功能将在后续版本实现") # 加载映射表 待实现 if not self.event_source: messagebox.showwarning("无数据", "请先加载EventConfig源文件") return if not self.event_template: messagebox.showwarning("无数据", "请先加载EventConfig模板文件") return fill_count = 0 not_exist = [] not_exist_all = [] for data_elem in self.event_tree.findall('.//event'): name = data_elem.get('name') module, cat = self.split_by_type(name) print('module:'+module+'cat:'+cat) temp_name, temp_id = self.get_id_event(module, cat) #print('temp_name:'+temp_name+' temp_id:'+temp_id) if temp_name and temp_id: data_elem.set('name', temp_name) data_elem.set('id', temp_id) fill_count += 1 else: if temp_id is None and cat not in not_exist: not_exist.append(cat) if temp_id is None: not_exist_all.append(name) self.display_xml_content(self.event_tree, self.event_xml_text) self.status_var.set(f"已为{fill_count}个event补充id属性,剩余{len(not_exist)}个不在模板中,总共剩余{len(not_exist_all)}个") def export_event(self): """导出Event源文件""" if not self.event_source: messagebox.showwarning("无数据", "没有可导出的数据") return file_path = filedialog.asksaveasfilename( title="保存Event文件", defaultextension=".xml", filetypes=[("XML files", "*.xml"), ("All files", "*.*")] ) if not file_path: return try: self.event_tree.write(file_path, encoding='utf-8', xml_declaration=True) self.status_var.set(f"Event文件已成功导出到: {os.path.basename(file_path)}") messagebox.showinfo("导出成功", "Event配置文件已成功导出") except Exception as e: messagebox.showerror("导出错误", f"导出文件失败: {str(e)}") def split_export_event(self): return if __name__ == "__main__": root = tk.Tk() app = ConfigUpdaterApp(root) root.mainloop() 这是第一个程序import sys import xml.etree.ElementTree as ET from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QPushButton, QFileDialog, QLineEdit, QLabel, QMessageBox, QHBoxLayout, QVBoxLayout, QWidget, QHeaderView, QAbstractItemView, QComboBox, QSizePolicy, QStyledItemDelegate) from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QBrush, QColor, QFont import os class ComboBoxDelegate(QStyledItemDelegate): def __init__(self, parent=None, options=None): super().__init__(parent) self.options = options or [] def createEditor(self, parent, option, index): editor = QComboBox(parent) editor.setEditable(True) editor.addItems(self.options) return editor def setEditorData(self, editor, index): value = index.data(Qt.DisplayRole) if value: editor.setCurrentText(value) def setModelData(self, editor, model, index): model.setData(index, editor.currentText(), Qt.EditRole) class XMLDataManager(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Data编辑器") self.setGeometry(100, 100, 1400, 800) self.current_file = None self.original_data = [] self.display_data = [] self.temp_data = [] self.row_counter = 1 self.type_options = set() self.data_type_id_options = set() self.is_searching = False self.search_results_indices = [] self.search_text = "" self.block_updates = False # 用于阻止递归更新 # 定义字体 self.normal_font = QFont() self.bold_font = QFont() self.bold_font.setBold(True) self.column_names = [ "path", "type", "variable", "id", "data_type_id", "subsystem_id", "parts", "parts_id", "attribute", "attribute_id", "mapping" ] self.required_columns = [col for col in self.column_names if col != "mapping"] self.init_ui() def init_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) self.load_btn = QPushButton("加载文件") self.add_btn = QPushButton("添加记录") self.delete_btn = QPushButton("删除记录") self.export_btn = QPushButton("导出文件") self.search_btn = QPushButton("搜索") self.clear_btn = QPushButton("清空搜索") button_style = """ QPushButton { background-color: rgb(65, 105, 225); color: white; border: none; padding: 8px 16px; text-align: center; font-size: 14px; margin: 4px 2px; border-radius: 4px; } QPushButton:hover { background-color: rgb(55, 95, 215); } QPushButton:pressed { background-color: rgb(45, 85, 205); } """ for btn in [self.load_btn, self.export_btn, self.add_btn, self.delete_btn, self.search_btn, self.clear_btn]: btn.setStyleSheet(button_style) self.search_box = QLineEdit() self.search_box.setPlaceholderText("输入搜索内容...") self.search_box.setMinimumWidth(300) self.search_box.setStyleSheet(""" QLineEdit { padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; } """) self.column_combo = QComboBox() self.column_combo.setMinimumWidth(120) self.column_combo.addItem("所有列") self.column_combo.addItems(self.column_names) self.column_combo.setCurrentIndex(0) self.column_combo.setStyleSheet(""" QComboBox { padding: 6px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; } """) self.table = QTableWidget() self.table.setColumnCount(len(self.column_names) + 1) headers = ["序号"] + self.column_names self.table.setHorizontalHeaderLabels(headers) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) self.table.horizontalHeader().setStretchLastSection(True) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setSelectionMode(QAbstractItemView.ExtendedSelection) # 使用cellChanged信号处理单元格编辑 self.table.cellChanged.connect(self.handle_cell_change) self.table.setStyleSheet(""" QTableWidget { gridline-color: #ddd; font-size: 12px; } QHeaderView::section { background-color: #f2f2f2; padding: 4px; border: 1px solid #ddd; font-weight: bold; } QTableCornerButton::section { background-color: #f2f2f2; border: 1px solid #ddd; } """) self.table.setColumnWidth(0, 60) self.table.verticalHeader().setVisible(False) self.table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) top_layout = QHBoxLayout() top_layout.addWidget(self.load_btn) top_layout.addWidget(self.add_btn) top_layout.addWidget(self.delete_btn) top_layout.addWidget(self.export_btn) top_layout.addStretch() search_col_label = QLabel("搜索列:") search_col_label.setStyleSheet("font-size: 14px;") top_layout.addWidget(search_col_label) top_layout.addWidget(self.column_combo) search_col_label1 = QLabel("搜索内容:") search_col_label1.setStyleSheet("font-size: 14px;") top_layout.addWidget(search_col_label1) top_layout.addWidget(self.search_box) top_layout.addWidget(self.search_btn) top_layout.addWidget(self.clear_btn) main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addWidget(self.table) central_widget.setLayout(main_layout) self.load_btn.clicked.connect(self.load_xml) self.export_btn.clicked.connect(self.export_xml) self.add_btn.clicked.connect(self.add_record) self.delete_btn.clicked.connect(self.delete_record) self.search_btn.clicked.connect(self.search_records) self.clear_btn.clicked.connect(self.clear_search) self.statusBar = self.statusBar() self.statusBar.setStyleSheet("background-color: #f0f0f0; padding: 4px;") def handle_cell_change(self, row, col): """处理单元格内容改变""" if self.block_updates or col == 0: # 忽略序号列的修改 return try: # 临时断开信号连接,避免递归调用 self.table.cellChanged.disconnect() self.block_updates = True col_idx = col - 1 if col_idx < 0 or col_idx >= len(self.column_names): return col_name = self.column_names[col_idx] item = self.table.item(row, col) new_value = item.text() # 更新数据 if self.is_searching: # 在搜索模式下,需要找到原始数据中的对应位置 if row < len(self.search_results_indices): original_idx = self.search_results_indices[row] if original_idx < len(self.temp_data): self.temp_data[original_idx][col_name] = new_value # 同步更新显示数据 if row < len(self.display_data): self.display_data[row][col_name] = new_value else: # 非搜索模式直接更新 if row < len(self.temp_data): self.temp_data[row][col_name] = new_value if row < len(self.display_data): self.display_data[row][col_name] = new_value # 重新应用样式 self.apply_cell_style(item, col_name, new_value) except Exception as e: QMessageBox.critical(self, "错误", f"修改数据时发生错误:\n{str(e)}") finally: # 恢复信号连接 self.block_updates = False self.table.cellChanged.connect(self.handle_cell_change) def apply_cell_style(self, item, col_name, cell_text): """应用单元格样式""" # 重置样式 item.setFont(self.normal_font) item.setBackground(QColor(255, 255, 255)) # 检查必填项 if col_name in self.required_columns and not cell_text.strip(): item.setBackground(QColor(255, 200, 200)) # 如果是搜索状态且单元格文本包含搜索词,则加粗 if self.is_searching and self.search_text: lower_text = cell_text.lower() if self.search_text in lower_text: item.setFont(self.bold_font) def load_xml(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择XML文件", "", "XML文件 (*.xml)" ) if not file_path: return try: tree = ET.parse(file_path) root = tree.getroot() self.current_file = file_path file_name = os.path.basename(file_path) self.setWindowTitle(f"Data编辑器 - {file_name}") self.table.setRowCount(0) self.original_data = [] self.display_data = [] self.temp_data = [] self.row_counter = 1 self.type_options = set() self.data_type_id_options = set() self.is_searching = False self.search_results_indices = [] self.search_text = "" for record in root.findall('Record'): row_data = {} for col_name in self.column_names: value = record.get(col_name, '') row_data[col_name] = value if col_name == 'type' and value: self.type_options.add(value) elif col_name == 'data_type_id' and value: self.data_type_id_options.add(value) self.original_data.append(row_data) self.temp_data = [row.copy() for row in self.original_data] self.display_data = self.temp_data.copy() self.populate_table() self.adjust_column_widths() self.statusBar.showMessage(f"成功加载文件: {file_path}", 3000) except Exception as e: QMessageBox.critical(self, "错误", f"加载XML文件失败:\n{str(e)}") def populate_table(self, data=None, indices=None): if data is None: data = self.display_data try: self.table.cellChanged.disconnect() except: pass self.table.setRowCount(len(data)) type_col_index = self.column_names.index('type') + 1 data_type_id_col_index = self.column_names.index('data_type_id') + 1 type_delegate = ComboBoxDelegate(self.table, sorted(self.type_options)) data_type_id_delegate = ComboBoxDelegate(self.table, sorted(self.data_type_id_options)) self.table.setItemDelegateForColumn(type_col_index, type_delegate) self.table.setItemDelegateForColumn(data_type_id_col_index, data_type_id_delegate) for row_idx, record in enumerate(data): # 保留原始序号 if indices and row_idx < len(indices): original_idx = indices[row_idx] seq_item = QTableWidgetItem(str(original_idx + 1)) else: # 在非搜索模式下,使用当前行号 seq_item = QTableWidgetItem(str(row_idx + 1)) seq_item.setTextAlignment(Qt.AlignCenter) seq_item.setFlags(seq_item.flags() & ~Qt.ItemIsEditable) seq_item.setFont(self.normal_font) self.table.setItem(row_idx, 0, seq_item) for col_idx, col_name in enumerate(self.column_names): value = record.get(col_name, '') item = QTableWidgetItem(value) self.apply_cell_style(item, col_name, value) if col_name in ['type', 'data_type_id']: item.setFlags(item.flags() | Qt.ItemIsEditable) self.table.setItem(row_idx, col_idx + 1, item) self.table.cellChanged.connect(self.handle_cell_change) def adjust_column_widths(self): self.table.setColumnWidth(0, 60) for col in range(1, self.table.columnCount()): self.table.resizeColumnToContents(col) if self.table.columnWidth(col) < 100: self.table.setColumnWidth(col, 100) def export_xml(self): if not self.temp_data: QMessageBox.warning(self, "警告", "没有数据可导出") return # 检查必填字段 empty_fields = [] for idx, record in enumerate(self.temp_data): for col_name in self.required_columns: if not record.get(col_name, '').strip(): # 行号为idx+1 empty_fields.append(f"行 {idx+1} 的 '{col_name}' 列") if empty_fields: error_msg = "以下必填项为空,请填写完整后导出:\n" error_msg += "\n".join(empty_fields[:10]) # 最多显示10条 if len(empty_fields) > 10: error_msg += f"\n...(共{len(empty_fields)}个错误)" QMessageBox.warning(self, "导出失败", error_msg) return file_path, _ = QFileDialog.getSaveFileName( self, "导出XML文件", "", "XML文件 (*.xml)" ) if not file_path: return try: with open(file_path, 'wb') as f: f.write(b'<?xml version="1.0" ?>\n') f.write(b'<DataRecords>\n') for record in self.temp_data: attrs = ' '.join([f'{k}="{v}"' for k, v in record.items()]) f.write(f' <Record {attrs}/>\n'.encode('utf-8')) f.write(b'</DataRecords>') self.statusBar.showMessage(f"成功导出到: {file_path}", 3000) except Exception as e: QMessageBox.critical(self, "错误", f"导出XML文件失败:\n{str(e)}") def add_record(self): selected_rows = set(index.row() for index in self.table.selectedIndexes()) insert_row = self.table.rowCount() if selected_rows: insert_row = min(selected_rows) + 1 self.table.insertRow(insert_row) new_record = {col_name: "" for col_name in self.column_names} # 根据当前状态添加到不同的数据列表 if self.is_searching: # 在搜索状态下添加记录到临时数据和显示数据 self.display_data.insert(insert_row, new_record) # 找到合适的原始索引位置插入 if insert_row < len(self.search_results_indices): insert_original_pos = self.search_results_indices[insert_row] else: insert_original_pos = len(self.temp_data) self.temp_data.insert(insert_original_pos, new_record) # 更新后续的搜索结果索引 self.search_results_indices = [x if x < insert_original_pos else x + 1 for x in self.search_results_indices] self.search_results_indices.insert(insert_row, insert_original_pos) else: self.display_data.insert(insert_row, new_record) self.temp_data.insert(insert_row, new_record) # 序号将在populate_table中设置 self.populate_table() self.table.scrollToItem(self.table.item(insert_row, 0)) self.table.selectRow(insert_row) self.statusBar.showMessage("已添加新记录", 2000) def delete_record(self): selected_rows = set(index.row() for index in self.table.selectedIndexes()) if not selected_rows: QMessageBox.warning(self, "警告", "请先选择要删除的记录") return reply = QMessageBox.question(self, "确认删除", f"确定要删除选中的 {len(selected_rows)} 条记录吗?", QMessageBox.Yes | QMessageBox.No) if reply != QMessageBox.Yes: return # 按降序删除以避免索引问题 for row in sorted(selected_rows, reverse=True): self.table.removeRow(row) if row < len(self.display_data): del self.display_data[row] if self.is_searching and row < len(self.search_results_indices): # 如果是搜索状态,需要从原始数据中删除 original_idx = self.search_results_indices[row] if original_idx < len(self.temp_data): del self.temp_data[original_idx] # 更新搜索结果索引 self.search_results_indices = [x if x < original_idx else x - 1 for x in self.search_results_indices] self.search_results_indices.pop(row) elif not self.is_searching and row < len(self.temp_data): del self.temp_data[row] self.populate_table() self.statusBar.showMessage(f"已删除 {len(selected_rows)} 条记录", 3000) def search_records(self): search_text = self.search_box.text().strip() selected_column = self.column_combo.currentText() if not search_text: QMessageBox.warning(self, "警告", "请输入搜索内容") return self.is_searching = True self.search_text = search_text.lower() self.search_results_indices = [] matched_rows = [] # 搜索基于临时数据(包含所有修改) for original_idx, record in enumerate(self.temp_data): match_found = False if selected_column == "所有列": for col_name in self.column_names: value = record.get(col_name, '').lower() if self.search_text in value: match_found = True break else: value = record.get(selected_column, '').lower() if self.search_text in value: match_found = True if match_found: self.search_results_indices.append(original_idx) matched_rows.append(record) if matched_rows: self.display_data = matched_rows # 传递原始索引用于显示正确的序号 self.populate_table(self.display_data, self.search_results_indices) self.adjust_column_widths() self.statusBar.showMessage(f"找到 {len(matched_rows)} 条匹配记录", 3000) else: QMessageBox.information(self, "搜索结果", "未找到匹配记录") self.statusBar.showMessage("未找到匹配记录", 2000) def clear_search(self): self.display_data = [row.copy() for row in self.temp_data] self.is_searching = False self.search_text = "" self.search_box.clear() self.search_results_indices = [] self.populate_table() self.adjust_column_widths() self.statusBar.showMessage("已显示所有记录", 2000) if __name__ == "__main__": app = QApplication(sys.argv) window = XMLDataManager() window.show() sys.exit(app.exec_()) 这是第二个程序。第一个程序现在包含两个子界面,将第二个程序合并到第一个程序中,作为第一个程序的第一个子界面,修改后的界面包含三个子界面。第一个子界面风格按照第一个程序。界面的功能不变。输出完整的程序代码。
08-27
根据以下代码写一个读取kml文件并展示读出的坐标列表,再写一个坐标系选择的下拉选择。打包成exe。 ```python import xml.etree.ElementTree as ET import json def kml_to_geojson(kml_file): tree = ET.parse(kml_file) root = tree.getroot() namespace = {'kml': 'http://www.opengis.net/kml/2.2'} geojson = {"type": "FeatureCollection", "features": []} for placemark in root.findall('.//kml:Placemark', namespace): feature = {"type": "Feature", "properties": {}, "geometry": {}} name = placemark.find('kml:name', namespace) if name is not None: feature['properties']['name'] = name.text point = placemark.find('.//kml:Point/kml:coordinates', namespace) if point is not None: coordinates = point.text.strip().split(',') feature['geometry']['type'] = 'Point' feature['geometry']['coordinates'] = [float(coordinates[0]), float(coordinates[1])] else: polygon = placemark.find('.//kml:Polygon/kml:outerBoundaryIs/kml:LinearRing/kml:coordinates', namespace) if polygon is not None: coordinates = polygon.text.strip().split() feature['geometry']['type'] = 'Polygon' feature['geometry']['coordinates'] = [[list(map(float, coord.split(','))) for coord in coordinates]] if feature['geometry']: geojson['features'].append(feature) return geojson def save_geojson(geojson, filename): print('Saving GeoJSON to file:', geojson ) with open(filename, 'w', encoding='utf-8') as f: json.dump(geojson, f, ensure_ascii=False, indent=4) # 示例使用 kml_file_path = './Dataset_203sup_3范围.kml' # 替换为你的 KML 文件路径 geojson_result = kml_to_geojson(kml_file_path) save_geojson(geojson_result, './output123.geojson') # 保存为 GeoJSON 文件 ```
最新发布
09-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值