简述
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类的数据导入部分优化空间还很大,接受起来可能不是很方便。但是所有完整的代码都在,直接拷贝放到两个类文件中,可以直接用起来。