Showing tooltip in a Qt chart with multiple y axes

2019-06-06 16:18发布

问题:

I am working on implementing permanent tooltip functionality, i.e, display tooltip on hover and make tooltip stay permanently on clicking in my chart where I have a line series and spline series. This is basically a different version of the Qt Callout example Here's the link. The only change I have implemented is that I have used QValueAxis to add two y axes on the left side, one for each of the series. They're sharing the same x-axis. Now when I try to run the program, the tooltip appears correctly for the line curve but it is misplaced in case of spline curve. It appears at some distance perpendicularly below the point on the curve where I am hovering the mouse pointer.

This issue does not arise when I use single y axis by using createdefaultaxes().

Result with single y axis

Result with multiple y axes

I have posted the code related to the click and hover functions below. Objects of the class "Callout" are being used in this code so here's the code for Callout (same link as the one I posted before). I wasn't able to figure out which snippets of Callout code are the most relevant regarding this question and hence I had to post the link.

// chart
    m_chart = new QChart;

    QLineSeries *series = new QLineSeries;
    //appended some (x,y) values to series and added to chart
    m_chart->addSeries(series);

    QSplineSeries *series2 = new QSplineSeries;
    //appended some (x,y) values to series2 and added to chart
    m_chart->addSeries(series2);

    //m_chart->createDefaultAxes(); for single y axis
    QValueAxis *axisX = new QValueAxis; 
    QValueAxis *axisY1 = new QValueAxis;  //for multiple y axes
    QValueAxis *axisY2 = new QValueAxis;

    m_chart->addAxis(axisX, Qt::AlignBottom);
    m_chart->addAxis(axisY1, Qt::AlignLeft);
    m_chart->addAxis(axisY2, Qt::AlignLeft);

    series->attachAxis(axisX);
    series->attachAxis(axisY1);
    series2->attachAxis(axisX);
    series2->attachAxis(axisY2);

    m_chart->setAcceptHoverEvents(true);

    setRenderHint(QPainter::Antialiasing);
    scene()->addItem(m_chart);

    connect(series, &QLineSeries::clicked, this, &View::keepCallout);
    connect(series, &QLineSeries::hovered, this, &View::tooltip);

    connect(series2, &QSplineSeries::clicked, this, &View::keepCallout);
    connect(series2, &QSplineSeries::hovered, this, &View::tooltip);

    this->setMouseTracking(true);

// keepCallout() function
    {
     m_callouts.append(m_tooltip);//m_callouts is a QList of "Callout" class type
     m_tooltip = new Callout(m_chart);
    }

//tooltip(QPointF point, bool state) function
{
    if (m_tooltip == 0)
        m_tooltip = new Callout(m_chart);

    if (state) {
        m_tooltip->setText(QString("X: %1 \nY: %2").arg(point.x()).arg(point.y()));
        m_tooltip->setAnchor(point);
        m_tooltip->setZValue(11);
        m_tooltip->updateGeometry();
        m_tooltip->show();
  } else {
        m_tooltip->hide();
  }
}

回答1:

mapToPosition() returns the given position a value that depends on the series to which they belong, in your case having 2 axes this value requires that the series be explicitly indicated. For this, callout must have an attribute that stores the series.

view.cpp

...
void View::keepCallout()
{
    QAbstractSeries *series = qobject_cast<QAbstractSeries *>(sender());
    m_callouts.append(m_tooltip);

    m_tooltip = new Callout(m_chart, series);
    m_tooltip->hide();
}

void View::tooltip(QPointF point, bool state)
{
    QAbstractSeries *series = qobject_cast<QAbstractSeries *>(sender());
    if (m_tooltip == 0){
        m_tooltip = new Callout(m_chart, series);
    }
    if (state) {
        m_tooltip->setSeries(series);
        m_tooltip->setText(QString("X: %1 \nY: %2 ").arg(point.x()).arg(point.y()));
        m_tooltip->setAnchor(point);
        m_tooltip->setZValue(11);
        m_tooltip->updateGeometry();
        m_tooltip->show();
    } else {
        m_tooltip->hide();
    }
}

callout.h

#ifndef CALLOUT_H
#define CALLOUT_H

#include <QtCharts/QChartGlobal>
#include <QtWidgets/QGraphicsItem>
#include <QtGui/QFont>
QT_BEGIN_NAMESPACE
class QGraphicsSceneMouseEvent;
QT_END_NAMESPACE

QT_CHARTS_BEGIN_NAMESPACE
class QChart;
class QAbstractSeries;
QT_CHARTS_END_NAMESPACE

QT_CHARTS_USE_NAMESPACE

class Callout : public QGraphicsItem
{
public:
    Callout(QChart *chart, QAbstractSeries *series);

    void setText(const QString &text);
    void setAnchor(QPointF point);
    void updateGeometry();

    QRectF boundingRect() const;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget);
    void setSeries(QAbstractSeries *series);

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event);
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event);

private:
    QString m_text;
    QRectF m_textRect;
    QRectF m_rect;
    QPointF m_anchor;
    QFont m_font;
    QChart *m_chart;
    QAbstractSeries *m_series;
};

#endif // CALLOUT_H

callout.cpp

#include "callout.h"
#include <QtGui/QPainter>
#include <QtGui/QFontMetrics>
#include <QtWidgets/QGraphicsSceneMouseEvent>
#include <QtGui/QMouseEvent>
#include <QtCharts/QChart>

Callout::Callout(QChart *chart, QAbstractSeries *series):
    QGraphicsItem(chart),
    m_chart(chart),
    m_series(series)
{
}

QRectF Callout::boundingRect() const
{
    QPointF anchor = mapFromParent(m_chart->mapToPosition(m_anchor, m_series));
    QRectF rect;
    rect.setLeft(qMin(m_rect.left(), anchor.x()));
    rect.setRight(qMax(m_rect.right(), anchor.x()));
    rect.setTop(qMin(m_rect.top(), anchor.y()));
    rect.setBottom(qMax(m_rect.bottom(), anchor.y()));
    return rect;
}

void Callout::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option)
    Q_UNUSED(widget)
    QPainterPath path;
    path.addRoundedRect(m_rect, 5, 5);

    QPointF anchor = mapFromParent(m_chart->mapToPosition(m_anchor, m_series));
    if (!m_rect.contains(anchor)) {
        QPointF point1, point2;

        // establish the position of the anchor point in relation to m_rect
        bool above = anchor.y() <= m_rect.top();
        bool aboveCenter = anchor.y() > m_rect.top() && anchor.y() <= m_rect.center().y();
        bool belowCenter = anchor.y() > m_rect.center().y() && anchor.y() <= m_rect.bottom();
        bool below = anchor.y() > m_rect.bottom();

        bool onLeft = anchor.x() <= m_rect.left();
        bool leftOfCenter = anchor.x() > m_rect.left() && anchor.x() <= m_rect.center().x();
        bool rightOfCenter = anchor.x() > m_rect.center().x() && anchor.x() <= m_rect.right();
        bool onRight = anchor.x() > m_rect.right();

        // get the nearest m_rect corner.
        qreal x = (onRight + rightOfCenter) * m_rect.width();
        qreal y = (below + belowCenter) * m_rect.height();
        bool cornerCase = (above && onLeft) || (above && onRight) || (below && onLeft) || (below && onRight);
        bool vertical = qAbs(anchor.x() - x) > qAbs(anchor.y() - y);

        qreal x1 = x + leftOfCenter * 10 - rightOfCenter * 20 + cornerCase * !vertical * (onLeft * 10 - onRight * 20);
        qreal y1 = y + aboveCenter * 10 - belowCenter * 20 + cornerCase * vertical * (above * 10 - below * 20);;
        point1.setX(x1);
        point1.setY(y1);

        qreal x2 = x + leftOfCenter * 20 - rightOfCenter * 10 + cornerCase * !vertical * (onLeft * 20 - onRight * 10);;
        qreal y2 = y + aboveCenter * 20 - belowCenter * 10 + cornerCase * 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(m_textRect, m_text);
}

void Callout::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    event->setAccepted(true);
}

void Callout::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if (event->buttons() & Qt::LeftButton){
        setPos(mapToParent(event->pos() - event->buttonDownPos(Qt::LeftButton)));
        event->setAccepted(true);
    } else {
        event->setAccepted(false);
    }
}

void Callout::setSeries(QAbstractSeries *series)
{
    m_series = series;
}

void Callout::setText(const QString &text)
{
    m_text = text;
    QFontMetrics metrics(m_font);
    m_textRect = metrics.boundingRect(QRect(0, 0, 150, 150), Qt::AlignLeft, m_text);
    m_textRect.translate(5, 5);
    prepareGeometryChange();
    m_rect = m_textRect.adjusted(-5, -5, 5, 5);
}

void Callout::setAnchor(QPointF point)
{
    m_anchor = point;
}

void Callout::updateGeometry()
{
    prepareGeometryChange();
    setPos(m_chart->mapToPosition(m_anchor, m_series) + QPoint(10, -50));
}

You can find the complete code in the following link



标签: qt qt5 qtchart