QChart 移动 缩放 加速

发布时间 2024-01-08 14:31:27作者: 方头狮

qchart 和 qchartview 的运用的例子


qchart 存在一些问题

一般用在2000个点以下的场景,点多了,就会卡。 解决的办法就是 开启opengl加速。
但这时,对qchartview 进行transform 时, 绘制的点,并不能同时变换。原因是QT把opengl的图案是单独绘制的,要找到这个单独的view 进行变换才行。 但找不到。。。
这就不能使用的底层的函数了,只能用 qchart的setrange,变换的事情,交给qt 自己去实现。

这个例子,实现了 拖动 滚轮缩放 ,加了一个取值的限定框。

python 3.10 pyside6


import sys


from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QVBoxLayout,
    QGraphicsSceneWheelEvent,
    QGraphicsSceneMouseEvent,
    QLabel,
    QHBoxLayout,
    QFormLayout,
    QPushButton,
    QSlider,
)
from PySide6.QtCharts import QChartView, QChart, QScatterSeries, QValueAxis, QLineSeries
from PySide6.QtGui import QMouseEvent
from PySide6.QtCore import Qt, QPointF, Signal, QRectF, QTimer, QPoint


import random
import math
import threading


class zn_chart(QChart):
    sg_mouse_db_click = Signal(QPointF)

    def __init__(self):
        super().__init__()
        self._left_pos = None

        # data series
        self.se = QScatterSeries()
        self.ax_x = QValueAxis()
        self.ax_y = QValueAxis()
        self.addSeries(self.se)
        self.addAxis(self.ax_x, Qt.AlignmentFlag.AlignBottom)
        self.addAxis(self.ax_y, Qt.AlignmentFlag.AlignLeft)
        self.se.attachAxis(self.ax_x)
        self.se.attachAxis(self.ax_y)
        self.se.setBorderColor(self.se.color())
        self.se.setMarkerSize(2.0)
        self.ax_x.setRange(-51, 51)
        self.ax_y.setRange(-51, 51)
        self.setTitle("Line Chart")
        self.se.setUseOpenGL(True)

        # data
        self._data: list[QPointF] = []
        self._x_min: float = None
        self._x_max: float = None
        self._y_min: float = None
        self._y_max: float = None

        # limit line data

        self.lm_x_min: float = None
        self.lm_x_max: float = None

        self.lm_se = QLineSeries()
        self.addSeries(self.lm_se)
        self.lm_se.attachAxis(self.ax_x)
        self.lm_se.attachAxis(self.ax_y)
        self.lm_se.setUseOpenGL(True)
        self.lm_pts: list[QPointF] = []
        self.lm_se.setMarkerSize(2.0)
        self.lm_se.setVisible(True)

        # signal
        self.drag_start_pos: QPointF = None
        self.drag_start_range: QRectF = None
        self.on_drag: bool = False

        # tm
        self.tm: QTimer = None
        self._tmp_data: list[QPointF] = []
        self._tm_lock = threading.Lock()

    @property
    def range(self) -> QRectF:
        return QRectF(
            self.ax_x.min(),
            self.ax_y.min(),
            self.ax_x.max() - self.ax_x.min(),
            self.ax_y.max() - self.ax_y.min(),
        )

    def set_range_x(self, p1: float, p2: float):
        self.ax_x.setRange(p1, p2)

    def set_range_y(self, p1: float, p2: float):
        self.ax_y.setRange(p1, p2)

    def setrange(self, r: QRectF):
        self.ax_x.setRange(r.x(), r.x() + r.width())
        self.ax_y.setRange(r.y(), r.y() + r.height())

    def wheelEvent(self, event: QGraphicsSceneWheelEvent) -> None:
        y = event.delta()
        factor = 1
        if y > 0:
            factor = 1.1
        elif y < 0:
            factor = 1 / 1.1

        if factor != 1:
            f = 1 / factor
            r = self.range
            r_center_x = r.x() + r.width() * 0.5
            r_center_y = r.y() + r.height() * 0.5
            p_center = self.mapToValue(event.pos())

            r_c_x2 = p_center.x() + f * (r_center_x - p_center.x())
            r_c_y2 = p_center.y() + f * (r_center_y - p_center.y())
            w2 = r.width() * f
            h2 = r.height() * f
            r2 = QRectF(r_c_x2 - w2 * 0.5, r_c_y2 - h2 * 0.5, w2, h2)
            self.setrange(r2)

    def on_drag_enter(self, p: QPointF) -> None:
        self.drag_start_pos = p
        self.drag_start_range = self.range
        self.on_drag = True

    def on_drag_exit(self) -> None:
        self.on_drag = False
        self.drag_start_pos = None
        self.drag_start_range = None

    def on_drag_move(self, p: QPointF) -> None:
        if self.on_drag:
            vp = self.plotArea()
            now_p = p
            dx = -(
                (now_p.x() - self.drag_start_pos.x())
                / vp.width()
                * self.drag_start_range.width()
            )
            dy = (
                (now_p.y() - self.drag_start_pos.y())
                / vp.height()
                * self.drag_start_range.height()
            )
            r2 = QRectF(
                self.drag_start_range.x() + dx,
                self.drag_start_range.y() + dy,
                self.drag_start_range.width(),
                self.drag_start_range.height(),
            )
            self.setrange(r2)

    def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        p = self.mapToValue(event.pos())
        self.sg_mouse_db_click.emit(p)
        print(p)
        return super().mouseDoubleClickEvent(event)

    # data op
    @property
    def data(self) -> list[QPointF]:
        return self._data

    def data_add_xy(self, x: float, y: float):
        self._data.append(QPointF(x, y))
        self.se.append(x, y)
        if self._x_min is None or x < self._x_min:
            self._x_min = x
        if self._x_max is None or x > self._x_max:
            self._x_max = x
        if self._y_min is None or y < self._y_min:
            self._y_min = y
        if self._y_max is None or y > self._y_max:
            self._y_max = y

    def data_add_p(self, d: QPointF | QPoint):
        self.data_add_xy(d.x(), d.y())

    def data_add_list_point(self, lp: list[QPointF | QPointF]):
        for i in lp:
            self.data_add_p(i)
        self.se.append(lp)

    def data_clear(self):
        self._data.clear()
        self.se.clear()
        self._x_min = self._x_max = self._y_min = self._y_max = None

    def data_load_more(self, sp: list[QPointF]):
        with self._tm_lock:
            if self.tm is not None:
                self.tm.stop()
                self.tm = None
            self.data_clear()

            self._tmp_data.clear()
            self._tmp_data += sp[:]
            self.tm = QTimer()
            self.tm.setInterval(15)
            self.tm.timeout.connect(self._do_add)
            self.tm.start()

    def _do_add(self):
        with self._tm_lock:
            if self.tm is None:
                self._tmp_data.clear()
            elif len(self._tmp_data) == 0:
                self.tm.stop()
                self.tm = None
                print("over")
            else:
                if len(self._tmp_data) > 1000:
                    to_add = self._tmp_data[:1000]
                    self._tmp_data = self._tmp_data[1000:]
                else:
                    to_add = self._tmp_data[:]
                    self._tmp_data.clear()

                self.data_add_list_point(to_add)

    # limit setting
    def lm_refresh(self):
        if (
            self.lm_x_min is not None
            and self.lm_x_max is not None
            and self._y_max is not None
            and self._y_min is not None
        ):
            if self._y_max > self._y_min:
                x1 = float(self.lm_x_min)
                x2 = float(self.lm_x_max)
                d = self._y_max - self._y_min
                y1 = self._y_min - 0.2 * d
                y2 = self._y_max + 0.2 * d
                self.lm_pts.clear()
                for i in [[x1, y1], [x1, y2], [x2, y2], [x2, y1], [x1, y1]]:
                    self.lm_pts.append(QPointF(*i))
                self.lm_se.clear()
                self.lm_se.append(self.lm_pts)

    def lm_set_x_min(self, x: float):
        xx = float(x)
        if self.lm_x_max is None or xx <= self.lm_x_max:
            self.lm_x_min = xx
            self.lm_refresh()

    def lm_set_x_max(self, x: float):
        xx = float(x)
        if self.lm_x_min is None or xx >= self.lm_x_min:
            self.lm_x_max = xx
            self.lm_refresh()

    def lm_set_x_min_percent(self, v: int):
        f = float(v / 100)
        if self._x_min is not None and self._x_max is not None:
            dd = self._x_max - self._x_min
            self.lm_set_x_min(dd * f + self._x_min)

    def lm_set_x_max_percent(self, v: int):
        f = float(v / 100)
        if self._x_min is not None and self._x_max is not None:
            dd = self._x_max - self._x_min
            self.lm_set_x_max(dd * f + self._x_min)

    def lm_show(self):
        self.lm_se.setVisible(True)

    def lm_hide(self):
        self.lm_se.setVisible(False)

    def lm_new_from_data(self):
        self.lm_set_x_min(self._x_min)
        self.lm_set_x_max(self._x_max)

    def lm_clear(self):
        self.lm_se.clear()
        self.lm_x_min = None
        self.lm_x_max = None
        self.lm_pts = []


class my_view(QChartView):
    sg_drag_enter = Signal(QPointF)
    sg_drag_exit = Signal()
    sg_drag_move = Signal(QPointF)
    on_drag: bool = False

    def mousePressEvent(self, event: QMouseEvent) -> None:
        if event.button() == Qt.MouseButton.LeftButton:
            self.sg_drag_enter.emit(event.position())
            self.on_drag = True
        return super().mousePressEvent(event)

    def mouseReleaseEvent(self, event: QMouseEvent) -> None:
        self.sg_drag_exit.emit()
        self.on_drag = False
        return super().mouseReleaseEvent(event)

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        if self.on_drag:
            self.sg_drag_move.emit(event.position())
        return super().mouseMoveEvent(event)

    def setChart(self, chart: QChart) -> None:
        if isinstance(chart, zn_chart):
            self.sg_drag_enter.connect(chart.on_drag_enter)
            self.sg_drag_exit.connect(chart.on_drag_exit)
            self.sg_drag_move.connect(chart.on_drag_move)
        return super().setChart(chart)


class zn_slider(QSlider):
    def __init__(self):
        super().__init__(Qt.Orientation.Horizontal)
        self.setMinimum(0)
        self.setMaximum(100)
        self.setSingleStep(1)
        self.setValue(0)
        self.setTickPosition(self.TickPosition.TicksBelow)


if __name__ == "__main__":

    class test_wg(QMainWindow):
        def __init__(self):
            super().__init__()
            self.setWindowTitle("PySide6 QChart Example")
            self.setGeometry(50, 50, 800, 600)

            self.central_widget = QWidget()
            self.setCentralWidget(self.central_widget)

            self.main_layout = QVBoxLayout(self.central_widget)

            self.chart_view = my_view()

            self.main_layout.addWidget(self.chart_view)

            self.chart = zn_chart()

            self.chart_view.setChart(self.chart)

            ##
            self.c_ly = QHBoxLayout()
            self.main_layout.addLayout(self.c_ly)

            self.b1 = QPushButton("add data")
            self.b1.clicked.connect(self.adddata)
            self.c_ly.addWidget(self.b1)

            self.b2 = QPushButton("init limit")
            self.b2.clicked.connect(self.lm_init)
            self.c_ly.addWidget(self.b2)

            self.b3 = QPushButton("show limit")
            self.b3.clicked.connect(self.chart.lm_show)
            self.c_ly.addWidget(self.b3)

            self.b4 = QPushButton("hide limit")
            self.b4.clicked.connect(self.chart.lm_hide)
            self.c_ly.addWidget(self.b4)
            # slider
            self.f_ly = QFormLayout()

            self.s1 = zn_slider()
            self.s1.valueChanged.connect(self.on_s1_change)
            self.s1_lb = QLabel("下限")
            self.s1_lb.setFixedWidth(120)
            self.f_ly.addRow(self.s1_lb, self.s1)

            self.s2 = zn_slider()
            self.s2.setValue(100)
            self.s2.valueChanged.connect(self.on_s2_change)
            self.s2_lb = QLabel("上限")
            self.s2_lb.setFixedWidth(120)
            self.f_ly.addRow(self.s2_lb, self.s2)

            self.main_layout.addLayout(self.f_ly)
            # show
            self.show()

        def adddata(self):
            self.chart.data_clear()
            dt = []
            for i in range(10000):
                x = random.randint(0, 10000) / 100 - 50
                xx = math.pi * 2 * x / 100
                y = math.sin(xx) * 50
                dt.append(QPointF(x, y))
            self.chart.data_load_more(dt)

        def lm_init(self):
            self.chart.lm_set_x_min_percent(0)
            self.chart.lm_set_x_max_percent(100)
            self.chart.lm_show()

        def on_s1_change(self, v: int):
            self.chart.lm_set_x_min_percent(v)
            if self.chart.lm_x_min is None:
                self.s1_lb.setText(f"下限")
            else:
                self.s1_lb.setText(f"下限 {self.chart.lm_x_min:<.3f}")

        def on_s2_change(self, v: int):
            self.chart.lm_set_x_max_percent(v)
            if self.chart.lm_x_max is None:
                self.s2_lb.setText(f"上限")
            else:
                self.s2_lb.setText(f"上限 {self.chart.lm_x_max:<.3f}")

    app = QApplication(sys.argv)
    window = test_wg()
    app.exec()