Column 'TABLE_NAME' not found.

本文探讨了在使用Java进行MySQL数据库查询时遇到的Column 'TABLE_NAME' not found错误,分析了两种常见原因:一是查询字段在数据库中不存在;二是字段名存在空格或拼写错误。通过实例展示了如何定位并修复此类错误。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用java在mysql数据库中查询数据时,有时会出现Column 'TABLE_NAME' not found.这样的错误,如下图:

出现这样的问题主要有两种:

1.你查询的这个字段在你的数据库中没有,数据库中没有这个字段当然不可以查到了

2.第二中时比较常见的。在数据库中有这个字段但是还是有这个错误,很多情况下时数据库中的这个字段多了空格或者字母,尤其是空格,在数据库中写该字段名的时候不注意会在字段前或者后加一个空格,所以会出现查询不到的错误,如图所示:

可以明显看到,在performance字段之前多了一个不明显的空格,时建表的时候不仔细导致的错误,多注意就好!

 

""" title: SQL Server Access author: MENG author_urls: - https://github.com/mengvision description: A tool for reading database information and executing SQL queries, supporting multiple databases such as MySQL, PostgreSQL, SQLite, and Oracle. It provides functionalities for listing all tables, describing table schemas, and returning query results in CSV format. A versatile DB Agent for seamless database interactions. required_open_webui_version: 0.5.4 requirements: pymysql, sqlalchemy, cx_Oracle version: 0.1.6 licence: MIT # Changelog ## [0.1.6] - 2025-03-11 ### Added - Added `get_table_indexes` method to retrieve index information for a specific table, supporting MySQL, PostgreSQL, SQLite, and Oracle. - Enhanced metadata capabilities by providing detailed index descriptions (e.g., index name, columns, and type). - Improved documentation to include the new `get_table_indexes` method and its usage examples. - Updated error handling in `get_table_indexes` to provide more detailed feedback for unsupported database types. ## [0.1.5] - 2025-01-20 ### Changed - Updated `list_all_tables` and `table_data_schema` methods to accept `db_name` as a function parameter instead of using `self.valves.db_name`. - Improved flexibility by decoupling database name from class variables, allowing dynamic database selection at runtime. ## [0.1.4] - 2025-01-17 ### Added - Added support for Oracle database using `cx_Oracle` driver. - Added dynamic engine creation in each method to ensure fresh database connections for every operation. - Added support for Oracle-specific queries in `list_all_tables` and `table_data_schema` methods. ### Changed - Moved `self._get_engine()` from `__init__` to individual methods for better flexibility and tool compatibility. - Updated `_get_engine` method to support Oracle database connection URL. - Improved `table_data_schema` method to handle Oracle-specific column metadata. ### Fixed - Fixed potential connection issues by ensuring each method creates its own database engine. - Improved error handling for Oracle-specific queries and edge cases. ## [0.1.3] - 2025-01-17 ### Added - Added support for multiple database types (e.g., MySQL, PostgreSQL, SQLite) using SQLAlchemy. - Added configuration flexibility through environment variables or external configuration files. - Enhanced query security with stricter validation and SQL injection prevention. - Improved error handling with detailed exception messages for better debugging. ### Changed - Replaced `pymysql` with SQLAlchemy for broader database compatibility. - Abstracted database connection logic into a reusable `_get_engine` method. - Updated `table_data_schema` method to support multiple database types. ### Fixed - Fixed potential SQL injection vulnerabilities in query execution. - Improved handling of edge cases in query validation and execution. ## [0.1.2] - 2025-01-16 ### Added - Added support for specifying the database port with a default value of `3306`. - Abstracted database connection logic into a reusable `_get_connection` method. ## [0.1.1] - 2025-01-16 ### Added - Support for additional read-only query types: `SHOW`, `DESCRIBE`, `EXPLAIN`, and `USE`. - Enhanced query validation to block sensitive keywords (e.g., `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, `ALTER`). ### Fixed - Improved handling of queries starting with `WITH` (CTE queries). - Fixed case sensitivity issues in query validation. ## [0.1.0] - 2025-01-09 ### Initial Release - Basic functionality for listing tables, describing table schemas, and executing `SELECT` queries. - Query results returned in CSV format. """ import os from typing import List, Dict, Any from pydantic import BaseModel, Field import re from sqlalchemy import create_engine, text from sqlalchemy.engine.base import Engine from sqlalchemy.exc import SQLAlchemyError class Tools: class Valves(BaseModel): db_host: str = Field( default="localhost", description="The host of the database. Replace with your own host.", ) db_user: str = Field( default="admin", description="The username for the database. Replace with your own username.", ) db_password: str = Field( default="admin", description="The password for the database. Replace with your own password.", ) db_name: str = Field( default="db", description="The name of the database. Replace with your own database name.", ) db_port: int = Field( default=3306, # Oracle 默认端口 description="The port of the database. Replace with your own port.", ) db_type: str = Field( default="mysql", description="The type of the database (e.g., mysql, postgresql, sqlite, oracle).", ) def __init__(self): """ Initialize the Tools class with the credentials for the database. """ print("Initializing database tool class") self.citation = True self.valves = Tools.Valves() def _get_engine(self) -> Engine: """ Create and return a database engine using the current configuration. """ if self.valves.db_type == "mysql": db_url = f"mysql+pymysql://{self.valves.db_user}:{self.valves.db_password}@{self.valves.db_host}:{self.valves.db_port}/{self.valves.db_name}" elif self.valves.db_type == "postgresql": db_url = f"postgresql://{self.valves.db_user}:{self.valves.db_password}@{self.valves.db_host}:{self.valves.db_port}/{self.valves.db_name}" elif self.valves.db_type == "sqlite": db_url = f"sqlite:///{self.valves.db_name}" elif self.valves.db_type == "oracle": db_url = f"oracle+cx_oracle://{self.valves.db_user}:{self.valves.db_password}@{self.valves.db_host}:{self.valves.db_port}/?service_name={self.valves.db_name}" else: raise ValueError(f"Unsupported database type: {self.valves.db_type}") return create_engine(db_url) def list_all_tables(self, db_name: str) -> str: """ List all tables in the database. :param db_name: The name of the database. :return: A string containing the names of all tables. """ print("Listing all tables in the database") engine = self._get_engine() # 动态创建引擎 try: with engine.connect() as conn: if self.valves.db_type == "mysql": result = conn.execute(text("SHOW TABLES;")) elif self.valves.db_type == "postgresql": result = conn.execute( text( "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';" ) ) elif self.valves.db_type == "sqlite": result = conn.execute( text("SELECT name FROM sqlite_master WHERE type='table';") ) elif self.valves.db_type == "oracle": result = conn.execute(text("SELECT table_name FROM user_tables;")) else: return "Unsupported database type." tables = [row[0] for row in result.fetchall()] if tables: return ( "Here is a list of all the tables in the database:\n\n" + "\n".join(tables) ) else: return "No tables found." except SQLAlchemyError as e: return f"Error listing tables: {str(e)}" def get_table_indexes(self, db_name: str, table_name: str) -> str: """ Get the indexes of a specific table in the database. :param db_name: The name of the database. :param table_name: The name of the table. :return: A string describing the indexes of the table. """ print(f"Getting indexes for table: {table_name}") engine = self._get_engine() try: key, cloumn = 0, 1 with engine.connect() as conn: if self.valves.db_type == "mysql": query = text(f"SHOW INDEX FROM {table_name}") key, cloumn = 2, 4 elif self.valves.db_type == "postgresql": query = text( """ SELECT indexname, indexdef FROM pg_indexes WHERE tablename = :table_name; """ ) elif self.valves.db_type == "sqlite": query = text( """ PRAGMA index_list(:table_name); """ ) elif self.valves.db_type == "oracle": query = text( """ SELECT index_name, column_name FROM user_ind_columns WHERE table_name = :table_name; """ ) else: return "Unsupported database type." result = conn.execute(query) indexes = result.fetchall() if not indexes: return f"No indexes found for table: {table_name}" description = f"Indexes for table '{table_name}':\n" for index in indexes: description += f"- {index[key]}: {index[cloumn]}\n" return description # result = conn.execute(query) # description = result.fetchall() # if not description: # return f"No indexes found for table: {table_name}" # column_names = result.keys() # description = f"Query executed successfully. Below is the actual result of the query {query} running against the database in CSV format:\n\n" # description += ",".join(column_names) + "\n" # for row in description: # description += ",".join(map(str, row)) + "\n" # return description except SQLAlchemyError as e: return f"Error getting indexes: {str(e)}" def table_data_schema(self, db_name: str, table_name: str) -> str: """ Describe the schema of a specific table in the database, including column comments. :param db_name: The name of the database. :param table_name: The name of the table to describe. :return: A string describing the data schema of the table. """ print(f"Database: {self.valves.db_name}") print(f"Describing table: {table_name}") engine = self._get_engine() # 动态创建引擎 try: with engine.connect() as conn: if self.valves.db_type == "mysql": query = text( " SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_COMMENT " " FROM INFORMATION_SCHEMA.COLUMNS " f" WHERE TABLE_SCHEMA = '{self.valves.db_name}' AND TABLE_NAME = '{table_name}';" ) elif self.valves.db_type == "postgresql": query = text( """ SELECT column_name, data_type, is_nullable, column_default, '' FROM information_schema.columns WHERE table_name = :table_name; """ ) elif self.valves.db_type == "sqlite": query = text("PRAGMA table_info(:table_name);") elif self.valves.db_type == "oracle": query = text( """ SELECT column_name, data_type, nullable, data_default, comments FROM user_tab_columns LEFT JOIN user_col_comments ON user_tab_columns.table_name = user_col_comments.table_name AND user_tab_columns.column_name = user_col_comments.column_name WHERE user_tab_columns.table_name = :table_name; """ ) else: return "Unsupported database type." # result = conn.execute( # query, {"db_name": db_name, "table_name": table_name} # ) result = conn.execute(query) columns = result.fetchall() if not columns: return f"No such table: {table_name}" description = ( f"Table '{table_name}' in the database has the following columns:\n" ) for column in columns: if self.valves.db_type == "sqlite": column_name, data_type, is_nullable, _, _, _ = column column_comment = "" elif self.valves.db_type == "oracle": ( column_name, data_type, is_nullable, data_default, column_comment, ) = column else: ( column_name, data_type, is_nullable, column_key, column_comment, ) = column description += f"- {column_name} ({data_type})" if is_nullable == "YES" or is_nullable == "Y": description += " [Nullable]" if column_key == "PRI": description += " [Primary Key]" if column_comment: description += f" [Comment: {column_comment}]" description += "\n" return description except SQLAlchemyError as e: return f"Error describing table: {str(e)}" def execute_read_query(self, query: str) -> str: """ Execute a read query and return the result in CSV format. :param query: The SQL query to execute. :return: A string containing the result of the query in CSV format. """ print(f"Executing query: {query}") normalized_query = query.strip().lower() if not re.match( r"^\s*(select|with|show|describe|desc|explain|use)\s", normalized_query ): return "Error: Only read-only queries (SELECT, WITH, SHOW, DESCRIBE, EXPLAIN, USE) are allowed. CREATE, DELETE, INSERT, UPDATE, DROP, and ALTER operations are not permitted." sensitive_keywords = [ "insert", "update", "delete", "create", "drop", "alter", "truncate", "grant", "revoke", "replace", ] for keyword in sensitive_keywords: if re.search(rf"\b{keyword}\b", normalized_query): return f"Error: Query contains a sensitive keyword '{keyword}'. Only read operations are allowed." engine = self._get_engine() # 动态创建引擎 try: with engine.connect() as conn: result = conn.execute(text(query)) rows = result.fetchall() if not rows: return "No data returned from query." column_names = result.keys() csv_data = f"Query executed successfully. Below is the actual result of the query {query} running against the database in CSV format:\n\n" csv_data += ",".join(column_names) + "\n" for row in rows: csv_data += ",".join(map(str, row)) + "\n" return csv_data except SQLAlchemyError as e: return f"Error executing query: {str(e)}" 将上面的工具连接到Microsoft sql server
07-23
DECLARE v_max_add_time TIMESTAMP; v_check_time TIMESTAMP := SYSTIMESTAMP; -- 统一检测时间 v_sql VARCHAR2(4000); v_actual_value NVARCHAR2(2000); v_null_token CONSTANT NVARCHAR2(20) := '<<<NULL>>>'; -- 特殊标记用于空值比较 TYPE result_rec IS RECORD ( order_no NVARCHAR2(64), tb_name NVARCHAR2(128), col_name NVARCHAR2(128), cf_value NVARCHAR2(512), add_time TIMESTAMP, scene_code NVARCHAR2(32), scene_name NVARCHAR2(128), strategy_id NUMBER(19), strategy_name NVARCHAR2(128), rule_id NUMBER(19), rule_name NVARCHAR2(128) ); TYPE result_tab IS TABLE OF result_rec; v_results result_tab; -- 检查表名是否存在的函数 FUNCTION table_exists(p_table_name VARCHAR2) RETURN BOOLEAN IS v_count NUMBER; BEGIN SELECT COUNT(*) INTO v_count FROM all_tables WHERE table_name = UPPER(p_table_name) AND owner = 'GHANA_RISK'; -- 替换为实际schema名 RETURN (v_count > 0); EXCEPTION WHEN OTHERS THEN RETURN FALSE; END; -- 检查列名是否存在的函数 FUNCTION column_exists(p_table_name VARCHAR2, p_column_name VARCHAR2) RETURN BOOLEAN IS v_count NUMBER; BEGIN SELECT COUNT(*) INTO v_count FROM all_tab_columns WHERE table_name = UPPER(p_table_name) AND column_name = UPPER(p_column_name) AND owner = 'GHANA_RISK'; -- 替换为实际schema名 RETURN (v_count > 0); EXCEPTION WHEN OTHERS THEN RETURN FALSE; END; -- 获取列数据类型函数 FUNCTION get_column_data_type( p_table_name VARCHAR2, p_column_name VARCHAR2 ) RETURN VARCHAR2 IS v_data_type VARCHAR2(128); BEGIN SELECT data_type INTO v_data_type FROM all_tab_columns WHERE owner = 'GHANA_RISK' AND table_name = UPPER(p_table_name) AND column_name = UPPER(p_column_name); RETURN v_data_type; EXCEPTION WHEN OTHERS THEN RETURN 'UNKNOWN'; END; BEGIN -- 1. 获取最新检测时间范围 SELECT MAX(ADD_TIME) INTO v_max_add_time FROM GHANA_RISK.TQ_RULE_ITEM_RESULT_7; -- 2. 获取半小时内需检测的数据 SELECT TQ_ORDER_NO, TB_NAME, COL_NAME, CF_VALUE, ADD_TIME, SCENE_CODE, SCENE_NAME, STRATEGY_ID, STRATEGY_NAME, RULE_ID, RULE_NAME BULK COLLECT INTO v_results FROM GHANA_RISK.TQ_RULE_ITEM_RESULT_7 WHERE ADD_TIME BETWEEN v_max_add_time - INTERVAL '10' MINUTE AND v_max_add_time and TB_NAME not in('tq_user','tq_user_device_info','tq_user_identity','tq_user_info'); -- 3. 遍历每条记录并动态检测 FOR i IN 1..v_results.COUNT LOOP DECLARE v_data_type VARCHAR2(128); v_actual_raw_value NVARCHAR2(4000); v_cf_converted_value NUMBER; v_cf_converted_bdouble BINARY_DOUBLE; v_comparison_result BOOLEAN := FALSE; BEGIN -- 验证表名和列名是否存在 IF NOT table_exists(v_results(i).tb_name) THEN RAISE_APPLICATION_ERROR(-20001, 'Table not found: ' || v_results(i).tb_name); END IF; IF NOT column_exists(v_results(i).tb_name, v_results(i).col_name) THEN RAISE_APPLICATION_ERROR(-20002, 'Column not found: ' || v_results(i).tb_name || '.' || v_results(i).col_name); END IF; -- 获取列数据类型 v_data_type := get_column_data_type(v_results(i).tb_name, v_results(i).col_name); -- 构建动态SQL获取原始值 v_sql := 'SELECT ' || v_results(i).col_name || ' FROM GHANA_RISK.' || v_results(i).tb_name || ' WHERE TQ_ORDER_NO = :1'; -- 执行动态查询获取原始值 BEGIN EXECUTE IMMEDIATE v_sql INTO v_actual_raw_value USING v_results(i).order_no; EXCEPTION WHEN NO_DATA_FOUND THEN v_actual_raw_value := NULL; -- 明确设置为NULL END; -- 处理NULL值比较逻辑 IF v_actual_raw_value IS NULL AND v_results(i).cf_value IS NULL THEN -- 双方均为NULL,视为相等 v_comparison_result := TRUE; ELSIF v_actual_raw_value IS NULL OR v_results(i).cf_value IS NULL THEN -- 仅一方为NULL,视为不相等 v_comparison_result := FALSE; ELSE -- 双方均非NULL,按数据类型比较 CASE v_data_type WHEN 'NUMBER' THEN -- 对于数字类型,尝试将cf_value转换为数字进行比较 BEGIN v_cf_converted_value := TO_NUMBER(v_results(i).cf_value); v_comparison_result := (v_actual_raw_value = v_cf_converted_value); EXCEPTION WHEN VALUE_ERROR OR INVALID_NUMBER THEN -- 如果转换失败,则使用字符串比较 v_comparison_result := (v_actual_raw_value = v_results(i).cf_value); END; WHEN 'BINARY_DOUBLE' THEN BEGIN v_cf_converted_bdouble := TO_BINARY_DOUBLE(v_results(i).cf_value); v_comparison_result := (v_actual_raw_value = v_cf_converted_bdouble); EXCEPTION WHEN VALUE_ERROR OR INVALID_NUMBER THEN -- 如果转换失败,则使用字符串比较 v_comparison_result := (TO_CHAR(v_actual_raw_value) = v_results(i).cf_value); END; ELSE -- 对于其他类型(字符串等),直接比较 v_comparison_result := (v_actual_raw_value = v_results(i).cf_value); END CASE; END IF; -- 4. 比较值并插入差异(仅当不相等时) IF NOT v_comparison_result THEN -- 将实际值转换为可读字符串 BEGIN v_actual_value := CASE WHEN v_actual_raw_value IS NULL THEN v_null_token -- NULL值使用特殊标记 ELSE TO_CHAR(v_actual_raw_value) END; EXCEPTION WHEN OTHERS THEN v_actual_value := 'CONVERSION_ERROR: ' || SQLERRM; END; -- 插入差异记录 INSERT INTO FK_ADS.ADS_MONITORING_RESULT_DI ( TQ_ORDER_NO, TB_NAME, COL_NAME, CF_VALUE, ACTUAL_VALUE, CHECK_TIME, ADD_TIME, SCENE_CODE, SCENE_NAME, STRATEGY_ID, STRATEGY_NAME, RULE_ID, RULE_NAME ) VALUES ( v_results(i).order_no, v_results(i).tb_name, v_results(i).col_name, v_results(i).cf_value, v_actual_value, v_check_time, v_results(i).add_time, v_results(i).scene_code, v_results(i).scene_name, v_results(i).strategy_id, v_results(i).strategy_name, v_results(i).rule_id, v_results(i).rule_name ); END IF; END; END LOOP; COMMIT; END;如果匹配字段需要用tq_order_no关联loan_borrow_info的RISK_SERIAL_NO,然后用关联到的数据中的mer_user_id去关联tb_name对应的其他表该怎么改?
07-17
# -*- 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
import os import pandas as pd import tkinter as tk from tkinter import ttk, filedialog, scrolledtext, messagebox from tkinter.colorchooser import askcolor from difflib import SequenceMatcher import re import openpyxl import threading import numpy as np from openpyxl.utils import get_column_letter import xlrd import gc import hashlib import json import tempfile from concurrent.futures import ThreadPoolExecutor, as_completed import unicodedata from datetime import datetime class EnhancedSignalComparator: def __init__(self, root): self.root = root self.root.title("增强版信号功能对比工具") self.root.geometry("1200x800") self.root.configure(bg="#f0f0f0") # 初始化变量 self.folder_path = tk.StringVar() self.search_text = tk.StringVar() self.files = [] self.results = {} # 存储信号对比结果 self.highlight_color = "#FFD700" # 默认高亮色 self.search_running = False self.stop_requested = False self.cache_dir = os.path.join(tempfile.gettempdir(), "excel_cache") self.file_cache = {} # 文件缓存 self.column_cache = {} # 列名缓存 self.max_workers = 4 # 最大并发线程数 # 创建缓存目录 os.makedirs(self.cache_dir, exist_ok=True) # 创建界面 self.create_widgets() self.log_file = "comparator.log" self.setup_logging() def setup_logging(self): """初始化日志系统""" with open(self.log_file, "w", encoding="utf-8") as log_file: log_file.write(f"{datetime.now().isoformat()} - 日志初始化\n") def log(self, message): """记录日志""" timestamp = datetime.now().isoformat() log_entry = f"{timestamp} - {message}\n" # 文件记录 with open(self.log_file, "a", encoding="utf-8") as log_file: log_file.write(log_entry) # 状态栏显示(缩短版本) if len(message) > 60: self.status_var.set(message[:57] + "...") else: self.status_var.set(message) def create_widgets(self): # 顶部控制面板 control_frame = ttk.Frame(self.root, padding=10) control_frame.pack(fill=tk.X) # 文件夹选择 ttk.Label(control_frame, text="选择文件夹:").grid(row=0, column=0, sticky=tk.W) folder_entry = ttk.Entry(control_frame, textvariable=self.folder_path, width=50) folder_entry.grid(row=0, column=1, padx=5, sticky=tk.EW) ttk.Button(control_frame, text="浏览...", command=self.browse_folder).grid(row=0, column=2) # 搜索输入 ttk.Label(control_frame, text="搜索信号:").grid(row=1, column=0, sticky=tk.W, pady=(10,0)) search_entry = ttk.Entry(control_frame, textvariable=self.search_text, width=50) search_entry.grid(row=1, column=1, padx=5, pady=(10,0), sticky=tk.EW) search_entry.bind("<Return>", lambda event: self.start_search_thread()) ttk.Button(control_frame, text="搜索", command=self.start_search_thread).grid(row=1, column=2, pady=(10,0)) ttk.Button(control_frame, text="停止", command=self.stop_search).grid(row=1, column=3, pady=(10,0), padx=5) # 高级选项 ttk.Label(control_frame, text="并发线程:").grid(row=2, column=0, sticky=tk.W, pady=(10,0)) self.thread_var = tk.StringVar(value="4") ttk.Combobox(control_frame, textvariable=self.thread_var, values=["1", "2", "4", "8"], width=5).grid(row=2, column=1, sticky=tk.W, padx=5, pady=(10,0)) # 文件过滤 ttk.Label(control_frame, text="文件过滤:").grid(row=2, column=2, sticky=tk.W, pady=(10,0)) self.filter_var = tk.StringVar(value="*.xlsx;*.xlsm;*.xls") ttk.Entry(control_frame, textvariable=self.filter_var, width=20).grid(row=2, column=3, sticky=tk.W, padx=5, pady=(10,0)) # 精确匹配选项 self.exact_match_var = tk.BooleanVar(value=False) exact_match_check = ttk.Checkbutton( control_frame, text="精确匹配", variable=self.exact_match_var ) exact_match_check.grid(row=2, column=4, sticky=tk.W, padx=5, pady=(10,0)) # 高亮颜色选择 ttk.Label(control_frame, text="高亮颜色:").grid(row=3, column=0, sticky=tk.W, pady=(10,0)) self.color_btn = tk.Button(control_frame, bg=self.highlight_color, width=3, command=self.choose_color) self.color_btn.grid(row=3, column=1, sticky=tk.W, padx=5, pady=(10,0)) # 进度条 self.progress = ttk.Progressbar(control_frame, orient="horizontal", length=200, mode="determinate") self.progress.grid(row=3, column=2, columnspan=2, sticky=tk.EW, padx=5, pady=(10,0)) # 结果标签 self.result_label = ttk.Label(control_frame, text="") self.result_label.grid(row=3, column=4, sticky=tk.W, padx=5, pady=(10,0)) # 对比面板 notebook = ttk.Notebook(self.root) notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 表格视图 self.table_frame = ttk.Frame(notebook) notebook.add(self.table_frame, text="表格视图") # 文本对比视图 self.text_frame = ttk.Frame(notebook) notebook.add(self.text_frame, text="行内容对比") # 状态栏 self.status_var = tk.StringVar() status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 初始化表格和文本区域 self.init_table_view() self.init_text_view() def init_table_view(self): """初始化表格视图""" # 创建树状表格 columns = ("信号", "文件", "行内容摘要") self.tree = ttk.Treeview(self.table_frame, columns=columns, show="headings") # 设置列标题 for col in columns: self.tree.heading(col, text=col) self.tree.column(col, width=200, anchor=tk.W) # 添加滚动条 scrollbar = ttk.Scrollbar(self.table_frame, orient=tk.VERTICAL, command=self.tree.yview) self.tree.configure(yscrollcommand=scrollbar.set) self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 绑定选择事件 self.tree.bind("<<TreeviewSelect>>", self.on_table_select) def init_text_view(self): """初始化文本对比视图""" self.text_panes = {} self.text_frame.columnconfigure(0, weight=1) self.text_frame.rowconfigure(0, weight=1) # 创建对比容器 self.compare_container = ttk.Frame(self.text_frame) self.compare_container.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) # 添加差异高亮按钮 btn_frame = ttk.Frame(self.text_frame) btn_frame.grid(row=1, column=0, sticky="ew", padx=5, pady=5) ttk.Button(btn_frame, text="高亮显示差异", command=self.highlight_differences).pack(side=tk.LEFT) ttk.Button(btn_frame, text="导出差异报告", command=self.export_report).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="清除缓存", command=self.clear_cache).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="手动指定列名", command=self.manual_column_select).pack(side=tk.LEFT, padx=5) def browse_folder(self): """选择文件夹""" folder = filedialog.askdirectory(title="选择包含Excel文件的文件夹") if folder: self.folder_path.set(folder) self.load_files() def load_files(self): """加载文件夹中的Excel文件(优化特殊字符处理)""" folder = self.folder_path.get() if not folder or not os.path.isdir(folder): return # 获取文件过滤模式 filter_patterns = self.filter_var.get().split(';') self.files = [] for file in os.listdir(folder): file_path = os.path.join(folder, file) # 跳过临时文件 if file.startswith('~$'): continue # 检查文件扩展名 file_lower = file.lower() matched = False for pattern in filter_patterns: # 移除通配符并转换为小写 ext = pattern.replace('*', '').lower() if file_lower.endswith(ext): matched = True break if matched: # 规范化文件名处理特殊字符 normalized_path = self.normalize_file_path(file_path) if normalized_path and os.path.isfile(normalized_path): self.files.append(normalized_path) self.status_var.set(f"找到 {len(self.files)} 个Excel文件") def normalize_file_path(self, path): """规范化文件路径,处理特殊字符""" try: # 尝试直接访问文件 if os.path.exists(path): return path # 尝试Unicode规范化 normalized = unicodedata.normalize('NFC', path) if os.path.exists(normalized): return normalized # 尝试不同编码方案 encodings = ['utf-8', 'shift_jis', 'euc-jp', 'cp932'] for encoding in encodings: try: decoded = path.encode('latin1').decode(encoding) if os.path.exists(decoded): return decoded except: continue # 最终尝试原始路径 return path except Exception as e: self.status_var.set(f"文件路径处理错误: {str(e)}") return path def get_file_hash(self, file_path): """计算文件哈希值用于缓存""" try: hash_md5 = hashlib.md5() with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() except Exception as e: self.status_var.set(f"计算文件哈希失败: {str(e)}") return str(os.path.getmtime(file_path)) def get_cache_filename(self, file_path): """获取缓存文件名""" file_hash = self.get_file_hash(file_path) return os.path.join(self.cache_dir, f"{os.path.basename(file_path)}_{file_hash}.cache") def load_header_cache(self, file_path): """加载列名缓存""" cache_file = self.get_cache_filename(file_path) if os.path.exists(cache_file): try: with open(cache_file, "r", encoding='utf-8') as f: return json.load(f) except: return None return None def save_header_cache(self, file_path, header_info): """保存列名缓存""" cache_file = self.get_cache_filename(file_path) try: with open(cache_file, "w", encoding='utf-8') as f: json.dump(header_info, f) return True except: return False def find_header_row(self, file_path): """查找列名行(增强版)""" # 禁用缓存进行测试 # return None, None # 检查缓存 cache = self.load_header_cache(file_path) if cache: return cache.get("header_row"), cache.get("signal_col") # 没有缓存则重新查找 if file_path.lower().endswith((".xlsx", ".xlsm")): return self.find_header_row_openpyxl(file_path) elif file_path.lower().endswith(".xls"): return self.find_header_row_xlrd(file_path) return None, None def find_header_row_openpyxl(self, file_path): """使用openpyxl查找列名行(增强版)""" try: self.log(f"开始处理文件: {os.path.basename(file_path)}") wb = openpyxl.load_workbook(file_path, read_only=True, data_only=True) ws = wb.active # 尝试多种列名匹配模式 patterns = [ r'データ名', r'データ名', r'信号名', r'Signal Name', r'Data Name', r'信号名称', r'データ名称', r'信号', r'データー名', r'DataItem', r'Signal' # 新增模式 ] # 扩大搜索范围:前100行和前100列 for row_idx in range(1, 101): for col_idx in range(1, 101): try: cell = ws.cell(row=row_idx, column=col_idx) cell_value = cell.value if not cell_value: continue cell_str = str(cell_value) for pattern in patterns: if re.search(pattern, cell_str, re.IGNORECASE): self.log(f"找到匹配模式 '{pattern}' 在行{row_idx}列{col_idx}") # 找到列名行后,尝试确定信号列 signal_col = None # 在同行中查找信号列 for col_idx2 in range(1, 101): # 1-100列 try: cell2 = ws.cell(row=row_idx, column=col_idx2) cell2_value = cell2.value if not cell2_value: continue cell2_str = str(cell2_value) if re.search(pattern, cell2_str, re.IGNORECASE): signal_col = col_idx2 break except: continue # 保存缓存 if signal_col is not None: header_info = {"header_row": row_idx, "signal_col": signal_col} self.save_header_cache(file_path, header_info) wb.close() return row_idx, signal_col except: continue wb.close() except Exception as e: self.log(f"查找列名行出错: {str(e)}") return None, None def add_result_to_table(self, signal_value, file_name, summary): """在主线程中添加结果到表格""" if not self.root: return # 检查是否在主线程 if threading.current_thread() is not threading.main_thread(): self.root.after(0, self.add_result_to_table, signal_value, file_name, summary) return # 在主线程中添加结果 self.tree.insert("", tk.END, values=(signal_value, file_name, summary)) def find_header_row_xlrd(self, file_path): """使用xlrd查找列名行(增强版)""" try: wb = xlrd.open_workbook(file_path) ws = wb.sheet_by_index(0) # 尝试多种列名匹配模式 patterns = [ r'データ名', # 半角片假名 r'データ名', # 全角片假名 r'信号名', # 中文 r'Signal Name', # 英文 r'Data Name', r'信号名称', r'データ名称' ] # 扩大搜索范围:前100行和前100列 for row_idx in range(0, 100): # 0-99行 # 扩大列搜索范围到100列 for col_idx in range(0, 100): # 0-99列 try: cell_value = ws.cell_value(row_idx, col_idx) if not cell_value: continue # 尝试所有匹配模式 cell_str = str(cell_value) for pattern in patterns: if re.search(pattern, cell_str, re.IGNORECASE): # 找到列名行后,尝试确定信号列 signal_col = None # 在同行中查找信号列 for col_idx2 in range(0, 100): # 0-99列 try: cell2_value = ws.cell_value(row_idx, col_idx2) if not cell2_value: continue cell2_str = str(cell2_value) if re.search(pattern, cell2_str, re.IGNORECASE): signal_col = col_idx2 break except: continue # 保存缓存 if signal_col is not None: header_info = {"header_row": row_idx, "signal_col": signal_col} self.save_header_cache(file_path, header_info) return row_idx, signal_col except: continue except Exception as e: self.status_var.set(f"查找列名行出错: {str(e)}") return None, None def start_search_thread(self): """启动搜索线程""" if self.search_running: return self.search_running = True self.stop_requested = False self.max_workers = int(self.thread_var.get()) threading.Thread(target=self.search_files, daemon=True).start() def stop_search(self): """停止搜索""" self.stop_requested = True self.status_var.set("正在停止搜索...") def search_files(self): """在文件中搜索内容(优化特殊文件处理)""" search_term = self.search_text.get().strip() if not search_term: self.status_var.set("请输入搜索内容") self.search_running = False return if not self.files: self.status_var.set("请先选择文件夹") self.search_running = False return # 重置结果和UI self.results = {} for item in self.tree.get_children(): self.tree.delete(item) total_files = len(self.files) processed_files = 0 found_signals = 0 # 使用线程池处理文件 # 在search_files方法中添加详细进度 with ThreadPoolExecutor(max_workers=self.max_workers) as executor: futures = {} for i, file_path in enumerate(self.files): if self.stop_requested: break future = executor.submit(self.process_file, file_path, search_term) futures[future] = (file_path, i) # 保存文件索引 for future in as_completed(futures): if self.stop_requested: break file_path, idx = futures[future] try: found = future.result() found_signals += found processed_files += 1 # 更详细的进度反馈 progress = int(processed_files / total_files * 100) self.progress["value"] = progress self.status_var.set( f"已处理 {processed_files}/{total_files} 个文件 | " f"当前: {os.path.basename(file_path)} | " f"找到: {found_signals} 个匹配" ) self.root.update_idletasks() except Exception as e: self.status_var.set(f"处理文件 {os.path.basename(file_path)} 出错: {str(e)}") # 更新结果 if self.stop_requested: self.status_var.set(f"搜索已停止,已处理 {processed_files}/{total_files} 个文件") elif found_signals == 0: self.status_var.set(f"未找到包含 '{search_term}' 的信号") else: self.status_var.set(f"找到 {len(self.results)} 个匹配信号,共 {found_signals} 处匹配") self.update_text_view() self.progress["value"] = 0 self.search_running = False gc.collect() # 强制垃圾回收释放内存 def manual_find_header_row(self, file_path): """手动查找列名行(需要实现)""" # 这里应该实现手动查找逻辑 # 为简化问题,暂时返回默认值 return 1, 1 # 默认行1列1 def process_file(self, file_path, search_term): """处理单个文件(增强异常处理和调试)""" found = 0 short_name = os.path.basename(file_path) try: # 获取列名行和信号列 header_row, signal_col = self.find_header_row(file_path) self.log(f"文件 {short_name}: 自动查找结果 - 列名行: {header_row}, 信号列: {signal_col}") # 如果自动查找失败,尝试手动模式 if header_row is None or signal_col is None: self.log(f"文件 {short_name} 未找到列名行,尝试手动查找...") header_row, signal_col = self.manual_find_header_row(file_path) self.log(f"文件 {short_name}: 手动查找结果 - 列名行: {header_row}, 信号列: {signal_col}") if header_row is None or signal_col is None: self.log(f"文件 {short_name} 无法确定列名行,已跳过") return found # 使用pandas处理 found = self.process_file_with_pandas(file_path, search_term, header_row, signal_col) self.log(f"文件 {short_name} 处理完成,找到 {found} 个匹配") except Exception as e: error_msg = f"处理文件 {short_name} 出错: {str(e)}" self.log(error_msg) import traceback traceback.print_exc() return found def manual_column_select(self): """手动指定列名位置(增强版)""" if not self.files: messagebox.showinfo("提示", "请先选择文件夹") return # 创建手动选择窗口 manual_window = tk.Toplevel(self.root) manual_window.title("手动指定列名位置") manual_window.geometry("500x400") # 文件选择 ttk.Label(manual_window, text="选择文件:").pack(pady=(10, 5)) file_var = tk.StringVar() file_combo = ttk.Combobox(manual_window, textvariable=file_var, values=[os.path.basename(f) for f in self.files], width=40) file_combo.pack(fill=tk.X, padx=20, pady=5) file_combo.current(0) # 预览框架 preview_frame = ttk.Frame(manual_window) preview_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 表格预览 columns = ("列", "值") self.preview_tree = ttk.Treeview(preview_frame, columns=columns, show="headings", height=10) # 设置列标题 for col in columns: self.preview_tree.heading(col, text=col) self.preview_tree.column(col, width=100, anchor=tk.W) # 添加滚动条 scrollbar = ttk.Scrollbar(preview_frame, orient=tk.VERTICAL, command=self.preview_tree.yview) self.preview_tree.configure(yscrollcommand=scrollbar.set) self.preview_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 加载预览数据 def load_preview(event=None): file_idx = file_combo.current() file_path = self.files[file_idx] # 清空现有预览 for item in self.preview_tree.get_children(): self.preview_tree.delete(item) # 加载前10行数据 try: if file_path.lower().endswith((".xlsx", ".xlsm")): wb = openpyxl.load_workbook(file_path, read_only=True, data_only=True) ws = wb.active # 读取前10行 for row_idx in range(1, 11): for col_idx in range(1, 51): # 前50列 try: cell = ws.cell(row=row_idx, column=col_idx) if cell.value is not None: self.preview_tree.insert("", tk.END, values=( f"行{row_idx}列{col_idx}", str(cell.value)[:50] # 限制显示长度 )) except: continue wb.close() elif file_path.lower().endswith(".xls"): wb = xlrd.open_workbook(file_path) ws = wb.sheet_by_index(0) # 读取前10行 for row_idx in range(0, 10): for col_idx in range(0, 50): # 前50列 try: cell_value = ws.cell_value(row_idx, col_idx) if cell_value: self.preview_tree.insert("", tk.END, values=( f"行{row_idx+1}列{col_idx+1}", str(cell_value)[:50] # 限制显示长度 )) except: continue except Exception as e: messagebox.showerror("错误", f"加载预览失败: {str(e)}") file_combo.bind("<<ComboboxSelected>>", load_preview) load_preview() # 初始加载 # 输入框架 input_frame = ttk.Frame(manual_window) input_frame.pack(fill=tk.X, padx=20, pady=10) # 行号输入 ttk.Label(input_frame, text="列名行号:").grid(row=0, column=0, sticky=tk.W) row_var = tk.StringVar(value="1") row_entry = ttk.Entry(input_frame, textvariable=row_var, width=10) row_entry.grid(row=0, column=1, padx=5) # 列号输入 ttk.Label(input_frame, text="信号列号:").grid(row=0, column=2, sticky=tk.W, padx=(10,0)) col_var = tk.StringVar(value="1") col_entry = ttk.Entry(input_frame, textvariable=col_var, width=10) col_entry.grid(row=0, column=3, padx=5) # 确认按钮 def confirm_selection(): try: file_idx = file_combo.current() file_path = self.files[file_idx] header_row = int(row_var.get()) signal_col = int(col_var.get()) # 保存到缓存 header_info = {"header_row": header_row, "signal_col": signal_col} self.save_header_cache(file_path, header_info) messagebox.showinfo("成功", f"已为 {os.path.basename(file_path)} 设置列名位置:行{header_row} 列{signal_col}") manual_window.destroy() except Exception as e: messagebox.showerror("错误", f"无效输入: {str(e)}") ttk.Button(manual_window, text="确认", command=confirm_selection).pack(pady=10) def process_file_with_pandas(self, file_path, search_term, header_row, signal_col): """使用pandas高效处理Excel文件(修复版)""" found = 0 short_name = os.path.basename(file_path) try: # 添加文件信息日志 file_size = os.path.getsize(file_path) self.log(f"处理文件: {short_name} ({file_size}字节)") # 使用pandas读取Excel文件 file_ext = os.path.splitext(file_path)[1].lower() engine = 'openpyxl' if file_ext in ['.xlsx', '.xlsm'] else 'xlrd' # 修复1: 正确计算列范围 # 计算最大可用列数 max_columns = self.get_max_columns(file_path) # 确保信号列在合理范围内 if signal_col < 1 or signal_col > max_columns: self.log(f"文件 {short_name}: 信号列{signal_col}超出范围(1-{max_columns})") return 0 # 修复2: 简化列范围计算 # 直接读取所有列,避免复杂的范围计算 df = pd.read_excel( file_path, engine=engine, header=header_row-1, # pandas的header是从0开始计数的 dtype=str ) # 修复3: 检查读取结果 if df.empty: self.log(f"文件 {short_name}: 读取到空DataFrame") return 0 # 获取列名 column_names = df.columns.tolist() # 修复4: 检查信号列索引是否有效 if signal_col-1 >= len(column_names): self.log(f"文件 {short_name}: 信号列索引{signal_col-1}超出列数{len(column_names)}") return 0 # 获取信号列名称 signal_col_name = column_names[signal_col-1] self.log(f"文件 {short_name}: 使用信号列 '{signal_col_name}' (位置 {signal_col})") # 获取信号列数据 signal_series = df.iloc[:, signal_col-1].fillna('') # 搜索匹配的信号 # 修复5: 添加精确匹配选项支持 if self.exact_match_var.get(): # 精确匹配 matches = df[signal_series.str.strip().str.lower() == search_term.lower().strip()] else: # 模糊匹配 matches = df[signal_series.str.contains( re.escape(search_term), case=False, na=False, regex=True )] # 处理匹配行 for idx, row in matches.iterrows(): # 只显示有值的列 row_content = [] for col_idx, value in enumerate(row): # 跳过空值 if pd.notna(value) and str(value).strip() != '': # 使用实际列名 if col_idx < len(column_names): col_name = column_names[col_idx] else: col_name = f"列{col_idx+1}" row_content.append(f"{col_name}: {str(value).strip()}") row_content = "\n".join(row_content) signal_value = row.iloc[signal_col-1] # 使用位置索引获取信号值 # 使用更唯一的复合键(包含行索引) signal_key = f"{signal_value}||{short_name}||{idx}" # 生成摘要 summary = row_content[:50] + "..." if len(row_content) > 50 else row_content # 添加到结果集 self.results[signal_key] = { "signal": signal_value, "file": short_name, "content": row_content, "file_path": file_path, # 添加完整路径 "row_idx": idx # 添加行索引 } # 在主线程中添加结果到表格 self.add_result_to_table(signal_value, short_name, summary) found += 1 # 每处理10行更新一次UI if found % 10 == 0: self.status_var.set(f"处理 {short_name}: 找到 {found} 个匹配") self.root.update_idletasks() # 添加完成日志 self.log(f"文件 {short_name} 处理完成: 找到 {found} 个匹配") except Exception as e: import traceback traceback.print_exc() self.log(f"处理文件 {short_name} 出错: {str(e)}") self.status_var.set(f"处理文件 {short_name} 出错: {str(e)}") finally: # 显式释放内存 if 'df' in locals(): del df if 'matches' in locals(): del matches gc.collect() return found def get_max_columns(self, file_path): """获取Excel文件的最大列数""" try: if file_path.lower().endswith((".xlsx", ".xlsm")): wb = openpyxl.load_workbook(file_path, read_only=True) ws = wb.active max_col = ws.max_column wb.close() return max_col elif file_path.lower().endswith(".xls"): wb = xlrd.open_workbook(file_path) ws = wb.sheet_by_index(0) return ws.ncols except: return 100 # 默认值 return 100 # 默认值 def update_text_view(self): """更新文本对比视图(添加空结果检查)""" # 清除现有文本区域 for widget in self.compare_container.winfo_children(): widget.destroy() if not self.results: # 添加空状态提示 empty_label = ttk.Label(self.text_frame, text="未找到匹配结果", font=("Arial", 12)) empty_label.pack(pady=20) return # 获取第一个信号作为默认显示 first_signal_key = next(iter(self.results.keys())) self.display_signal_comparison(first_signal_key) def on_table_select(self, event): """表格选择事件处理""" selected = self.tree.selection() if not selected: return item = self.tree.item(selected[0]) signal_value = item["values"][0] # 获取信号值 # 直接传递信号值给显示方法 self.display_signal_comparison(signal_value) def display_signal_comparison(self, signal_value): """显示指定信号在不同文件中的对比""" # 清除现有文本区域 for widget in self.compare_container.winfo_children(): widget.destroy() # 获取包含该信号的所有结果项 signal_items = [ (key, data) for key, data in self.results.items() if data["signal"] == signal_value ] if not signal_items: return # 按文件名排序 signal_items.sort(key=lambda x: x[1]["file"]) # 创建列框架 for i, (signal_key, signal_data) in enumerate(signal_items): col_frame = ttk.Frame(self.compare_container) col_frame.grid(row=0, column=i, sticky="nsew", padx=5, pady=5) self.compare_container.columnconfigure(i, weight=1) # 文件名标签 file_label = ttk.Label(col_frame, text=signal_data["file"], font=("Arial", 10, "bold")) file_label.pack(fill=tk.X, pady=(0, 5)) # 信号名标签 signal_label = ttk.Label(col_frame, text=signal_data["signal"], font=("Arial", 9, "italic")) signal_label.pack(fill=tk.X, pady=(0, 5)) # 文本区域 text_area = scrolledtext.ScrolledText(col_frame, wrap=tk.WORD, width=30, height=15) text_area.insert(tk.INSERT, signal_data["content"]) text_area.configure(state="disabled") text_area.pack(fill=tk.BOTH, expand=True) # 保存引用 self.text_panes[signal_key] = text_area # 添加"查看完整内容"按钮 btn_frame = ttk.Frame(col_frame) btn_frame.pack(fill=tk.X, pady=(5, 0)) ttk.Button( btn_frame, text="查看完整内容", command=lambda f=signal_data["file_path"], r=signal_data["row_idx"]: self.show_full_content(f, r) ).pack(side=tk.LEFT) def show_full_content(self, file_path, row_idx): """显示行的完整内容""" # 实现完整内容显示逻辑 self.log(f"显示完整内容: 文件={os.path.basename(file_path)}, 行={row_idx}") # [具体实现代码] def highlight_differences(self): """高亮显示文本差异""" if not self.text_panes: return # 获取所有行内容 all_contents = [] for text_area in self.text_panes.values(): text_area.configure(state="normal") text = text_area.get("1.0", tk.END).strip() text_area.configure(state="disabled") all_contents.append(text) # 如果所有内容相同,则不需要高亮 if len(set(all_contents)) == 1: self.status_var.set("所有文件行内容完全一致") return # 使用第一个文件作为基准 base_text = all_contents[0] # 对比并高亮差异 for i, (file, text_area) in enumerate(self.text_panes.items()): if i == 0: # 基准文件不需要处理 continue text_area.configure(state="normal") text_area.tag_configure("diff", background=self.highlight_color) # 清除之前的高亮 text_area.tag_remove("diff", "1.0", tk.END) # 获取当前文本 compare_text = text_area.get("1.0", tk.END).strip() # 使用序列匹配器查找差异 s = SequenceMatcher(None, base_text, compare_text) # 高亮差异部分 for tag in s.get_opcodes(): opcode = tag[0] start = tag[3] end = tag[4] if opcode != "equal": # 添加高亮标签 text_area.tag_add("diff", f"1.0+{start}c", f"1.0+{end}c") text_area.configure(state="disabled") self.status_var.set("差异已高亮显示") def choose_color(self): """选择高亮颜色""" color = askcolor(title="选择高亮颜色", initialcolor=self.highlight_color) if color[1]: self.highlight_color = color[1] self.color_btn.configure(bg=self.highlight_color) def export_report(self): """导出差异报告(修复结果集访问)""" if not self.results: messagebox.showwarning("警告", "没有可导出的结果") return try: # 创建报告数据结构(修复结果集访问方式) report_data = [] for signal_key, data in self.results.items(): report_data.append({ "信号": data["signal"], "文件": data["file"], "行内容": data["content"] }) # 转换为DataFrame df = pd.DataFrame(report_data) # 保存到Excel save_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel文件", "*.xlsx")], title="保存差异报告" ) if save_path: df.to_excel(save_path, index=False) self.status_var.set(f"报告已保存到: {save_path}") except Exception as e: messagebox.showerror("错误", f"导出报告失败: {str(e)}") def clear_cache(self): """清除缓存""" try: for file in os.listdir(self.cache_dir): if file.endswith(".cache"): os.remove(os.path.join(self.cache_dir, file)) self.file_cache = {} self.column_cache = {} self.status_var.set("缓存已清除") except Exception as e: self.status_var.set(f"清除缓存失败: {str(e)}") def manual_column_select(self): """手动指定列名位置""" if not self.files: messagebox.showinfo("提示", "请先选择文件夹") return # 创建手动选择窗口 manual_window = tk.Toplevel(self.root) manual_window.title("手动指定列名位置") manual_window.geometry("400x300") # 文件选择 ttk.Label(manual_window, text="选择文件:").pack(pady=(10, 5)) file_var = tk.StringVar() file_combo = ttk.Combobox(manual_window, textvariable=file_var, values=[os.path.basename(f) for f in self.files]) file_combo.pack(fill=tk.X, padx=20, pady=5) file_combo.current(0) # 行号输入 ttk.Label(manual_window, text="列名行号:").pack(pady=(10, 5)) row_var = tk.StringVar(value="1") row_entry = ttk.Entry(manual_window, textvariable=row_var) row_entry.pack(fill=tk.X, padx=20, pady=5) # 列号输入 ttk.Label(manual_window, text="信号列号:").pack(pady=(10, 5)) col_var = tk.StringVar(value="1") col_entry = ttk.Entry(manual_window, textvariable=col_var) col_entry.pack(fill=tk.X, padx=20, pady=5) # 确认按钮 def confirm_selection(): try: file_idx = file_combo.current() file_path = self.files[file_idx] header_row = int(row_var.get()) signal_col = int(col_var.get()) # 保存到缓存 header_info = {"header_row": header_row, "signal_col": signal_col} self.save_header_cache(file_path, header_info) messagebox.showinfo("成功", f"已为 {os.path.basename(file_path)} 设置列名位置:行{header_row} 列{signal_col}") manual_window.destroy() except Exception as e: messagebox.showerror("错误", f"无效输入: {str(e)}") ttk.Button(manual_window, text="确认", command=confirm_selection).pack(pady=20) if __name__ == "__main__": root = tk.Tk() app = EnhancedSignalComparator(root) root.mainloop() 1、将单信号搜索改为多信号搜索 2、将页面布局进行修改,以更合理的方式进行布局
07-25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值