# plot_window.py """独立绘图窗口:嵌入 matplotlib (QtAgg 后端) 显示控制数据曲线。 注意:matplotlib backend 由 main.py 在最早期统一设置,此处不再重复调用。 使用 Figure() 直接创建图形,避免 plt.subplots() 污染 pyplot 全局状态导致闪退。 """ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT from matplotlib.figure import Figure from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget ) from PySide6.QtCore import Qt class PlotWindow(QDialog): """压力控制数据曲线窗口""" def __init__(self, time_data, pressure_data, target_data, valve_data, parent=None): super().__init__(parent) self.setWindowTitle("控制数据曲线图") self.resize(1100, 800) self.setAttribute(Qt.WA_DeleteOnClose) self.time_data = list(time_data) self.pressure_data = list(pressure_data) self.target_data = list(target_data) self.valve_data = list(valve_data) self._fig = None self._ax1 = None self._ax2 = None self._canvas = None self._setup_ui() def _setup_ui(self): layout = QVBoxLayout(self) # ---- 控制面板 ---- ctrl_widget = QWidget() ctrl_layout = QHBoxLayout(ctrl_widget) ctrl_layout.setContentsMargins(0, 0, 0, 0) ctrl_layout.setSpacing(8) ctrl_layout.addWidget(QLabel("时间轴范围 (秒):")) self.x_min_entry = QLineEdit("0") self.x_min_entry.setMaximumWidth(80) ctrl_layout.addWidget(self.x_min_entry) ctrl_layout.addWidget(QLabel("到")) x_max_default = f"{max(self.time_data):.1f}" if self.time_data else "10" self.x_max_entry = QLineEdit(x_max_default) self.x_max_entry.setMaximumWidth(80) ctrl_layout.addWidget(self.x_max_entry) apply_btn = QPushButton("应用") apply_btn.clicked.connect(self._apply_x_limits) ctrl_layout.addWidget(apply_btn) reset_btn = QPushButton("重置") reset_btn.clicked.connect(self._reset_view) ctrl_layout.addWidget(reset_btn) all_btn = QPushButton("全部") all_btn.clicked.connect(self._show_all) ctrl_layout.addWidget(all_btn) last30_btn = QPushButton("最后30秒") last30_btn.clicked.connect(lambda: self._zoom_last_n(30)) ctrl_layout.addWidget(last30_btn) ctrl_layout.addStretch() layout.addWidget(ctrl_widget) # ---- matplotlib 画布 ---- if not self.time_data or len(self.time_data) < 2: layout.addWidget(QLabel("数据不足,无法绘制图表")) return try: # 使用 Figure() 直接创建,避免 plt.subplots() 将图形注册到 pyplot 全局状态 self._fig = Figure(figsize=(10, 7), dpi=100) self._ax1 = self._fig.add_subplot(2, 1, 1) self._ax2 = self._fig.add_subplot(2, 1, 2) # 压力曲线 self._ax1.plot(self.time_data, self.pressure_data, 'b-o', linewidth=1, markersize=1, alpha=0.8, label='实际压力') self._ax1.plot(self.time_data, self.target_data, 'r--', linewidth=1.5, alpha=0.8, label='目标压力') self._ax1.set_ylabel('压力 (kPa)', fontsize=12) self._ax1.set_title('压力控制性能', fontsize=14, fontweight='bold') self._ax1.legend(loc='upper right', fontsize=10) self._ax1.grid(True, alpha=0.3) # 阀门开度曲线 self._ax2.plot(self.time_data, self.valve_data, 'm-o', linewidth=1, markersize=1, alpha=0.8, label='实际阀门指令') self._ax2.set_xlabel('时间 (秒)', fontsize=12) self._ax2.set_ylabel('阀门开度 (%)', fontsize=12) self._ax2.legend(loc='upper right', fontsize=10) self._ax2.set_ylim([0, 105]) self._ax2.grid(True, alpha=0.3) self._fig.tight_layout() # 创建 canvas self._canvas = FigureCanvasQTAgg(self._fig) layout.addWidget(self._canvas, stretch=1) # 导航工具栏(macOS 上某些版本可能崩溃,加容错) try: toolbar = NavigationToolbar2QT(self._canvas, self) layout.addWidget(toolbar) except Exception as e: print(f"[PlotWindow] 工具栏创建失败: {e}") # 提示标签 hint = QLabel("提示: 使用工具栏缩放/平移 | 拖动矩形区域可局部放大") hint.setStyleSheet("color: gray; font-size: 12px;") layout.addWidget(hint) except Exception as e: import traceback traceback.print_exc() layout.addWidget(QLabel(f"绘图创建失败: {e}")) def _apply_x_limits(self): try: x_min = float(self.x_min_entry.text()) x_max = float(self.x_max_entry.text()) if x_min >= x_max or self._ax1 is None: return self._ax1.set_xlim([x_min, x_max]) self._ax2.set_xlim([x_min, x_max]) self._canvas.draw() except ValueError: pass def _reset_view(self): if not self._ax1 or not self.time_data: return x_min = min(self.time_data) x_max = max(self.time_data) self._ax1.set_xlim([x_min, x_max]) self._ax2.set_xlim([x_min, x_max]) self.x_min_entry.setText(f"{x_min:.1f}") self.x_max_entry.setText(f"{x_max:.1f}") self._canvas.draw() def _show_all(self): if not self._ax1 or not self.time_data: return x_min = min(self.time_data) x_max = max(self.time_data) self._ax1.set_xlim([x_min, x_max]) self._ax2.set_xlim([x_min, x_max]) self.x_min_entry.setText(f"{x_min:.1f}") self.x_max_entry.setText(f"{x_max:.1f}") self._canvas.draw() def _zoom_last_n(self, n_seconds): if not self._ax1 or not self.time_data: return x_max = max(self.time_data) x_min = max(0, x_max - n_seconds) self._ax1.set_xlim([x_min, x_max]) self._ax2.set_xlim([x_min, x_max]) self.x_min_entry.setText(f"{x_min:.1f}") self.x_max_entry.setText(f"{x_max:.1f}") self._canvas.draw() def closeEvent(self, event): """Qt 会按控件树父子关系自动销毁所有子控件(canvas + toolbar)。 此处只需清空 Python 侧引用,让 Figure 能被 GC 正常回收。 严禁 plt.close(self._fig)!plt.close() 内部绕过 Qt 直接销毁 canvas widget,与 WA_DeleteOnClose 冲突导致 double-free → SIGSEGV 闪退。 """ self._canvas = None self._ax1 = None self._ax2 = None self._fig = None super().closeEvent(event)