Files
Iperf_GUI/iperf_gui - 6.0.2 已覆盖的主要选项.py
2026-01-13 07:38:36 +08:00

1215 lines
45 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
iperf3 图形化工具
支持TCP/UDP测试,多线程处理,设置保存,进度显示等功能
"""
import os
import sys
import json
import subprocess
import threading
import time
import re
from datetime import datetime
from pathlib import Path
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QGridLayout, QGroupBox, QLabel,
QLineEdit, QComboBox, QPushButton, QTextEdit,
QCheckBox, QSpinBox, QDoubleSpinBox, QProgressBar,
QSplitter, QTabWidget, QMessageBox, QFileDialog,
QStatusBar, QSizePolicy, QHeaderView, QTableWidget,
QTableWidgetItem)
from PyQt5.QtCore import (Qt, QThread, pyqtSignal, QSettings, QTimer,
QSize, QRect, QMetaObject, Q_ARG, QByteArray)
from PyQt5.QtGui import QFont, QIcon, QTextCursor, QColor
# 配置文件路径
CONFIG_FILE = "iperf3_gui_config.json"
class Iperf3Worker(QThread):
"""iperf3工作线程"""
# 信号定义
output_received = pyqtSignal(str)
test_completed = pyqtSignal(dict)
progress_updated = pyqtSignal(int, str)
error_occurred = pyqtSignal(str)
def __init__(self, params):
super().__init__()
self.params = params
self.is_running = True
self.process = None
def run(self):
"""执行iperf3测试"""
try:
# 构建命令参数
cmd = ["iperf3"]
if self.params["mode"] == "server":
cmd.extend(["-s"])
if self.params["server_port"]:
cmd.extend(["-p", str(self.params["server_port"])])
if self.params["one_off"]:
cmd.append("-1")
else: # client mode
cmd.extend(["-c", self.params["server_ip"]])
if self.params["server_port"]:
cmd.extend(["-p", str(self.params["server_port"])])
# 通用参数
if self.params["protocol"] == "udp":
cmd.append("-u")
if self.params["bandwidth"]:
cmd.extend(["-b", self.params["bandwidth"]])
if self.params["reverse"]:
cmd.append("-R")
if self.params["time"]:
cmd.extend(["-t", str(self.params["time"])])
if self.params["parallel"]:
# 修复参数错误:缺少"-P"参数
cmd.extend(["-P", str(self.params["parallel"])])
if self.params["window_size"]:
cmd.extend(["-w", self.params["window_size"]])
if self.params["mss"]:
cmd.extend(["-M", str(self.params["mss"])])
if self.params["num"]:
cmd.extend(["-n", self.params["num"]])
if self.params["interval"]:
cmd.extend(["-i", str(self.params["interval"])])
if self.params["format"]:
cmd.extend(["--format", self.params["format"]])
if self.params["json_output"]:
cmd.append("-J")
if self.params["verbose"]:
cmd.append("-V")
if self.params["version"]:
cmd.append("--version")
if self.params["debug"]:
cmd.append("-d")
self.output_received.emit(f"执行命令: {' '.join(cmd)}\n")
# 执行iperf3
self.process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding='utf-8',
errors='ignore',
bufsize=1,
universal_newlines=True
)
# 读取输出
for line in iter(self.process.stdout.readline, ''):
if not self.is_running:
break
if line.strip():
self.output_received.emit(line)
# 解析进度信息
progress_info = self.parse_progress(line)
if progress_info:
self.progress_updated.emit(*progress_info)
# 等待进程结束
return_code = self.process.wait()
if return_code == 0:
self.test_completed.emit({"status": "success", "code": return_code})
else:
self.error_occurred.emit(f"测试失败,返回码: {return_code}")
except FileNotFoundError:
self.error_occurred.emit("未找到iperf3程序,请确保已安装iperf3")
except Exception as e:
self.error_occurred.emit(f"执行错误: {str(e)}")
def parse_progress(self, line):
"""从输出行解析进度信息"""
# 匹配iperf3进度输出(示例:[ 5] 0.00-1.00 sec 1.10 GBytes 9.42 Gbits/sec)
pattern = r'\[\s*\d+\]\s+[\d\.]+-([\d\.]+)\s+sec'
match = re.search(pattern, line)
if match:
current_time = float(match.group(1))
if self.params.get("time"):
total_time = self.params["time"]
progress = int((current_time / total_time) * 100)
return min(progress, 100), f"测试进度: {current_time}/{total_time}秒"
return None
def stop(self):
"""停止测试"""
self.is_running = False
if self.process:
self.process.terminate()
self.process.wait()
class SettingsManager:
"""设置管理器"""
def __init__(self, config_file=CONFIG_FILE):
self.config_file = config_file
self.settings = self.load_settings()
def load_settings(self):
"""加载设置"""
default_settings = {
"server_ip": "127.0.0.1",
"server_port": "5201",
"protocol": 0, # 索引
"mode": 0, # 索引
"time": 10,
"parallel": 1,
"bandwidth": "",
"window_size": "",
"mss": 0, # 改为整数而不是字符串
"num": "",
"interval": 1.0,
"format": 0, # 索引
"reverse": False,
"json_output": False,
"verbose": False,
"version": False,
"debug": False,
"one_off": False,
"window_size_custom": "",
"geometry": None,
"window_state": None
}
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r', encoding='utf-8') as f:
loaded = json.load(f)
# 合并设置,保留默认值用于新增字段
for key in default_settings:
if key in loaded:
# 兼容旧版本:如果是字符串,转换为索引
if key == "protocol" and isinstance(loaded[key], str):
if loaded[key] == "tcp":
default_settings[key] = 0
elif loaded[key] == "udp":
default_settings[key] = 1
else:
default_settings[key] = 0
elif key == "mode" and isinstance(loaded[key], str):
if loaded[key] == "client":
default_settings[key] = 0
elif loaded[key] == "server":
default_settings[key] = 1
else:
default_settings[key] = 0
elif key == "format" and isinstance(loaded[key], str):
format_map = {"": 0, "k": 0, "m": 1, "g": 2}
default_settings[key] = format_map.get(loaded[key].lower(), 0)
elif key == "mss":
# 处理mss字段,确保是整数
mss_value = loaded[key]
if isinstance(mss_value, str):
if mss_value.strip() == "":
default_settings[key] = 0
else:
try:
default_settings[key] = int(mss_value)
except:
default_settings[key] = 0
else:
default_settings[key] = int(mss_value)
elif key == "time" or key == "parallel":
# 确保这些字段是整数
value = loaded[key]
if isinstance(value, str):
if value.strip() == "":
default_settings[key] = 0 if key == "parallel" else 10
else:
try:
default_settings[key] = int(value)
except:
default_settings[key] = default_settings[key]
else:
default_settings[key] = int(value)
elif key == "interval":
# 确保interval是浮点数
value = loaded[key]
if isinstance(value, str):
if value.strip() == "":
default_settings[key] = 1.0
else:
try:
default_settings[key] = float(value)
except:
default_settings[key] = 1.0
else:
default_settings[key] = float(value)
else:
default_settings[key] = loaded[key]
except json.JSONDecodeError:
# JSON文件损坏,删除并重新创建
print(f"JSON配置文件损坏,使用默认设置")
try:
os.remove(self.config_file)
except:
pass
except Exception as e:
print(f"加载设置失败: {e}")
return default_settings
def save_settings(self, settings):
"""保存设置"""
try:
# 处理window_state,如果是QByteArray则转换为十六进制字符串
settings_to_save = settings.copy()
if isinstance(settings_to_save.get("window_state"), QByteArray):
settings_to_save["window_state"] = settings_to_save["window_state"].toHex().data().decode()
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(settings_to_save, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"保存设置失败: {e}")
return False
def get(self, key, default=None):
"""获取设置值"""
return self.settings.get(key, default)
def set(self, key, value):
"""设置值"""
self.settings[key] = value
def save_all(self):
"""保存所有设置"""
return self.save_settings(self.settings)
class Iperf3GUI(QMainWindow):
"""主窗口类"""
def __init__(self):
super().__init__()
self.settings_manager = SettingsManager()
self.worker = None
self.init_ui()
self.load_settings()
self.setup_connections()
def init_ui(self):
"""初始化界面"""
# 设置窗口属性
self.setWindowTitle("iperf3 网络性能测试工具")
self.setMinimumSize(1000, 700)
# 尝试设置图标
icon_path = "icon.ico"
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
# 创建中心部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
# 创建标签页
self.tab_widget = QTabWidget()
main_layout.addWidget(self.tab_widget)
# 创建测试配置标签页
self.create_test_tab()
# 创建结果标签页
self.create_result_tab()
# 创建历史记录标签页
self.create_history_tab()
# 创建底部控制区域
self.create_bottom_controls(main_layout)
# 创建状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪")
# 进度条
self.progress_bar = QProgressBar()
self.status_bar.addPermanentWidget(self.progress_bar)
self.progress_bar.hide()
# 应用样式
self.apply_styles()
def create_test_tab(self):
"""创建测试配置标签页"""
test_tab = QWidget()
self.tab_widget.addTab(test_tab, "测试配置")
layout = QVBoxLayout(test_tab)
layout.setSpacing(15)
# 基础设置组
basic_group = QGroupBox("基础设置")
basic_layout = QGridLayout()
basic_layout.setSpacing(10)
# 测试模式
basic_layout.addWidget(QLabel("测试模式:"), 0, 0)
self.mode_combo = QComboBox()
self.mode_combo.addItems(["客户端", "服务器"])
basic_layout.addWidget(self.mode_combo, 0, 1)
# 服务器地址
basic_layout.addWidget(QLabel("服务器地址:"), 0, 2)
self.server_ip_edit = QLineEdit()
basic_layout.addWidget(self.server_ip_edit, 0, 3)
# 端口
basic_layout.addWidget(QLabel("端口:"), 1, 0)
self.port_edit = QLineEdit()
basic_layout.addWidget(self.port_edit, 1, 1)
# 协议
basic_layout.addWidget(QLabel("协议:"), 1, 2)
self.protocol_combo = QComboBox()
self.protocol_combo.addItems(["TCP", "UDP"])
basic_layout.addWidget(self.protocol_combo, 1, 3)
# 测试时间
basic_layout.addWidget(QLabel("测试时间(秒):"), 2, 0)
self.time_spin = QSpinBox()
self.time_spin.setRange(1, 3600)
self.time_spin.setValue(10)
basic_layout.addWidget(self.time_spin, 2, 1)
# 并行流
basic_layout.addWidget(QLabel("并行流:"), 2, 2)
self.parallel_spin = QSpinBox()
self.parallel_spin.setRange(1, 128)
self.parallel_spin.setValue(1)
basic_layout.addWidget(self.parallel_spin, 2, 3)
basic_group.setLayout(basic_layout)
layout.addWidget(basic_group)
# 高级设置组
advanced_group = QGroupBox("高级设置")
advanced_layout = QGridLayout()
advanced_layout.setSpacing(10)
# 带宽限制
advanced_layout.addWidget(QLabel("带宽限制:"), 0, 0)
self.bandwidth_combo = QComboBox()
self.bandwidth_combo.addItems(["", "1M", "10M", "100M", "1G", "10G"])
self.bandwidth_combo.setEditable(True)
advanced_layout.addWidget(self.bandwidth_combo, 0, 1)
# 窗口大小
advanced_layout.addWidget(QLabel("窗口大小:"), 0, 2)
self.window_combo = QComboBox()
self.window_combo.addItems(["", "32K", "64K", "128K", "256K", "512K", "1M"])
self.window_combo.setEditable(True)
advanced_layout.addWidget(self.window_combo, 0, 3)
# MSS
advanced_layout.addWidget(QLabel("MSS:"), 1, 0)
self.mss_spin = QSpinBox()
self.mss_spin.setRange(0, 9000)
self.mss_spin.setSpecialValueText("默认")
advanced_layout.addWidget(self.mss_spin, 1, 1)
# 数据量
advanced_layout.addWidget(QLabel("传输数据量:"), 1, 2)
self.data_amount_edit = QLineEdit()
advanced_layout.addWidget(self.data_amount_edit, 1, 3)
# 报告间隔
advanced_layout.addWidget(QLabel("报告间隔(秒):"), 2, 0)
self.interval_spin = QDoubleSpinBox()
self.interval_spin.setRange(0.1, 10.0)
self.interval_spin.setSingleStep(0.1)
self.interval_spin.setValue(1.0)
advanced_layout.addWidget(self.interval_spin, 2, 1)
# 单位格式
advanced_layout.addWidget(QLabel("单位格式:"), 2, 2)
self.format_combo = QComboBox()
self.format_combo.addItems(["自动", "K", "M", "G"])
advanced_layout.addWidget(self.format_combo, 2, 3)
advanced_group.setLayout(advanced_layout)
layout.addWidget(advanced_group)
# 选项组
options_group = QGroupBox("选项")
options_layout = QGridLayout()
options_layout.setSpacing(10)
# 复选框
self.reverse_check = QCheckBox("反向测试")
options_layout.addWidget(self.reverse_check, 0, 0)
self.json_check = QCheckBox("JSON输出")
options_layout.addWidget(self.json_check, 0, 1)
self.verbose_check = QCheckBox("详细输出")
options_layout.addWidget(self.verbose_check, 0, 2)
self.version_check = QCheckBox("显示版本")
options_layout.addWidget(self.version_check, 0, 3)
self.debug_check = QCheckBox("调试模式")
options_layout.addWidget(self.debug_check, 1, 0)
self.one_off_check = QCheckBox("单次连接")
options_layout.addWidget(self.one_off_check, 1, 1)
options_group.setLayout(options_layout)
layout.addWidget(options_group)
# 占位弹簧
layout.addStretch()
def create_result_tab(self):
"""创建结果标签页"""
result_tab = QWidget()
self.tab_widget.addTab(result_tab, "测试结果")
layout = QVBoxLayout(result_tab)
# 结果文本区域
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
self.result_text.setFont(QFont("Consolas", 10))
layout.addWidget(self.result_text)
# 结果统计区域
stats_group = QGroupBox("统计信息")
stats_layout = QGridLayout()
self.stats_labels = {}
stats_fields = [
("带宽:", "bandwidth"),
("抖动:", "jitter"),
("丢包:", "loss"),
("时间:", "duration"),
("数据量:", "bytes"),
("包数量:", "packets")
]
for i, (label_text, key) in enumerate(stats_fields):
row = i // 3
col = (i % 3) * 2
stats_layout.addWidget(QLabel(label_text), row, col)
value_label = QLabel("--")
value_label.setStyleSheet("font-weight: bold; color: #2c3e50;")
stats_layout.addWidget(value_label, row, col + 1)
self.stats_labels[key] = value_label
stats_group.setLayout(stats_layout)
layout.addWidget(stats_group)
def create_history_tab(self):
"""创建历史记录标签页"""
history_tab = QWidget()
self.tab_widget.addTab(history_tab, "历史记录")
layout = QVBoxLayout(history_tab)
# 历史记录表格
self.history_table = QTableWidget()
self.history_table.setColumnCount(7)
self.history_table.setHorizontalHeaderLabels([
"时间", "模式", "协议", "服务器", "带宽", "抖动", "丢包"
])
self.history_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
layout.addWidget(self.history_table)
# 操作按钮
button_layout = QHBoxLayout()
self.clear_history_btn = QPushButton("清空历史")
self.export_history_btn = QPushButton("导出记录")
self.load_history_btn = QPushButton("加载历史")
button_layout.addWidget(self.clear_history_btn)
button_layout.addWidget(self.export_history_btn)
button_layout.addWidget(self.load_history_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
def create_bottom_controls(self, parent_layout):
"""创建底部控制区域"""
control_layout = QHBoxLayout()
# 开始/停止按钮
self.start_btn = QPushButton("开始测试")
self.start_btn.setMinimumHeight(40)
self.start_btn.setStyleSheet("""
QPushButton {
background-color: #27ae60;
color: white;
font-weight: bold;
border-radius: 5px;
padding: 10px;
}
QPushButton:hover {
background-color: #219653;
}
QPushButton:disabled {
background-color: #95a5a6;
}
""")
self.stop_btn = QPushButton("停止测试")
self.stop_btn.setMinimumHeight(40)
self.stop_btn.setEnabled(False)
self.stop_btn.setStyleSheet("""
QPushButton {
background-color: #e74c3c;
color: white;
font-weight: bold;
border-radius: 5px;
padding: 10px;
}
QPushButton:hover {
background-color: #c0392b;
}
QPushButton:disabled {
background-color: #95a5a6;
}
""")
# 保存/加载按钮
self.save_btn = QPushButton("保存配置")
self.load_btn = QPushButton("加载配置")
self.reset_btn = QPushButton("重置配置")
# 添加到布局
control_layout.addWidget(self.start_btn)
control_layout.addWidget(self.stop_btn)
control_layout.addStretch()
control_layout.addWidget(self.save_btn)
control_layout.addWidget(self.load_btn)
control_layout.addWidget(self.reset_btn)
parent_layout.addLayout(control_layout)
def setup_connections(self):
"""设置信号连接"""
# 按钮连接
self.start_btn.clicked.connect(self.start_test)
self.stop_btn.clicked.connect(self.stop_test)
self.save_btn.clicked.connect(self.save_config)
self.load_btn.clicked.connect(self.load_config)
self.reset_btn.clicked.connect(self.reset_config)
self.clear_history_btn.clicked.connect(self.clear_history)
self.export_history_btn.clicked.connect(self.export_history)
self.load_history_btn.clicked.connect(self.load_history)
# 模式切换
self.mode_combo.currentTextChanged.connect(self.on_mode_changed)
def on_mode_changed(self, mode):
"""处理模式切换"""
is_client = (mode == "客户端")
self.server_ip_edit.setEnabled(is_client)
self.time_spin.setEnabled(is_client)
self.bandwidth_combo.setEnabled(is_client)
self.reverse_check.setEnabled(is_client)
def start_test(self):
"""开始测试"""
if self.worker and self.worker.isRunning():
QMessageBox.warning(self, "警告", "测试正在进行中")
return
# 验证输入
if not self.validate_input():
return
# 准备参数
params = self.get_test_params()
# 创建并启动工作线程
self.worker = Iperf3Worker(params)
self.worker.output_received.connect(self.append_output)
self.worker.test_completed.connect(self.on_test_completed)
self.worker.progress_updated.connect(self.update_progress)
self.worker.error_occurred.connect(self.on_test_error)
# 更新界面状态
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.progress_bar.show()
self.progress_bar.setValue(0)
self.status_bar.showMessage("测试进行中...")
# 清空结果
self.result_text.clear()
for label in self.stats_labels.values():
label.setText("--")
# 开始测试
self.worker.start()
def stop_test(self):
"""停止测试"""
if self.worker and self.worker.isRunning():
self.worker.stop()
self.worker.wait()
self.status_bar.showMessage("测试已停止")
self.reset_ui_state()
def validate_input(self):
"""验证输入"""
mode = self.mode_combo.currentText()
if mode == "客户端":
server_ip = self.server_ip_edit.text().strip()
if not server_ip:
QMessageBox.warning(self, "输入错误", "请输入服务器地址")
self.server_ip_edit.setFocus()
return False
port = self.port_edit.text().strip()
if port and not port.isdigit():
QMessageBox.warning(self, "输入错误", "端口必须是数字")
self.port_edit.setFocus()
return False
return True
def get_test_params(self):
"""获取测试参数"""
params = {
"mode": "server" if self.mode_combo.currentText() == "服务器" else "client",
"server_ip": self.server_ip_edit.text().strip(),
"server_port": self.port_edit.text().strip() or "5201",
"protocol": "udp" if self.protocol_combo.currentText() == "UDP" else "tcp",
"time": self.time_spin.value(),
"parallel": self.parallel_spin.value(),
"bandwidth": self.bandwidth_combo.currentText().strip(),
"window_size": self.window_combo.currentText().strip(),
"mss": self.mss_spin.value() if self.mss_spin.value() > 0 else "",
"num": self.data_amount_edit.text().strip(),
"interval": self.interval_spin.value(),
"format": {"自动": "", "K": "k", "M": "m", "G": "g"}[self.format_combo.currentText()],
"reverse": self.reverse_check.isChecked(),
"json_output": self.json_check.isChecked(),
"verbose": self.verbose_check.isChecked(),
"version": self.version_check.isChecked(),
"debug": self.debug_check.isChecked(),
"one_off": self.one_off_check.isChecked()
}
return params
def append_output(self, text):
"""追加输出文本"""
self.result_text.moveCursor(QTextCursor.End)
self.result_text.insertPlainText(text)
self.result_text.ensureCursorVisible()
# 尝试解析结果
self.parse_results(text)
def parse_results(self, text):
"""解析测试结果"""
try:
# 简化的结果解析
lines = text.strip().split('\n')
for line in lines:
line = line.strip()
# 解析带宽
if 'Gbits/sec' in line:
match = re.search(r'([\d\.]+)\s+Gbits/sec', line)
if match:
self.stats_labels['bandwidth'].setText(f"{match.group(1)} Gbps")
elif 'Mbits/sec' in line:
match = re.search(r'([\d\.]+)\s+Mbits/sec', line)
if match:
self.stats_labels['bandwidth'].setText(f"{match.group(1)} Mbps")
# 解析抖动
if 'ms' in line and 'jitter' in line.lower():
match = re.search(r'jitter\s*[\:=]\s*([\d\.]+)\s*ms', line, re.IGNORECASE)
if match:
self.stats_labels['jitter'].setText(f"{match.group(1)} ms")
# 解析丢包
if 'loss' in line.lower():
match = re.search(r'([\d\.]+)%', line)
if match:
self.stats_labels['loss'].setText(f"{match.group(1)}%")
except Exception as e:
print(f"解析结果错误: {e}")
def update_progress(self, progress, message):
"""更新进度"""
self.progress_bar.setValue(progress)
self.status_bar.showMessage(message)
def on_test_completed(self, result):
"""测试完成处理"""
self.status_bar.showMessage("测试完成")
self.reset_ui_state()
# 保存到历史记录
self.save_to_history()
# 显示完成消息
if result["status"] == "success":
QMessageBox.information(self, "完成", "测试完成")
def on_test_error(self, error_message):
"""测试错误处理"""
self.append_output(f"\n错误: {error_message}\n")
self.status_bar.showMessage("测试出错")
self.reset_ui_state()
QMessageBox.critical(self, "错误", error_message)
def reset_ui_state(self):
"""重置UI状态"""
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.progress_bar.hide()
def save_config(self):
"""保存配置"""
config = {
"server_ip": self.server_ip_edit.text(),
"server_port": self.port_edit.text(),
"protocol": self.protocol_combo.currentIndex(),
"mode": self.mode_combo.currentIndex(),
"time": self.time_spin.value(),
"parallel": self.parallel_spin.value(),
"bandwidth": self.bandwidth_combo.currentText(),
"window_size": self.window_combo.currentText(),
"mss": self.mss_spin.value(),
"num": self.data_amount_edit.text(),
"interval": self.interval_spin.value(),
"format": self.format_combo.currentIndex(),
"reverse": self.reverse_check.isChecked(),
"json_output": self.json_check.isChecked(),
"verbose": self.verbose_check.isChecked(),
"version": self.version_check.isChecked(),
"debug": self.debug_check.isChecked(),
"one_off": self.one_off_check.isChecked()
}
self.settings_manager.settings.update(config)
if self.settings_manager.save_all():
QMessageBox.information(self, "成功", "配置已保存")
else:
QMessageBox.warning(self, "错误", "保存配置失败")
def load_config(self):
"""加载配置"""
# 处理字符串类型的设置
server_ip = self.settings_manager.get("server_ip", "")
if server_ip is None:
server_ip = ""
self.server_ip_edit.setText(str(server_ip))
server_port = self.settings_manager.get("server_port", "")
if server_port is None:
server_port = "5201"
self.port_edit.setText(str(server_port))
# 协议:处理字符串和索引两种情况
protocol = self.settings_manager.get("protocol", 0)
if isinstance(protocol, str):
if protocol.lower() == "tcp":
self.protocol_combo.setCurrentIndex(0)
elif protocol.lower() == "udp":
self.protocol_combo.setCurrentIndex(1)
else:
self.protocol_combo.setCurrentIndex(0)
else:
try:
self.protocol_combo.setCurrentIndex(int(protocol))
except:
self.protocol_combo.setCurrentIndex(0)
# 模式:处理字符串和索引两种情况
mode = self.settings_manager.get("mode", 0)
if isinstance(mode, str):
if mode.lower() == "client":
self.mode_combo.setCurrentIndex(0)
elif mode.lower() == "server":
self.mode_combo.setCurrentIndex(1)
else:
self.mode_combo.setCurrentIndex(0)
else:
try:
self.mode_combo.setCurrentIndex(int(mode))
except:
self.mode_combo.setCurrentIndex(0)
# 处理数值类型的设置
try:
self.time_spin.setValue(int(self.settings_manager.get("time", 10)))
except:
self.time_spin.setValue(10)
try:
self.parallel_spin.setValue(int(self.settings_manager.get("parallel", 1)))
except:
self.parallel_spin.setValue(1)
# 处理字符串类型的设置
bandwidth = self.settings_manager.get("bandwidth", "")
if bandwidth is None:
bandwidth = ""
self.bandwidth_combo.setCurrentText(str(bandwidth))
window_size = self.settings_manager.get("window_size", "")
if window_size is None:
window_size = ""
self.window_combo.setCurrentText(str(window_size))
# 处理mss
mss_value = self.settings_manager.get("mss", 0)
try:
if isinstance(mss_value, str):
if mss_value.strip() == "":
self.mss_spin.setValue(0)
else:
self.mss_spin.setValue(int(mss_value))
else:
self.mss_spin.setValue(int(mss_value))
except:
self.mss_spin.setValue(0)
# 处理其他字符串设置
num_value = self.settings_manager.get("num", "")
if num_value is None:
num_value = ""
self.data_amount_edit.setText(str(num_value))
# 处理interval
try:
interval_value = self.settings_manager.get("interval", 1.0)
if isinstance(interval_value, str):
if interval_value.strip() == "":
self.interval_spin.setValue(1.0)
else:
self.interval_spin.setValue(float(interval_value))
else:
self.interval_spin.setValue(float(interval_value))
except:
self.interval_spin.setValue(1.0)
# 单位格式
format_index = self.settings_manager.get("format", 0)
try:
if isinstance(format_index, str):
# 兼容旧版本字符串格式
format_map = {"": 0, "k": 0, "m": 1, "g": 2}
format_index = format_map.get(format_index.lower(), 0)
self.format_combo.setCurrentIndex(int(format_index))
except:
self.format_combo.setCurrentIndex(0)
# 处理布尔值设置
try:
self.reverse_check.setChecked(bool(self.settings_manager.get("reverse", False)))
self.json_check.setChecked(bool(self.settings_manager.get("json_output", False)))
self.verbose_check.setChecked(bool(self.settings_manager.get("verbose", False)))
self.version_check.setChecked(bool(self.settings_manager.get("version", False)))
self.debug_check.setChecked(bool(self.settings_manager.get("debug", False)))
self.one_off_check.setChecked(bool(self.settings_manager.get("one_off", False)))
except:
pass
QMessageBox.information(self, "成功", "配置已加载")
def reset_config(self):
"""重置配置"""
reply = QMessageBox.question(
self, "确认",
"确定要重置所有配置吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
defaults = {
"server_ip": "127.0.0.1",
"server_port": "5201",
"protocol": 0,
"mode": 0,
"time": 10,
"parallel": 1,
"bandwidth": "",
"window_size": "",
"mss": 0,
"num": "",
"interval": 1.0,
"format": 0,
"reverse": False,
"json_output": False,
"verbose": False,
"version": False,
"debug": False,
"one_off": False
}
self.settings_manager.settings.update(defaults)
self.load_config()
def save_to_history(self):
"""保存到历史记录"""
try:
# 从统计标签获取数据
bandwidth = self.stats_labels['bandwidth'].text()
jitter = self.stats_labels['jitter'].text()
loss = self.stats_labels['loss'].text()
if bandwidth != "--":
row = self.history_table.rowCount()
self.history_table.insertRow(row)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
mode = self.mode_combo.currentText()
protocol = self.protocol_combo.currentText()
server = self.server_ip_edit.text()
self.history_table.setItem(row, 0, QTableWidgetItem(current_time))
self.history_table.setItem(row, 1, QTableWidgetItem(mode))
self.history_table.setItem(row, 2, QTableWidgetItem(protocol))
self.history_table.setItem(row, 3, QTableWidgetItem(server))
self.history_table.setItem(row, 4, QTableWidgetItem(bandwidth))
self.history_table.setItem(row, 5, QTableWidgetItem(jitter))
self.history_table.setItem(row, 6, QTableWidgetItem(loss))
except Exception as e:
print(f"保存历史记录错误: {e}")
def clear_history(self):
"""清空历史记录"""
reply = QMessageBox.question(
self, "确认",
"确定要清空历史记录吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.history_table.setRowCount(0)
def export_history(self):
"""导出历史记录"""
if self.history_table.rowCount() == 0:
QMessageBox.warning(self, "警告", "没有历史记录可导出")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "导出历史记录", "", "CSV文件 (*.csv);;所有文件 (*)"
)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
# 写入表头
headers = []
for col in range(self.history_table.columnCount()):
headers.append(self.history_table.horizontalHeaderItem(col).text())
f.write(','.join(headers) + '\n')
# 写入数据
for row in range(self.history_table.rowCount()):
row_data = []
for col in range(self.history_table.columnCount()):
item = self.history_table.item(row, col)
row_data.append(item.text() if item else "")
f.write(','.join(row_data) + '\n')
QMessageBox.information(self, "成功", f"历史记录已导出到: {file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出失败: {str(e)}")
def load_history(self):
"""加载历史记录"""
file_path, _ = QFileDialog.getOpenFileName(
self, "加载历史记录", "", "CSV文件 (*.csv);;所有文件 (*)"
)
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if len(lines) > 0:
self.history_table.setRowCount(0)
for i, line in enumerate(lines[1:]): # 跳过表头
data = line.strip().split(',')
if len(data) >= 7:
row = self.history_table.rowCount()
self.history_table.insertRow(row)
for col in range(min(7, len(data))):
self.history_table.setItem(row, col, QTableWidgetItem(data[col]))
QMessageBox.information(self, "成功", "历史记录已加载")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载失败: {str(e)}")
def apply_styles(self):
"""应用样式"""
style = """
QMainWindow {
background-color: #f5f6fa;
}
QGroupBox {
font-weight: bold;
border: 2px solid #dcdde1;
border-radius: 5px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
QLabel {
color: #2c3e50;
}
QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox {
padding: 5px;
border: 1px solid #bdc3c7;
border-radius: 3px;
min-height: 25px;
}
QTextEdit {
border: 1px solid #bdc3c7;
border-radius: 3px;
background-color: white;
}
QTableWidget {
border: 1px solid #bdc3c7;
border-radius: 3px;
background-color: white;
gridline-color: #ecf0f1;
}
QHeaderView::section {
background-color: #3498db;
color: white;
padding: 5px;
border: none;
}
QStatusBar {
background-color: #34495e;
color: white;
}
QTabWidget::pane {
border: 1px solid #bdc3c7;
border-radius: 3px;
background-color: white;
}
QTabBar::tab {
background-color: #ecf0f1;
padding: 8px 15px;
margin-right: 2px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
QTabBar::tab:selected {
background-color: #3498db;
color: white;
}
"""
self.setStyleSheet(style)
def load_settings(self):
"""加载窗口设置"""
geometry = self.settings_manager.get("geometry")
if geometry and isinstance(geometry, list) and len(geometry) == 4:
self.setGeometry(*geometry)
window_state = self.settings_manager.get("window_state")
if window_state and isinstance(window_state, str):
try:
# 将十六进制字符串转换回QByteArray
byte_array = QByteArray.fromHex(window_state.encode())
self.restoreState(byte_array)
except:
pass
# 加载配置
self.load_config()
# 更新模式相关控件状态
self.on_mode_changed(self.mode_combo.currentText())
def closeEvent(self, event):
"""关闭事件处理"""
# 保存窗口状态
self.settings_manager.set("geometry", [
self.geometry().x(),
self.geometry().y(),
self.geometry().width(),
self.geometry().height()
])
# 保存窗口状态为QByteArray
window_state = self.saveState()
self.settings_manager.set("window_state", window_state)
# 保存当前配置
self.save_config()
# 停止正在运行的测试
if self.worker and self.worker.isRunning():
self.worker.stop()
self.worker.wait()
# 保存所有设置
self.settings_manager.save_all()
event.accept()
def main():
"""主函数"""
app = QApplication(sys.argv)
app.setStyle('Fusion')
# 设置应用名称
app.setApplicationName("iperf3 GUI")
app.setOrganizationName("Network Tools")
# 创建并显示主窗口
window = Iperf3GUI()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()