QtChart实现曲线图表绘制之直角坐标系(支持曲线消隐、数据点突出、数据驱动刷新、鼠标进入显示数值)

简述

Qt下绘制曲线图表的方法选择很多,下面我将介绍如何使用QtCharts绘制优雅图表。
本文的Demo支持点击Mark图标消隐曲线;数据点的突出显示;鼠标进入提示数值;数据驱动刷新显示;图表自动缩放,可移植性比较好。
需要说明Demo的编码环境是Qt Creator 5.8,使用Create5.2-5.6版本的用户,网上下载编译安装QtCharts库即可,5.7版本之后只需在.PRO文件中加入charts模块即可。
Demo显示效果如下:
在这里插入图片描述
鼠标进入提示数值:
在这里插入图片描述
点击legend提示信息,可以消隐对应的曲线,如下图,点击曲线2的提示信息,曲线2消隐;再次点击,曲线还原。
在这里插入图片描述

代码之路

主要由两个类组成,一个Callout类,用来显示数值的消息框,核心代码就是根据鼠标位置绘制提示框;一个baseChart类,绘制整个图表,实现曲线显隐、更新数据重绘曲线等。
Callout.h

#include <QtCharts>
#include <QGraphicsItem>
#include <QFont>

QT_CHARTS_USE_NAMESPACE //相当于 using namespace QtCharts;

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

    void setText(const QString &text); //消息框显示内容
    void setAnchor(QPointF point); //消息框作下角坐标
    void updateGeometry();

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

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;
};

Callout.cpp

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

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) + QPoint(10, -50));
}

QRectF Callout::boundingRect() const
{
    QPointF anchor = mapFromParent(m_chart->mapToPosition(m_anchor));
    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));
    if (!m_rect.contains(anchor))
    {
        QPointF point1, point2;

        //建立锚点与矩形的相关联系
        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);
    }
}

basechart.h

#include <QtCharts>
#include <QWidget>
#include <QGraphicsView>
#include <QScatterSeries>
#include <callout.h>
#include <QLegendMarker>

using namespace QtCharts;
//QT_CHARTS_USE_NAMESPACE

class baseChart : public QGraphicsView
{
    Q_OBJECT
public:
    explicit baseChart(QWidget *parent = 0);
    ~baseChart();

protected:
    void resizeEvent(QResizeEvent *event);
    void mouseMoveEvent(QMouseEvent *event);

public slots:
    void toolTip(QPointF point, bool state); 

    //控制曲线显隐
    void removeSeries();
    void connectMarkers();
    void disconnectMarkers();

    void handleMarkerClicked();

    //更新数据 重绘曲线和坐标轴
    void chartdataSlot(QList<QList<QPoint> > dataList, float Xmin, float Xmax, float Ymin, float Ymax);

private:
    QGraphicsSimpleTextItem *m_coordX;
    QGraphicsSimpleTextItem *m_coordY;
    QChart *m_chart;
    Callout *m_tooltip;
    QList<Callout*> m_callouts;

    //控制曲线显隐
    QList <QLineSeries*> m_series;
    QList <QScatterSeries*> m_scatterseries;
};

basechart.cpp

baseChart::baseChart(QWidget *parent) : QGraphicsView(new QGraphicsScene, parent), m_coordX(0),m_coordY(0),m_chart(0),m_tooltip(0)
{
    setDragMode(QGraphicsView::NoDrag);
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    //chart
    m_chart = new QChart;
//    m_chart->setMinimumSize(640, 480);
    m_chart->setTitle("Hover the line to show callout and hide");
//    m_chart->legend()->hide();


    //添加数据,后面封装成一个接口
    QLineSeries *series = new QLineSeries;
    series->append(1,3);
    series->append(4,5);
    series->append(5, 4.5);
    series->append(7, 1);
    series->append(11, 2);
    m_chart->addSeries(series);

    QSplineSeries *series2 = new QSplineSeries;
    series2->append(1.6, 1.4);
    series2->append(2.4, 3.5);
    series2->append(3.7, 2.5);
    series2->append(7, 4);
    series2->append(10, 2);
    m_chart->addSeries(series2);

    QScatterSeries *series3 = new QScatterSeries;
    series3->append(1,3);
    series3->append(4,5);
    series3->append(5, 4.5);
    series3->append(7, 1);
    series3->append(11, 2);
    m_chart->addSeries(series3);

    QScatterSeries *series4 = new QScatterSeries;
    series4->append(1.6, 1.4);
    series4->append(2.4, 3.5);
    series4->append(3.7, 2.5);
    series4->append(7, 4);
    series4->append(10, 2);
    m_chart->addSeries(series4);

    m_series.append(series);
    m_series.append(series2);
    m_scatterseries.append(series3);
    m_scatterseries.append(series4);

    m_chart->createDefaultAxes();
    m_chart->setTheme(QChart::ChartThemeBlueCerulean);  //设置图表theme
    m_chart->setAcceptHoverEvents(true);

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

//    connect(series, SIGNAL(clicked(QPointF)), this, SLOT(keepCallout()));
    connect(series3, SIGNAL(hovered(QPointF,bool)), this, SLOT(toolTip(QPointF,bool)));

//    connect(series2, SIGNAL(clicked(QPointF)),this, SLOT(keepCallout()));
    connect(series4, SIGNAL(hovered(QPointF,bool)), this, SLOT(toolTip(QPointF,bool)));
    this->setMouseTracking(true);

    //控制曲线显隐
    connectMarkers();
    m_chart->legend()->setVisible(true);
    m_chart->legend()->setAlignment(Qt::AlignBottom);
    series->setName("1");
    series2->setName("2");
    series3->setName("3");
    series4->setName("4");


//    m_chart->legend()->markers();
    foreach (QLegendMarker* marker, m_chart->legend()->markers())
    {
        if ((marker->series() == series3 ) || (marker->series() == series4)) //) // == series3)
        {
            marker->setVisible(false);
        }
    }
}

baseChart::~baseChart()
{

}

void baseChart::resizeEvent(QResizeEvent *event)
{
    if (scene())
    {
        scene()->setSceneRect(QRect(QPoint(0, 0), event->size()));
        m_chart->resize(event->size());
        foreach (Callout *callout, m_callouts) {
            callout->updateGeometry();
        }
        QGraphicsView::resizeEvent(event);
    }
}

void baseChart::mouseMoveEvent(QMouseEvent *event)
{
    QGraphicsView::mouseMoveEvent(event);
}

void baseChart::toolTip(QPointF point, bool state)
{
    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();
    }
}

void baseChart::removeSeries()
{
    if (m_series.count() > 0)
    {
        QLineSeries *series = m_series.last();
        m_chart->removeSeries(series);
        m_series.removeLast();
        delete series;
    }
}

void baseChart::connectMarkers()
{
    foreach (QLegendMarker* marker, m_chart->legend()->markers()) {
        QObject::disconnect(marker, SIGNAL(clicked()), this, SLOT(handleMarkerClicked()));
        QObject::connect(marker, SIGNAL(clicked()), this, SLOT(handleMarkerClicked()));
    }
}

void baseChart::disconnectMarkers()
{
    foreach(QLegendMarker* marker, m_chart->legend()->markers())
    {
        QObject::disconnect(marker, SIGNAL(clicked()), this, SLOT(handleMarkerClicked()));
    }
}

void baseChart::handleMarkerClicked()
{
    QLegendMarker* marker = qobject_cast<QLegendMarker*>(sender());
    Q_ASSERT(marker);

    switch(marker->type())
    {
    case QLegendMarker::LegendMarkerTypeXY:
    {
        marker->series()->setVisible(!marker->series()->isVisible());
        marker->setVisible(true);

        for (int i = 0; i < m_series.size(); ++i)
        {
            if (marker->series() == m_series.at(i))
            {
                m_scatterseries.at(i)->setVisible(marker->series()->isVisible());

            }
        }

        foreach (QLegendMarker* marker, m_chart->legend()->markers())
        {
            if (marker->series() == m_scatterseries.at(0) || marker->series() == m_scatterseries.at(1))
            {
                marker->setVisible(false);
            }
        }

        qreal alpha = 1.0;
        if (!marker->series()->isVisible())
        {
            alpha = 0.5;
        }

        QColor color;
        QBrush brush =  marker->labelBrush();
        color = brush.color();
        color.setAlphaF(alpha);
        brush.setColor(color);
        marker->setLabelBrush(brush);

        brush = marker->brush();
        color = brush.color();
        color.setAlphaF(alpha);
        brush.setColor(color);

        QPen pen = marker->pen();
        color = pen.color();
        color.setAlphaF(alpha);
        pen.setColor(color);
        marker->setPen(pen);

        break;
    }
    default:
        break;

    }
}

void baseChart::chartdataSlot(QList<QList<QPoint> > dataList, float Xmin, float Xmax, float Ymin, float Ymax)
{
    if (isVisible())
    {
        //数据归零
        m_series.at(0)->clear();
        m_series.at(1)->clear();
        m_scatterseries.at(0)->clear();
        m_scatterseries.at(1)->clear();

        //添加数据
        int lineNum = dataList.size();
        for (int i = 0; i < lineNum; ++i)
        {
            int pointNum = dataList.at(i).size();
            for (int j = 0; j < pointNum; ++j)
            {
                m_series.at(i)->append(dataList.at(i).at(j).x(), dataList.at(i).at(j).y());
                m_scatterseries.at(i)->append(dataList.at(i).at(j).x(), dataList.at(i).at(j).y());
            }
        }


        m_chart->createDefaultAxes();
        m_chart->axisX()->setRange(Xmin, Xmax);
        m_chart->axisY()->setRange(Ymin, Ymax);
    }

    chartRenewSlot();
}

举例应用一下,
在一个h文件中,声明baseChart:

private:
	baseChart* m_baseChart;

在cpp文件中,初始化这个baseChart,并写一个事件或时间驱动的槽函数,执行代码如下:

m_baseChart  = new baseChart;

void ondataChanged()
{
    QPoint p1 (1,2);
    QPoint p2(2,4);
    QPoint p3(3,3);
    QPoint p4(0,3);
    QList<QPoint> line1;
    QList<QPoint> line2;
    line1.append(p1);
    line1.append(p2);
    line1.append(p3);
    line2.append(p3);
    line2.append(p2);
    line2.append(p4);
    QList<QList<QPoint>> mdata;
    mdata.append(line1);
    mdata.append(line2);
    m_baseChart->chartdataSlot(mdata, 0, 3, 2, 4);
}

小结

代码量比较大,而且baseChart类的数据导入部分优化空间还很大,接受起来可能不是很方便。但是所有完整的代码都在,直接拷贝放到两个类文件中,可以直接用起来。

猜你喜欢

转载自blog.csdn.net/lusanshui/article/details/84395651