You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 11:09:16