QtChart實現曲線圖表繪製之直角座標系(支援曲線消隱、資料點突出、資料驅動重新整理、滑鼠進入顯示數值)
阿新 • • 發佈:2018-12-10
簡述
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類的資料匯入部分優化空間還很大,接受起來可能不是很方便。但是所有完整的程式碼都在,直接拷貝放到兩個類檔案中,可以直接用起來。