import csv
import os
import traceback
import numpy as np
import matplotlib.pyplot as plt
from decimal import Decimal, getcontext
from PyQt5.QtWidgets import (QApplication, QMainWindow, QFileDialog, QPushButton,
QListWidget, QVBoxLayout, QHBoxLayout, QWidget,
QLabel, QTextEdit, QProgressBar, QMessageBox,
QTabWidget, QGroupBox, QSplitter, QAction, QMenuBar, QStatusBar,
QDoubleSpinBox)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib
import pywt
from scipy import signal as sp_signal
# 中文字体配置
matplotlib.use('Qt5Agg')
plt.rcParams["font.family"] = ["Microsoft YaHei", "SimHei", "SimSun"]
plt.rcParams["axes.unicode_minus"] = False # 正确显示负号
# Excel支持配置
EXCEL_SUPPORT = False
Workbook = None
Image = None
Font = None
Alignment = None
Border = None
Side = None
PatternFill = None
get_column_letter = None
try:
from openpyxl import Workbook
from openpyxl.drawing.image import Image
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter
EXCEL_SUPPORT = True
except ImportError:
EXCEL_SUPPORT = False
class MplCanvas(FigureCanvas):
"""Matplotlib画布:适配Qt界面"""
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = self.fig.add_subplot(111)
super(MplCanvas, self).__init__(self.fig)
self.fig.tight_layout()
class WorkerThread(QThread):
"""后台处理线程:保留原始计算逻辑"""
progress_updated = pyqtSignal(int)
status_updated = pyqtSignal(str)
result_ready = pyqtSignal(object)
finished = pyqtSignal()
error_occurred = pyqtSignal(str)
def __init__(self, file_paths, target_freq_range=[100, 5000]):
super().__init__()
self.file_paths = file_paths
self.results = []
self.running = True
self.target_freq_min, self.target_freq_max = target_freq_range
def run(self):
total_files = len(self.file_paths)
for i, file_path in enumerate(self.file_paths):
if not self.running:
break
try:
filename = os.path.basename(file_path)
self.status_updated.emit(f"正在处理: {filename}")
# 读取CSV数据(使用原始程序的读取逻辑)
time, input_sig, output_sig = self.read_csv_data(file_path)
# 数据长度校验
if len(time) < 200:
raise ValueError(f"有效数据行数不足(仅{len(time)}行),至少需要200行")
# 计算增益和相位(核心逻辑与原始程序一致)
gain, phase_shift, output_fundamental, freq = self.compute_gain_and_phase_shift(
time, input_sig, time, output_sig
)
# 保存完整结果
result = {
'file_path': file_path,
'file_name': filename,
'time': time,
'input_signal': input_sig,
'output_signal': output_sig,
'output_fundamental': output_fundamental,
'frequency': freq,
'gain': gain,
'phase_shift': phase_shift,
# 补充原始程序没有但实用的信息
'sampling_rate': 1 / np.mean(np.diff(time)) if len(time) > 1 else 0
}
self.results.append(result)
self.result_ready.emit(result)
self.status_updated.emit(f"处理完成: {filename}")
except Exception as e:
error_type = type(e).__name__
error_msg = f"处理 {os.path.basename(file_path)} 失败: {error_type} - {str(e)}"
self.status_updated.emit(error_msg)
self.error_occurred.emit(error_msg)
print(f"\n{error_msg}")
traceback.print_exc()
progress = int((i + 1) / total_files * 100)
self.progress_updated.emit(progress)
if self.running:
success_count = len(self.results)
fail_count = total_files - success_count
status_msg = f"全部处理完成,成功 {success_count}/{total_files} 个文件"
self.status_updated.emit(status_msg)
else:
self.status_updated.emit("处理已取消")
self.finished.emit()
def stop(self):
self.running = False
if self.isRunning():
self.wait(2000)
def read_csv_data(self, filename):
"""读取CSV文件(与原始程序逻辑一致)"""
time, input_sig, output_sig = [], [], []
try:
with open(filename, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
# 原始程序逻辑:读取标题行后开始读取数据
header = next(reader) # 假设第一行为标题
for row in reader:
if len(row) < 3:
continue # 跳过不完整的行
try:
t = float(row[0])
x = float(row[1])
y = float(row[2])
time.append(t)
input_sig.append(x)
output_sig.append(y)
except ValueError:
continue # 跳过格式错误的行
if not time:
raise ValueError("未找到有效数据")
return np.array(time), np.array(input_sig), np.array(output_sig)
except Exception as e:
raise type(e)(f"读取CSV失败: {str(e)}")
def estimate_frequency(self, time, signal):
"""频率估计(完全保留原始程序逻辑)"""
# 原始程序的采样率计算方式
fs = 1 / np.mean(np.diff(time))
N = len(signal)
fft_vals = np.fft.rfft(signal)
freqs = np.fft.rfftfreq(N, d=1 / fs)
# 找到最大幅值对应的频率(排除0频)
peak_idx = np.argmax(np.abs(fft_vals[1:])) + 1
dominant_freq = freqs[peak_idx]
# 频率范围检查(使用可配置的范围)
if not (self.target_freq_min <= dominant_freq <= self.target_freq_max):
raise ValueError(
f"检测到的频率 {dominant_freq:.2f} Hz 不在有效范围内 ({self.target_freq_min}-{self.target_freq_max} Hz)")
return dominant_freq, fs
def extract_fundamental_component(self, time, signal, target_freq):
"""基波提取(完全保留原始程序逻辑)"""
# 构造设计矩阵 [sin, cos]
design_matrix = np.column_stack([
np.sin(2 * np.pi * target_freq * time),
np.cos(2 * np.pi * target_freq * time)
])
# 最小二乘求解系数 [I, Q]
try:
coeffs, residuals, rank, s = np.linalg.lstsq(design_matrix, signal, rcond=None)
except:
raise RuntimeError("最小二乘求解失败")
I, Q = coeffs[0], coeffs[1]
# 重建信号
fundamental = I * np.sin(2 * np.pi * target_freq * time) + \
Q * np.cos(2 * np.pi * target_freq * time)
# 幅度和相位
amplitude = np.sqrt(I ** 2 + Q ** 2)
phase_rad = np.arctan2(Q, I) # 与原始程序一致的相位计算
return fundamental, amplitude, phase_rad
def compute_gain_and_phase_shift(self, input_time, input_signal, output_time, output_signal):
"""核心计算逻辑(完全保留原始程序)"""
# 步骤1:估计输入信号频率
freq, fs = self.estimate_frequency(input_time, input_signal)
print(f"检测到信号频率: {freq:.2f} Hz")
# 确保时间轴一致(原始程序的校验逻辑)
if not np.allclose(input_time, output_time, atol=1e-6):
raise ValueError("输入和输出的时间轴不一致")
time = input_time
# 步骤2:提取基波(与原始程序一致)
_, A_in, phi_in = self.extract_fundamental_component(time, input_signal, freq)
output_fundamental, A_out, phi_out = self.extract_fundamental_component(time, output_signal, freq)
# 步骤3:计算增益和相位差(原始程序逻辑)
gain = A_out / A_in
phase_shift_rad = phi_out - phi_in
phase_shift_deg = np.rad2deg(phase_shift_rad)
# 相位归一化(原始程序处理)
phase_shift_deg = (phase_shift_deg + 180) % 360 - 180
print(f"增益(放大系数): {gain:.4f}")
print(f"相位偏移: {phase_shift_deg:.2f}°")
return gain, phase_shift_deg, output_fundamental, freq
class WaveformAnalyzer(QMainWindow):
"""主窗口:还原所有功能"""
def __init__(self):
super().__init__()
self.setWindowTitle("波形分析工具(完整功能版)")
self.setGeometry(100, 100, 1200, 800)
# 初始化变量
self.file_paths = []
self.results = []
self.current_result_idx = -1
self.worker = None
self.temp_image_dir = os.path.join(os.path.dirname(__file__), "temp_images")
if not os.path.exists(self.temp_image_dir):
os.makedirs(self.temp_image_dir)
# 初始化UI和菜单
self.init_ui()
self.init_menu()
def init_menu(self):
"""初始化菜单栏(还原完整菜单)"""
menubar = self.menuBar()
# 文件菜单
file_menu = menubar.addMenu("文件")
add_file_action = QAction("添加CSV文件", self)
add_file_action.triggered.connect(self.add_files)
file_menu.addAction(add_file_action)
save_csv_action = QAction("保存CSV汇总", self)
save_csv_action.triggered.connect(self.save_all_results)
file_menu.addAction(save_csv_action)
export_excel_action = QAction("导出Excel报告", self)
export_excel_action.triggered.connect(self.export_excel_report)
file_menu.addAction(export_excel_action)
exit_action = QAction("退出", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 帮助菜单
help_menu = menubar.addMenu("帮助")
about_action = QAction("关于", self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
help_action = QAction("使用帮助", self)
help_action.triggered.connect(self.show_help)
help_menu.addAction(help_action)
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪")
def init_ui(self):
"""初始化UI组件(还原完整功能)"""
# 主布局
main_widget = QWidget()
main_layout = QHBoxLayout(main_widget)
self.setCentralWidget(main_widget)
# 左侧面板
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(10, 10, 10, 10)
left_layout.setSpacing(10)
# 1. 待处理文件列表
file_group = QGroupBox("待处理文件(CSV格式)")
file_layout = QVBoxLayout(file_group)
self.file_list = QListWidget()
self.file_list.setSelectionMode(QListWidget.ExtendedSelection)
file_layout.addWidget(self.file_list)
file_btn_layout = QHBoxLayout()
self.add_btn = QPushButton("添加文件")
self.remove_btn = QPushButton("移除选中")
self.clear_btn = QPushButton("清空列表")
self.add_btn.clicked.connect(self.add_files)
self.remove_btn.clicked.connect(self.remove_files)
self.clear_btn.clicked.connect(self.clear_files)
file_btn_layout.addWidget(self.add_btn)
file_btn_layout.addWidget(self.remove_btn)
file_btn_layout.addWidget(self.clear_btn)
file_layout.addLayout(file_btn_layout)
left_layout.addWidget(file_group)
# 2. 处理控制(还原频率范围设置)
process_group = QGroupBox("处理控制")
process_layout = QVBoxLayout(process_group)
range_layout = QHBoxLayout()
range_layout.addWidget(QLabel("目标频率区间(Hz):"))
self.freq_min_input = QDoubleSpinBox()
self.freq_min_input.setRange(1, 10000)
self.freq_min_input.setSingleStep(10)
self.freq_min_input.setValue(100)
self.freq_min_input.setDecimals(0)
range_layout.addWidget(self.freq_min_input)
range_layout.addWidget(QLabel("-"))
self.freq_max_input = QDoubleSpinBox()
self.freq_max_input.setRange(1, 10000)
self.freq_max_input.setSingleStep(10)
self.freq_max_input.setValue(5000)
self.freq_max_input.setDecimals(0)
range_layout.addWidget(self.freq_max_input)
process_layout.addLayout(range_layout)
self.process_btn = QPushButton("开始处理")
self.retry_btn = QPushButton("重试处理")
self.process_btn.clicked.connect(self.start_processing)
self.retry_btn.clicked.connect(self.start_processing)
self.retry_btn.setEnabled(False)
process_layout.addWidget(self.process_btn)
process_layout.addWidget(self.retry_btn)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
process_layout.addWidget(self.progress_bar)
self.status_label = QLabel("就绪")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("color: #2E86AB; font-weight: bold;")
process_layout.addWidget(self.status_label)
self.error_label = QLabel("")
self.error_label.setAlignment(Qt.AlignCenter)
self.error_label.setStyleSheet("color: #A23B72;")
self.error_label.setWordWrap(True)
process_layout.addWidget(self.error_label)
left_layout.addWidget(process_group)
# 3. 处理结果列表
result_group = QGroupBox("处理结果(双击失败项看详情)")
result_layout = QVBoxLayout(result_group)
self.result_list = QListWidget()
self.result_list.itemClicked.connect(self.on_result_selected)
self.result_list.itemDoubleClicked.connect(self.show_result_error)
result_layout.addWidget(self.result_list)
left_layout.addWidget(result_group)
# 右侧面板
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(10, 10, 10, 10)
right_layout.setSpacing(10)
# 标签页控件(还原所有标签页)
self.tabs = QTabWidget()
self.waveform_tab = QWidget()
waveform_layout = QVBoxLayout(self.waveform_tab)
self.waveform_canvas = MplCanvas(self, width=8, height=5, dpi=100)
waveform_layout.addWidget(self.waveform_canvas)
self.tabs.addTab(self.waveform_tab, "波形图(输入+输出+基波)")
self.data_tab = QWidget()
data_layout = QVBoxLayout(self.data_tab)
self.data_display = QTextEdit()
self.data_display.setReadOnly(True)
data_layout.addWidget(self.data_display)
self.tabs.addTab(self.data_tab, "分析数据")
self.fft_tab = QWidget()
fft_layout = QVBoxLayout(self.fft_tab)
self.fft_canvas = MplCanvas(self, width=8, height=5, dpi=100)
fft_layout.addWidget(self.fft_canvas)
self.tabs.addTab(self.fft_tab, "FFT分析")
self.error_tab = QWidget()
error_layout = QVBoxLayout(self.error_tab)
self.error_display = QTextEdit()
self.error_display.setReadOnly(True)
error_layout.addWidget(self.error_display)
self.tabs.addTab(self.error_tab, "错误日志")
right_layout.addWidget(self.tabs)
# 结果操作按钮(还原所有功能按钮)
result_btn_layout = QHBoxLayout()
self.save_plot_btn = QPushButton("保存当前图表")
self.save_data_btn = QPushButton("保存当前数据")
self.save_all_btn = QPushButton("保存CSV汇总")
self.export_excel_btn = QPushButton("导出Excel报告")
self.save_plot_btn.clicked.connect(self.save_current_plot)
self.save_data_btn.clicked.connect(self.save_current_data)
self.save_all_btn.clicked.connect(self.save_all_results)
self.export_excel_btn.clicked.connect(self.export_excel_report)
result_btn_layout.addWidget(self.save_plot_btn)
result_btn_layout.addWidget(self.save_data_btn)
result_btn_layout.addWidget(self.save_all_btn)
result_btn_layout.addWidget(self.export_excel_btn)
right_layout.addLayout(result_btn_layout)
# 水平分割器
splitter = QSplitter(Qt.Horizontal)
splitter.addWidget(left_panel)
splitter.addWidget(right_panel)
splitter.setSizes([350, 800])
main_layout.addWidget(splitter)
def add_files(self):
"""添加CSV文件到列表"""
file_paths, _ = QFileDialog.getOpenFileNames(
self, "选择CSV文件", "", "CSV文件 (*.csv);;所有文件 (*.*)"
)
if file_paths:
added_count = 0
for path in file_paths:
if path not in self.file_paths:
if os.path.exists(path) and os.path.getsize(path) > 0:
self.file_paths.append(path)
self.file_list.addItem(os.path.basename(path))
added_count += 1
else:
QMessageBox.warning(self, "文件无效", f"文件「{os.path.basename(path)}」不存在或为空")
self.status_bar.showMessage(f"已添加 {added_count} 个有效CSV文件")
def remove_files(self):
"""移除选中的文件"""
selected_items = self.file_list.selectedItems()
if not selected_items:
QMessageBox.warning(self, "无选中文件", "请先选中要移除的文件")
return
for item in selected_items:
idx = self.file_list.row(item)
self.file_list.takeItem(idx)
del self.file_paths[idx]
self.status_bar.showMessage(f"已移除 {len(selected_items)} 个文件")
def clear_files(self):
"""清空文件列表和结果"""
self.file_list.clear()
self.result_list.clear()
self.file_paths = []
self.results = []
self.current_result_idx = -1
self.error_display.clear()
self.error_label.setText("")
self.status_bar.showMessage("文件列表已清空")
def start_processing(self):
"""启动处理线程"""
if not self.file_paths:
QMessageBox.warning(self, "无待处理文件", "请先添加CSV文件")
return
if self.worker and self.worker.isRunning():
QMessageBox.information(self, "处理中", "当前已有文件在处理,请稍候")
return
freq_min = self.freq_min_input.value()
freq_max = self.freq_max_input.value()
if freq_min >= freq_max:
QMessageBox.warning(self, "参数错误", "频率最小值不能大于或等于最大值")
return
# 重置状态
self.results = []
self.result_list.clear()
self.current_result_idx = -1
self.progress_bar.setValue(0)
self.status_label.setText("准备处理...")
self.error_label.setText("")
self.error_display.clear()
# 创建并启动工作线程(使用原始计算逻辑)
self.worker = WorkerThread(
self.file_paths,
target_freq_range=[freq_min, freq_max]
)
self.worker.progress_updated.connect(self.update_progress)
self.worker.status_updated.connect(self.update_status)
self.worker.result_ready.connect(self.add_result)
self.worker.finished.connect(self.process_finished)
self.worker.error_occurred.connect(self.log_error)
self.worker.start()
# 更新按钮状态
self.process_btn.setText("取消处理")
self.process_btn.clicked.disconnect()
self.process_btn.clicked.connect(self.cancel_processing)
self.retry_btn.setEnabled(False)
self.add_btn.setEnabled(False)
self.remove_btn.setEnabled(False)
self.clear_btn.setEnabled(False)
self.status_bar.showMessage(f"开始处理(目标区间:{freq_min:.0f}-{freq_max:.0f}Hz)")
def cancel_processing(self):
"""取消正在进行的处理"""
reply = QMessageBox.question(
self, "确认取消", "确定要取消处理吗?当前文件可能处理不完整",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes and self.worker and self.worker.isRunning():
self.worker.stop()
self.status_label.setText("正在取消处理...")
self.status_bar.showMessage("正在取消处理")
def update_progress(self, value):
"""更新进度条"""
self.progress_bar.setValue(value)
def update_status(self, text):
"""更新状态文本"""
self.status_label.setText(text)
self.status_bar.showMessage(text)
def log_error(self, error_msg):
"""记录错误信息到日志"""
self.error_label.setText("存在处理失败的文件,详见「错误日志」标签页")
self.error_display.append(f"[{self.get_current_time()}] {error_msg}")
filename = error_msg.split("处理 ")[1].split(" 失败")[0]
self.result_list.addItem(f"❌ {filename}(处理失败)")
def add_result(self, result):
"""添加成功处理的结果"""
self.results.append(result)
self.result_list.addItem(
f"✅ {result['file_name']} - 频率: {result['frequency']:.2f}Hz | "
f"增益: {result['gain']:.4f} | 相位偏移: {result['phase_shift']:.2f}°"
)
def process_finished(self):
"""处理完成后恢复状态"""
self.process_btn.setText("开始处理")
self.process_btn.clicked.disconnect()
self.process_btn.clicked.connect(self.start_processing)
self.add_btn.setEnabled(True)
self.remove_btn.setEnabled(True)
self.clear_btn.setEnabled(True)
self.retry_btn.setEnabled(True)
success_count = len(self.results)
total_count = len(self.file_paths)
if success_count == 0:
QMessageBox.warning(self, "处理结果", "所有文件处理失败,请查看错误日志")
elif success_count < total_count:
QMessageBox.information(self, "处理结果",
f"部分文件处理成功:\n成功 {success_count} 个,失败 {total_count - success_count} 个\n"
"双击失败项可查看详细错误信息")
else:
QMessageBox.information(self, "处理结果", "所有文件处理成功!")
def on_result_selected(self, item):
"""选中结果时显示详情"""
idx = self.result_list.row(item)
if 0 <= idx < len(self.results):
if item.text().startswith("✅"):
self.current_result_idx = idx
self.display_result(self.results[idx])
else:
filename = item.text()[2:].split("(")[0]
error_log = self.error_display.toPlainText()
error_detail = ""
for line in error_log.split("\n"):
if f"处理 {filename} 失败:" in line:
error_detail = line.split("失败: ")[1]
break
if not error_detail:
error_detail = "未找到详细错误信息"
self.data_display.setText(f"文件处理失败:\n文件名:{filename}\n错误原因:{error_detail}")
def show_result_error(self, item):
"""双击失败项显示详细错误"""
if item.text().startswith("❌"):
filename = item.text()[2:].split("(")[0]
error_log = self.error_display.toPlainText()
error_detail = "未找到对应错误信息"
for line in error_log.split("\n"):
if f"处理 {filename} 失败:" in line:
error_detail = line.split("失败: ")[1]
break
QMessageBox.warning(self, f"处理错误 - {filename}", f"错误原因:{error_detail}")
def display_result(self, result):
"""显示结果数据和波形图"""
# 1. 显示分析数据
data_text = f"=== 波形分析结果 ===\n"
data_text += f"文件名:{result['file_name']}\n"
data_text += f"频率:{result['frequency']:.6f} Hz\n"
data_text += f"采样率:{result['sampling_rate']:.0f} Hz\n"
data_text += f"增益(输出/输入):{result['gain']:.6f}\n"
data_text += f"相位偏移(输出-输入):{result['phase_shift']:.2f} °\n"
data_text += f"\n=== 数据维度 ===\n"
data_text += f"时间点数量:{len(result['time'])} 个\n"
data_text += f"信号时长:{result['time'][-1] - result['time'][0]:.6f} 秒\n"
self.data_display.setText(data_text)
# 2. 绘制波形图(与原始程序的绘图逻辑一致)
self.waveform_canvas.axes.clear()
self.waveform_canvas.axes.plot(
result['time'], result['input_signal'],
label=f'输入信号 ({result["frequency"]:.0f} Hz)',
color='blue', linewidth=1.5
)
self.waveform_canvas.axes.plot(
result['time'], result['output_signal'],
label=f'输出信号 ({result["frequency"]:.0f} Hz)',
color='green', linewidth=1.5
)
self.waveform_canvas.axes.plot(
result['time'], result['output_fundamental'],
label='提取的基波', color='red', linestyle='--', linewidth=2
)
self.waveform_canvas.axes.set_xlabel('时间 (s)')
self.waveform_canvas.axes.set_ylabel('幅度')
self.waveform_canvas.axes.set_title(f'{result["file_name"]} 波形分析')
self.waveform_canvas.axes.legend()
self.waveform_canvas.axes.grid(True, alpha=0.3)
self.waveform_canvas.fig.tight_layout()
self.waveform_canvas.draw()
# 3. 绘制FFT图
self.plot_fft(result)
def plot_fft(self, result):
"""绘制FFT频谱图"""
self.fft_canvas.axes.clear()
time = result['time']
signal = result['input_signal']
freq = result['frequency']
# 计算采样率(与原始程序一致)
fs = 1 / np.mean(np.diff(time))
N = len(signal)
# 计算FFT(与原始程序一致)
fft_vals = np.fft.rfft(signal)
freqs = np.fft.rfftfreq(N, d=1 / fs)
fft_abs = np.abs(fft_vals)
# 绘制FFT
self.fft_canvas.axes.plot(freqs, fft_abs, color='blue', linewidth=1.0)
self.fft_canvas.axes.axvline(x=freq, color='red', linestyle='--',
label=f'主频: {freq:.2f} Hz')
self.fft_canvas.axes.set_xlabel('频率 (Hz)')
self.fft_canvas.axes.set_ylabel('幅度')
self.fft_canvas.axes.set_title(f'{result["file_name"]} FFT分析')
self.fft_canvas.axes.legend()
self.fft_canvas.axes.grid(True, alpha=0.3)
# 限制频率范围以便更好地查看
self.fft_canvas.axes.set_xlim(0, self.freq_max_input.value() + 1000)
self.fft_canvas.fig.tight_layout()
self.fft_canvas.draw()
def save_current_plot(self):
"""保存当前显示的图表"""
if self.current_result_idx == -1 or self.current_result_idx >= len(self.results):
QMessageBox.warning(self, "无选中结果", "请先选中一个处理成功的结果")
return
result = self.results[self.current_result_idx]
plot_type = "波形图" if self.tabs.currentIndex() == 0 else "FFT图"
default_filename = f"{os.path.splitext(result['file_name'])[0]}_{plot_type}.png"
file_path, _ = QFileDialog.getSaveFileName(
self, f"保存{plot_type}", default_filename, "PNG文件 (*.png);;PDF文件 (*.pdf);;所有文件 (*.*)"
)
if file_path:
try:
if self.tabs.currentIndex() == 0:
self.waveform_canvas.fig.savefig(file_path, dpi=300, bbox_inches='tight')
elif self.tabs.currentIndex() == 2:
self.fft_canvas.fig.savefig(file_path, dpi=300, bbox_inches='tight')
self.status_bar.showMessage(f"{plot_type}已保存至:{os.path.basename(file_path)}")
except Exception as e:
QMessageBox.critical(self, "保存失败", f"保存{plot_type}时出错:{str(e)}")
def save_current_data(self):
"""保存当前选中结果的详细数据"""
if self.current_result_idx == -1 or self.current_result_idx >= len(self.results):
QMessageBox.warning(self, "无选中结果", "请先选中一个处理成功的结果")
return
result = self.results[self.current_result_idx]
default_filename = f"{os.path.splitext(result['file_name'])[0]}_分析数据.csv"
file_path, _ = QFileDialog.getSaveFileName(
self, "保存分析数据", default_filename, "CSV文件 (*.csv);;文本文件 (*.txt);;所有文件 (*.*)"
)
if file_path:
try:
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(["时间(s)", "输入信号", "输出信号", "输出基波"])
for t, in_sig, out_sig, out_fund in zip(
result['time'], result['input_signal'],
result['output_signal'], result['output_fundamental']):
writer.writerow([t, in_sig, out_sig, out_fund])
# 保存计算结果摘要
with open(file_path.replace('.csv', '_结果摘要.txt'), 'w', encoding='utf-8') as f:
f.write(f"文件名: {result['file_name']}\n")
f.write(f"频率: {result['frequency']:.6f} Hz\n")
f.write(f"采样率: {result['sampling_rate']:.0f} Hz\n")
f.write(f"增益: {result['gain']:.6f}\n")
f.write(f"相位偏移: {result['phase_shift']:.2f}°\n")
self.status_bar.showMessage(f"分析数据已保存至:{os.path.basename(file_path)}")
except Exception as e:
QMessageBox.critical(self, "保存失败", f"保存分析数据时出错:{str(e)}")
def save_all_results(self):
"""保存所有成功结果的汇总CSV"""
if not self.results:
QMessageBox.warning(self, "无结果可保存", "暂无处理成功的结果")
return
default_filename = "波形分析结果汇总.csv"
file_path, _ = QFileDialog.getSaveFileName(
self, "保存所有结果汇总", default_filename, "CSV文件 (*.csv);;文本文件 (*.txt);;所有文件 (*.*)"
)
if file_path:
try:
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([
"序号", "文件名", "频率(Hz)", "采样率(Hz)",
"增益", "相位偏移(°)", "信号时长(s)", "数据点数"
])
for i, result in enumerate(self.results, 1):
duration = result['time'][-1] - result['time'][0]
writer.writerow([
i,
result['file_name'],
f"{result['frequency']:.6f}",
f"{result['sampling_rate']:.0f}",
f"{result['gain']:.6f}",
f"{result['phase_shift']:.2f}",
f"{duration:.6f}",
len(result['time'])
])
self.status_bar.showMessage(f"所有结果汇总已保存至:{os.path.basename(file_path)}")
except Exception as e:
QMessageBox.critical(self, "保存失败", f"保存结果汇总时出错:{str(e)}")
def export_excel_report(self):
"""导出Excel报告(还原此功能)"""
global EXCEL_SUPPORT
if not self.results:
QMessageBox.warning(self, "无结果可导出", "暂无处理成功的结果")
return
if not EXCEL_SUPPORT:
reply = QMessageBox.question(
self, "缺少依赖库",
"导出Excel报告需要安装openpyxl库,是否现在安装?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes
)
if reply == QMessageBox.Yes:
try:
import subprocess
import sys
subprocess.check_call([sys.executable, "-m", "pip", "install", "openpyxl"])
global Workbook, Image, Font, Alignment, Border, Side, PatternFill, get_column_letter
from openpyxl import Workbook
from openpyxl.drawing.image import Image
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter
EXCEL_SUPPORT = True
QMessageBox.information(self, "安装成功", "openpyxl已安装完成,请重新尝试导出")
except Exception as e:
QMessageBox.critical(self, "安装失败", f"安装openpyxl时出错:{str(e)}")
return
default_filename = "波形分析报告.xlsx"
file_path, _ = QFileDialog.getSaveFileName(
self, "导出Excel报告", default_filename, "Excel文件 (*.xlsx);;所有文件 (*.*)"
)
if file_path:
try:
wb = Workbook()
summary_ws = wb.active
summary_ws.title = "结果汇总"
header_font = Font(bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
border = Border(
left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin')
)
center_align = Alignment(horizontal="center", vertical="center")
headers = ["序号", "文件名", "频率(Hz)", "采样率(Hz)", "增益", "相位偏移(°)", "信号时长(s)", "数据点数"]
for col, header in enumerate(headers, 1):
cell = summary_ws.cell(row=1, column=col)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.border = border
cell.alignment = center_align
for row, result in enumerate(self.results, 2):
duration = result['time'][-1] - result['time'][0]
summary_ws.cell(row=row, column=1, value=row - 1).border = border
summary_ws.cell(row=row, column=1).alignment = center_align
summary_ws.cell(row=row, column=2, value=result['file_name']).border = border
summary_ws.cell(row=row, column=3, value=result['frequency']).border = border
summary_ws.cell(row=row, column=3).alignment = center_align
summary_ws.cell(row=row, column=4, value=result['sampling_rate']).border = border
summary_ws.cell(row=row, column=4).alignment = center_align
summary_ws.cell(row=row, column=5, value=result['gain']).border = border
summary_ws.cell(row=row, column=5).alignment = center_align
summary_ws.cell(row=row, column=6, value=result['phase_shift']).border = border
summary_ws.cell(row=row, column=6).alignment = center_align
summary_ws.cell(row=row, column=7, value=duration).border = border
summary_ws.cell(row=row, column=7).alignment = center_align
summary_ws.cell(row=row, column=8, value=len(result['time'])).border = border
summary_ws.cell(row=row, column=8).alignment = center_align
col_widths = [8, 30, 15, 12, 12, 15, 15, 12]
for col, width in enumerate(col_widths, 1):
summary_ws.column_dimensions[get_column_letter(col)].width = width
temp_image_paths = []
for idx, result in enumerate(self.results, 1):
ws_name = f"详情_{idx}_{os.path.splitext(result['file_name'])[0]}"
if len(ws_name) > 31:
ws_name = ws_name[:28] + "..."
ws = wb.create_sheet(title=ws_name)
params = [
["文件名", result['file_name']],
["频率", f"{result['frequency']:.6f} Hz"],
["采样率", f"{result['sampling_rate']:.0f} Hz"],
["增益", f"{result['gain']:.6f}"],
["相位偏移", f"{result['phase_shift']:.2f} °"],
["信号时长", f"{result['time'][-1] - result['time'][0]:.6f} s"],
["数据点数", f"{len(result['time'])} 个"]
]
ws.cell(row=1, column=1, value="=== 分析参数 ===").font = Font(bold=True, size=12)
for row, (name, value) in enumerate(params, 3):
ws.cell(row=row, column=1, value=name).font = Font(bold=True)
ws.cell(row=row, column=2, value=value)
temp_img_path = self._save_temp_waveform(result, idx)
temp_image_paths.append(temp_img_path)
img = Image(temp_img_path)
img.width = 800
img.height = 500
ws.add_image(img, 'D3')
ws.cell(row=20, column=1, value="=== 抽样数据 ===").font = Font(bold=True, size=12)
data_headers = ["时间(s)", "输入信号", "输出信号", "输出基波"]
for col, header in enumerate(data_headers, 1):
ws.cell(row=22, column=col, value=header).font = Font(bold=True)
step = max(1, len(result['time']) // 100)
for row, i in enumerate(range(0, len(result['time']), step), 23):
ws.cell(row=row, column=1, value=result['time'][i])
ws.cell(row=row, column=2, value=result['input_signal'][i])
ws.cell(row=row, column=3, value=result['output_signal'][i])
ws.cell(row=row, column=4, value=result['output_fundamental'][i])
ws.column_dimensions['A'].width = 15
ws.column_dimensions['B'].width = 30
ws.column_dimensions['D'].width = 5
wb.save(file_path)
for img_path in temp_image_paths:
if os.path.exists(img_path):
os.remove(img_path)
self.status_bar.showMessage(f"Excel报告已导出至:{os.path.basename(file_path)}")
QMessageBox.information(self, "导出成功", f"Excel报告已保存至:\n{file_path}")
except Exception as e:
QMessageBox.critical(self, "导出失败", f"导出Excel报告时出错:{str(e)}")
if 'temp_image_paths' in locals():
for img_path in temp_image_paths:
if os.path.exists(img_path):
try:
os.remove(img_path)
except:
pass
def _save_temp_waveform(self, result, index):
"""生成临时波形图用于Excel导出"""
safe_filename = os.path.splitext(result['file_name'])[0].replace('/', '_').replace('\\', '_')
temp_image_path = os.path.join(self.temp_image_dir, f"temp_waveform_{index}_{safe_filename}.png")
fig, ax = plt.subplots(figsize=(12, 8), dpi=100)
ax.plot(result['time'], result['input_signal'], label=f'输入信号 ({result["frequency"]:.0f} Hz)', color='blue',
linewidth=1.2)
ax.plot(result['time'], result['output_signal'], label=f'输出信号 ({result["frequency"]:.0f} Hz)',
color='green', linewidth=1.2)
ax.plot(result['time'], result['output_fundamental'], label='输出基波', color='red', linestyle='--',
linewidth=1.5)
ax.set_xlabel('时间 (s)', fontsize=11)
ax.set_ylabel('幅度', fontsize=11)
ax.set_title(f'波形分析 - {result["file_name"]}', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
fig.savefig(temp_image_path, dpi=100, bbox_inches='tight')
plt.close(fig)
return temp_image_path
def show_about(self):
"""显示关于对话框"""
QMessageBox.about(self, "关于波形分析工具",
"波形分析工具 v1.0\n\n"
"功能:分析信号频率、增益和相位偏移\n"
"特点:\n"
"- 保留原始计算逻辑,确保结果准确\n"
"- 支持批量处理和结果导出\n"
"- 提供波形图和FFT频谱分析\n\n"
"依赖库:PyQt5、numpy、matplotlib、scipy、openpyxl")
def show_help(self):
"""显示使用帮助"""
help_text = """
<h3>波形分析工具使用帮助</h3>
<p><b>一、数据准备</b><br>
1. CSV文件格式:第一行为标题,后续行为数据<br>
2. 数据列:时间(秒)、输入信号、输出信号<br>
3. 编码格式:UTF-8</p>
<p><b>二、操作步骤</b><br>
1. 点击「添加文件」选择CSV文件<br>
2. 设置「目标频率区间」(默认100-5000Hz)<br>
3. 点击「开始处理」启动分析<br>
4. 在「处理结果」列表查看详情</p>
<p><b>三、结果导出</b><br>
1. 保存当前图表:保存当前显示的波形图或FFT图<br>
2. 保存当前数据:保存单个文件的详细数据<br>
3. 保存CSV汇总:导出所有结果的汇总表格<br>
4. 导出Excel报告:生成包含图表和数据的完整报告</p>
"""
QMessageBox.information(self, "使用帮助", help_text)
def get_current_time(self):
"""获取当前时间(用于错误日志)"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def closeEvent(self, event):
"""关闭窗口时清理资源"""
if self.worker and self.worker.isRunning():
reply = QMessageBox.question(
self, "确认退出", "当前有文件正在处理,确定要退出吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
self.worker.stop()
self.worker.wait()
event.accept()
else:
event.ignore()
return
# 清理临时图片文件夹
if os.path.exists(self.temp_image_dir):
try:
for file in os.listdir(self.temp_image_dir):
file_path = os.path.join(self.temp_image_dir, file)
os.remove(file_path)
os.rmdir(self.temp_image_dir)
except:
pass
event.accept()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
window = WaveformAnalyzer()
window.show()
sys.exit(app.exec_())这个程序对相位偏移的误差计算还是有点大,大概在1-2°,我需要的数据是在0.2°左右的精确数据,怎么样让数据受干扰少一些准确一些
最新发布