Files
ReinLoopTest/ReinLoop/ui/plot_window.py
2026-07-30 11:12:31 +08:00

190 lines
7.1 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.
# 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)