How to enumerate the system fonts in the combobox control using VC++ - 用VC++如何在ComboBox控件中枚举系统字体

本文介绍了一个使用C++实现的功能,该功能可以枚举所有可用的字体,并将这些字体名称添加到Windows应用程序的组合框中。通过使用Windows API函数`EnumFontFamilies`和发送`CB_ADDSTRING`消息,实现了字体的枚举与添加。

import tkinter as tk from tkinter import messagebox, ttk, filedialog import pyodbc import datetime import re import pandas as pd import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.dates import date2num, DateFormatter import matplotlib.font_manager as fm from matplotlib.patches import Patch # 自动检测系统中可用的中文字体 def detect_chinese_fonts(): """检测系统中可用的中文字体""" chinese_fonts = [] test_chars = ["文", "字", "测试"] # 用于测试的中文字符 # 常见中文字体列表(按优先级排列) font_candidates = [ "SimHei", # Windows/Linux "WenQuanYi Micro Hei", # Linux "Heiti TC", # macOS "Microsoft YaHei", # Windows "SimSun", # Windows "KaiTi", # Windows "Arial Unicode MS" # macOS ] # 检查候选字体是否可用 for font in font_candidates: try: fp = fm.FontProperties(family=font) if all(fp.get_name() for char in test_chars): chinese_fonts.append(font) except: continue # 如果没有找到候选字体,尝试遍历所有系统字体 if not chinese_fonts: for font_path in fm.findSystemFonts(): try: font = fm.FontProperties(fname=font_path) font_name = font.get_name() if any(keyword in font_name.lower() for keyword in ["hei", "song", "kai", "micro", "yahei"]): chinese_fonts.append(font_name) except: continue return chinese_fonts # 配置中文字体 available_fonts = detect_chinese_fonts() if available_fonts: plt.rcParams["font.family"] = available_fonts[0] # 选择第一个可用字体 print(f"找到可用中文字体: {', '.join(available_fonts)}") else: plt.rcParams["font.family"] = ["sans-serif"] print("警告: 未找到可用的中文字体,中文可能无法正常显示") plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 plt.rcParams["font.size"] = 10 # 全局字体大小 # 数据库连接信息 db_connection_string = ( 'DRIVER={SQL Server};' 'SERVER=192.168.1.104,1433;' 'DATABASE=归档数据库;' 'UID=sa;' 'PWD=fandaoshi_atdc;' 'Connect Timeout=10' # 延长超时时间,避免网络波动导致连接失败 ) # 正则表达式:匹配可能的日期前缀(如250519格式) DATE_PREFIX_PATTERN = re.compile(r'^\d{6}') def clean_name(name): """清理姓名,去除可能的日期前缀""" if not isinstance(name, str): return name match = DATE_PREFIX_PATTERN.match(name) if match: return re.sub(r'^\d{6}[\s\-_]*', '', name).strip() return name def connect_to_database(): """连接数据库""" try: connection = pyodbc.connect(db_connection_string) return connection except pyodbc.Error as e: messagebox.showerror("数据库连接错误", f"连接数据库时出错: {str(e)}") return None def load_combobox_data(): """加载下拉列表数据并设置优先级排序""" connection = connect_to_database() if connection: try: cursor = connection.cursor() # 需要优先显示的名字(按希望的顺序排列) priority_names = ["胡先锋", "张单", "张家顶", "吴征锋", "储小旺", "周其运"] # 加载制定人 cursor.execute(""" SELECT DISTINCT 制定人 FROM 计划 WHERE 制定日期 >= DATEADD(DAY, -100, GETDATE()) ORDER BY 制定人 """) db_makers = [clean_name(row[0]) for row in cursor.fetchall()] # 1. 提取并保留优先名字(确保顺序与priority_names一致) sorted_priority_makers = [name for name in priority_names if name in db_makers] # 2. 处理剩余名字(排除优先名字,并按字母排序) remaining_makers = [name for name in db_makers if name not in priority_names] sorted_remaining_makers = sorted(remaining_makers) # 按拼音排序 # 3. 组合结果(空选项 + 优先名字 + 其他名字) final_makers = [""] + sorted_priority_makers + sorted_remaining_makers # 加载担当人 cursor.execute(""" SELECT DISTINCT 担当人 FROM 计划 WHERE 制定日期 >= DATEADD(DAY, -100, GETDATE()) ORDER BY 担当人 """) db_assignees = [clean_name(row[0]) for row in cursor.fetchall()] # 1. 提取并保留优先名字(确保顺序与priority_names一致) sorted_priority_assignees = [name for name in priority_names if name in db_assignees] # 2. 处理剩余名字(排除优先名字,并按字母排序) remaining_assignees = [name for name in db_assignees if name not in priority_names] sorted_remaining_assignees = sorted(remaining_assignees) # 按拼音排序 # 3. 组合结果(空选项 + 优先名字 + 其他名字) final_assignees = [""] + sorted_priority_assignees + sorted_remaining_assignees # 设置下拉框值(初始值为空) maker_combobox['values'] = final_makers assignee_combobox['values'] = final_assignees # 设置计划状态选项 status_combobox['values'] = ["", "未完成", "已完成", "已关闭"] status_combobox.current(0) except pyodbc.Error as e: messagebox.showerror("数据加载错误", f"加载下拉列表数据时出错: {str(e)}") maker_combobox['values'] = ["加载失败"] assignee_combobox['values'] = ["加载失败"] status_combobox['values'] = ["", "未完成", "已完成", "已关闭"] finally: connection.close() # -------------------------- 排序核心功能 -------------------------- def sort_treeview(col, reverse): """ 表格排序函数 :param col: 排序字段(表格列名) :param reverse: 是否反向排序(True=降序,False=升序) """ global current_results # 关联查询结果原始数据 if not current_results: messagebox.showwarning("警告", "无数据可排序,请先执行查询") return # 定义各字段的排序规则(处理日期格式转换) def get_sort_key(item): value = item[col] # 日期字段转换为datetime便于排序 if col in ["制定日期", "起始日期", "预定交付期", "交付期回答"]: if isinstance(value, (datetime.datetime, datetime.date)): return value try: # 处理字符串格式日期 return datetime.datetime.strptime(str(value).split()[0], '%Y-%m-%d') except: return datetime.datetime.min # 无效日期放最前 # 文本字段直接按字符串排序 elif col in ["承担部门", "承担任务", "担当人", "制定人", "品名", "其它备注", "便签", "计划状态"]: return str(value).lower() # 不区分大小写排序 # 数字字段按数值排序 elif col in ["数量"]: try: return float(value) except: return float('-inf') # 非数字放最前 # 其他字段按原始类型排序 else: return value # 对原始数据排序 sorted_results = sorted(current_results, key=get_sort_key, reverse=reverse) # 清空表格并重新插入排序后的数据 tree.delete(*tree.get_children()) for row_dict in sorted_results: formatted_row = [] for col_name in query_columns: value = row_dict[col_name] # 统一格式显示 if col_name in ["制定人", "担当人"]: formatted_row.append(clean_name(value)) elif isinstance(value, (datetime.datetime, datetime.date)): if isinstance(value, datetime.datetime): formatted_row.append(value.strftime('%Y-%m-%d %H:%M:%S')) else: formatted_row.append(f"{value.strftime('%Y-%m-%d')} 00:00:00") else: formatted_row.append(str(value) if value is not None else "NULL") tree.insert("", "end", values=formatted_row, tags=(col,)) # 标记排序字段 # 更新表头显示(添加排序方向标识) for c in tree["columns"]: if c == col: # 显示 ↑(升序)或 ↓(降序) tree.heading(c, text=f"{c} {'↓' if reverse else '↑'}", command=lambda _col=c: sort_treeview(_col, not reverse)) else: # 恢复默认表头 tree.heading(c, text=c, command=lambda _col=c: sort_treeview(_col, False)) # -------------------------- 甘特图双击定位功能 -------------------------- def on_gantt_click(event, current_tasks, start_idx): """ 甘特图双击事件:定位表格对应行 :param event: 鼠标事件 :param current_tasks: 当前页甘特图任务列表 :param start_idx: 当前页任务在总列表中的起始索引 """ # 获取点击位置对应的任务索引 ax = event.inaxes if not ax: return y_coord = event.ydata task_idx = int(round(y_coord)) # 任务条Y轴坐标对应索引 # 校验索引有效性 if 0 <= task_idx < len(current_tasks): # 获取当前点击的任务信息 task = current_tasks[task_idx] item_name, start_date, due_date, status, assignee = task # 转换日期格式用于匹配表格 start_str = start_date.strftime('%Y-%m-%d %H:%M:%S') if isinstance(start_date, datetime.date) else "NULL" due_str = due_date.strftime('%Y-%m-%d %H:%M:%S') if isinstance(due_date, datetime.date) else "NULL" # 遍历表格查找匹配行(按品名、起始日期、担当人联合匹配) for tree_item in tree.get_children(): row_data = tree.item(tree_item)["values"] # 表格列索引映射(需与query_columns顺序一致) name_idx = query_columns.index("品名") start_idx_col = query_columns.index("起始日期") assignee_idx = query_columns.index("担当人") # 匹配条件:品名一致 + 起始日期一致 + 担当人一致 if (row_data[name_idx] == item_name and row_data[start_idx_col].startswith(start_str.split()[0]) and clean_name(row_data[assignee_idx]) == assignee): # 选中并滚动到目标行 tree.selection_remove(tree.selection()) # 清空原有选择 tree.selection_add(tree_item) tree.see(tree_item) root.focus_force() # 切回主窗口 break def execute_query(): """执行查询并更新表格""" global timeline_data, current_assignee, current_results, query_results, query_columns maker = maker_combobox.get().strip() assignee = assignee_combobox.get().strip() status = status_combobox.get().strip() fuzzy_keyword = fuzzy_entry.get().strip() # 获取全范围模糊查询关键词 current_assignee = clean_name(assignee) if assignee else "全部" try: days = int(days_entry.get()) if days_entry.get().strip() else 7 if days <= 0: raise ValueError("天数必须为正整数") except ValueError: messagebox.showerror("输入错误", "请输入有效的正整数天数") return # 查询条件判断(允许仅输入模糊关键词) if not maker and not assignee and status == "" and not fuzzy_keyword: messagebox.showwarning("警告", "请至少输入以下任一条件:制定人、担当人、计划状态、模糊查询关键词") return # 定义需要查询的13列 query_columns = [ "品名", "制定日期", "承担部门", "承担任务", "担当人", "起始日期", "预定交付期", "交付期回答", "数量", "制定人", "其它备注", "便签", "计划状态" ] # 构建SQL查询 - 只查询指定的13列 query = f""" SELECT 零件数据表.品名, 计划.制定日期, 计划.承担部门, 计划.承担任务, 计划.担当人, 计划.起始日期, 计划.预定交付期, 计划.交付期回答, 计划.数量, 计划.制定人, 计划.其它备注, 计划.便签, 计划.计划状态 FROM 计划 INNER JOIN 零件数据表 ON 计划.零件ID = 零件数据表.ID WHERE 1=1 """ params = [] # 状态筛选逻辑 if status == "未完成": query += " AND (计划.计划状态 NOT LIKE '%完纳%' AND 计划.计划状态 NOT LIKE '%已完成%' AND 计划.计划状态 NOT LIKE '%已关闭%')" elif status == "已完成": query += " AND (计划.计划状态 LIKE '%完纳%' OR 计划.计划状态 LIKE '%已完成%')" elif status == "已关闭": query += " AND 计划.计划状态 LIKE '%已关闭%'" elif status != "": query += " AND 计划.计划状态 = ?" params.append(status) if maker and maker not in ["无数据", "加载失败"]: query += " AND (计划.制定人 LIKE ? OR 计划.制定人 = ?)" params.extend([f"%{maker}", maker]) if assignee and assignee != "": query += " AND (计划.担当人 LIKE ? OR 计划.担当人 = ?)" params.extend([f"%{assignee}", assignee]) # 全范围模糊查询逻辑(匹配所有13列中的文本列) if fuzzy_keyword: # 匹配13列中的所有文本列 fuzzy_clause = " AND (" fuzzy_clause += "零件数据表.品名 LIKE ? OR " # 品名 fuzzy_clause += "计划.承担部门 LIKE ? OR " # 承担部门 fuzzy_clause += "计划.承担任务 LIKE ? OR " # 承担任务 fuzzy_clause += "计划.担当人 LIKE ? OR " # 担当人 fuzzy_clause += "计划.制定人 LIKE ? OR " # 制定人 fuzzy_clause += "计划.其它备注 LIKE ? OR " # 其它备注 fuzzy_clause += "计划.便签 LIKE ? OR " # 便签 fuzzy_clause += "计划.计划状态 LIKE ?" # 计划状态 fuzzy_clause += ")" query += fuzzy_clause # 为每个匹配字段添加模糊参数(8个字段) fuzzy_param = f"%{fuzzy_keyword}%" params.extend([fuzzy_param for _ in range(8)]) query += f" AND 计划.制定日期 >= DATEADD(DAY, -{days}, GETDATE())" # 执行查询 connection = connect_to_database() if connection: try: cursor = connection.cursor() cursor.execute(query, params) results = cursor.fetchall() current_results = [dict(zip(query_columns, row)) for row in results] # 保存原始数据用于排序 # 清空表格并设置列(绑定排序事件) tree.delete(*tree.get_children()) tree["columns"] = query_columns for col in query_columns: # 为所有可排序字段添加排序功能 if col in ["预定交付期", "起始日期", "制定日期", "交付期回答", "承担部门", "承担任务", "担当人", "制定人", "品名", "其它备注", "便签", "计划状态", "数量"]: tree.heading(col, text=col, command=lambda _col=col: sort_treeview(_col, False)) else: tree.heading(col, text=col) # 优化列宽 if col == "品名": tree.column(col, width=120, minwidth=50) elif col in ["其它备注", "便签"]: tree.column(col, width=120, minwidth=50) elif col in ["制定人", "担当人", "承担部门", "计划状态"]: tree.column(col, width=15, minwidth=15) elif col in ["制定日期", "起始日期", "预定交付期", "交付期回答", "承担任务"]: tree.column(col, width=20, minwidth=20) elif col == "数量": tree.column(col, width=7, minwidth=5) else: tree.column(col, width=20, minwidth=10) # 处理结果数据 query_results = [] timeline_data = [] for row in results: row_dict = dict(zip(query_columns, row)) item_name = row_dict["品名"] start_date = row_dict["起始日期"] or row_dict["制定日期"] due_date = row_dict["预定交付期"] status_val = row_dict["计划状态"] assignee_val = clean_name(row_dict["担当人"]) # 确保日期类型一致 if isinstance(start_date, datetime.datetime): start_date = start_date.date() if isinstance(due_date, datetime.datetime): due_date = due_date.date() # 收集时间线数据 timeline_data.append((item_name, start_date, due_date, status_val, assignee_val)) # 格式化行数据 formatted_row = [] for col_name in query_columns: value = row_dict[col_name] if col_name in ["制定人", "担当人"]: formatted_row.append(clean_name(value)) elif isinstance(value, (datetime.datetime, datetime.date)): if isinstance(value, datetime.datetime): formatted_row.append(value.strftime('%Y-%m-%d %H:%M:%S')) else: formatted_row.append(f"{value.strftime('%Y-%m-%d')} 00:00:00") else: formatted_row.append(str(value) if value is not None else "NULL") query_results.append(formatted_row) tree.insert("", "end", values=formatted_row) # 更新统计信息 row_count_label.config(text=f"当前查询的数据行数: {len(query_results)}") selected_row_count_label.config(text="当前选取的行数: 0") except pyodbc.Error as e: messagebox.showerror("查询错误", f"执行查询时出错: {str(e)}") finally: connection.close() def plot_gantt(): """绘制甘特图(带双击定位功能)""" try: if not timeline_data: messagebox.showwarning("警告", "请先执行查询获取数据") return # 去重逻辑:保留所有项目,只去除完全重复的条目 unique_tasks = [] seen_tasks = set() for task in timeline_data: item_name, start_date, due_date, status, assignee = task start_str = start_date.strftime('%Y-%m-%d') if start_date else "None" due_str = due_date.strftime('%Y-%m-%d') if due_date else "None" task_identifier = (item_name, start_str, due_str, status, assignee) if task_identifier not in seen_tasks: seen_tasks.add(task_identifier) unique_tasks.append(task) total_tasks = len(unique_tasks) if total_tasks == 0: messagebox.showinfo("提示", "没有可显示的任务数据") return # 创建独立窗口 gantt_window = tk.Toplevel(root) window_title = f"{current_assignee}的任务甘特图" gantt_window.title(window_title) gantt_window.geometry("1200x700") gantt_window.resizable(True, True) # 分页控制变量 items_per_page = 15 # 每页显示的任务数量 current_page = [1] # 使用列表存储以允许在内部函数中修改 total_pages = max(1, (total_tasks + items_per_page - 1) // items_per_page) # 创建容纳图表和分页控件的框架 main_frame = ttk.Frame(gantt_window) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 图表框架 chart_frame = ttk.Frame(main_frame) chart_frame.pack(fill=tk.BOTH, expand=True) # 分页控件框架 pagination_frame = ttk.Frame(main_frame) pagination_frame.pack(fill=tk.X, pady=5) # 显示当前页码标签 page_label = ttk.Label(pagination_frame, text=f"第 {current_page[0]} / {total_pages} 页") page_label.pack(side=tk.LEFT, padx=10) # 定义绘制当前页图表的函数 def draw_current_page(): # 清除现有图表 for widget in chart_frame.winfo_children(): widget.destroy() # 计算当前页的任务范围 start_idx = (current_page[0] - 1) * items_per_page end_idx = min(start_idx + items_per_page, total_tasks) current_tasks = unique_tasks[start_idx:end_idx] # 绘制甘特图 # 动态调整高度,确保每个任务有足够空间显示 fig, ax = plt.subplots(figsize=(10, min(8, len(current_tasks) * 0.4 + 1))) today = datetime.date.today() # 获取当前日期(仅日期部分) # 统计各类任务数量 overdue_count = 0 completed_count = 0 closed_count = 0 pending_count = 0 # 绘制每个任务条 for i, (item_name, start_date, due_date, status, assignee) in enumerate(current_tasks): # 跳过日期不完整的任务 if start_date is None or due_date is None: continue # 确保日期类型统一为date if isinstance(start_date, datetime.datetime): start_date = start_date.date() if isinstance(due_date, datetime.datetime): due_date = due_date.date() start_num = date2num(start_date) end_num = date2num(due_date) duration = end_num - start_num # 判断任务状态 is_completed = "已完成" in status or "完纳" in status is_closed = "已关闭" in status is_overdue = due_date < today # 根据状态选择颜色 if is_completed: color = "#2E8B57" # 海绿色:已完成 completed_count += 1 elif is_closed: color = "#808080" # 灰色:已关闭 closed_count += 1 elif is_overdue: color = "#DC143C" # 深红色:未完成且逾期 overdue_count += 1 else: color = "#FFD700" # 金黄色:未完成且未逾期 pending_count += 1 # 绘制任务条 ax.broken_barh([(start_num, duration)], (i, 0.8), facecolors=color, edgecolor="#333333", linewidth=0.5) # 显示任务名称和担当人(帮助区分相同名称的任务) max_name_length = 18 if len(item_name) > max_name_length: display_name = item_name[:max_name_length] + "..." else: display_name = item_name # 显示担当人以区分相同名称的任务 display_text = f"{display_name}\n({assignee})" ax.text(start_num + duration/2, i + 0.4, display_text, ha='center', va='center', fontsize=8, wrap=True) # 设置图表标题和坐标轴 title_suffix = f"(第 {current_page[0]}/{total_pages} 页,共 {total_tasks} 个任务)" status_summary = f"逾期: {overdue_count}, 已完成: {completed_count}, 已关闭: {closed_count}, 未完成: {pending_count}" if current_assignee != "全部": ax.set_title(f"{current_assignee}的任务甘特图 {title_suffix}\n{status_summary}", fontsize=14, pad=20) else: ax.set_title(f"全部任务甘特图 {title_suffix}\n{status_summary}", fontsize=14, pad=20) ax.set_xlabel('日期', fontsize=12, labelpad=10) ax.set_yticks([]) # 隐藏Y轴刻度 ax.grid(axis='x', linestyle='--', alpha=0.7, color="#CCCCCC") # 设置X轴日期格式 date_format = DateFormatter('%y-%m-%d') ax.xaxis.set_major_formatter(date_format) fig.autofmt_xdate() # 自动旋转日期标签 # 添加图例 legend_elements = [ Patch(facecolor="#2E8B57", label="已完成"), Patch(facecolor="#FFD700", label="未完成(未逾期)"), Patch(facecolor="#DC143C", label="未完成(已逾期)"), Patch(facecolor="#808080", label="已关闭") ] ax.legend(handles=legend_elements, loc='upper left', fontsize=10, framealpha=0.9) # 在窗口中显示图表 canvas = FigureCanvasTkAgg(fig, master=chart_frame) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) # 绑定双击事件用于定位表格行 canvas.mpl_connect('button_press_event', lambda event: on_gantt_click(event, current_tasks, start_idx) if event.dblclick else None) # 更新页码标签 page_label.config(text=f"第 {current_page[0]} / {total_pages} 页") # 更新按钮状态 prev_btn.config(state=tk.NORMAL if current_page[0] > 1 else tk.DISABLED) next_btn.config(state=tk.NORMAL if current_page[0] < total_pages else tk.DISABLED) # 分页按钮逻辑 def prev_page(): if current_page[0] > 1: current_page[0] -= 1 draw_current_page() def next_page(): if current_page[0] < total_pages: current_page[0] += 1 draw_current_page() def first_page(): if current_page[0] != 1: current_page[0] = 1 draw_current_page() def last_page(): if current_page[0] != total_pages: current_page[0] = total_pages draw_current_page() # 创建分页按钮 first_btn = ttk.Button(pagination_frame, text="首页", command=first_page) first_btn.pack(side=tk.LEFT, padx=2) prev_btn = ttk.Button(pagination_frame, text="上一页", command=prev_page) prev_btn.pack(side=tk.LEFT, padx=2) next_btn = ttk.Button(pagination_frame, text="下一页", command=next_page) next_btn.pack(side=tk.LEFT, padx=2) last_btn = ttk.Button(pagination_frame, text="末页", command=last_page) last_btn.pack(side=tk.LEFT, padx=2) # 每页显示数量选择 ttk.Label(pagination_frame, text="每页显示:").pack(side=tk.RIGHT, padx=5) def change_items_per_page(event): nonlocal items_per_page items_per_page = int(items_per_page_var.get()) current_page[0] = 1 # 重置到第一页 nonlocal total_pages total_pages = max(1, (total_tasks + items_per_page - 1) // items_per_page) draw_current_page() items_per_page_var = tk.StringVar(value=str(items_per_page)) items_per_page_combo = ttk.Combobox(pagination_frame, textvariable=items_per_page_var, width=5, state="readonly") items_per_page_combo['values'] = ["10", "15", "20", "30", "50"] items_per_page_combo.bind("<<ComboboxSelected>>", change_items_per_page) items_per_page_combo.pack(side=tk.RIGHT, padx=5) # 绘制第一页 draw_current_page() except Exception as e: messagebox.showerror("绘图错误", f"绘制甘特图失败: {str(e)}") def export_to_excel(): """将查询结果导出到Excel""" try: if not query_results: messagebox.showwarning("警告", "当前无查询结果可导出") return # 创建DataFrame,保留原始格式 df = pd.DataFrame(query_results, columns=query_columns) # 优化导出文件名(包含状态和日期) status_suffix = status_combobox.get().strip() if status_combobox.get().strip() else "全部状态" keyword_suffix = f"_关键词[{fuzzy_entry.get().strip()}]" if fuzzy_entry.get().strip() else "" current_time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel文件", "*.xlsx")], initialfile=f"计划查询结果_{status_suffix}{keyword_suffix}_{current_time}" ) if not file_path: return df.to_excel(file_path, index=False) messagebox.showinfo("成功", f"数据已导出至:\n{file_path}") except Exception as e: messagebox.showerror("导出失败", f"导出过程中出错: {str(e)}") # 创建主窗口 root = tk.Tk() # 设置窗口图标 try: root.iconbitmap(r"C:\WPy64-39100_pywin32\fd.ico") except tk.TclError: print("未能找到图标文件,请确保图标文件存在于指定路径。") root.title("计划查询系统(0921)") root.geometry("1400x800") root.resizable(True, True) # 筛选条件框架 filter_frame = ttk.Frame(root, padding=10) filter_frame.pack(fill=tk.X) # 筛选控件 ttk.Label(filter_frame, text="制定人:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) maker_combobox = ttk.Combobox(filter_frame, width=6) maker_combobox.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(filter_frame, text="担当人:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W) assignee_combobox = ttk.Combobox(filter_frame, width=6) assignee_combobox.grid(row=0, column=3, padx=5, pady=5, sticky=tk.W) ttk.Label(filter_frame, text="计划状态:").grid(row=0, column=4, padx=5, pady=5, sticky=tk.W) status_combobox = ttk.Combobox(filter_frame, width=6) status_combobox.grid(row=0, column=5, padx=5, pady=5, sticky=tk.W) # 全范围模糊查询输入框 ttk.Label(filter_frame, text="查询:").grid(row=0, column=6, padx=5, pady=5, sticky=tk.W) fuzzy_entry = ttk.Entry(filter_frame, width=10) fuzzy_entry.grid(row=0, column=7, padx=5, pady=5, sticky=tk.W) fuzzy_entry.insert(0, "") # 初始为空 ttk.Label(filter_frame, text="(模糊)").grid(row=0, column=8, padx=5, pady=5, sticky=tk.W) ttk.Label(filter_frame, text="最近天数:").grid(row=0, column=9, padx=5, pady=5, sticky=tk.W) days_entry = ttk.Entry(filter_frame, width=5) days_entry.grid(row=0, column=10, padx=5, pady=5, sticky=tk.W) days_entry.insert(0, "7") # 按钮 execute_btn = ttk.Button(filter_frame, text="查询", command=execute_query) execute_btn.grid(row=0, column=11, padx=5, pady=5) export_btn = ttk.Button(filter_frame, text="导出到Excel", command=export_to_excel) export_btn.grid(row=0, column=12, padx=10, pady=5) gantt_btn = ttk.Button(filter_frame, text="生成甘特图", command=plot_gantt) gantt_btn.grid(row=0, column=13, padx=10, pady=5) # 表格框架 tree_frame = ttk.Frame(root, padding=10) tree_frame.pack(fill=tk.BOTH, expand=True) # 表格组件 tree = ttk.Treeview(tree_frame, show="headings") tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 滚动条 vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview) vsb.pack(side=tk.RIGHT, fill=tk.Y) tree.configure(yscrollcommand=vsb.set) hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=tree.xview) hsb.pack(side=tk.BOTTOM, fill=tk.X) tree.configure(xscrollcommand=hsb.set) # 统计标签 stats_frame = ttk.Frame(root, padding=10) stats_frame.pack(fill=tk.X) row_count_label = ttk.Label(stats_frame, text="当前查询的数据行数: 0") row_count_label.pack(side=tk.LEFT, padx=5) selected_row_count_label = ttk.Label(stats_frame, text="当前选取的行数: 0") selected_row_count_label.pack(side=tk.RIGHT, padx=5) # 初始化全局变量 query_results = [] query_columns = [ "品名", "制定日期", "承担部门", "承担任务", "担当人", "起始日期", "预定交付期", "交付期回答", "数量", "制定人", "其它备注", "便签", "计划状态" ] timeline_data = [] current_assignee = "" current_results = [] # 初始化下拉列表(初始值为空) load_combobox_data() maker_combobox.current(0) assignee_combobox.current(0) # 运行主循环 root.mainloop() 这个代码有没有冗余
09-22
import os import re import sys import tkinter as tk from tkinter import ttk, Frame, Scrollbar, Canvas from tkinter import scrolledtext, messagebox import pandas as pd import numpy as np from glob import glob import seaborn as sns from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk import matplotlib.pyplot as plt from matplotlib.figure import Figure import matplotlib as mpl import matplotlib.font_manager as fm from datetime import datetime def configure_chinese_font(): # 尝试寻找可用中文字体 chinese_fonts = [] chinese_fonts = [ "SimHei", "Microsoft YaHei", "SimSun", "KaiTi", "FangSong", "Arial Unicode MS", ] # 检查系统中有哪些字体可用 available_fonts = [f.name for f in fm.fontManager.ttflist] selected_font = None # 寻找第一个可用的中文字体 for font in chinese_fonts: if font in available_fonts: selected_font = font break # 如果找不到任何中文字体,使用默认字体并警告 if selected_font is None: print(f"警告: 系统中未找到以下中文: {', '.join(chinese_fonts)}") print("将使用系统默认字体,中文可能无法正确显示") selected_font = fm.FontProperties().get_name() # 设置matplotlib使用选定的字体 plt.rcParams["font.family"] = selected_font plt.rcParams["axes.unicode_minus"] = False print(f"已选择字体: {selected_font} 用于中文显示") def find_batch_folders(path, batch_number): target_dir = "4WCSVLog" batch_folders = [] for root, dirs, files in os.walk(path): path_components = os.path.normpath(root).split(os.sep) if target_dir in path_components and batch_number in dirs: full_path = os.path.join(root, batch_number) batch_folders.append(full_path) return batch_folders def get_csv_files_path1(folder): all_files = glob(os.path.join(folder, "*-SVP1B*.csv")) return [f for f in all_files if "HEAD" not in f] def get_csv_files_path2(folder): return glob(os.path.join(folder, "*-SVP1B*.csv")) def parse_svp1b(filename): match = re.search(r"svp1b([0-9A-Z])([0-9])([0-9])", filename, re.IGNORECASE) if match: piece, line, file_num = match.groups() piece_num = ( 10 + ord(piece.upper()) - ord("A") if piece.isalpha() else int(piece) ) return piece_num, int(line), int(file_num) return None def read_csv_file(file, start_row=0, start_col=0): df = pd.read_csv(file, header=None, skiprows=start_row) return df.iloc[:, start_col:] if start_col > 0 else df def merge_files(files, path_type): piece_dict = {} for file in files: svp_info = parse_svp1b(os.path.basename(file).lower()) if not svp_info: continue piece, line, file_num = svp_info key = (piece, line) df = read_csv_file( file, start_row=(2 if path_type == 1 else 4), start_col=(0 if path_type == 1 else 18), ) if df.empty: continue if key not in piece_dict: piece_dict[key] = [] piece_dict[key].append((file_num, df)) sorted_keys = sorted(piece_dict.keys(), key=lambda x: (x[0], x[1])) global_col_counter = 1 final_dfs = [] for key in sorted_keys: file_dfs = sorted(piece_dict[key], key=lambda x: x[0]) base_df = None for file_num, df in file_dfs: if base_df is None: global_col_counter = 1 new_columns = [] for _ in df.columns: new_columns.append(f"net{global_col_counter}") global_col_counter += 1 df.columns = new_columns base_df = df else: new_columns = [] for _ in df.columns: new_columns.append(f"net{global_col_counter}") global_col_counter += 1 df.columns = new_columns base_df = pd.concat([base_df, df], axis=1) # 横向拼接 if base_df is not None: final_dfs.append(base_df) return ( pd.concat(final_dfs, axis=0, ignore_index=True) if final_dfs else pd.DataFrame() ) def collect_batch_data(batch_list): batch_data_dict = {} path1 = r"\\10.127.1.248\c1tst\7750" path2 = r"\\10.127.1.248\c1tst\7755" for idx, batch_number in enumerate(batch_list, 1): print(f"=== 处理第 {idx}/{len(batch_list)} 个批次:{batch_number} ===") batch_data = pd.DataFrame() folders1 = find_batch_folders(path1, batch_number) if folders1: for folder1 in folders1: files = get_csv_files_path1(folder1) if files: current_data = merge_files(files, path_type=1) batch_data = pd.concat( [batch_data, current_data], axis=0, ignore_index=True ) print(f"从{folder1}找到 {len(files)} 个数据文件") else: folders2 = find_batch_folders(path2, batch_number) if folders2: for folder2 in folders2: files = get_csv_files_path2(folder2) if files: current_data = merge_files(files, path_type=2) batch_data = pd.concat( [batch_data, current_data], axis=0, ignore_index=True ) print(f"从{folder2}找到 {len(files)} 个数据文件") else: print(f"批次 {batch_number}:未找到对应文件夹,跳过\n") continue if not batch_data.empty: batch_data_dict[batch_number] = batch_data print(f"批次 {batch_number} 数据合并完成,共 {len(batch_data)} 行\n") else: print(f"批次 {batch_number}:无有效数据,跳过\n") return batch_data_dict class RedirectText: def __init__(self, text_widget): self.text_widget = text_widget def write(self, string): self.text_widget.insert(tk.END, string) self.text_widget.see(tk.END) def flush(self): pass class NetPlotViewer: """高级NET图表查看器,支持分页和无限滚动""" def __init__(self, root, batch_data_dict, batch_list, net_list): self.root = root self.batch_data_dict = batch_data_dict self.batch_list = batch_list self.net_list = net_list # 预先计算并缓存所有NET的数据 self.net_data_dict = self._precompute_net_data() # 记录无效的NET self.invalid_nets = [net for net in net_list if net not in self.net_data_dict] # 打印无效NET警告 if self.invalid_nets: print(f"警告: 以下NET在批次数据中不存在: {', '.join(self.invalid_nets)}") # 只保留有效的NET self.valid_nets = [net for net in net_list if net in self.net_data_dict] # 分页配置 self.per_page = 10 self.current_page = 0 self.total_pages = max( 1, (len(self.valid_nets) + self.per_page - 1) // self.per_page ) # 无限滚动配置 self.loaded_count = 0 self.load_step = 10 self.loading = False self.view_mode = "pagination" # 或 "infinite_scroll" # 创建主窗口 self.create_main_window() # 居中显示窗口 self.center_window() def center_window(self): """使窗口在屏幕上居中显示""" self.plot_window.update_idletasks() # 确保窗口尺寸已更新 width = self.plot_window.winfo_width() height = self.plot_window.winfo_height() screen_width = self.plot_window.winfo_screenwidth() screen_height = self.plot_window.winfo_screenheight() x = (screen_width - width) // 2 y = (screen_height - height) // 2 # 设置窗口位置居中 self.plot_window.geometry(f"+{x}+{y}") def _precompute_net_data(self): """预先计算并缓存所有NET的数据""" net_data_dict = {} for net in self.net_list: net_data = pd.DataFrame() for batch in self.batch_list: if ( batch in self.batch_data_dict and net in self.batch_data_dict[batch].columns ): net_data[f"{batch}"] = self.batch_data_dict[batch][net] # 只添加有数据的NET if not net_data.empty: net_data_dict[net] = net_data return net_data_dict def create_main_window(self): """创建主窗口和控件""" self.plot_window = tk.Toplevel(self.root) self.plot_window.title("批次数据分布箱线图") self.plot_window.geometry("1200x800") # 添加无效NET警告 if self.invalid_nets: warning_frame = tk.Frame( self.plot_window, bg="#ffebee", bd=1, relief=tk.SOLID ) warning_frame.pack(fill=tk.X, padx=10, pady=5) warning_label = tk.Label( warning_frame, text=f"警告: 以下NET在批次数据中不存在: {', '.join(self.invalid_nets)}", fg="#d32f2f", bg="#ffebee", font=("SimHei", 10, "bold"), ) warning_label.pack(padx=10, pady=5) # 添加有效NET数量信息 # info_frame = tk.Frame(self.plot_window, bg="#e3f2fd", bd=1, relief=tk.SOLID) # info_frame.pack(fill=tk.X, padx=10, pady=5) # info_label = tk.Label( # info_frame, # text=f"显示 {len(self.valid_nets)} 个有效的NET中的图表", # fg="#1565c0", # bg="#e3f2fd", # font=("SimHei", 10), # ) # info_label.pack(padx=10, pady=5) # 创建顶部控制面板 self.create_control_panel() # 创建内容区域 self.create_content_area() # 初始显示 self.update_display() self.plot_window.update_idletasks() self.center_window() def create_control_panel(self): """创建顶部控制面板""" control_frame = tk.Frame( self.plot_window, bd=1, relief=tk.RIDGE, padx=10, pady=10 ) control_frame.pack(fill=tk.X, padx=10, pady=(10, 5)) # 视图模式选择 tk.Label(control_frame, text="视图模式:", font=("SimHei", 10)).pack( side=tk.LEFT, padx=5 ) self.view_mode_var = tk.StringVar( value="分页视图" if self.view_mode == "pagination" else "无限滚动" ) view_mode_menu = ttk.Combobox( control_frame, textvariable=self.view_mode_var, values=["分页视图", "无限滚动"], width=12, state="readonly", ) view_mode_menu.pack(side=tk.LEFT, padx=5) view_mode_menu.bind("<<ComboboxSelected>>", self.change_view_mode) # 分页控制 self.pagination_frame = tk.Frame(control_frame) self.pagination_frame.pack(side=tk.LEFT, padx=20) # 分页控件 self.prev_btn = tk.Button( self.pagination_frame, text="上一页", command=lambda: self.change_page(-1), state=tk.DISABLED, ) self.prev_btn.pack(side=tk.LEFT, padx=5) self.page_label = tk.Label( self.pagination_frame, text="1/1", font=("SimHei", 10) ) self.page_label.pack(side=tk.LEFT, padx=5) self.next_btn = tk.Button( self.pagination_frame, text="下一页", command=lambda: self.change_page(1), state=tk.NORMAL if self.total_pages > 1 else tk.DISABLED, ) self.next_btn.pack(side=tk.LEFT, padx=5) # 跳转控件 tk.Label(self.pagination_frame, text="跳转到:", font=("SimHei", 9)).pack( side=tk.LEFT, padx=(20, 0) ) self.page_entry = tk.Entry(self.pagination_frame, width=4, font=("SimHei", 9)) self.page_entry.pack(side=tk.LEFT, padx=2) self.page_entry.insert(0, "1") self.goto_btn = tk.Button( self.pagination_frame, text="跳转", command=self.goto_page, font=("SimHei", 9), ) self.goto_btn.pack(side=tk.LEFT, padx=2) # 每页数量选择 tk.Label(self.pagination_frame, text="每页数量:", font=("SimHei", 9)).pack( side=tk.LEFT, padx=(20, 0) ) self.per_page_var = tk.StringVar(value=str(self.per_page)) self.per_page_dropdown = ttk.Combobox( self.pagination_frame, textvariable=self.per_page_var, values=["1", "5", "10", "20", "50"], width=4, state="readonly", ) self.per_page_dropdown.pack(side=tk.LEFT, padx=2) self.per_page_dropdown.bind("<<ComboboxSelected>>", self.change_per_page) # 关闭按钮 close_btn = tk.Button( control_frame, text="关闭窗口", command=self.plot_window.destroy, bg="#f44336", fg="white", font=("SimHei", 10), ) close_btn.pack(side=tk.RIGHT, padx=10) # 状态标签 self.status_label = tk.Label( control_frame, text="", font=("SimHei", 9), fg="#666" ) self.status_label.pack(side=tk.RIGHT, padx=10) def create_content_area(self): """创建内容显示区域""" # 创建滚动框架 self.scroll_frame = tk.Frame(self.plot_window) self.scroll_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) # 添加水平和垂直滚动条 self.hscrollbar = tk.Scrollbar(self.scroll_frame, orient=tk.HORIZONTAL) self.vscrollbar = tk.Scrollbar(self.scroll_frame, orient=tk.VERTICAL) # 创建画布用于滚动 self.canvas = tk.Canvas( self.scroll_frame, yscrollcommand=self.vscrollbar.set, xscrollcommand=self.hscrollbar.set, ) # 配置滚动条 self.vscrollbar.config(command=self.canvas.yview) self.hscrollbar.config(command=self.canvas.xview) # 布局 self.hscrollbar.pack(side=tk.BOTTOM, fill=tk.X) self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 创建内容框架 self.content_frame = tk.Frame(self.canvas) self.canvas_frame = self.canvas.create_window( (0, 0), window=self.content_frame, anchor="nw" ) # 绑定配置事件 self.content_frame.bind("<Configure>", self.on_frame_configure) self.canvas.bind("<Configure>", self.on_canvas_configure) # 绑定鼠标滚轮事件 self.canvas.bind_all("<MouseWheel>", self.on_mousewheel) def on_frame_configure(self, event=None): """更新滚动区域""" self.canvas.configure(scrollregion=self.canvas.bbox("all")) # 检查是否需要加载更多(无限滚动模式) if self.view_mode == "infinite_scroll": self.check_load_more() # 动态调整列宽 self.content_frame.update_idletasks() self.canvas.itemconfig( self.canvas_frame, width=self.content_frame.winfo_width() ) def on_canvas_configure(self, event): """调整内容框架宽度以适应画布""" self.canvas.itemconfig(self.canvas_frame, width=event.width) def on_mousewheel(self, event): """处理鼠标滚轮事件""" self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") # 检查是否需要加载更多(无限滚动模式) if self.view_mode == "infinite_scroll": self.check_load_more() def change_view_mode(self, event=None): """切换视图模式""" new_mode = ( "pagination" if self.view_mode_var.get() == "分页视图" else "infinite_scroll" ) if new_mode != self.view_mode: self.view_mode = new_mode self.update_display() # 更新分页控件的可见性 if self.view_mode == "pagination": self.pagination_frame.pack(side=tk.LEFT, padx=20) else: self.pagination_frame.pack_forget() def change_per_page(self, event=None): """更改每页显示数量""" try: new_per_page = int(self.per_page_var.get()) if new_per_page != self.per_page: self.per_page = new_per_page self.total_pages = max( 1, (len(self.valid_nets) + self.per_page - 1) // self.per_page ) self.current_page = 0 self.update_display() except ValueError: pass def change_page(self, delta): """翻页""" new_page = max(0, min(self.total_pages - 1, self.current_page + delta)) if new_page != self.current_page: self.current_page = new_page self.update_display() def goto_page(self): """跳转到指定页码""" try: page_num = int(self.page_entry.get()) - 1 if 0 <= page_num < self.total_pages: self.current_page = page_num self.update_display() else: messagebox.showerror("错误", f"页码必须在 1 到 {self.total_pages} 之间") except ValueError: messagebox.showerror("错误", "请输入有效的页码数字") def check_load_more(self): """检查是否需要加载更多(无限滚动模式)""" if self.view_mode != "infinite_scroll" or self.loading: return # 获取滚动位置 scroll_position = self.canvas.yview() # 如果滚动到底部附近(90%位置),加载更多 if scroll_position[1] > 0.9 and self.loaded_count < len(self.valid_nets): self.load_more() def load_more(self): """加载更多图表(无限滚动模式)""" if self.loading or self.loaded_count >= len(self.valid_nets): return self.loading = True # 显示加载状态 self.status_label.config( text=f"正在加载数据... ({self.loaded_count}/{len(self.valid_nets)})" ) self.plot_window.update() start_idx = self.loaded_count end_idx = min(self.loaded_count + self.load_step, len(self.valid_nets)) current_nets = self.valid_nets[start_idx:end_idx] # 创建加载动画 loading_label = tk.Label( self.content_frame, text="加载中...", font=("SimHei", 10, "italic"), fg="#666", ) loading_label.grid( row=self.content_frame.grid_size()[1], column=0, sticky="ew", pady=10 ) # 短暂延迟,让用户看到加载提示 self.plot_window.after( 50, lambda: self._create_charts(current_nets, loading_label) ) def _create_charts(self, nets, loading_label): """创建图表并移除加载提示""" # 移除加载提示 loading_label.destroy() # 创建图表 for net in nets: self.create_net_chart(net) self.loaded_count += len(nets) self.loading = False # 更新状态 self.status_label.config( text=f"已加载 {self.loaded_count} 个NET,共 {len(self.valid_nets)} 个" ) # 检查是否全部加载完成 if self.loaded_count >= len(self.valid_nets): self.status_label.config( text=f"全部加载完成,共 {len(self.valid_nets)} 个NET", fg="green" ) def create_net_chart(self, net): """为单个NET创建图表""" net_data = self.net_data_dict.get(net, pd.DataFrame()) if net_data.empty: return # 获取当前内容框架中的行数 row_idx = self.content_frame.grid_size()[1] # 创建图表框架 frame = tk.Frame(self.content_frame, bd=2, relief=tk.GROOVE, padx=10, pady=10) frame.grid(row=row_idx, column=0, sticky="nsew", padx=5, pady=5) # # 添加NET标签 # net_label = tk.Label(frame, text=f" {net}", font=("SimHei", 12, "bold")) # net_label.pack(anchor=tk.W) # # 添加批次信息标签 # batch_info = ", ".join(net_data.columns) # batch_label = tk.Label( # frame, text=f"{batch_info}", font=("SimHei", 9), fg="#666" # ) # batch_label.pack(anchor=tk.W) # 创建图表 fig = Figure(figsize=(10, 4), dpi=100) ax = fig.add_subplot(111) # 准备数据 long_data = net_data.melt(var_name="批號", value_name="测量值") long_data["批次序号"] = long_data["批號"].str.extract(r"(\d+)").astype(int) long_data = long_data.sort_values("批次序号") # 绘制箱线图 sns.boxplot( x="批號", y="测量值", data=long_data, ax=ax, boxprops=dict(facecolor="lightblue", alpha=0.7), medianprops=dict(color="red", linewidth=2), ) # 设置图表样式 ax.set_title(f"{net} ", fontsize=12) ax.set_xlabel("", fontsize=10) ax.set_ylabel("NET值", fontsize=10) ax.tick_params(axis="x", labelsize=8, rotation=30) ax.grid(axis="y", linestyle="--", alpha=0.7) fig.tight_layout() # 标注统计值 all_values = long_data["测量值"].dropna() if not all_values.empty: data_range = all_values.max() - all_values.min() for i, col in enumerate(net_data.columns): col_values = net_data[col].dropna() if col_values.empty: continue q1 = col_values.quantile(0.25) median = col_values.median() q3 = col_values.quantile(0.75) text_offset = data_range * 0.03 ax.text(i, q1 - text_offset, f"Q1: {q1:.2f}", ha="center", fontsize=8) ax.text( i, median, f"Med: {median:.2f}", ha="center", fontsize=8, color="red", ) ax.text(i, q3 + text_offset, f"Q3: {q3:.2f}", ha="center", fontsize=8) # 嵌入图表到Tkinter canvas_plot = FigureCanvasTkAgg(fig, master=frame) canvas_plot.draw() canvas_plot.get_tk_widget().pack(fill=tk.X) # 添加工具栏 toolbar = NavigationToolbar2Tk(canvas_plot, frame) toolbar.update() canvas_plot.get_tk_widget().pack(fill=tk.X) def update_display(self): """根据当前视图模式更新显示""" # 清除当前内容 for widget in self.content_frame.winfo_children(): widget.destroy() # 重置无限滚动计数器 self.loaded_count = 0 if self.view_mode == "pagination": # 分页模式 page_idx = self.current_page start_idx = page_idx * self.per_page end_idx = min((page_idx + 1) * self.per_page, len(self.valid_nets)) current_nets = self.valid_nets[start_idx:end_idx] # 更新标题和页码标签 self.plot_window.title( f"批次数据分布箱线图 ({page_idx + 1}/{self.total_pages})" ) self.page_label.config(text=f"{page_idx + 1}/{self.total_pages}") # 更新按钮状态 self.prev_btn.config(state=tk.NORMAL if page_idx > 0 else tk.DISABLED) self.next_btn.config( state=tk.NORMAL if page_idx < self.total_pages - 1 else tk.DISABLED ) # 创建当前页的图表 for net in current_nets: self.create_net_chart(net) # 更新状态 self.status_label.config( text=f"显示 {start_idx+1}-{end_idx} 个NET,共 {len(self.valid_nets)} 个" ) # 居中窗口 self.center_window() else: # 无限滚动模式 self.plot_window.title("批次数据分布箱线图 (无限滚动模式)") self.status_label.config( text=f"已加载 0 个NET,共 {len(self.valid_nets)} 个" ) # 初始加载第一组图表 self.load_more() # 居中窗口 self.center_window() class BatchDataManager: """批次数据管理器,负责数据收集和缓存""" def __init__(self): self.batch_data_dict = {} self.collected_batches = set() self.current_batch_list = [] def collect_data(self, batch_list): """收集批次数据并缓存""" # 检查哪些批次尚未收集 new_batches = [b for b in batch_list if b not in self.collected_batches] if new_batches: # 只收集新增批次的数据 new_data = collect_batch_data(new_batches) self.batch_data_dict.update(new_data) self.collected_batches.update(new_batches) print(f"已收集 {len(new_batches)} 个新批次的数据") # 更新当前批次列表 self.current_batch_list = batch_list # 打印所有NET信息 self.print_net_info() return self.batch_data_dict def get_net_names(self): """获取当前批次的所有NET名称""" net_names = set() for batch in self.current_batch_list: if batch in self.batch_data_dict: net_names.update(self.batch_data_dict[batch].columns) # 转换为列表并按数字排序 net_list = sorted( net_names, key=lambda x: int(x[3:]) if x.startswith("net") and x[3:].isdigit() else 0, ) return net_list def print_net_info(self): """打印所有批次中的NET信息""" print("\n===== 批次中的NET信息 =====") total_nets = set() # 用于统计所有批次中的唯一NET for batch in self.current_batch_list: if batch in self.batch_data_dict: df = self.batch_data_dict[batch] net_columns = [col for col in df.columns if col.startswith("net")] # 添加NET到总集合 total_nets.update(net_columns) print( f"批次 {batch} 包含 {len(net_columns)} 个NET: {', '.join(net_columns[:10])}{'...' if len(net_columns) > 10 else ''}" ) else: print(f"批次 {batch} 无可用数据") # 打印总NET数量 print(f"批次总共包含 {len(total_nets)} 个不同的NET") print("=========================\n") def get_available_nets(self, batch_list): """获取特定批次中可用的NET""" available_nets = set() for batch in batch_list: if batch in self.batch_data_dict: available_nets.update(self.batch_data_dict[batch].columns) return [net for net in available_nets if net.startswith("net")] def process_batches(): """处理批次数据""" input_text = batch_entry.get().strip() if not input_text: messagebox.showwarning("输入警告", "请输入批次号后再处理!") return batch_list = re.split(r"[,\s]+", input_text) batch_list = [b for b in batch_list if b] if not batch_list: messagebox.showwarning("输入警告", "未识别到有效批次号!") return process_btn.config(state=tk.DISABLED) log_text.delete(1.0, tk.END) print(f"开始处理批次:{', '.join(batch_list)} - {datetime.now()}\n") # 收集批次数据 data_manager.collect_data(batch_list) # 获取所有NET名称 all_net_names = data_manager.get_net_names() net_entry.delete(0, tk.END) if all_net_names: net_entry.insert(0, ", ".join(all_net_names[:5])) # 默认显示前5个NET print(f"批次中包含的NET总数: {len(all_net_names)}") else: print("警告: 批次中未找到任何NET数据") # 启用显示图表按钮 show_plot_btn.config(state=tk.NORMAL) process_btn.config(state=tk.NORMAL) print(f"批次数据收集完成!现在可以输入NET列表并点击'显示图表'按钮 {datetime.now()}") def show_plots(): """显示NET图表""" # 解析NET输入 net_text = net_entry.get().strip() net_list = [] if net_text: raw_nets = re.split(r"[,\s]+", net_text) raw_nets = [n.strip() for n in raw_nets if n.strip()] for net in raw_nets: if net.startswith("net") and net[3:].isdigit(): net_list.append(net) elif net.isdigit(): net_list.append(f"net{net}") else: print(f"警告:无效NET格式 '{net}',已忽略") net_list = list(dict.fromkeys(net_list)) net_list.sort(key=lambda x: int(x[3:])) print(f"已解析NET列表(排序后):{', '.join(net_list)}") else: # 默认显示前20个NET net_list = [f"net{i}" for i in range(1, 21)] print("未指定NET编号,将显示前20个NET") if not net_list: messagebox.showwarning("输入警告", "未识别到有效的NET编号!") return # 检查NET是否在批次中 available_nets = data_manager.get_available_nets(data_manager.current_batch_list) invalid_nets = [net for net in net_list if net not in available_nets] if invalid_nets: print(f"警告: 以下NET在批次数据中不存在: {', '.join(invalid_nets)}") # 只保留有效的NET valid_nets = [net for net in net_list if net in available_nets] if not valid_nets: messagebox.showwarning("输入警告", "没有有效的NET可显示!") return print(f"将显示 {len(valid_nets)} 个有效的NET") # 创建NET图表查看器 NetPlotViewer( root, data_manager.batch_data_dict, data_manager.current_batch_list, valid_nets ) def create_gui(): global root, batch_entry, log_text, process_btn, net_entry, show_plot_btn, data_manager # 初始化数据管理器 data_manager = BatchDataManager() root = tk.Tk() root.title("電測阻值箱線圖") root.geometry("800x650") root.resizable(True, True) # 批次号输入区域 input_frame = tk.Frame(root, padx=10, pady=10) input_frame.pack(fill=tk.X) tk.Label( input_frame, text="批次号输入(多个批次用逗号/空格分隔):", font=("SimHei", 10) ).pack(side=tk.LEFT) batch_entry = tk.Entry(input_frame, width=50, font=("SimHei", 10)) batch_entry.pack(side=tk.LEFT, padx=5) batch_entry.insert(0, "25AL90070800,25AL90070400") # NET编号输入 net_frame = tk.Frame(root, padx=10, pady=5) net_frame.pack(fill=tk.X) tk.Label( net_frame, text="NET编号输入(多个用逗号/空格分隔):", font=("SimHei", 10), ).pack(side=tk.LEFT) net_entry = tk.Entry(net_frame, width=50, font=("SimHei", 10)) net_entry.pack(side=tk.LEFT, padx=5) net_entry.insert(0, "1,2,3,4,5") # 按钮 btn_frame = tk.Frame(root, padx=10, pady=5) btn_frame.pack(fill=tk.X) process_btn = tk.Button( btn_frame, text="收集批次数据", command=process_batches, bg="#4CAF50", fg="white", font=("SimHei", 10), ) process_btn.pack(side=tk.LEFT, padx=5) show_plot_btn = tk.Button( btn_frame, text="显示图表", command=show_plots, bg="#2196F3", fg="white", font=("SimHei", 10), state=tk.DISABLED, # 初始禁用 ) show_plot_btn.pack(side=tk.LEFT, padx=5) clear_btn = tk.Button( btn_frame, text="清空日志", command=lambda: log_text.delete(1.0, tk.END), bg="#f44336", fg="white", font=("SimHei", 10), ) clear_btn.pack(side=tk.LEFT, padx=5) # 日志 log_frame = tk.Frame(root, padx=10, pady=5) log_frame.pack(fill=tk.BOTH, expand=True) tk.Label(log_frame, text="处理日志:", font=("SimHei", 10)).pack(anchor=tk.W) log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, font=("SimHei", 9)) log_text.pack(fill=tk.BOTH, expand=True) sys.stdout = RedirectText(log_text) root.mainloop() if __name__ == "__main__": configure_chinese_font() create_gui() 我想要將net圖標居中,但是現在程式寫了居中,可以沒有起作用。將這個功能重新改寫一下。此外,我現在想要將每一個net的圖都可以水平拉動。
11-29
import tkinter as tk from tkinter import ttk, filedialog, messagebox import pandas as pd import threading import subprocess import datetime import queue import os import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties from collections import defaultdict import time import openpyxl from openpyxl.styles import PatternFill import matplotlib import platform import psutil import re matplotlib.use('Agg') # 避免与Tkinter冲突 class NetworkMonitorApp: def __init__(self, root): self.root = root self.root.title("高性能网络设备监控系统") self.root.geometry("1200x800") # 排序状态 self.sort_column = "" # 当前排序的列 self.sort_direction = "asc" # 排序方向: asc/desc # 创建状态栏 self.status_var = tk.StringVar(value="就绪") 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.config = { 'thread_count': 50, 'ping_interval': 5, 'ping_timeout': 1, 'excel_path': r"D:\1.xlsx", 'key_devices': ["二层交换机", "三层交换机", "UPS"], 'tvm_count': 3, 'agm_count': 4 } # 数据存储 self.device_data = {} self.disconnect_count = defaultdict(int) self.last_ping_results = {} self.current_status = {} self.sheet_data = {} self.pending_updates = set() # 需要更新的设备IP集合 self.last_update_time = time.time() # 上次GUI更新时间 self.update_threshold = 0.5 # GUI更新间隔(秒) # GUI组件初始化 self.create_widgets() self.load_excel_data() # 启动GUI更新线程 self.gui_update_queue = queue.Queue() self.root.after(100, self.process_gui_queue) # 设置中文字体 try: self.chinese_font = FontProperties(fname=r"C:\Windows\Fonts\simhei.ttf") except: self.chinese_font = FontProperties() print("警告:未找到中文字体文件,中文显示可能不正常") def create_widgets(self): # 控制面板 control_frame = ttk.LabelFrame(self.root, text="控制面板") control_frame.pack(fill=tk.X, padx=10, pady=5) self.start_btn = ttk.Button(control_frame, text="开始监控", command=self.start_monitoring) self.start_btn.pack(side=tk.LEFT, padx=5, pady=5) ttk.Button(control_frame, text="清除断网次数", command=self.reset_counters).pack(side=tk.LEFT, padx=5, pady=5) ttk.Button(control_frame, text="设置", command=self.open_settings).pack(side=tk.LEFT, padx=5, pady=5) export_frame = ttk.Frame(control_frame) export_frame.pack(side=tk.LEFT, padx=10) ttk.Label(export_frame, text="导出格式:").pack(side=tk.LEFT) self.export_format = tk.StringVar(value="Excel") ttk.Combobox(export_frame, textvariable=self.export_format, values=["Excel", "TXT", "图片"], width=8).pack(side=tk.LEFT, padx=2) ttk.Button(export_frame, text="导出", command=self.export_data).pack(side=tk.LEFT) # 设备表格框架 frame = ttk.Frame(self.root) frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) self.columns = ("序号", "时间戳", "线路", "设备IP", "车站", "设备名称", "断开次数", "状态") # 创建表格容器 container = ttk.Frame(frame) container.pack(fill=tk.BOTH, expand=True) # 创建Treeview self.tree = ttk.Treeview( container, columns=self.columns, show="headings", height=20 ) # 设置列宽 col_widths = [40, 150, 100, 120, 100, 150, 80, 100] for col, width in zip(self.columns, col_widths): self.tree.heading(col, text=col, command=lambda c=col: self.sort_treeview(c)) self.tree.column(col, width=width, anchor=tk.CENTER) # 添加垂直滚动条 vsb = ttk.Scrollbar(container, orient="vertical", command=self.tree.yview) self.tree.configure(yscrollcommand=vsb.set) # 添加水平滚动条 hsb = ttk.Scrollbar(container, orient="horizontal", command=self.tree.xview) self.tree.configure(xscrollcommand=hsb.set) # 布局 self.tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") # 配置网格行列权重 container.grid_rowconfigure(0, weight=1) container.grid_columnconfigure(0, weight=1) # 设置标签颜色 self.tree.tag_configure('disconnected', background='#ffcccc') self.tree.tag_configure('key_disconnected', background='#ffffcc') self.tree.tag_configure('critical_disconnected', background='#cce5ff') # 性能监控标签 perf_frame = ttk.LabelFrame(self.root, text="性能监控") perf_frame.pack(fill=tk.X, padx=10, pady=5) self.device_count_var = tk.StringVar(value="设备总数: 0") ttk.Label(perf_frame, textvariable=self.device_count_var).pack(side=tk.LEFT, padx=10) self.memory_usage_var = tk.StringVar(value="内存使用: 0 MB") ttk.Label(perf_frame, textvariable=self.memory_usage_var).pack(side=tk.LEFT, padx=10) self.ping_time_var = tk.StringVar(value="平均Ping时间: 0 ms") ttk.Label(perf_frame, textvariable=self.ping_time_var).pack(side=tk.LEFT, padx=10) # 启动性能监控 self.root.after(1000, self.update_performance_stats) def update_performance_stats(self): """更新性能统计信息""" # 计算内存使用 process = psutil.Process(os.getpid()) mem_usage = process.memory_info().rss / (1024 * 1024) self.memory_usage_var.set(f"内存使用: {mem_usage:.1f} MB") # 更新设备计数 device_count = len(self.device_data) self.device_count_var.set(f"设备总数: {device_count}") # 计算平均Ping时间 total_time = 0 count = 0 for ip, data in self.last_ping_results.items(): if data['success']: total_time += data['response_time'] count += 1 avg_time = total_time / count if count > 0 else 0 self.ping_time_var.set(f"平均Ping时间: {avg_time:.1f} ms") # 继续监控 self.root.after(5000, self.update_performance_stats) def sort_treeview(self, column): """点击列标题进行排序""" # 获取当前所有项目 items = [(self.tree.item(item, 'values'), item) for item in self.tree.get_children('')] if not items: return # 确定排序方向 if self.sort_column == column: self.sort_direction = "desc" if self.sort_direction == "asc" else "asc" else: self.sort_column = column self.sort_direction = "asc" # 获取列的索引 col_index = self.columns.index(column) # 定义排序函数 def sort_key(item): value = item[0][col_index] # 尝试转换为数字 try: return float(value) if value else 0 except ValueError: # 处理状态列的特殊情况 if column == "状态" and value == "断开": return -1 # 断开状态排在前面 return value.lower() # 字符串转换为小写比较,不区分大小写 # 排序 reverse = (self.sort_direction == "desc") items.sort(key=sort_key, reverse=reverse) # 更新表格 for index, (values, item_id) in enumerate(items): # 更新序号 new_values = list(values) new_values[0] = index + 1 # 更新行 self.tree.item(item_id, values=tuple(new_values)) # 每100条更新一次界面,避免卡顿 if index % 100 == 0: self.root.update_idletasks() # 更新列标题箭头指示 self.update_column_headers() self.status_var.set(f"已按 [{column}] 进行{'降序' if reverse else '升序'}排序") def update_column_headers(self): """更新列标题,显示排序指示器""" for col in self.columns: current_text = self.tree.heading(col, "text") # 移除现有的排序指示器 if current_text.endswith(" ↑") or current_text.endswith(" ↓"): current_text = current_text[:-2] # 添加新的排序指示器 if col == self.sort_column: direction = "↑" if self.sort_direction == "asc" else "↓" current_text += f" {direction}" self.tree.heading(col, text=current_text) def load_excel_data(self): try: # 清空现有数据 self.device_data.clear() self.sheet_data.clear() self.disconnect_count.clear() self.last_ping_results.clear() # 读取Excel所有sheet if not os.path.exists(self.config['excel_path']): messagebox.showerror("错误", f"Excel文件不存在: {self.config['excel_path']}") return # 使用openpyxl加速读取 wb = openpyxl.load_workbook(self.config['excel_path'], read_only=True) device_count = 0 for sheet_name in wb.sheetnames: sheet = wb[sheet_name] headers = [cell.value for cell in sheet[1]] # 确保列名存在 required_columns = ['设备IP', '车站', '设备名称'] if not all(col in headers for col in required_columns): missing = [col for col in required_columns if col not in headers] print(f"Sheet '{sheet_name}' 缺少必要列: {', '.join(missing)}") continue # 获取列索引 ip_col_idx = headers.index('设备IP') station_col_idx = headers.index('车站') device_name_col_idx = headers.index('设备名称') # 提取设备信息 for row in sheet.iter_rows(min_row=2): ip_cell = row[ip_col_idx] if not ip_cell.value: continue ip = str(ip_cell.value).strip() if not ip or ip.lower() == 'nan': continue # 存储设备信息 self.device_data[ip] = { 'sheet': sheet_name, 'station': row[station_col_idx].value if station_col_idx < len(row) else '', 'device_name': row[device_name_col_idx].value if device_name_col_idx < len(row) else '', 'ip': ip } device_count += 1 # 内存保护 - 检查当前内存使用 current_mem = psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024) if current_mem > 500: # 500MB内存限制 messagebox.showwarning("内存警告", f"达到内存限制 (500MB),停止加载更多设备") break self.status_var.set(f"Sheet '{sheet_name}' 加载完成, 设备数: {device_count}") device_count = 0 # 重置计数器 self.status_var.set(f"Excel数据加载成功,共 {len(self.device_data)} 台设备") # 初始化ping结果 timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") for ip in self.device_data: self.last_ping_results[ip] = { 'timestamp': timestamp, 'success': True, 'response_time': 0, 'disconnect_count': 0 } # 初始化表格 self.initialize_treeview() except Exception as e: messagebox.showerror("错误", f"加载Excel失败: {str(e)}") finally: # 确保关闭工作簿 if 'wb' in locals(): wb.close() def initialize_treeview(self): """初始化表格视图(优化版)""" # 清空现有数据 for item in self.tree.get_children(): self.tree.delete(item) # 批量添加设备(每100个更新一次) devices = list(self.device_data.items()) batch_size = 100 for i in range(0, len(devices), batch_size): batch = devices[i:i + batch_size] for idx, (ip, device) in enumerate(batch, start=i + 1): ping_data = self.last_ping_results.get(ip, { 'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 'success': True, 'response_time': 0, 'disconnect_count': 0 }) # 确定标签 tags = () if not ping_data['success']: if device['device_name'] in self.config['key_devices']: tags = ('key_disconnected',) elif self.is_critical_device(ip): tags = ('critical_disconnected',) else: tags = ('disconnected',) # 添加行 self.tree.insert("", "end", values=( idx, ping_data['timestamp'], device['sheet'], ip, device['station'], device['device_name'], ping_data['disconnect_count'], f"{ping_data['response_time']}ms" if ping_data['success'] else "断开" ), tags=tags) # 更新GUI以显示进度 self.root.update_idletasks() # 初始化列标题排序指示器 self.update_column_headers() def start_monitoring(self): if not self.device_data: messagebox.showwarning("警告", "没有可监控的设备,请检查Excel文件路径和内容") return if self.start_btn.cget('text') == '开始监控': self.monitoring_active = True self.start_btn.config(text='停止监控') self.status_var.set("监控已启动...") # 启动监控线程 self.monitor_thread = threading.Thread(target=self.run_monitoring, daemon=True) self.monitor_thread.start() else: self.monitoring_active = False self.start_btn.config(text='开始监控') self.status_var.set("监控已停止") def run_monitoring(self): """优化后的监控循环,支持5000台设备""" from concurrent.futures import ThreadPoolExecutor, as_completed # 使用固定线程池 executor = ThreadPoolExecutor(max_workers=self.config['thread_count']) # 分批处理参数 batch_size = 200 # 每批处理200台设备 devices = list(self.device_data.keys()) # 性能统计 total_ping_time = 0 ping_count = 0 while self.monitoring_active: start_time = time.time() # 分批处理设备 for i in range(0, len(devices), batch_size): if not self.monitoring_active: break batch = devices[i:i + batch_size] futures = {executor.submit(self.ping_device, ip): ip for ip in batch} for future in as_completed(futures): ip = futures[future] try: success, response_time = future.result() except Exception as e: success, response_time = False, 0 # 更新性能统计 if success and response_time > 0: total_ping_time += response_time ping_count += 1 # 更新状态 timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") device_info = self.device_data[ip] # 只记录断开状态的设备 if not success: self.disconnect_count[ip] += 1 # 获取之前的状态 prev_data = self.last_ping_results.get(ip, {'success': True}) prev_status = prev_data['success'] prev_count = prev_data.get('disconnect_count', 0) # 仅当状态变化时才更新结果 if prev_status != success or prev_count != self.disconnect_count[ip]: self.last_ping_results[ip] = { 'timestamp': timestamp, 'success': success, 'response_time': response_time, 'disconnect_count': self.disconnect_count[ip] } # 标记需要更新GUI的设备 self.pending_updates.add(ip) # 智能GUI更新 current_time = time.time() if current_time - self.last_update_time > self.update_threshold: self.update_gui() self.last_update_time = current_time self.pending_updates.clear() # 计算剩余等待时间 elapsed = time.time() - start_time sleep_time = max(0, self.config['ping_interval'] - elapsed) # 更新状态栏显示进度 avg_ping = total_ping_time / ping_count if ping_count > 0 else 0 self.status_var.set( f"监控中... 设备数: {len(devices)} | " f"本轮耗时: {elapsed:.1f}s | " f"平均Ping: {avg_ping:.1f}ms | " f"下一轮: {sleep_time:.1f}s" ) time.sleep(sleep_time) def ping_device(self, ip): """高性能Ping实现,针对大规模网络优化""" try: # 根据操作系统选择不同的Ping命令 if platform.system().lower() == "windows": # Windows使用无窗口模式ping命令 timeout_ms = int(self.config['ping_timeout'] * 1000) command = ['ping', '-n', '1', '-w', str(timeout_ms), ip] creation_flags = subprocess.CREATE_NO_WINDOW else: # Linux/Mac command = ['ping', '-c', '1', '-W', str(self.config['ping_timeout']), ip] creation_flags = 0 # 使用subprocess.Popen实现非阻塞Ping with subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=creation_flags, text=True, bufsize=1 ) as process: # 设置超时 try: # 等待进程完成,但不超过超时时间 process.wait(timeout=self.config['ping_timeout']) except subprocess.TimeoutExpired: process.kill() return False, 0 # 检查进程返回码 if process.returncode == 0: # 快速检查输出中是否包含TTL/ttl字段 output = process.stdout.read() if "TTL=" in output or "ttl=" in output.lower(): # 提取响应时间 time_lines = [line for line in output.split('\n') if '时间=' in line or 'time=' in line] if time_lines: time_str = time_lines[0] match = re.search(r'(\d+)\s?ms', time_str) if match: response_time = int(match.group(1)) return True, response_time return True, 1 # 成功返回1ms(不计算具体时间) return False, 0 except: return False, 0 def update_gui(self): """只更新需要刷新的设备行""" if not self.pending_updates: return # 获取当前显示的项目 current_items = self.tree.get_children() update_count = 0 # 只更新需要更新的设备 for ip in list(self.pending_updates): if ip not in self.device_data: continue device = self.device_data[ip] ping_data = self.last_ping_results.get(ip, {}) if not ping_data: continue # 找到并更新对应的行 for item in current_items: item_values = self.tree.item(item, 'values') if item_values and len(item_values) > 3 and item_values[3] == ip: # IP地址在第4列 # 更新值 new_values = ( item_values[0], # 保持序号不变 ping_data['timestamp'], device['sheet'], ip, device['station'], device['device_name'], ping_data['disconnect_count'], f"{ping_data['response_time']}ms" if ping_data.get('success', False) else "断开" ) self.tree.item(item, values=new_values) # 根据状态设置颜色 if not ping_data.get('success', False): if device['device_name'] in self.config['key_devices']: self.tree.item(item, tags=('key_disconnected',)) elif self.is_critical_device(ip): self.tree.item(item, tags=('critical_disconnected',)) else: self.tree.item(item, tags=('disconnected',)) else: self.tree.item(item, tags=()) update_count += 1 break # 更新状态栏 self.status_var.set(f"更新了 {update_count} 台设备状态") self.pending_updates.clear() def is_critical_device(self, ip): device_info = self.device_data[ip] sheet = device_info['sheet'] station = device_info['station'] # 计算同站同类设备断网数量 same_station_devices = [ d for d in self.device_data.values() if d['sheet'] == sheet and d['station'] == station ] # 检查TVM if "TVM" in device_info['device_name']: disconnected_tvm = sum( 1 for d in same_station_devices if "TVM" in d['device_name'] and not self.last_ping_results.get(d['ip'], {'success': True})['success'] ) return disconnected_tvm >= self.config['tvm_count'] # 检查AGM elif "AGM" in device_info['device_name']: disconnected_agm = sum( 1 for d in same_station_devices if "AGM" in d['device_name'] and not self.last_ping_results.get(d['ip'], {'success': True})['success'] ) return disconnected_agm >= self.config['agm_count'] return False def reset_counters(self): self.disconnect_count.clear() for ip in self.last_ping_results: self.last_ping_results[ip]['disconnect_count'] = 0 self.update_gui() self.status_var.set("断网次数已清零") def open_settings(self): settings_win = tk.Toplevel(self.root) settings_win.title("系统设置") settings_win.geometry("450x400") # 创建设置控件 entries = {} row = 0 # 配置项中文标签映射 config_labels = { 'thread_count': '并发线程数:', 'ping_interval': 'Ping间隔(秒):', 'ping_timeout': 'Ping超时(秒):', 'excel_path': 'Excel文件路径:', 'key_devices': '关键设备类型:', 'tvm_count': '同站TVM断网阈值:', 'agm_count': '同站AGM断网阈值:' } for key, label in config_labels.items(): ttk.Label(settings_win, text=label).grid(row=row, column=0, padx=10, pady=5, sticky="e") # 特殊处理关键设备类型(逗号分隔) if key == 'key_devices': entry = ttk.Entry(settings_win, width=30) entry.insert(0, ",".join(self.config[key])) else: entry = ttk.Entry(settings_win, width=30) entry.insert(0, str(self.config[key])) entry.grid(row=row, column=1, padx=5, pady=5, sticky="we") entries[key] = entry # 仅为Excel路径添加浏览按钮 if key == 'excel_path': def browse_excel(entry_widget=entry): # 使用闭包绑定正确的entry file_path = filedialog.askopenfilename( filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")] ) if file_path: entry_widget.delete(0, tk.END) entry_widget.insert(0, file_path) browse_btn = ttk.Button(settings_win, text="浏览", command=browse_excel) browse_btn.grid(row=row, column=2, padx=5, pady=5) row += 1 def save_settings(): try: for key, entry in entries.items(): value = entry.get() if key == 'thread_count': self.config[key] = int(value) elif key in ['ping_interval', 'ping_timeout']: self.config[key] = float(value) elif key == 'key_devices': self.config[key] = [d.strip() for d in value.split(",")] elif key in ['tvm_count', 'agm_count']: self.config[key] = int(value) else: self.config[key] = value # 重新加载Excel self.load_excel_data() settings_win.destroy() self.status_var.set("设置已保存并应用") except Exception as e: messagebox.showerror("错误", f"保存设置失败: {str(e)}") # 保存按钮 btn_frame = ttk.Frame(settings_win) btn_frame.grid(row=row, column=0, columnspan=3, pady=10) ttk.Button(btn_frame, text="保存", command=save_settings).pack(side=tk.LEFT, padx=10) ttk.Button(btn_frame, text="取消", command=settings_win.destroy).pack(side=tk.LEFT, padx=10) def export_data(self): export_type = self.export_format.get() # 获取断网设备数据 disconnected_devices = [] for ip, ping_data in self.last_ping_results.items(): if not ping_data['success']: device = self.device_data[ip] disconnected_devices.append({ **device, **ping_data, 'ip': ip }) if not disconnected_devices: messagebox.showinfo("导出", "没有断网设备可导出") return # 创建DataFrame df = pd.DataFrame(disconnected_devices) df = df[['timestamp', 'sheet', 'ip', 'station', 'device_name', 'disconnect_count']] df.columns = ['时间戳', '线路', '设备IP', '车站', '设备名称', '断开次数'] # 排序 df['类型'] = df.apply(lambda row: '关键设备' if row['设备名称'] in self.config['key_devices'] else '重要设备' if self.is_critical_device(row['设备IP']) else '普通设备', axis=1) df = df.sort_values(by=['类型', '断开次数'], ascending=[True, False]) try: # 设置默认文件名 default_filename = f"断网设备报告_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}" if export_type == "Excel": file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", initialfile=default_filename, filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")] ) if not file_path: return # 添加样式 writer = pd.ExcelWriter(file_path, engine='openpyxl') df.to_excel(writer, index=False, sheet_name='断网设备') # 获取工作簿和工作表 workbook = writer.book worksheet = writer.sheets['断网设备'] # 添加颜色样式 yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") lightblue_fill = PatternFill(start_color="00FFFF", end_color="00FFFF", fill_type="solid") red_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid") # 设置标题行样式 for col in range(1, len(df.columns) + 1): cell = worksheet.cell(row=1, column=col) cell.fill = PatternFill(start_color="D3D3D3", end_color="D3D3D3", fill_type="solid") # 设置数据行样式 for idx, row in enumerate(df.itertuples(), start=2): device_name = row[5] # 设备名称在第五列 ip = row[3] # 设备IP在第三列 if device_name in self.config['key_devices']: fill = yellow_fill elif self.is_critical_device(ip): fill = lightblue_fill else: fill = red_fill # 设置整行颜色 for col in range(1, len(df.columns) + 1): cell = worksheet.cell(row=idx, column=col) cell.fill = fill # 调整列宽 for col in worksheet.columns: max_length = 0 column = col[0].column_letter for cell in col: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = (max_length + 2) worksheet.column_dimensions[column].width = adjusted_width writer.close() elif export_type == "TXT": file_path = filedialog.asksaveasfilename( defaultextension=".txt", initialfile=default_filename, filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")] ) if not file_path: return with open(file_path, 'w', encoding='utf-8') as f: f.write("断网设备报告\n") f.write(f"生成时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"总断网设备数: {len(df)}\n") f.write("=" * 80 + "\n") for _, row in df.iterrows(): f.write(f"线路: {row['线路']}\n") f.write(f"设备IP: {row['设备IP']}\n") f.write(f"车站: {row['车站']}\n") f.write(f"设备名称: {row['设备名称']}\n") f.write(f"断开次数: {row['断开次数']}\n") f.write(f"最后检测时间: {row['时间戳']}\n") f.write(f"设备类型: {row['类型']}\n") f.write("-" * 80 + "\n") elif export_type == "图片": file_path = filedialog.asksaveasfilename( defaultextension=".png", initialfile=default_filename, filetypes=[("PNG图片", "*.png"), ("JPEG图片", "*.jpg"), ("所有文件", "*.*")] ) if not file_path: return # 设置中文显示 plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体 plt.rcParams['axes.unicode_minus'] = False # 正常显示负号 # 创建图表 plt.figure(figsize=(15, 8)) plt.title(f"断网设备统计 ({datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')})", fontsize=14) # 按线路分组统计 line_counts = df.groupby('线路').size() line_counts.plot(kind='bar', color='skyblue') plt.xlabel('线路') plt.ylabel('断网设备数量') plt.xticks(rotation=45) plt.tight_layout() # 保存图片 plt.savefig(file_path, dpi=150) plt.close() self.status_var.set(f"导出成功: {os.path.basename(file_path)}") except Exception as e: messagebox.showerror("导出错误", f"导出失败: {str(e)}") def process_gui_queue(self): """处理GUI更新队列""" try: while True: task = self.gui_update_queue.get_nowait() task() except queue.Empty: pass finally: self.root.after(100, self.process_gui_queue) def is_critical_device(self, ip): device_info = self.device_data[ip] sheet = device_info['sheet'] station = device_info['station'] # 计算同站同类设备断网数量 same_station_devices = [ d for d in self.device_data.values() if d['sheet'] == sheet and d['station'] == station ] # 检查TVM if "TVM" in device_info['device_name']: disconnected_tvm = sum( 1 for d in same_station_devices if "TVM" in d['device_name'] and not self.last_ping_results.get(d['ip'], {'success': True})['success'] ) return disconnected_tvm >= self.config['tvm_count'] # 检查AGM elif "AGM" in device_info['device_name']: disconnected_agm = sum( 1 for d in same_station_devices if "AGM" in d['device_name'] and not self.last_ping_results.get(d['ip'], {'success': True})['success'] ) return disconnected_agm >= self.config['agm_count'] return False if __name__ == "__main__": root = tk.Tk() app = NetworkMonitorApp(root) root.mainloop() 提升窗口刷新速度,不卡顿,实时更新ping测试结果,源代码尽量保留,全显示
11-28
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值