如何将QGraphicsScene位置关联到Matplotlib绘图轴位置
问题描述
我有一个包含两个绘图的窗口,一个是2D绘图,另一个是该绘图沿y轴的截取图。我希望通过移动水平条到指定位置来选择截取区域,并让1D绘图实时更新。目前我无法将线条所在QGraphicsScene中的位置关联到绘图轴的位置,需要在MainWindow的plot_position函数中解决该问题,同时也接受其他实现思路。
界面说明:左侧为2D热力图,右侧为1D曲线,红色水平条可在2D图上拖动以选择截取位置。
解决方案
核心是将QGraphicsScene中的像素坐标转换为Matplotlib坐标轴的数据坐标,同时优化交互逻辑和性能。以下是修改后的完整代码:
import sys from PyQt5.Qt import Qt, QObject, QPen, QPointF from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QSizePolicy from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsLineItem, QGraphicsView, \ QGraphicsScene, QWidget, QHBoxLayout from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import xarray as xr import numpy as np class Signals(QObject): bttnReleased = pyqtSignal(float, float) class HLineItem(QGraphicsLineItem): def __init__(self, signals, canvas): super(HLineItem, self).__init__() self.signals = signals self.canvas = canvas # 关联Matplotlib画布,用于获取轴范围 self.setPen(QPen(Qt.red, 3)) self.setFlag(QGraphicsLineItem.ItemIsMovable) self.setCursor(Qt.OpenHandCursor) self.setAcceptHoverEvents(True) # 限制仅垂直移动 self.setFlag(QGraphicsLineItem.ItemSendsGeometryChanges) def itemChange(self, change, value): if change == QGraphicsLineItem.ItemPositionChange: # 固定x坐标,只允许y方向拖动 return QPointF(self.scenePos().x(), value.y()) return super().itemChange(change, value) def mouseMoveEvent(self, event): orig_cursor_position = event.lastScenePos() updated_cursor_position = event.scenePos() orig_position = self.scenePos() updated_cursor_y = updated_cursor_position.y() - orig_cursor_position.y() + orig_position.y() # 获取Matplotlib轴的像素边界,限制线条在有效区域内 ax_bbox = self.canvas.axes.get_window_extent().transformed(self.canvas.figure.dpi_scale_trans.inverted()) ax_pixel_y_min = ax_bbox.y0 * self.canvas.figure.dpi ax_pixel_y_max = (ax_bbox.y0 + ax_bbox.height) * self.canvas.figure.dpi updated_cursor_y = max(ax_pixel_y_min, min(updated_cursor_y, ax_pixel_y_max)) self.setPos(QPointF(orig_position.x(), updated_cursor_y)) def mouseReleaseEvent(self, event): x_pos = event.scenePos().x() y_pos = event.scenePos().y() self.signals.bttnReleased.emit(x_pos, y_pos) class PlotCanvas(FigureCanvas): def __init__(self, parent=None, width=5, height=4, dpi=100): self.fig = Figure(figsize=(width, height), dpi=dpi) super(PlotCanvas, self).__init__(self.fig) self.setParent(parent) FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) self.data = xr.DataArray() self.axes = None def plot(self, data): self.data = data if self.axes is None: self.axes = self.fig.add_subplot(111) else: self.axes.clear() # 清空现有轴内容,避免重复创建子图 self.data.plot(ax=self.axes) self.axes.set_xlim(-.5, .5) self.draw() class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.signals = Signals() x = np.linspace(-1, 1, 51) y = np.linspace(-1, 1, 51) z = np.linspace(-1, 1, 51) xyz = np.meshgrid(x, y, z, indexing='ij') d = np.sin(np.pi * np.exp(-1 * (xyz[0] ** 2 + xyz[1] ** 2 + xyz[2] ** 2))) * np.cos(np.pi / 2 * xyz[1]) self.xar = xr.DataArray(d, coords={"slit": x, 'perp': y, "energy": z}, dims=["slit", "perp", "energy"]) self.cut = self.xar.sel({"perp": 0}, method='nearest') self.edc = self.cut.sel({'slit': 0}, method='nearest') self.canvas = PlotCanvas() self.canvas_edc = PlotCanvas() self.canvas.plot(self.cut) self.canvas_edc.plot(self.edc) self.view = QGraphicsView() self.scene = QGraphicsScene() self.line = HLineItem(self.signals, self.canvas) # 传入画布对象用于坐标转换 self.line_pos = [0, 0] self.layout1 = QHBoxLayout() self.layout2 = QHBoxLayout() self.connect_scene() self.layout1.addWidget(self.view) self.layout2.addWidget(self.canvas_edc) self.central = QWidget() self.main_layout = QHBoxLayout() self.main_layout.addLayout(self.layout1) self.main_layout.addLayout(self.layout2) self.central.setLayout(self.main_layout) self.setCentralWidget(self.central) self.signals.bttnReleased.connect(self.plot_position) def connect_scene(self): s = self.canvas.figure.get_size_inches() * self.canvas.figure.dpi self.view.setScene(self.scene) self.scene.addWidget(self.canvas) self.scene.setSceneRect(0, 0, s[0], s[1]) # 初始化线条到2D图轴的中心位置 ax_bbox = self.canvas.axes.get_window_extent().transformed(self.canvas.figure.dpi_scale_trans.inverted()) ax_pixel_y_center = (ax_bbox.y0 + ax_bbox.height/2) * self.canvas.figure.dpi self.line.setLine(0, 0, self.scene.sceneRect().width(), 0) self.line.setPos(self.line_pos[0], ax_pixel_y_center) self.scene.addItem(self.line) def refresh_edc(self): # 直接更新现有画布,无需重建 self.canvas_edc.plot(self.edc) def plot_position(self, x, y): self.line_pos = [x, y] # 将场景像素坐标转换为Matplotlib数据坐标 ax_bbox = self.canvas.axes.get_window_extent().transformed(self.canvas.figure.dpi_scale_trans.inverted()) ax_pixel_y_min = ax_bbox.y0 * self.canvas.figure.dpi ax_pixel_y_max = (ax_bbox.y0 + ax_bbox.height) * self.canvas.figure.dpi # 归一化像素坐标到轴的0-1范围,再映射到数据坐标 normalized_y = (ax_pixel_y_max - y) / (ax_pixel_y_max - ax_pixel_y_min) y_data_min, y_data_max = self.canvas.axes.get_ylim() selected_y_data = y_data_min + normalized_y * (y_data_max - y_data_min) # 选择最接近的数据集点 sel_val = min(self.cut.slit.values, key=lambda f: abs(f - selected_y_data)) # 更新截取数据并刷新1D图 self.edc = self.cut.sel({"slit": sel_val}, method='nearest') self.refresh_edc() class App(QApplication): def __init__(self, sys_argv): super(App, self).__init__(sys_argv) self.setAttribute(Qt.AA_EnableHighDpiScaling) self.mainWindow = MainWindow() self.mainWindow.setWindowTitle("arpys") self.mainWindow.show() def main(): app = App(sys.argv) sys.exit(app.exec_()) if __name__ == "__main__": main()
关键修改说明
- 坐标映射:在
plot_position函数中,完成了场景像素坐标到Matplotlib数据坐标的转换,实现了拖动位置与数据值的准确关联 - 交互限制:限制水平条仅可垂直移动,且只能在2D绘图的有效轴范围内拖动,避免无效操作
- 性能优化:修改
PlotCanvas的绘图逻辑,清空现有轴内容而非重建画布,减少资源开销 - 初始位置优化:初始化时将水平条放在2D图轴的中心,符合直观交互体验
内容的提问来源于stack exchange,提问作者Amanda.py
相关产品推荐
相关产品推荐

