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

PySide2图表光标垂直线卡顿优化及解决光标仅在信号线上移动问题

我来帮你搞定这两个问题,咱们一步步优化你的代码:

问题分析

1. 垂直线卡顿延迟

  • 没有在鼠标移动时主动触发重绘,导致垂直线更新不及时
  • 直接使用视图坐标(event.x())绘制,没有考虑图表缩放、滚动的变换,坐标不准确且增加无效计算
  • 重复创建QPen对象,额外消耗性能

2. 光标离开信号线后垂直线停止跟随

  • 只有鼠标在信号线上触发系列事件时,图表才会被动重绘;空白区域移动时无主动重绘触发,导致垂直线停滞

修改后的完整代码

# This Python file uses the following encoding: utf-8
import sys, os
from random import randint
from PySide2 import QtCore, QtGui
from PySide2.QtWidgets import *
from PySide2.QtCharts import *
from PySide2.QtGui import QPainter, QKeySequence, QColor
from PySide2.QtCore import Qt, QPointF, QPoint, Signal
from enum import Enum
from numpy import math

class Derived(QtCharts.QChartView): # Derived class
    def __init__(self, chart):
        super().__init__(chart)
        # 提前初始化画笔,避免重复创建
        self.pen = QtGui.QPen(QColor("black"), 1, QtCore.Qt.SolidLine)
        self.m_MousePos = QPointF(0,0)

    def drawForeground(self, painter, rect):
        # 只在鼠标位置有效时绘制垂直线
        if self.m_MousePos.x() != 0:
            chart = self.chart()
            # 获取Y轴范围,确保垂直线覆盖整个图表高度
            y_min = chart.axisY().min()
            y_max = chart.axisY().max()
            # 转换坐标为场景坐标,保证缩放滚动后位置正确
            top_scene = chart.mapToScene(QPointF(self.m_MousePos.x(), y_max))
            bottom_scene = chart.mapToScene(QPointF(self.m_MousePos.x(), y_min))
            painter.setPen(self.pen)
            painter.drawLine(top_scene.x(), top_scene.y(), bottom_scene.x(), bottom_scene.y())

    def mouseMoveEvent(self, event):
        # 视图坐标转场景坐标,适配图表的缩放和滚动
        scene_pos = self.mapToScene(event.pos())
        self.m_MousePos = scene_pos
        self.update() # 主动触发重绘,解决卡顿和不跟随问题
        super().mouseMoveEvent(event)

class MainWindow(QMainWindow):
    def __init__(self):
        QMainWindow.__init__(self)
        widget = QWidget()
        self.setCentralWidget(widget)
        self.currentFileIndex = 0
        self.Signal1Data = []
        self.Signal2Data = []
        self.selectedPoints = []
        self.chart = QtCharts.QChart()
        self.loadSignalFile()
        self.chartView = Derived(self.chart)
        self.chartView.setMouseTracking(True)
        self.chartView.setRubberBand(QtCharts.QChartView.HorizontalRubberBand)
        self.chartView.setRenderHint(QPainter.Antialiasing)
        self.removeLastButton = QPushButton(self.tr("Remove last point"))
        self.removeLastButton.clicked.connect(self.popSelectedPoint)
        layout = QGridLayout()
        self.instructionLabel = QLabel(self.tr("Start by identifying feature (mouse click on signal)"))
        font = QtGui.QFont( "Arial", 18, QtGui.QFont.Bold)
        self.instructionLabel.setFont(font)
        layout.addWidget(self.instructionLabel, 0, 0 )
        layout.addWidget(self.chartView, 1, 0, 1, 2)
        layout.addWidget(self.removeLastButton, 2, 0)
        widget.setLayout(layout)
        self.statusBar().showMessage("message")
        self.setWindowTitle(self.tr("Feature Identifier"))
        self.setMinimumSize(160, 160)
        self.resize(800, 600)

    def updateChart(self):
        self.chart.removeAllSeries()
        self.Signal1LineSeries = QtCharts.QLineSeries()
        self.Signal2LineSeries = QtCharts.QLineSeries()
        self.Signal1LineSeries.setName("1")
        self.Signal2LineSeries.setName("2")
        pen = QtGui.QPen()
        pen.setWidth(3)
        pen.setColor(QColor("red"))
        self.Signal1LineSeries.setPen(pen)
        pen.setColor(QColor("royalblue"))
        self.Signal2LineSeries.setPen(pen)
        self.SelectedPointsSeries = QtCharts.QScatterSeries()
        self.SelectedPointsSeries.setName("Selected points")
        self.SelectedPointsSeries.setMarkerSize(10.0)
        pen.setColor(QColor("green"))
        pen.setWidth(1)
        self.SelectedPointsSeries.setPen(pen)
        dataPoints = [QtCore.QPointF(i+1, self.Signal1Data[i]) for i in range(len(self.Signal1Data))]
        self.Signal1LineSeries.replace(dataPoints)
        dataPoints = [QtCore.QPointF(i+1, self.Signal2Data[i]) for i in range(len(self.Signal2Data))]
        self.Signal2LineSeries.replace(dataPoints)
        self.Signal1LineSeries.clicked.connect(self.on_clicked1)
        self.Signal2LineSeries.clicked.connect(self.on_clicked2)
        self.chart.addSeries(self.Signal1LineSeries)
        self.chart.addSeries(self.Signal2LineSeries)
        self.addSelectedPoints()
        self.chart.addSeries(self.SelectedPointsSeries)
        self.chart.createDefaultAxes()
        self.chart.setTitle("Title")

    def verifyClick(self, series, p, threshold=2):
        xIndex = round(p.x())
        if 0 <= xIndex < len(series.pointsVector()):
            diff = p.y()- series.pointsVector()[xIndex].y()
            if abs( diff ) < threshold:
                return xIndex

    @QtCore.Slot()
    def on_clicked2(self, p):
        x = self.verifyClick(self.Signal2LineSeries, p)
        if x is not None:
            print("2 clicked: ", x)
            self.recordSelectedPoint(int(x), self.Signal2LineSeries)
            self.updateChart()

    @QtCore.Slot()
    def on_clicked1(self, p):
        x = self.verifyClick(self.Signal1LineSeries, p)
        if x is not None:
            print("1 clicked: ", x)
            self.recordSelectedPoint(int(x), self.Signal1LineSeries)
            self.updateChart()

    def recordSelectedPoint(self, i, lineSeries):
        self.selectedPoints.append(QPointF(i, lineSeries.at(i).y()))

    def addSelectedPoints(self):
        self.SelectedPointsSeries.replace(self.selectedPoints)

    def loadSignalFile(self):
        self.Signal1Data = []
        self.Signal2Data = []
        for row in range(1200):
            self.Signal1Data.append(math.sin(row/125))
            self.Signal2Data.append(math.sin((row+25)/125))
        self.updateChart()

    @QtCore.Slot()
    def popSelectedPoint(self):
        if self.selectedPoints: # 避免空列表弹出报错
            self.selectedPoints.pop()
            self.updateChart() # 替换loadSignalFile,保留原始数据

if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

关键修改说明

  1. 坐标转换优化:

    • 使用mapToScene将鼠标视图坐标转为场景坐标,确保垂直线在图表缩放、滚动时位置始终准确
    • 基于Y轴范围绘制垂直线,保证覆盖整个图表高度
  2. 主动触发重绘:

    • 在mouseMoveEvent中调用self.update(),让鼠标每移动一次就立即更新垂直线,彻底解决卡顿和离开信号线不跟随的问题
  3. 性能优化:

    • 将QPen初始化移到__init__中,避免重复创建对象
    • 增加鼠标位置有效性判断,减少无效绘制
  4. 修复选中点移除bug:

    • 把popSelectedPoint中的loadSignalFile改为updateChart,避免误删原始信号数据

现在运行代码,你会发现垂直线流畅跟随鼠标(无论在图表哪个区域),点击选择数据点的功能也正常工作。

内容的提问来源于stack exchange,提问作者cricardol

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 04:44:06