具有多个 y 轴的 Qt 标注示例

Qt callout example with more than one y axis

我有一个来自 PySide6 的 Callout exampleQChart。现在我已经为我的项目重写了一些代码,但是当我将鼠标悬停在“QLineSeries”上时,标注看起来比我实际指向的位置更高或更低。 这是一些代码:

标注 class(与 example 中的几乎相同)

class Callout(QGraphicsItem):

    def __init__(self, chart):
        QGraphicsItem.__init__(self, chart)
        self.chart = chart
        self._text = ""
        self._textRect = QRectF()
        self._anchor = QPointF()
        self._font = QFont()
        self._rect = QRectF()


    def boundingRect(self):
        anchor = self.mapFromParent(self.chart.mapToPosition(self._anchor))
        rect = QRectF()
        rect.setLeft(min(self._rect.left(), anchor.x()))
        rect.setRight(max(self._rect.right(), anchor.x()))
        rect.setTop(min(self._rect.top(), anchor.y()))
        rect.setBottom(max(self._rect.bottom(), anchor.y()))

        return rect

    def paint(self, painter, option, widget):
        path = QPainterPath()
        path.addRoundedRect(self._rect, 5, 5)
        anchor = self.mapFromParent(self.chart.mapToPosition(self._anchor))
        if not self._rect.contains(anchor) and not self._anchor.isNull():
            point1 = QPointF()
            point2 = QPointF()

            # establish the position of the anchor point in relation to _rect
            above = anchor.y() <= self._rect.top()
            above_center = (anchor.y() > self._rect.top() and
                anchor.y() <= self._rect.center().y())
            below_center = (anchor.y() > self._rect.center().y() and
                anchor.y() <= self._rect.bottom())
            below = anchor.y() > self._rect.bottom()

            on_left = anchor.x() <= self._rect.left()
            left_of_center = (anchor.x() > self._rect.left() and
                anchor.x() <= self._rect.center().x())
            right_of_center = (anchor.x() > self._rect.center().x() and
                anchor.x() <= self._rect.right())
            on_right = anchor.x() > self._rect.right()

            # get the nearest _rect corner.
            x = (on_right + right_of_center) * self._rect.width()
            y = (below + below_center) * self._rect.height()
            corner_case = ((above and on_left) or (above and on_right) or
                (below and on_left) or (below and on_right))
            vertical = abs(anchor.x() - x) > abs(anchor.y() - y)

            x1 = (x + left_of_center * 10 - right_of_center * 20 + corner_case *
                int(not vertical) * (on_left * 10 - on_right * 20))
            y1 = (y + above_center * 10 - below_center * 20 + corner_case *
                vertical * (above * 10 - below * 20))
            point1.setX(x1)
            point1.setY(y1)

            x2 = (x + left_of_center * 20 - right_of_center * 10 + corner_case *
                int(not vertical) * (on_left * 20 - on_right * 10))
            y2 = (y + above_center * 20 - below_center * 10 + corner_case *
                vertical * (above * 20 - below * 10))
            point2.setX(x2)
            point2.setY(y2)

            path.moveTo(point1)
            path.lineTo(anchor)
            path.lineTo(point2)
            path = path.simplified()

        painter.setBrush(QColor(255, 255, 255))
        painter.drawPath(path)
        painter.drawText(self._textRect, self._text)

    def mousePressEvent(self, event):
        if event.button() == Qt.RightButton:
            self.removecallout()
        event.setAccepted(True)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            self.setPos(self.mapToParent(
                event.pos() - event.buttonDownPos(Qt.LeftButton)))
            event.setAccepted(True)
        else:
            event.setAccepted(False)

    def set_text(self, text):
        self._text = text
        metrics = QFontMetrics(self._font)
        self._textRect = QRectF(metrics.boundingRect(
            QRect(0.0, 0.0, 150.0, 150.0), Qt.AlignLeft, self._text))
        self._textRect.translate(5, 5)
        self.prepareGeometryChange()
        self._rect = self._textRect.adjusted(-5, -5, 5, 5)

    def set_anchor(self, point):
        self._anchor = QPointF(point)

    def update_geometry(self):
        self.prepareGeometryChange()
        self.setPos(self.chart.mapToPosition(
            self._anchor) + QPointF(10, -50))

    def removecallout(self):
        self.hide()

生成图表的Class:

class CreateChart(QChartView):
    def __init__(self, data):
        super().__init__()

        self.serieses = self.getserieses(data)

        i = 0

        self.chart = QChart()
        self.buddy = None
        self.chart.legend().setVisible(False)
        xaxis = QValueAxis()
        xaxis.setTitleText("Time")
        self.chart.addAxis(xaxis, Qt.AlignBottom)
        for key in self.serieses.keys():
            if i >= 100:
                break
            else:
                self.serieses[key].setName(key)
                self.chart.addSeries(self.serieses[key])
                self.serieses[key].hovered.connect(self.tooltip)   #The connections for the temporary callout of the coordinates
                self.serieses[key].clicked.connect(self.keep_callout)  #The connection for the permanent callout of the coordinates
                axis = QValueAxis()
                axis.setTitleText(key)
                axis.setTitleBrush(self.serieses[key].color())
                self.chart.addAxis(axis, Qt.AlignLeft if ((i % 2) == 0) else Qt.AlignRight)
                self.serieses[key].attachAxis(axis)
                self.serieses[key].attachAxis(xaxis)
                i += 1
        print("Finished Loading")
        
        self.chart.legend().setMarkerShape(QLegend.MarkerShapeFromSeries)

        self.chart_view = super()
        self.chart_view.setRenderHint(QPainter.Antialiasing)
        self.chart_view.setChart(self.chart)
        #self.chart_view.setMaximumWidth(300)

        #QGraphicsView.RubberbandDrag = Selecting an area which can be retrieved by **Your QChartView**.rubberBandRect()
        self.chart_view.setDragMode(QGraphicsView.RubberBandDrag)
        self.chart_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)       #Don't need these, as they don't move the Graph. but the whole window
        self.chart_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)     # ^
        self.chart.setFocusPolicy(Qt.NoFocus)

        self._tooltip = Callout(self.chart)
        self._callouts = []

        


        self.setMouseTracking(True)     #Must be on

        
    
    def tooltip(self, point, state):
        #point = self.Mouse
        if self._tooltip == 0:
            self._tooltip = Callout(self._chart)

        if state:
            x = point.x()
            y = point.y()
            self._tooltip.set_text(f"X: {x:.1f} \nY: {y:.1f} ")
            self._tooltip.set_anchor(point)
            self._tooltip.setZValue(11)
            self._tooltip.update_geometry()
            self._tooltip.show()
        else:
            self._tooltip.hide()

    def keep_callout(self):
        self._callouts.append(self._tooltip)
        self._tooltip = Callout(self.chart)

现在,当我执行此操作时,标注在一个系列上看起来很完美,但在所有其他系列上,标注出现在我鼠标实际所在位置的上方或下方,因为标注绘制在与系列不同的轴上

在具有多个 y 轴的 Qt 图表中显示工具提示

在 C++ 中,由 eyllanesc 回答。

这是答案的 PySide6 版本。

"""PySide6 port of the Callout example from Qt v5.x"""

import sys
from PySide6.QtWidgets import (QApplication, QGraphicsScene,
                               QGraphicsView, QGraphicsSimpleTextItem, QGraphicsItem)
from PySide6.QtCore import Qt, QPointF, QRectF, QRect
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QSplineSeries, QValueAxis
from PySide6.QtGui import QPainter, QFont, QFontMetrics, QPainterPath, QColor


class Callout(QGraphicsItem):

    def __init__(self, chart, series):
        QGraphicsItem.__init__(self, chart)
        self._chart = chart
        self._series = series
        self._text = ""
        self._textRect = QRectF()
        self._anchor = QPointF()
        self._font = QFont()
        self._rect = QRectF()

    def boundingRect(self):
        anchor = self.mapFromParent(
            self._chart.mapToPosition(self._anchor, self._series))
        rect = QRectF()
        rect.setLeft(min(self._rect.left(), anchor.x()))
        rect.setRight(max(self._rect.right(), anchor.x()))
        rect.setTop(min(self._rect.top(), anchor.y()))
        rect.setBottom(max(self._rect.bottom(), anchor.y()))

        return rect

    def paint(self, painter, option, widget):
        path = QPainterPath()
        path.addRoundedRect(self._rect, 5, 5)
        anchor = self.mapFromParent(
            self._chart.mapToPosition(self._anchor, self._series))
        if not self._rect.contains(anchor) and not self._anchor.isNull():
            point1 = QPointF()
            point2 = QPointF()

            # establish the position of the anchor point in relation to _rect
            above = anchor.y() <= self._rect.top()
            above_center = (anchor.y() > self._rect.top() and
                            anchor.y() <= self._rect.center().y())
            below_center = (anchor.y() > self._rect.center().y() and
                            anchor.y() <= self._rect.bottom())
            below = anchor.y() > self._rect.bottom()

            on_left = anchor.x() <= self._rect.left()
            left_of_center = (anchor.x() > self._rect.left() and
                              anchor.x() <= self._rect.center().x())
            right_of_center = (anchor.x() > self._rect.center().x() and
                               anchor.x() <= self._rect.right())
            on_right = anchor.x() > self._rect.right()

            # get the nearest _rect corner.
            x = (on_right + right_of_center) * self._rect.width()
            y = (below + below_center) * self._rect.height()
            corner_case = ((above and on_left) or (above and on_right) or
                           (below and on_left) or (below and on_right))
            vertical = abs(anchor.x() - x) > abs(anchor.y() - y)

            x1 = (x + left_of_center * 10 - right_of_center * 20 + corner_case *
                  int(not vertical) * (on_left * 10 - on_right * 20))
            y1 = (y + above_center * 10 - below_center * 20 + corner_case *
                  vertical * (above * 10 - below * 20))
            point1.setX(x1)
            point1.setY(y1)

            x2 = (x + left_of_center * 20 - right_of_center * 10 + corner_case *
                  int(not vertical) * (on_left * 20 - on_right * 10))
            y2 = (y + above_center * 20 - below_center * 10 + corner_case *
                  vertical * (above * 20 - below * 10))
            point2.setX(x2)
            point2.setY(y2)

            path.moveTo(point1)
            path.lineTo(anchor)
            path.lineTo(point2)
            path = path.simplified()

        painter.setBrush(QColor(255, 255, 255))
        painter.drawPath(path)
        painter.drawText(self._textRect, self._text)

    def mousePressEvent(self, event):
        event.setAccepted(True)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            self.setPos(self.mapToParent(
                event.pos() - event.buttonDownPos(Qt.LeftButton)))
            event.setAccepted(True)
        else:
            event.setAccepted(False)

    def set_text(self, text):
        self._text = text
        metrics = QFontMetrics(self._font)
        self._textRect = QRectF(metrics.boundingRect(
            QRect(0.0, 0.0, 150.0, 150.0), Qt.AlignLeft, self._text))
        self._textRect.translate(5, 5)
        self.prepareGeometryChange()
        self._rect = self._textRect.adjusted(-5, -5, 5, 5)

    def set_anchor(self, point):
        self._anchor = QPointF(point)

    def update_geometry(self):
        self.prepareGeometryChange()
        self.setPos(self._chart.mapToPosition(
            self._anchor, self._series) + QPointF(10, -50))

    def setSeries(self, series):
        self._series = series


class View(QChartView):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setScene(QGraphicsScene(self))

        self.setDragMode(QGraphicsView.RubberBandDrag)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        # Chart
        self._chart = QChart()
        self._chart.setMinimumSize(640, 480)
        self._chart.setTitle("Hover the line to show callout. Click the line "
                             "to make it stay")
        self._chart.legend().hide()

        self.series = QLineSeries()
        self.series.append(1, 3)
        self.series.append(4, 5)
        self.series.append(5, 4.5)
        self.series.append(7, 1)
        self.series.append(11, 2)
        self._chart.addSeries(self.series)

        self.series2 = QSplineSeries()
        self.series2.append(1.6, 1.4)
        self.series2.append(2.4, 3.5)
        self.series2.append(3.7, 2.5)
        self.series2.append(7, 4)
        self.series2.append(10, 2)

        self._chart.addSeries(self.series2)

        xaxis = QValueAxis()
        xaxis.setTitleText("X")
        self._chart.addAxis(xaxis, Qt.AlignBottom)
        yaxis = QValueAxis()
        yaxis.setTitleText("YR")
        self._chart.addAxis(yaxis, Qt.AlignRight)
        y2axis = QValueAxis()
        y2axis.setTitleText("YL")
        self._chart.addAxis(y2axis, Qt.AlignLeft)

        self.series.attachAxis(xaxis)
        self.series.attachAxis(y2axis)

        self.series2.attachAxis(xaxis)
        self.series2.attachAxis(yaxis)

        self._chart.setAcceptHoverEvents(True)

        self.setRenderHint(QPainter.Antialiasing)
        self.scene().addItem(self._chart)

        self._coordX = QGraphicsSimpleTextItem(self._chart)
        self._coordX.setPos(
            self._chart.size().width() / 2 - 50, self._chart.size().height())
        self._coordX.setText("X: ")
        self._coordY = QGraphicsSimpleTextItem(self._chart)
        self._coordY.setPos(
            self._chart.size().width() / 2 + 50, self._chart.size().height())
        self._coordY.setText("Y: ")

        self._callouts = []
        self._tooltip = Callout(self._chart, self.series)

        self.series.clicked.connect(self.keep_callout)
        self.series.hovered.connect(self.tooltip)

        self.series2.clicked.connect(self.keep_callout)
        self.series2.hovered.connect(self.tooltip)

        self.setMouseTracking(True)

    def resizeEvent(self, event):
        if self.scene():
            self.scene().setSceneRect(QRectF(QPointF(0, 0), event.size()))
            self._chart.resize(event.size())
            self._coordX.setPos(
                self._chart.size().width() / 2 - 50,
                self._chart.size().height() - 20)
            self._coordY.setPos(
                self._chart.size().width() / 2 + 50,
                self._chart.size().height() - 20)
            for callout in self._callouts:
                callout.update_geometry()
        QGraphicsView.resizeEvent(self, event)

    def mouseMoveEvent(self, event):
        pos = self._chart.mapToValue(event.pos())
        x = pos.x()
        y = pos.y()
        self._coordX.setText(f"X: {x:.2f}")
        self._coordY.setText(f"Y: {y:.2f}")
        QGraphicsView.mouseMoveEvent(self, event)

    def keep_callout(self):
        series = self.sender()
        self._callouts.append(self._tooltip)
        self._tooltip = Callout(self._chart, series)

    def tooltip(self, point, state):
        series = self.sender()
        if self._tooltip == 0:
            self._tooltip = Callout(self._chart, series)

        if state:
            x = point.x()
            y = point.y()
            self._tooltip.setSeries(series)
            self._tooltip.set_text(f"X: {x:.2f} \nY: {y:.2f} ")
            self._tooltip.set_anchor(point)
            self._tooltip.setZValue(11)
            self._tooltip.update_geometry()
            self._tooltip.show()
        else:
            self._tooltip.hide()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    v = View()
    v.show()
    sys.exit(app.exec())