matplotlib.pyplot.subplot_tool

本文介绍如何使用matplotlib的subplot_tool函数来调整多子图的布局。通过实例演示了为当前图像及指定图像启动子图工具窗口的方法,从而实现对子图布局的灵活调整。

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

为图像启动一个子图工具窗口。子图工具窗口可以用来调整图像中子图的布局。调用格式如下:

subplot_tool(targetfig=None)

参数

targetfig:可选的,目标图像。为此图像启动子图工具窗口。如果未指定或为None,则为当前图像启动子图工具窗口。

返回值

返回值为matplotlib.widgets.SubplotTool实例。

简单示例

import matplotlib.pyplot as plt

x=[1,2,3]
y=[4,5,6]
fig, axs = plt.subplots(2, 2)
axs[0, 0].plot(x,y)
axs[0, 1].plot(x,y)
axs[1, 0].plot(x,y)
axs[1, 1].plot(x,y)

fig1, axs1 = plt.subplots(2, 2)
axs1[0, 0].plot(x,y)
axs1[0, 1].plot(x,y)
axs1[1, 0].plot(x,y)
axs1[1, 1].plot(x,y)

plt.subplot_tool()
plt.subplot_tool(fig)

plt.show()

运行结果如下,通过调节Figure 3和Figure 4中参数的值,可以分别调整图像Figure2和Figure1中的子图布局:

1、测试组新增存在BUG,新增的测试组还是测试组1,而且删除后图标中原本的测试组1选项(实际只删除了新增的假测试组1,原本的测试组1还在),也没了。说明新增测试组的这里,序号有问题。2、报错,严重BUG,阻塞运行Traceback (most recent call last): File "e:\VS Python\Iperf3 GUI Tool_DS.py", line 1095, in start_test server_port = group_widget.server_port_spin.value() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AttributeError: 'QGroupBox' object has no attribute 'server_port_spin' 源代码: import sys import os import ctypes import json import subprocess import threading import time import psutil import pythoncom from datetime import datetime, timedelta from PyQt5.QtWidgets import ( QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QCheckBox, QComboBox, QSpinBox, QGroupBox, QScrollArea, QTextEdit, QFileDialog, QMessageBox, QDoubleSpinBox, QSizePolicy, QListWidget, QAbstractItemView, QListWidgetItem ) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer from PyQt5.QtGui import QColor, QIcon import wmi # Matplotlib 导入 import matplotlib matplotlib.use('Qt5Agg') import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import matplotlib.dates as mdates from matplotlib import ticker """# 检查管理员权限并请求提升 def is_admin(): try: return ctypes.windll.shell32.IsUserAnAdmin() except: return False if not is_admin(): # 请求管理员权限 ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1) sys.exit(0)""" # 设置中文字体支持 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'SimSun', 'KaiTi'] # 设置中文字体 plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 # 网卡事件监听线程 class NicMonitorThread(QThread): nic_event = pyqtSignal(dict) # 发送事件:{'name': 网卡名, 'event': 'up' or 'down', 'time': 时间字符串} error_signal = pyqtSignal(str) # 错误信号 def __init__(self): super().__init__() self.running = True self.c = None self.watcher = None self.last_status = {} # 记录每个网卡的最后状态 def init_wmi(self): """初始化WMI连接""" try: pythoncom.CoInitialize() # 添加COM初始化 self.c = wmi.WMI() return True except Exception as e: self.error_signal.emit(f"无法初始化WMI连接: {str(e)}") return False def run(self): # 创建事件监听 if not self.init_wmi(): self.error_signal.emit("网卡事件监听器初始化失败") return try: # 获取当前网卡状态作为初始状态 for adapter in self.c.Win32_NetworkAdapter(NetConnectionStatus=2): self.last_status[adapter.Name] = 'up' for adapter in self.c.Win32_NetworkAdapter(NetConnectionStatus=7): self.last_status[adapter.Name] = 'down' # 创建事件监听器 self.watcher = self.c.watch_for( notification_type="__InstanceModificationEvent", wmi_class="Win32_NetworkAdapter", delay_secs=0.1, # 更短的延迟以提高响应速度 fields=["NetConnectionStatus", "Name"] ) while self.running: try: adapter = self.watcher() if adapter: name = adapter.Name status = adapter.NetConnectionStatus event_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] # 只处理状态变化 if status == 2 and self.last_status.get(name) != 'up': self.nic_event.emit({'name': name, 'event': 'up', 'time': event_time}) self.last_status[name] = 'up' elif status == 7 and self.last_status.get(name) != 'down': self.nic_event.emit({'name': name, 'event': 'down', 'time': event_time}) self.last_status[name] = 'down' except wmi.x_wmi_timed_out: # 超时是正常的,继续循环 pass except Exception as e: self.error_signal.emit(f"监听网卡事件出错: {str(e)}") # 尝试重新初始化WMI连接 time.sleep(1) if not self.init_wmi(): break except Exception as e: self.error_signal.emit(f"创建网卡监听器出错: {str(e)}") finally: pythoncom.CoUninitialize() # 清理COM资源 def stop(self): self.running = False if self.watcher: self.watcher.cancel() # iperf3 进程管理器 class IperfManager: def __init__(self, iperf_path=''): self.active_servers = {} self.active_clients = {} self.iperf_path = iperf_path or 'iperf3' def start_server(self, group_id, port, server_ip, nic_name=""): """启动iperf3服务器""" # 使用服务器IP地址作为绑定的IP地址 if not server_ip or not self.validate_ip(server_ip): raise ValueError(f"测试组{group_id}服务器IP地址无效: {server_ip}") command = f'"{self.iperf_path}" -s -p {port} -B {server_ip} -J --logfile server_{group_id}_{port}.log' proc = subprocess.Popen(command, shell=True) self.active_servers[(group_id, port)] = (proc, nic_name) # 保存网卡名称用于日志 return command def start_client(self, group_id, server_ip, port, duration, streams, client_ip, reverse=False, omit=5, nic_name=""): """启动iperf3客户端""" # 使用客户端IP地址作为绑定的IP地址 if not client_ip or not self.validate_ip(client_ip): raise ValueError(f"测试组{group_id}客户端IP地址无效: {client_ip}") if not server_ip or not self.validate_ip(server_ip): raise ValueError(f"测试组{group_id}服务器IP地址无效: {server_ip}") direction = "-R" if reverse else "" # 添加-B参数绑定客户端IP,-c参数连接服务器IP command = f'"{self.iperf_path}" -c {server_ip} -p {port} -t {duration} -P {streams} {direction} -O {omit} -B {client_ip} -J --logfile client_{group_id}_{port}.json' proc = subprocess.Popen(command, shell=True) self.active_clients[(group_id, port)] = (proc, nic_name) # 保存网卡名称用于日志 return command def validate_ip(self, ip_str): """验证IP地址格式""" if not ip_str: return False parts = ip_str.split('.') if len(parts) != 4: return False for part in parts: if not part.isdigit(): return False num = int(part) if num < 0 or num > 255: return False return True def stop_all(self): """停止所有iperf进程""" for key, (proc, _) in list(self.active_servers.items()) + list(self.active_clients.items()): try: parent = psutil.Process(proc.pid) for child in parent.children(recursive=True): child.kill() parent.kill() except Exception as e: print(f"停止进程时出错: {e}") self.active_servers = {} self.active_clients = {} # 实时数据收集线程 class DataCollectorThread(QThread): data_updated = pyqtSignal(dict) # {group_id: {timestamp: (up, down)}} test_completed = pyqtSignal(int, dict) # group_id, full_data test_timeout = pyqtSignal() # 测试超时信号 def __init__(self, manager, groups, parent=None): super().__init__(parent) self.manager = manager self.groups = groups self.running = True self.data = {} self.full_data = {} self.start_time = time.time() self.max_duration = 0 # 记录最长测试时间(秒) self.timeout_timer = None # 计算最长测试时间(用于超时检测) for config in groups.values(): if config['duration'] > self.max_duration: self.max_duration = config['duration'] def run(self): # 初始化数据结构 for group_id in self.groups: self.data[group_id] = {"up": {}, "down": {}} self.full_data[group_id] = {"up": {}, "down": {}} # 设置超时计时器(最长测试时间+5秒) self.timeout_timer = threading.Timer(self.max_duration + 5, self.handle_timeout) self.timeout_timer.start() # 主采集循环 while self.running: for group_id, config in self.groups.items(): # 处理上传日志 upload_log = f"client_{group_id}_{config['upload_port']}.json" if os.path.exists(upload_log): try: with open(upload_log, 'r') as f: data = json.load(f) # 解析上传数据 if 'intervals' in data: for interval in data['intervals']: ts = time.time() up = interval['sum']['bits_per_second'] / 1e6 # 转换为Mbps # 存储实时数据 self.data[group_id]["up"][ts] = up self.full_data[group_id]["up"][ts] = up except Exception as e: print(f"读取上传日志文件错误: {e}") # 处理下载日志(如果是双向测试) if config['direction'] in ["下载", "双向"]: download_log = f"client_{group_id}_{config['download_port']}.json" if os.path.exists(download_log): try: with open(download_log, 'r') as f: data = json.load(f) # 解析下载数据 if 'intervals' in data: for interval in data['intervals']: ts = time.time() down = interval['sum']['bits_per_second'] / 1e6 # 存储实时数据 self.data[group_id]["down"][ts] = down self.full_data[group_id]["down"][ts] = down except Exception as e: print(f"读取下载日志文件错误: {e}") # 发送更新信号 self.data_updated.emit(self.data) time.sleep(1) # 每秒更新一次 # 测试完成后发送完整数据 for group_id in self.groups: self.test_completed.emit(group_id, self.full_data[group_id]) def handle_timeout(self): """处理测试超时""" self.running = False self.test_timeout.emit() def stop(self): self.running = False if self.timeout_timer: self.timeout_timer.cancel() # Matplotlib 图表组件(支持中文) class MplCanvas(FigureCanvas): def __init__(self, parent=None, width=5, height=4, dpi=100): self.fig = Figure(figsize=(width, height), dpi=dpi, facecolor=(0.94, 0.97, 1.0)) # 淡蓝色背景 self.ax = self.fig.add_subplot(111) super().__init__(self.fig) self.setParent(parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.updateGeometry() # 初始化图表(中文) self.ax.set_xlabel('时间 (秒)') self.ax.set_ylabel('吞吐量 (Mbps)') self.ax.set_title('iPerf3 流量监控') self.ax.grid(True) self.ax.set_facecolor((0.98, 0.99, 1.0)) # 更淡的蓝色背景 self.relative_time = True # 默认使用相对时间 self.start_time = None self.end_time = None self.start_timestamp = None def update_plot(self, data, selected_groups=None): """更新图表数据""" self.ax.clear() # 颜色列表 colors = [ '#4169E1', # RoyalBlue '#4682B4', # SteelBlue '#6495ED', # CornflowerBlue '#1E90FF', # DodgerBlue '#00BFFF' # DeepSkyBlue ] max_value = 0 min_timestamp = float('inf') max_timestamp = float('-inf') for group_id, group_data in data.items(): if selected_groups is not None and group_id not in selected_groups: continue color = colors[(group_id-1) % len(colors)] # 上传数据(实线) if group_data["up"]: timestamps = sorted(group_data["up"].keys()) values = [group_data["up"][ts] for ts in timestamps] # 更新时间范围 if timestamps: min_timestamp = min(min_timestamp, min(timestamps)) max_timestamp = max(max_timestamp, max(timestamps)) if not self.relative_time and self.start_time: # 转换为绝对时间 abs_timestamps = [self.start_time + timedelta(seconds=ts - self.start_timestamp) for ts in timestamps] self.ax.plot(abs_timestamps, values, '-', color=color, label=f"组{group_id} 上传", linewidth=2) else: # 相对时间(从0开始) rel_timestamps = [ts - min_timestamp for ts in timestamps] self.ax.plot(rel_timestamps, values, '-', color=color, label=f"组{group_id} 上传", linewidth=2) if values: max_value = max(max_value, max(values)) # 下载数据(虚线) if group_data["down"]: timestamps = sorted(group_data["down"].keys()) values = [group_data["down"][ts] for ts in timestamps] # 更新时间范围 if timestamps: min_timestamp = min(min_timestamp, min(timestamps)) max_timestamp = max(max_timestamp, max(timestamps)) if not self.relative_time and self.start_time: abs_timestamps = [self.start_time + timedelta(seconds=ts - self.start_timestamp) for ts in timestamps] self.ax.plot(abs_timestamps, values, '--', color=color, label=f"组{group_id} 下载", linewidth=2) else: rel_timestamps = [ts - min_timestamp for ts in timestamps] self.ax.plot(rel_timestamps, values, '--', color=color, label=f"组{group_id} 下载", linewidth=2) if values: max_value = max(max_value, max(values)) # 设置坐标轴范围 if max_value > 0: self.ax.set_ylim(0, max_value * 1.2) # 添加图例(中文) self.ax.legend(loc='best', fontsize=9) # 设置标签和标题(中文) if self.relative_time: self.ax.set_xlabel('时间 (秒)') else: self.ax.set_xlabel('时间') # 设置时间格式 self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S')) self.ax.xaxis.set_major_locator(ticker.AutoLocator()) self.ax.set_ylabel('吞吐量 (Mbps)') self.ax.set_title('iPerf3 流量监控') self.ax.grid(True) self.draw() def switch_to_absolute_time(self, start_time, end_time): """切换到绝对时间模式""" self.relative_time = False self.start_time = start_time self.end_time = end_time # 记录测试开始时的第一个时间戳(作为相对时间的零点) if self.start_time: self.start_timestamp = time.time() - (datetime.now() - start_time).total_seconds() # 主应用窗口 class IperfVisualizer(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("iPerf3 流量可视化工具") self.setWindowIcon(QIcon('iperf_icon.ico')) # 设置软件图标 self.resize(1400, 900) # 应用配置 self.config = { 'iperf_path': '', 'auto_save_log': True, 'log_path': os.getcwd(), 'test_groups': {}, 'current_group_id': 1 } # 初始化管理器 self.manager = None self.data_collector = None self.nic_monitor = None self.nic_events = [] # 存储网卡事件 self.nic_summary = {} # 网卡事件汇总: {网卡名: {'down': 次数, 'up': 次数, 'last_status': 最后状态}} self.nic_map = {} # 网卡到测试组映射: {网卡名: [(group_id, port, role)]} # 设置UI self.init_ui() self.load_config() # 启动网卡事件监听 self.start_nic_monitor() def init_ui(self): # 主布局 main_widget = QWidget() main_layout = QVBoxLayout() main_widget.setLayout(main_layout) self.setCentralWidget(main_widget) # 标签页 self.tabs = QTabWidget() main_layout.addWidget(self.tabs) # 配置页 self.config_tab = QWidget() self.tabs.addTab(self.config_tab, "测试配置") self.setup_config_tab() # 图表页 self.chart_tab = QWidget() self.tabs.addTab(self.chart_tab, "流量图表") self.setup_chart_tab() # 信息页 self.info_tab = QWidget() self.tabs.addTab(self.info_tab, "测试信息") self.setup_info_tab() # 状态栏 self.statusBar().showMessage("就绪") def setup_config_tab(self): layout = QVBoxLayout() self.config_tab.setLayout(layout) # iperf路径配置 iperf_group = QGroupBox("iperf3 配置") iperf_layout = QVBoxLayout() path_layout = QHBoxLayout() path_layout.addWidget(QLabel("iperf3路径:")) self.iperf_path = QLineEdit(self.config['iperf_path']) path_layout.addWidget(self.iperf_path) self.iperf_browse_btn = QPushButton("浏览...") self.iperf_browse_btn.clicked.connect(self.browse_iperf_path) path_layout.addWidget(self.iperf_browse_btn) iperf_layout.addLayout(path_layout) # 测试按钮 test_btn_layout = QHBoxLayout() self.test_iperf_btn = QPushButton("测试路径") self.test_iperf_btn.clicked.connect(self.test_iperf_path) test_btn_layout.addWidget(self.test_iperf_btn) iperf_layout.addLayout(test_btn_layout) iperf_group.setLayout(iperf_layout) layout.addWidget(iperf_group) # 测试描述 desc_group = QGroupBox("测试描述") desc_layout = QVBoxLayout() self.test_desc = QLineEdit("iPerf3流量测试") desc_layout.addWidget(QLabel("测试描述:")) desc_layout.addWidget(self.test_desc) desc_group.setLayout(desc_layout) layout.addWidget(desc_group) # 日志设置 log_group = QGroupBox("日志设置") log_layout = QVBoxLayout() self.auto_save = QCheckBox("自动保存测试日志") self.auto_save.setChecked(True) log_layout.addWidget(self.auto_save) path_layout = QHBoxLayout() path_layout.addWidget(QLabel("保存路径:")) self.log_path = QLineEdit(os.getcwd()) path_layout.addWidget(self.log_path) self.browse_btn = QPushButton("浏览...") self.browse_btn.clicked.connect(self.browse_log_path) path_layout.addWidget(self.browse_btn) log_layout.addLayout(path_layout) log_group.setLayout(log_layout) layout.addWidget(log_group) # 测试组配置 self.test_groups_area = QScrollArea() self.test_groups_area.setWidgetResizable(True) self.test_groups_widget = QWidget() self.test_groups_layout = QVBoxLayout() self.test_groups_widget.setLayout(self.test_groups_layout) self.test_groups_area.setWidget(self.test_groups_widget) layout.addWidget(QLabel("测试组配置:")) layout.addWidget(self.test_groups_area) # 添加测试组按钮 self.add_group_btn = QPushButton("添加测试组") self.add_group_btn.clicked.connect(self.add_test_group) layout.addWidget(self.add_group_btn) # 操作按钮 btn_layout = QHBoxLayout() self.start_btn = QPushButton("开始测试") self.start_btn.clicked.connect(self.start_test) btn_layout.addWidget(self.start_btn) self.stop_btn = QPushButton("停止测试") self.stop_btn.clicked.connect(self.stop_test) self.stop_btn.setEnabled(False) btn_layout.addWidget(self.stop_btn) layout.addLayout(btn_layout) # 添加初始测试组 self.add_test_group() def setup_chart_tab(self): layout = QVBoxLayout() self.chart_tab.setLayout(layout) # 使用 Matplotlib 图表(支持中文) self.canvas = MplCanvas(self, width=10, height=6, dpi=100) layout.addWidget(self.canvas, 3) # 图表占3/4空间 # 测试组选择面板 group_select_layout = QHBoxLayout() # 测试组列表 group_list_layout = QVBoxLayout() group_list_layout.addWidget(QLabel("选择显示的测试组:")) self.group_list = QListWidget() self.group_list.setSelectionMode(QAbstractItemView.MultiSelection) # 多选 group_list_layout.addWidget(self.group_list) # 选择按钮 btn_layout = QHBoxLayout() self.select_all_btn = QPushButton("全选") self.select_all_btn.clicked.connect(self.select_all_groups) btn_layout.addWidget(self.select_all_btn) self.deselect_all_btn = QPushButton("全不选") self.deselect_all_btn.clicked.connect(self.deselect_all_groups) btn_layout.addWidget(self.deselect_all_btn) group_list_layout.addLayout(btn_layout) group_select_layout.addLayout(group_list_layout, 1) # 占1/4宽度 # 图表操作按钮 chart_btn_layout = QVBoxLayout() chart_btn_layout.addWidget(QLabel("图表操作:")) self.export_btn = QPushButton("导出图表") self.export_btn.clicked.connect(self.export_chart) chart_btn_layout.addWidget(self.export_btn) self.clear_btn = QPushButton("清除图表") self.clear_btn.clicked.connect(self.clear_chart) chart_btn_layout.addWidget(self.clear_btn) self.zoom_reset_btn = QPushButton("重置缩放") self.zoom_reset_btn.clicked.connect(self.reset_zoom) chart_btn_layout.addWidget(self.zoom_reset_btn) group_select_layout.addLayout(chart_btn_layout, 1) # 占1/4宽度 layout.addLayout(group_select_layout, 1) # 控制面板占1/4高度 # 初始化后更新组列表 self.update_group_list() def setup_info_tab(self): layout = QVBoxLayout() self.info_tab.setLayout(layout) # 执行日志 exec_group = QGroupBox("执行日志") exec_layout = QVBoxLayout() self.exec_log = QTextEdit() self.exec_log.setReadOnly(True) exec_layout.addWidget(self.exec_log) btn_layout = QHBoxLayout() self.clear_log_btn = QPushButton("清除日志") self.clear_log_btn.clicked.connect(lambda: self.exec_log.clear()) btn_layout.addWidget(self.clear_log_btn) self.export_log_btn = QPushButton("导出日志") self.export_log_btn.clicked.connect(self.export_log) btn_layout.addWidget(self.export_log_btn) exec_layout.addLayout(btn_layout) exec_group.setLayout(exec_layout) layout.addWidget(exec_group) # 网卡状态 nic_group = QGroupBox("网卡状态监控") nic_layout = QVBoxLayout() self.nic_status = QTextEdit() self.nic_status.setReadOnly(True) nic_layout.addWidget(self.nic_status) # 网卡事件摘要 self.nic_summary_text = QTextEdit() self.nic_summary_text.setReadOnly(True) nic_layout.addWidget(QLabel("网卡事件摘要:")) nic_layout.addWidget(self.nic_summary_text) # 网卡映射 self.nic_mapping_text = QTextEdit() self.nic_mapping_text.setReadOnly(True) nic_layout.addWidget(QLabel("网卡测试组映射:")) nic_layout.addWidget(self.nic_mapping_text) nic_btn_layout = QHBoxLayout() self.refresh_nic_btn = QPushButton("刷新状态") self.refresh_nic_btn.clicked.connect(self.update_nic_status) nic_btn_layout.addWidget(self.refresh_nic_btn) self.export_nic_btn = QPushButton("导出状态") self.export_nic_btn.clicked.connect(self.export_nic_status) nic_btn_layout.addWidget(self.export_nic_btn) self.clear_nic_btn = QPushButton("清除事件") self.clear_nic_btn.clicked.connect(self.clear_nic_events) nic_btn_layout.addWidget(self.clear_nic_btn) nic_layout.addLayout(nic_btn_layout) nic_group.setLayout(nic_layout) layout.addWidget(nic_group) # 初始更新网卡状态 self.update_nic_status(initial=True) def browse_iperf_path(self): """浏览并选择iperf3可执行文件""" file_path, _ = QFileDialog.getOpenFileName( self, "选择iperf3可执行文件", "", "可执行文件 (*.exe);;所有文件 (*)" ) if file_path: self.iperf_path.setText(file_path) self.config['iperf_path'] = file_path self.save_config() def test_iperf_path(self): """测试iperf3路径是否有效""" path = self.iperf_path.text() if not path: QMessageBox.warning(self, "警告", "请先选择iperf3路径") return try: result = subprocess.run( [path, '-v'], capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW ) if 'iperf 3.' in result.stdout: QMessageBox.information(self, "测试成功", f"iperf3版本信息:\n{result.stdout.splitlines()[0]}") self.config['iperf_path'] = path self.save_config() else: QMessageBox.critical(self, "错误", "选择的文件不是有效的iperf3可执行文件") except Exception as e: QMessageBox.critical(self, "错误", f"测试失败: {str(e)}") def start_nic_monitor(self): """启动网卡事件监听线程""" self.nic_monitor = NicMonitorThread() self.nic_monitor.nic_event.connect(self.handle_nic_event) self.nic_monitor.error_signal.connect(self.handle_nic_error) # 连接错误信号 self.nic_monitor.start() def handle_nic_error(self, error_msg): """处理网卡监听错误""" self.exec_log.append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {error_msg}") # 如果无法使用事件监听,回退到轮询方式 if "无法初始化WMI连接" in error_msg or "创建网卡监听器出错" in error_msg: self.exec_log.append("将使用轮询方式监控网卡状态") self.start_polling_nic_status() def start_polling_nic_status(self): """启动轮询方式监控网卡状态""" self.last_nic_status = {} # 初始化状态记录 self.nic_poll_timer = QTimer() self.nic_poll_timer.timeout.connect(self.poll_nic_status) self.nic_poll_timer.start(1000) # 每秒轮询一次 def poll_nic_status(self): """轮询网卡状态""" try: c = wmi.WMI() # 获取所有网卡状态 for adapter in c.Win32_NetworkAdapter(): status = 'unknown' if adapter.NetConnectionStatus == 2: status = 'up' elif adapter.NetConnectionStatus == 7: status = 'down' # 只处理状态变化的网卡 if adapter.Name in self.last_nic_status and self.last_nic_status[adapter.Name] != status: event_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] self.handle_nic_event({'name': adapter.Name, 'event': status, 'time': event_time}) # 记录当前状态 self.last_nic_status[adapter.Name] = status except Exception as e: self.exec_log.append(f"轮询网卡状态出错: {str(e)}") def handle_nic_event(self, event): """处理网卡事件""" name = event['name'] event_type = event['event'] event_time = event['time'] # 检查网卡映射 affected_groups = [] if name in self.nic_map: for group_id, port, role in self.nic_map[name]: affected_groups.append(f"测试组{group_id}的{role}端口{port}") # 记录事件 event_msg = f"[{event_time}] 网卡 '{name}' 状态变化: {event_type}" if affected_groups: event_msg += f" (影响: {', '.join(affected_groups)})" self.nic_events.append(event_msg) # 更新汇总 if name not in self.nic_summary: self.nic_summary[name] = {'down': 0, 'up': 0, 'last_status': 'unknown'} if event_type == 'down': self.nic_summary[name]['down'] += 1 elif event_type == 'up': self.nic_summary[name]['up'] += 1 self.nic_summary[name]['last_status'] = event_type # 更新网卡状态显示 self.update_nic_status() def add_test_group(self): # 计算当前可用最小ID existing_ids = set(self.config['test_groups'].keys()) group_id = 1 while group_id in existing_ids: group_id += 1 if group_id > 30: QMessageBox.warning(self, "警告", "最多只能添加30个测试组") return group = QGroupBox(f"测试组 {group_id}") group.setProperty('group_id', group_id) # 设置属性用于标识 group_layout = QVBoxLayout() # 组名 name_layout = QHBoxLayout() name_layout.addWidget(QLabel("组名:")) group_name = QLineEdit(f"测试组{group_id}") name_layout.addWidget(group_name) group_layout.addLayout(name_layout) # Server配置 server_group = QGroupBox("Server配置") server_layout = QVBoxLayout() ip_layout = QHBoxLayout() ip_layout.addWidget(QLabel("IP地址*:")) server_ip = QLineEdit() server_ip.setPlaceholderText("必须填写服务器IP") ip_layout.addWidget(server_ip) server_layout.addLayout(ip_layout) port_layout = QHBoxLayout() port_layout.addWidget(QLabel("端口号:")) server_port = QSpinBox() server_port.setRange(1024, 65535) server_port.setValue(5201 + group_id - 1) port_layout.addWidget(server_port) server_layout.addLayout(port_layout) # 添加网卡选择 nic_layout = QHBoxLayout() nic_layout.addWidget(QLabel("绑定网卡:")) server_nic = QComboBox() server_nic.addItem("") # 空选项 # 获取系统网卡列表 try: c = wmi.WMI() for adapter in c.Win32_NetworkAdapter(NetConnectionStatus=2): # 2表示已连接 server_nic.addItem(adapter.Name) except Exception as e: print(f"获取网卡列表失败: {e}") nic_layout.addWidget(server_nic) server_layout.addLayout(nic_layout) server_group.setLayout(server_layout) group_layout.addWidget(server_group) # Client配置 client_group = QGroupBox("Client配置") client_layout = QVBoxLayout() c_ip_layout = QHBoxLayout() c_ip_layout.addWidget(QLabel("IP地址*:")) client_ip = QLineEdit() client_ip.setPlaceholderText("必须填写客户端IP") c_ip_layout.addWidget(client_ip) client_layout.addLayout(c_ip_layout) c_name_layout = QHBoxLayout() c_name_layout.addWidget(QLabel("客户端名称:")) client_name = QLineEdit(f"客户端{group_id}") c_name_layout.addWidget(client_name) client_layout.addLayout(c_name_layout) # 添加网卡选择 c_nic_layout = QHBoxLayout() c_nic_layout.addWidget(QLabel("绑定网卡:")) client_nic = QComboBox() client_nic.addItem("") # 空选项 # 获取系统网卡列表 try: c = wmi.WMI() for adapter in c.Win32_NetworkAdapter(NetConnectionStatus=2): # 2表示已连接 client_nic.addItem(adapter.Name) except Exception as e: print(f"获取网卡列表失败: {e}") c_nic_layout.addWidget(client_nic) client_layout.addLayout(c_nic_layout) client_group.setLayout(client_layout) group_layout.addWidget(client_group) # 测试参数 param_group = QGroupBox("测试参数") param_layout = QVBoxLayout() direction_layout = QHBoxLayout() direction_layout.addWidget(QLabel("测试方向:")) test_direction = QComboBox() test_direction.addItems(["上传", "下载", "双向"]) test_direction.currentIndexChanged.connect(lambda idx, gid=group_id: self.update_port_visibility(gid, idx)) direction_layout.addWidget(test_direction) param_layout.addLayout(direction_layout) # 下载端口配置(默认隐藏) download_port_layout = QHBoxLayout() download_port_layout.addWidget(QLabel("下载端口号:")) download_port = QSpinBox() download_port.setRange(1024, 65535) download_port.setValue(server_port.value() + 1) download_port_layout.addWidget(download_port) # 初始时隐藏下载端口配置(除非是双向测试) if test_direction.currentIndex() != 2: download_port_layout.itemAt(0).widget().hide() download_port_layout.itemAt(1).widget().hide() param_layout.addLayout(download_port_layout) time_layout = QHBoxLayout() time_layout.addWidget(QLabel("测试时长:")) test_time = QDoubleSpinBox() test_time.setRange(1, 10000) test_time.setValue(10) time_layout.addWidget(test_time) time_unit = QComboBox() time_unit.addItems(["秒", "分钟", "小时"]) time_layout.addWidget(time_unit) param_layout.addLayout(time_layout) omit_layout = QHBoxLayout() omit_layout.addWidget(QLabel("跳过前N秒:")) omit_seconds = QSpinBox() omit_seconds.setRange(0, 60) omit_seconds.setValue(5) omit_layout.addWidget(omit_seconds) param_layout.addLayout(omit_layout) streams_layout = QHBoxLayout() streams_layout.addWidget(QLabel("并行流数量:")) streams = QSpinBox() streams.setRange(1, 100) streams.setValue(1) streams_layout.addWidget(streams) param_layout.addLayout(streams_layout) auto_restart = QCheckBox("每6小时自动重跑") auto_restart.setChecked(True) param_layout.addWidget(auto_restart) error_restart = QCheckBox("异常自动重跑") error_restart.setChecked(True) param_layout.addWidget(error_restart) restart_count_layout = QHBoxLayout() restart_count_layout.addWidget(QLabel("重跑次数:")) restart_count = QSpinBox() restart_count.setRange(1, 10) restart_count.setValue(3) restart_count_layout.addWidget(restart_count) param_layout.addLayout(restart_count_layout) param_group.setLayout(param_layout) group_layout.addWidget(param_group) # 删除按钮 delete_btn = QPushButton("删除此组") delete_btn.clicked.connect(lambda: self.remove_test_group(group)) group_layout.addWidget(delete_btn) group.setLayout(group_layout) self.test_groups_layout.addWidget(group) # 将控件保存为group的属性,以便后续读取 group.server_ip_edit = server_ip group.client_ip_edit = client_ip # 保存配置(只保存控件值) self.config['test_groups'][group_id] = { 'name': group_name.text(), 'server_ip': server_ip.text(), # 确保保存的是文本内容 'server_port': server_port.value(), 'server_nic': server_nic.currentText(), # 保存网卡名称 'download_port': download_port.value(), 'client_ip': client_ip.text(), # 确保保存的是文本内容 'client_name': client_name.text(), 'client_nic': client_nic.currentText(), # 保存网卡名称 'direction': test_direction.currentIndex(), 'test_time': test_time.value(), 'time_unit': time_unit.currentIndex(), 'omit': omit_seconds.value(), 'streams': streams.value(), 'auto_restart': auto_restart.isChecked(), 'error_restart': error_restart.isChecked(), 'restart_count': restart_count.value() } # 更新组选择列表 if hasattr(self, 'group_list'): self.update_group_list() def update_port_visibility(self, group_id, direction_idx): """根据测试方向显示/隐藏下载端口配置""" # 找到对应的group_widget for child in self.test_groups_widget.children(): if isinstance(child, QGroupBox) and child.property('group_id') == group_id: # 找到下载端口控件 download_port_label = None download_port_widget = None for layout in child.findChildren(QHBoxLayout): if layout.itemAt(0) and layout.itemAt(0).widget() and layout.itemAt(0).widget().text() == "下载端口号:": download_port_label = layout.itemAt(0).widget() download_port_widget = layout.itemAt(1).widget() break if download_port_label and download_port_widget: # 双向测试时显示下载端口配置 if direction_idx == 2: # 双向 download_port_label.show() download_port_widget.show() else: download_port_label.hide() download_port_widget.hide() break def remove_test_group(self, group_widget): group_id = group_widget.property('group_id') if group_id: # 从布局中移除 self.test_groups_layout.removeWidget(group_widget) group_widget.deleteLater() # 从配置中移除 if group_id in self.config['test_groups']: del self.config['test_groups'][group_id] self.save_config() # 更新组选择列表 if hasattr(self, 'group_list'): self.update_group_list() def update_group_list(self): """更新测试组选择列表""" if not hasattr(self, 'group_list'): return self.group_list.clear() for group_id in self.config['test_groups'].keys(): item = QListWidgetItem(f"测试组 {group_id}") item.setData(Qt.UserRole, group_id) item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) self.group_list.addItem(item) def select_all_groups(self): """选择所有测试组""" if not hasattr(self, 'group_list'): return for i in range(self.group_list.count()): item = self.group_list.item(i) item.setCheckState(Qt.Checked) self.update_chart() def deselect_all_groups(self): """取消选择所有测试组""" if not hasattr(self, 'group_list'): return for i in range(self.group_list.count()): item = self.group_list.item(i) item.setCheckState(Qt.Unchecked) self.update_chart() def browse_log_path(self): path = QFileDialog.getExistingDirectory(self, "选择日志保存路径") if path: self.log_path.setText(path) self.config['log_path'] = path self.save_config() def start_test(self): # 验证配置 if not self.config['iperf_path']: QMessageBox.critical(self, "错误", "请先配置iperf3路径") return # 创建管理器(使用配置的iperf路径) self.manager = IperfManager(self.config['iperf_path']) # 禁用UI控件 self.start_btn.setEnabled(False) self.add_group_btn.setEnabled(False) self.stop_btn.setEnabled(True) # 清除图表 self.clear_chart() # 记录开始时间 self.start_time = datetime.now() self.canvas.start_time = self.start_time self.canvas.start_timestamp = time.time() self.exec_log.append(f"[{self.start_time.strftime('%Y-%m-%d %H:%M:%S')}] 测试开始") # 收集测试组配置 - 直接从界面控件读取最新值 test_groups = {} self.nic_map = {} # 重置网卡映射 # 遍历所有测试组控件 for i in range(self.test_groups_layout.count()): group_widget = self.test_groups_layout.itemAt(i).widget() if not group_widget or not isinstance(group_widget, QGroupBox): continue group_id = group_widget.property('group_id') if not group_id: continue # 直接从控件读取值 server_ip = group_widget.server_ip_edit.text().strip() server_port = group_widget.server_port_spin.value() server_nic = group_widget.server_nic_combo.currentText() download_port = group_widget.download_port_spin.value() client_ip = group_widget.client_ip_edit.text().strip() client_name = group_widget.client_name_edit.text() client_nic = group_widget.client_nic_combo.currentText() test_direction_idx = group_widget.test_direction_combo.currentIndex() test_time_val = group_widget.test_time_spin.value() time_unit_idx = group_widget.time_unit_combo.currentIndex() omit = group_widget.omit_spin.value() streams = group_widget.streams_spin.value() # 计算测试时长(秒)- 转换为整数秒 if time_unit_idx == 1: # 分钟 duration = test_time_val * 60 elif time_unit_idx == 2: # 小时 duration = test_time_val * 3600 else: # 秒 duration = test_time_val duration = max(1, int(round(duration))) test_direction = ["上传", "下载", "双向"][test_direction_idx] # 调试信息 self.exec_log.append(f"测试组{group_id}配置: server_ip={server_ip}, client_ip={client_ip}") test_groups[group_id] = { 'server_ip': server_ip, 'server_port': server_port, 'client_ip': client_ip, 'direction': test_direction, 'duration': duration, # 使用整数秒 'streams': streams, 'omit': omit, 'upload_port': server_port, # 上传端口 'download_port': download_port # 下载端口 } # 记录网卡映射 if server_nic: if server_nic not in self.nic_map: self.nic_map[server_nic] = [] self.nic_map[server_nic].append((group_id, server_port, "Server")) if client_nic: if client_nic not in self.nic_map: self.nic_map[client_nic] = [] self.nic_map[client_nic].append((group_id, server_port, "Client")) # 启动服务器 for group_id, config in test_groups.items(): try: # 获取网卡名称 server_nic = "" for i in range(self.test_groups_layout.count()): group_widget = self.test_groups_layout.itemAt(i).widget() if group_widget.property('group_id') == group_id: server_nic = group_widget.server_nic_combo.currentText() break # 启动服务器 command = self.manager.start_server( group_id, config['server_port'], config['server_ip'], server_nic ) self.exec_log.append(f"[测试组{group_id}] 启动服务器: {command}") except ValueError as e: self.exec_log.append(f"[错误] 测试组{group_id}服务器启动失败: {str(e)}") QMessageBox.critical(self, "错误", f"测试组{group_id}服务器启动失败: {str(e)}") self.stop_test() return # 等待服务器启动 time.sleep(1) # 启动客户端 for group_id, config in test_groups.items(): try: # 获取网卡名称 client_nic = "" for i in range(self.test_groups_layout.count()): group_widget = self.test_groups_layout.itemAt(i).widget() if group_widget.property('group_id') == group_id: client_nic = group_widget.client_nic_combo.currentText() break # 启动客户端 reverse = config['direction'] in ["下载", "双向"] command = self.manager.start_client( group_id, config['server_ip'], config['server_port'], config['duration'], config['streams'], config['client_ip'], reverse, config['omit'], client_nic ) self.exec_log.append(f"[测试组{group_id}] 启动客户端: {command}") # 如果是双向测试,启动下载客户端 if config['direction'] == "双向": command = self.manager.start_client( group_id, config['server_ip'], config['download_port'], config['duration'], config['streams'], config['client_ip'], False, # 上传方向 config['omit'], client_nic ) self.exec_log.append(f"[测试组{group_id}] 启动下载客户端: {command}") except ValueError as e: self.exec_log.append(f"[错误] 测试组{group_id}客户端启动失败: {str(e)}") QMessageBox.critical(self, "错误", f"测试组{group_id}客户端启动失败: {str(e)}") self.stop_test() return # 启动数据收集线程 self.data_collector = DataCollectorThread(self.manager, test_groups) self.data_collector.data_updated.connect(self.update_chart) self.data_collector.test_completed.connect(self.test_finished) self.data_collector.test_timeout.connect(self.test_timeout_handler) self.data_collector.start() def stop_test(self): """停止所有测试""" if self.manager: self.manager.stop_all() if self.data_collector: self.data_collector.stop() self.exec_log.append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 测试停止") # 启用UI控件 self.start_btn.setEnabled(True) self.add_group_btn.setEnabled(True) self.stop_btn.setEnabled(False) def test_finished(self, group_id, full_data): """测试完成处理""" self.exec_log.append(f"[测试组{group_id}] 测试完成") # 自动保存日志 if self.auto_save.isChecked(): self.save_log(group_id, full_data) def test_timeout_handler(self): """测试超时处理""" self.exec_log.append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 测试超时") self.stop_test() def update_chart(self, data=None): """更新图表""" if not data: # 如果没有数据,显示空图表 self.canvas.ax.clear() self.canvas.ax.set_xlabel('时间 (秒)') self.canvas.ax.set_ylabel('吞吐量 (Mbps)') self.canvas.ax.set_title('iPerf3 流量监控') self.canvas.ax.grid(True) self.canvas.draw() return # 获取选中的测试组 selected_groups = set() for i in range(self.group_list.count()): item = self.group_list.item(i) if item.checkState() == Qt.Checked: selected_groups.add(item.data(Qt.UserRole)) # 更新图表 self.canvas.update_plot(data, selected_groups) def clear_chart(self): """清除图表数据""" self.canvas.ax.clear() self.canvas.ax.set_xlabel('时间 (秒)') self.canvas.ax.set_ylabel('吞吐量 (Mbps)') self.canvas.ax.set_title('iPerf3 流量监控') self.canvas.ax.grid(True) self.canvas.draw() def reset_zoom(self): """重置图表缩放""" self.canvas.ax.relim() self.canvas.ax.autoscale_view() self.canvas.draw() def export_chart(self): """导出图表为图片""" file_path, _ = QFileDialog.getSaveFileName( self, "保存图表", "", "PNG图片 (*.png);;所有文件 (*)" ) if file_path: self.canvas.fig.savefig(file_path, dpi=300) self.exec_log.append(f"图表已保存到: {file_path}") def export_log(self): """导出执行日志""" file_path, _ = QFileDialog.getSaveFileName( self, "保存日志", "", "文本文件 (*.txt);;所有文件 (*)" ) if file_path: with open(file_path, 'w', encoding='utf-8') as f: f.write(self.exec_log.toPlainText()) self.exec_log.append(f"日志已保存到: {file_path}") def update_nic_status(self, initial=False): """更新网卡状态显示""" # 网卡状态 self.nic_status.clear() try: c = wmi.WMI() for adapter in c.Win32_NetworkAdapter(): status = "未知" if adapter.NetConnectionStatus == 2: status = "已连接" elif adapter.NetConnectionStatus == 7: status = "已断开" self.nic_status.append(f"{adapter.Name}: {status}") except Exception as e: self.nic_status.append(f"获取网卡状态失败: {str(e)}") # 网卡事件摘要 self.nic_summary_text.clear() for nic, summary in self.nic_summary.items(): self.nic_summary_text.append(f"{nic}: 断开次数={summary['down']}, 连接次数={summary['up']}, 当前状态={summary['last_status']}") # 网卡映射 self.nic_mapping_text.clear() for nic, mappings in self.nic_map.items(): for group_id, port, role in mappings: self.nic_mapping_text.append(f"{nic} -> 测试组{group_id} {role}端口{port}") def export_nic_status(self): """导出网卡状态""" file_path, _ = QFileDialog.getSaveFileName( self, "保存网卡状态", "", "文本文件 (*.txt);;所有文件 (*)" ) if file_path: with open(file_path, 'w', encoding='utf-8') as f: f.write("=== 网卡状态 ===\n") f.write(self.nic_status.toPlainText() + "\n\n") f.write("=== 网卡事件摘要 ===\n") f.write(self.nic_summary_text.toPlainText() + "\n\n") f.write("=== 网卡测试组映射 ===\n") f.write(self.nic_mapping_text.toPlainText()) self.exec_log.append(f"网卡状态已保存到: {file_path}") def clear_nic_events(self): """清除网卡事件记录""" self.nic_events = [] self.nic_summary = {} self.update_nic_status() def save_log(self, group_id, full_data): """保存测试日志""" if not self.auto_save.isChecked(): return log_dir = self.log_path.text() if not os.path.exists(log_dir): os.makedirs(log_dir) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") file_path = os.path.join(log_dir, f"iperf_test_{group_id}_{timestamp}.json") with open(file_path, 'w', encoding='utf-8') as f: json.dump(full_data, f, indent=4) self.exec_log.append(f"[测试组{group_id}] 日志已保存到: {file_path}") def save_config(self): """保存配置到文件""" # 更新配置 self.config['iperf_path'] = self.iperf_path.text() self.config['log_path'] = self.log_path.text() # 保存到文件 with open('iperf_visualizer_config.json', 'w', encoding='utf-8') as f: json.dump(self.config, f, indent=4) def load_config(self): """从文件加载配置""" try: if os.path.exists('iperf_visualizer_config.json'): with open('iperf_visualizer_config.json', 'r', encoding='utf-8') as f: self.config = json.load(f) # 更新UI self.iperf_path.setText(self.config['iperf_path']) self.log_path.setText(self.config['log_path']) # 创建测试组 for group_id, group in self.config['test_groups'].items(): self.add_test_group_from_config(group_id, group) except Exception as e: print(f"加载配置失败: {str(e)}") def add_test_group_from_config(self, group_id, group_config): """从配置添加测试组""" group = QGroupBox(f"测试组 {group_id}") group.setProperty('group_id', group_id) group_layout = QVBoxLayout() # 组名 name_layout = QHBoxLayout() name_layout.addWidget(QLabel("组名:")) group_name = QLineEdit(group_config['name']) name_layout.addWidget(group_name) group_layout.addLayout(name_layout) # Server配置 server_group = QGroupBox("Server配置") server_layout = QVBoxLayout() ip_layout = QHBoxLayout() ip_layout.addWidget(QLabel("IP地址*:")) server_ip = QLineEdit(group_config['server_ip']) server_ip.setPlaceholderText("必须填写服务器IP") ip_layout.addWidget(server_ip) server_layout.addLayout(ip_layout) port_layout = QHBoxLayout() port_layout.addWidget(QLabel("端口号:")) server_port = QSpinBox() server_port.setRange(1024, 65535) server_port.setValue(group_config['server_port']) port_layout.addWidget(server_port) server_layout.addLayout(port_layout) # 添加网卡选择 nic_layout = QHBoxLayout() nic_layout.addWidget(QLabel("绑定网卡:")) server_nic = QComboBox() server_nic.addItem("") # 空选项 # 获取系统网卡列表 try: c = wmi.WMI() for adapter in c.Win32_NetworkAdapter(NetConnectionStatus=2): # 2表示已连接 server_nic.addItem(adapter.Name) if adapter.Name == group_config['server_nic']: server_nic.setCurrentText(adapter.Name) except Exception as e: print(f"获取网卡列表失败: {e}") nic_layout.addWidget(server_nic) server_layout.addLayout(nic_layout) server_group.setLayout(server_layout) group_layout.addWidget(server_group) # Client配置 client_group = QGroupBox("Client配置") client_layout = QVBoxLayout() c_ip_layout = QHBoxLayout() c_ip_layout.addWidget(QLabel("IP地址*:")) client_ip = QLineEdit(group_config['client_ip']) client_ip.setPlaceholderText("必须填写客户端IP") c_ip_layout.addWidget(client_ip) client_layout.addLayout(c_ip_layout) c_name_layout = QHBoxLayout() c_name_layout.addWidget(QLabel("客户端名称:")) client_name = QLineEdit(group_config['client_name']) c_name_layout.addWidget(client_name) client_layout.addLayout(c_name_layout) # 添加网卡选择 c_nic_layout = QHBoxLayout() c_nic_layout.addWidget(QLabel("绑定网卡:")) client_nic = QComboBox() client_nic.addItem("") # 空选项 # 获取系统网卡列表 try: c = wmi.WMI() for adapter in c.Win32_NetworkAdapter(NetConnectionStatus=2): # 2表示已连接 client_nic.addItem(adapter.Name) if adapter.Name == group_config['client_nic']: client_nic.setCurrentText(adapter.Name) except Exception as e: print(f"获取网卡列表失败: {e}") c_nic_layout.addWidget(client_nic) client_layout.addLayout(c_nic_layout) client_group.setLayout(client_layout) group_layout.addWidget(client_group) # 测试参数 param_group = QGroupBox("测试参数") param_layout = QVBoxLayout() direction_layout = QHBoxLayout() direction_layout.addWidget(QLabel("测试方向:")) test_direction = QComboBox() test_direction.addItems(["上传", "下载", "双向"]) test_direction.setCurrentIndex(group_config['direction']) test_direction.currentIndexChanged.connect(lambda idx, gid=group_id: self.update_port_visibility(gid, idx)) direction_layout.addWidget(test_direction) param_layout.addLayout(direction_layout) # 下载端口配置 download_port_layout = QHBoxLayout() download_port_layout.addWidget(QLabel("下载端口号:")) download_port = QSpinBox() download_port.setRange(1024, 65535) download_port.setValue(group_config['download_port']) download_port_layout.addWidget(download_port) # 初始时隐藏下载端口配置(除非是双向测试) if group_config['direction'] != 2: download_port_layout.itemAt(0).widget().hide() download_port_layout.itemAt(1).widget().hide() param_layout.addLayout(download_port_layout) time_layout = QHBoxLayout() time_layout.addWidget(QLabel("测试时长:")) test_time = QDoubleSpinBox() test_time.setRange(1, 10000) test_time.setValue(group_config['test_time']) time_layout.addWidget(test_time) time_unit = QComboBox() time_unit.addItems(["秒", "分钟", "小时"]) time_unit.setCurrentIndex(group_config['time_unit']) time_layout.addWidget(time_unit) param_layout.addLayout(time_layout) omit_layout = QHBoxLayout() omit_layout.addWidget(QLabel("跳过前N秒:")) omit_seconds = QSpinBox() omit_seconds.setRange(0, 60) omit_seconds.setValue(group_config['omit']) omit_layout.addWidget(omit_seconds) param_layout.addLayout(omit_layout) streams_layout = QHBoxLayout() streams_layout.addWidget(QLabel("并行流数量:")) streams = QSpinBox() streams.setRange(1, 100) streams.setValue(group_config['streams']) streams_layout.addWidget(streams) param_layout.addLayout(streams_layout) auto_restart = QCheckBox("每6小时自动重跑") auto_restart.setChecked(group_config['auto_restart']) param_layout.addWidget(auto_restart) error_restart = QCheckBox("异常自动重跑") error_restart.setChecked(group_config['error_restart']) param_layout.addWidget(error_restart) restart_count_layout = QHBoxLayout() restart_count_layout.addWidget(QLabel("重跑次数:")) restart_count = QSpinBox() restart_count.setRange(1, 10) restart_count.setValue(group_config['restart_count']) restart_count_layout.addWidget(restart_count) param_layout.addLayout(restart_count_layout) param_group.setLayout(param_layout) group_layout.addWidget(param_group) # 删除按钮 delete_btn = QPushButton("删除此组") delete_btn.clicked.connect(lambda: self.remove_test_group(group)) group_layout.addWidget(delete_btn) group.setLayout(group_layout) self.test_groups_layout.addWidget(group) # 将控件保存为group的属性,以便后续读取 group.server_ip_edit = server_ip group.client_ip_edit = client_ip def closeEvent(self, event): """关闭应用时保存配置""" self.save_config() if self.nic_monitor: self.nic_monitor.stop() event.accept() if __name__ == "__main__": app = QApplication(sys.argv) window = IperfVisualizer() window.show() sys.exit(app.exec_())
最新发布
08-14
import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib.dates as mdates import pywt from statsmodels.tsa.seasonal import seasonal_decompose from scipy import signal class HydrologicalAnalysis: def __init__(self, data_path=None): """ 水文时间序列分析工具 :param data_path: 数据文件路径(CSV格式) """ self.data = None if data_path: self.load_data(data_path) def load_data(self, file_path): """ 加载水文监测数据 :param file_path: CSV文件路径 """ # 读取数据并处理时间格式 self.data = pd.read_csv(file_path, parse_dates=['时间'], index_col='时间') # 检查必要字段 required_columns = ['水位(m)', '流量(m3/s)', '含沙量(kg/m3)'] if not all(col in self.data.columns for col in required_columns): raise ValueError("数据文件缺少必要列:水位(m), 流量(m3/s), 含沙量(kg/m3)") print(f"成功加载数据:{len(self.data)}条记录") return self.data def preprocess_data(self): """ 数据预处理流程 """ if self.data is None: raise ValueError("未加载数据,请先调用load_data方法") print("开始数据预处理...") # 1. 缺失值处理(前向填充+线性插值) self.data = self.data.ffill().interpolate(method='linear') # 2. 异常值处理(3σ原则) for col in ['流量(m3/s)', '含沙量(kg/m3)']: mean = self.data[col].mean() std = self.data[col].std() self.data[col] = np.where( (self.data[col] < mean - 3 * std) | (self.data[col] > mean + 3 * std), mean, self.data[col] ) # 3. 重采样为日数据 daily_data = self.data.resample('D').mean() print("数据预处理完成") return daily_data def seasonal_decomposition(self, series, period=12): """ 时间序列季节性分解 :param series: 时间序列数据 :param period: 季节性周期(月数据默认为12) :return: 分解结果 """ result = seasonal_decompose(series, model='additive', period=period) return result def wavelet_analysis(self, series, title='水文序列'): """ 小波分析主函数 :param series: 时间序列数据 :param title: 分析标题 :return: (小波系数, 主周期) """ # 1. 参数设置 scales = np.arange(1, 365) # 1天到1年尺度 wavelet = 'morl' # Morlet小波 # 2. 小波变换 coef, freqs = pywt.cwt(series, scales, wavelet) # 3. 小波方差计算 variance = np.mean(np.abs(coef) ** 2, axis=1) main_scale = scales[np.argmax(variance)] # 4. 可视化 self.plot_wavelet_results(series, scales, coef, variance, main_scale, title) return coef, main_scale def plot_wavelet_results(self, series, scales, coef, variance, main_scale, title): """ 绘制小波分析结果 """ plt.figure(figsize=(15, 12)) # 1. 原始序列图 plt.subplot(3, 1, 1) plt.plot(series.index, series) plt.title(f'{title} - 原始时间序列') plt.xlabel('日期') plt.ylabel('数值') plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) # 2. 小波系数实部等值线图 plt.subplot(3, 1, 2) plt.contourf(series.index, scales, np.real(coef), cmap='jet', levels=100) plt.colorbar(label='小波系数实部') plt.title(f'{title} - 小波系数实部等值线图') plt.ylabel('时间尺度(天)') plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) # 3. 小波方差图 plt.subplot(3, 1, 3) plt.plot(scales, variance) plt.axvline(main_scale, color='red', linestyle='--', label=f'主周期: {main_scale}天') plt.title(f'{title} - 小波方差分析') plt.xlabel('时间尺度(天)') plt.ylabel('方差') plt.legend() plt.tight_layout() plt.savefig(f'{title}_小波分析.png', dpi=300) plt.show() print(f"{title}主周期: {main_scale}天") def full_analysis(self): """ 完整分析流程 """ # 1. 数据预处理 daily_data = self.preprocess_data() # 2. 季节性分解 plt.figure(figsize=(15, 10)) for i, col in enumerate(['流量(m3/s)', '含沙量(kg/m3)'], 1): plt.subplot(2, 1, i) result = self.seasonal_decomposition(daily_data[col]) result.plot() plt.title(f'{col}季节性分解') plt.tight_layout() plt.savefig('季节性分解.png', dpi=300) plt.show() # 3. 小波分析 flow_coef, flow_period = self.wavelet_analysis( daily_data['流量(m3/s)'], '流量' ) sediment_coef, sediment_period = self.wavelet_analysis( daily_data['含沙量(kg/m3)'], '含沙量' ) # 4. 交叉小波分析(流量与含沙量关系) self.cross_wavelet_analysis( daily_data['流量(m3/s)'], daily_data['含沙量(kg/m3)'], '流量-含沙量' ) return { 'flow_period': flow_period, 'sediment_period': sediment_period } def cross_wavelet_analysis(self, series1, series2, title='交叉分析'): """ 交叉小波分析 :param series1: 第一个时间序列 :param series2: 第二个时间序列 :param title: 分析标题 """ # 1. 计算小波变换 scales = np.arange(1, 365) wavelet = 'morl' coef1, _ = pywt.cwt(series1, scales, wavelet) coef2, _ = pywt.cwt(series2, scales, wavelet) # 2. 计算交叉小波谱 cross_spectrum = coef1 * np.conj(coef2) # 3. 可视化 plt.figure(figsize=(15, 8)) # 交叉小波谱实部 plt.subplot(2, 1, 1) plt.contourf(series1.index, scales, np.real(cross_spectrum), cmap='RdBu_r', levels=100) plt.colorbar(label='实部') plt.title(f'{title} - 交叉小波谱实部') plt.ylabel('时间尺度(天)') plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) # 交叉小波谱相位 plt.subplot(2, 1, 2) phase = np.angle(cross_spectrum) plt.contourf(series1.index, scales, phase, cmap='hsv', levels=100) plt.colorbar(label='相位(弧度)') plt.title(f'{title} - 交叉小波谱相位') plt.xlabel('日期') plt.ylabel('时间尺度(天)') plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) plt.tight_layout() plt.savefig(f'{title}_交叉小波分析.png', dpi=300) plt.show() # ====================== # 使用示例 # ====================== if __name__ == "__main__": # 1. 创建分析对象 analyzer = HydrologicalAnalysis() # 2. 加载数据(替换为实际文件路径) # analyzer.load_data('水文监测数据.csv') # 3. 生成模拟数据(实际使用时请注释掉) print("生成模拟数据...") dates = pd.date_range(start='2016-01-01', end='2021-12-31', freq='D') flow = np.sin(2 * np.pi * dates.dayofyear / 365) * 100 + 500 + np.random.normal(0, 50, len(dates)) sediment = np.cos(2 * np.pi * dates.dayofyear / 365) * 2 + 5 + np.random.normal(0, 1, len(dates)) analyzer.data = pd.DataFrame({ '水位(m)': np.random.uniform(40, 45, len(dates)), '流量(m3/s)': flow, '含沙量(kg/m3)': sediment }, index=dates) # 4. 执行完整分析 results = analyzer.full_analysis() print("\n分析结果摘要:") print(f"流量主周期: {results['flow_period']}天") print(f"含沙量主周期: {results['sediment_period']}天") 运行结果他说不支持中文就改成英文吧
07-20
``` import tkinter as tk from tkinter import ttk, filedialog import serial import pandas as pd from matplotlib import pyplot as plt # Press Shift+F10 to execute it or replace it with your code. # Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. class SerialAssistant: def __init__(self): self.window = tk.Tk() self.window.title("智能串口助手 v1.0") # 新增初始化语句 # self.serial_connected = False # self.data_buffer = [] # 同时初始化数据缓冲区 # 串口参数配置区 print(f'Hi_1') self.port_label = ttk.Label(self.window, text="选择端口:") self.port_combo = ttk.Combobox(self.window) # 数据接收显示区 self.data_text = tk.Text(self.window, height=15) # print(f'Hi_1') #布局 self.port_label.grid(row=0, column=0, padx=5, pady=5) self.port_combo.grid(row=0, column=1, padx=5, pady=5) self.btn_connect.grid(row=0, column=2, padx=5, pady=5) self.data_text.grid(row=1, column=0, columnspan=3, padx=5, pady=5) self.btn_save.grid(row=2, column=0, padx=5, pady=5) # 控制按钮区 self.btn_connect = ttk.Button(self.window, text="连接", command=self.connect_serial) self.btn_save = ttk.Button(self.window, text="保存数据", command=self.save_data) print(f'Hi_2') #自动检测可用串口 # ports = [port.device for port in serial.tools.list_ports.comports()] # self.port_combo['values'] = ports # if ports: self.port_combo.current(0) def connect_serial(self): print(f'Hi_3') if not self.serial_connected: try: self.ser = serial.Serial( port=self.port_combo.get(), baudrate=115200, timeout=1 ) self.serial_connected = True # 更新状态 self.btn_connect.config(text="断开") self.start_receive_thread() except Exception as e: print(f"连接失败: {str(e)}") else: self.ser.close() self.serial_connected = False # 更新状态 self.btn_connect.config(text="连接")```怎么添加图像处理功能?例如把数据变成曲线图
04-01
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值