ChartView + LineSeries 虽然强大,但由于性能和效果显示上和期待的结果有偏差,仍无法满足需求,这种情况下,需要自定义绘图实现。
本例模拟实现 CT 仪器上面显示的患者的心电图,先上效果图:
通过本例,可以学习到,QML如何调用C++代码以及自定义绘图:
1. 在 QML 中创建 C++ 类对象,需要在使用前注册,在 QML 中需要先导入:
qmlRegisterType<DataPaintedItem>("com.diysoul", 1, 0, "DataPaintedItem");
参数1~3声明命名空间与版本号,用于在 QML 中导入时使用。参数4指定QML中使用的类名称。
import com.diysoul 1.0
2. 需要在 QML 中调用 C++ 类成员,需要使用 Q_INVOKABLE 宏进行声明,如: Q_INVOKABLE void appendPoint(int y);
关键代码:
import QtQuick 2.0 import QtQuick.Layouts 1.12 import com.diysoul 1.0 Rectangle { color: Qt.rgba(0, 0, 1, 0.1) anchors.fill: parent anchors.margins: 10 radius: 5 DataPaintedItem { id: paintItem pointCount: 800 spaceCount: 5 anchors.fill: parent title: qsTr("折线图") titleFont.bold: true titleFont.pixelSize: 35 titleColor: Qt.rgba(0, 0, 1, 0.5) xTickCount: 10 yTickCount: 6 xLineVisible: true yLineVisible: true yMax: 8000 yMin: 0 labelsVisible: true labelsColor: Qt.rgba(0, 0, 0, 0.9) labelsFont.pixelSize: 15 lineWidth: 2 gridLineWidth: 1 lineColor: Qt.rgba(1, 0, 0, 1) gridLineColor: Qt.rgba(0, 0, 1, 0.5) } // FIXME 2021-08-07 : timer for test Timer { running: true interval: 1 repeat: true property int current: 0 property var valueList: [ 2000, 2100, 2200, 2300, 2400, 2500, 2600, 2700, 2800, 2900, 3000, 3100, 3200, 3300, 3400, 3500, 3600, 3700, 3800, 3900, 4000, 4100, 4200, 4300, 4400, 4500, 4600, 4700, 4800, 4900, 4950, 4960, 4970, 4980, 4990, 5000, 4990, 4980, 4970, 4960, 4950, 4900, 4800, 4700, 4600, 4500, 4400, 4300, 4200, 4100, 4000, 3900, 3800, 3700, 3600, 3500, 3400, 3300, 3200, 3100, 3000, 2900, 2800, 2700, 2600, 2500, 2400, 2300, 2200, 2100, ] onTriggered: { var value = valueList[current] current = current + 1 if (current >= valueList.length) { current = 0 } paintItem.appendPoint(value) } } }
实际完成绘制图形功能的类 DataPaintedItem 定义如下:
#pragma once #include <QQuickPaintedItem> #include <QValueAxis> class DataPaintedItem : public QQuickPaintedItem { Q_OBJECT public: Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged) Q_PROPERTY(QFont titleFont READ getTitleFont WRITE setTitleFont NOTIFY titleFontChanged) Q_PROPERTY(QColor titleColor READ getTitleColor WRITE setTitleColor NOTIFY titleColorChanged) Q_PROPERTY(int pointCount READ getPointCount WRITE setPointCount NOTIFY pointCountChanged) Q_PROPERTY(int spaceCount READ getSpaceCount WRITE setSpaceCount NOTIFY spaceCountChanged) Q_PROPERTY(int xTickCount READ getXTickCount WRITE setXTickCount NOTIFY xTickCountChanged) Q_PROPERTY(int yTickCount READ getYTickCount WRITE setYTickCount NOTIFY yTickCountChanged) Q_PROPERTY(QColor lineColor READ getLineColor WRITE setLineColor NOTIFY lineColorChanged) Q_PROPERTY(QColor gridLineColor READ getGridLineColor WRITE setGridLineColor NOTIFY gridLineColorChanged) Q_PROPERTY(QFont lineFont READ getLineFont WRITE setLineFont NOTIFY lineFontChanged) Q_PROPERTY(QFont gridLineFont READ getGridLineFont WRITE setGridLineFont NOTIFY gridLineFontChanged) Q_PROPERTY(bool xLineVisible READ getXLineVisible WRITE setXLineVisible NOTIFY xLineVisibleChanged) Q_PROPERTY(bool yLineVisible READ getYLineVisible WRITE setYLineVisible NOTIFY yLineVisibleChanged) Q_PROPERTY(int lineWidth READ getLineWidth WRITE setLineWidth NOTIFY lineWidthChanged) Q_PROPERTY(int gridLineWidth READ getGridLineWidth WRITE setGridLineWidth NOTIFY gridLineWidthChanged) Q_PROPERTY(int yMin READ getYMin WRITE setYMin NOTIFY yMinChanged) Q_PROPERTY(int yMax READ getYMax WRITE setYMax NOTIFY yMaxChanged) Q_PROPERTY(QList<QPointF> points READ getPoints WRITE setPoints) Q_PROPERTY(bool labelsVisible READ getLabelsVisible WRITE setLabelsVisible NOTIFY labelsVisibleChanged) Q_PROPERTY(QColor labelsColor READ getLabelsColor WRITE setLabelsColor NOTIFY labelsColorChanged) Q_PROPERTY(QFont labelsFont READ getLabelsFont WRITE setLabelsFont NOTIFY labelsFontChanged) public: explicit DataPaintedItem(QQuickItem* parent = nullptr); ~DataPaintedItem() override; void paint(QPainter* painter) override; Q_INVOKABLE void appendPoint(int y); int getYMin() const; void setYMin(int value); int getYMax() const; void setYMax(int value); int getXTickCount() const; void setXTickCount(int tickCount); int getYTickCount() const; void setYTickCount(int tickCount); bool getXLineVisible() const; void setXLineVisible(bool newXLineVisible); bool getYLineVisible() const; void setYLineVisible(bool newYLineVisible); int getPointCount() const; void setPointCount(int pointCount); int getSpaceCount() const; void setSpaceCount(int spaceCount); const QList<QPointF>& getPoints() const; void setPoints(const QList<QPointF>& points); const QString& getTitle() const; void setTitle(const QString& title); const QFont& getTitleFont() const; void setTitleFont(const QFont& font); const QColor& getTitleColor() const; void setTitleColor(const QColor& color); const QColor& getLineColor() const; void setLineColor(const QColor& newLineColor); const QColor& getGridLineColor() const; void setGridLineColor(const QColor& newGridLineColor); const QFont& getLineFont() const; void setLineFont(const QFont& newLineFont); const QFont& getGridLineFont() const; void setGridLineFont(const QFont& newGridLineFont); int getLineWidth() const; void setLineWidth(int newLineWidth); int getGridLineWidth() const; void setGridLineWidth(int newGridLineWidth); bool getLabelsVisible() const; void setLabelsVisible(bool newLabelsVisible); const QColor& getLabelsColor() const; void setLabelsColor(const QColor& newLabelsColor); const QFont& getLabelsFont() const; void setLabelsFont(const QFont& newLabelsFont); signals: void titleChanged(); void titleFontChanged(); void titleColorChanged(); void pointCountChanged(); void spaceCountChanged(); void yMinChanged(); void yMaxChanged(); void xTickCountChanged(); void yTickCountChanged(); void xLineVisibleChanged(); void yLineVisibleChanged(); void lineColorChanged(); void gridLineColorChanged(); void lineFontChanged(); void gridLineFontChanged(); void lineWidthChanged(); void gridLineWidthChanged(); void labelsVisibleChanged(); void labelsColorChanged(); void labelsFontChanged(); private: void drawTitle(QPainter* painter); void drawLabels(QPainter* painter); void drawGridLine(QPainter* painter); void drawLine(QPainter* painter); QPointF transformPoint(const QPointF& pt) const; private: QString myTitle; QFont myTitleFont; QColor myTitleColor; int myPointCount{60}; int mySpaceCount{10}; int myXTickCount{1}; int myYTickCount{1}; bool myXLineVisible{}; bool myYLineVisible{}; QColor myLineColor; QColor myGridLineColor; QFont myLineFont; QFont myGridLineFont; int myLineWidth; int myGridLineWidth; bool myLabelsVisible{true}; QColor myLabelsColor; QFont myLabelsFont; int myYMaxValue{100}; int myYMinValue{}; QList<QPointF> myPoints; int myCurrentPointIndex{}; int myXStart{}; int myXEnd{}; int myYStart{}; int myYEnd{}; };
#include "DataPaintedItem.h" #include <QDebug> #include <QPainter> #include <QPainterPath> DataPaintedItem::DataPaintedItem(QQuickItem* parent) : QQuickPaintedItem(parent) { } DataPaintedItem::~DataPaintedItem() { } void DataPaintedItem::paint(QPainter* painter) { if (!painter) { return; } myXStart = myGridLineWidth; myXEnd = width() - myGridLineWidth; myYStart = myGridLineWidth; myYEnd = height() - myGridLineWidth; // The drawing order cannot be changed, // because there are dependencies between them drawTitle(painter); drawLabels(painter); drawGridLine(painter); drawLine(painter); } void DataPaintedItem::appendPoint(int y) { if (myPointCount <= 0) { return; } if (myPoints.size() < myPointCount) { myCurrentPointIndex = 0; myPoints.push_back(QPointF{static_cast<double>(myPoints.size()), static_cast<double>(y)}); } else { myPoints[myCurrentPointIndex].setY(y); ++myCurrentPointIndex; myCurrentPointIndex = myCurrentPointIndex % myPoints.size(); } update(); } int DataPaintedItem::getYMin() const { return myYMinValue; } void DataPaintedItem::setYMin(int value) { if (myYMinValue == value) { return; } myYMinValue = value; emit yMinChanged(); update(); } int DataPaintedItem::getYMax() const { return myYMaxValue; } void DataPaintedItem::setYMax(int value) { if (myYMaxValue == value) { return; } myYMaxValue = value; emit yMaxChanged(); update(); } int DataPaintedItem::getYTickCount() const { return myYTickCount; } void DataPaintedItem::setYTickCount(int tickCount) { if (myYTickCount == tickCount) { return; } if (tickCount <= 0) { return; } myYTickCount = tickCount; emit yTickCountChanged(); } bool DataPaintedItem::getYLineVisible() const { return myYLineVisible; } void DataPaintedItem::setYLineVisible(bool newYLineVisible) { if (myYLineVisible == newYLineVisible) { return; } myYLineVisible = newYLineVisible; emit yLineVisibleChanged(); } bool DataPaintedItem::getXLineVisible() const { return myXLineVisible; } void DataPaintedItem::setXLineVisible(bool newXLineVisible) { if (myXLineVisible == newXLineVisible) { return; } myXLineVisible = newXLineVisible; emit xLineVisibleChanged(); } int DataPaintedItem::getXTickCount() const { return myXTickCount; } void DataPaintedItem::setXTickCount(int tickCount) { if (myXTickCount == tickCount) { return; } if (tickCount <= 0) { return; } myXTickCount = tickCount; emit xTickCountChanged(); } int DataPaintedItem::getPointCount() const { return myPointCount; } void DataPaintedItem::setPointCount(int pointCount) { if (myPointCount == pointCount) { return; } if (pointCount <= 0) { return; } myPointCount = pointCount; // remove old point(s) myCurrentPointIndex = 0; if (myPoints.size() > pointCount) { myPoints.erase(myPoints.begin(), myPoints.begin() + (myPoints.size() - pointCount)); } emit pointCountChanged(); update(); } int DataPaintedItem::getSpaceCount() const { return mySpaceCount; } void DataPaintedItem::setSpaceCount(int spaceCount) { if (mySpaceCount == spaceCount) { return; } if (spaceCount < 0) { return; } mySpaceCount = spaceCount; emit spaceCountChanged(); } const QList<QPointF>& DataPaintedItem::getPoints() const { return myPoints; } void DataPaintedItem::setPoints(const QList<QPointF>& points) { if (myPoints == points) { return; } myPoints = points; update(); } const QString& DataPaintedItem::getTitle() const { return myTitle; } void DataPaintedItem::setTitle(const QString& title) { if (myTitle == title) { return; } myTitle = title; emit titleChanged(); update(); } const QFont& DataPaintedItem::getTitleFont() const { return myTitleFont; } void DataPaintedItem::setTitleFont(const QFont& font) { if (myTitleFont == font) return; myTitleFont = font; emit titleFontChanged(); } const QColor& DataPaintedItem::getTitleColor() const { return myTitleColor; } void DataPaintedItem::setTitleColor(const QColor& color) { if (myTitleColor == color) { return; } myTitleColor = color; emit titleColorChanged(); update(); } const QColor& DataPaintedItem::getLineColor() const { return myLineColor; } void DataPaintedItem::setLineColor(const QColor& newLineColor) { if (myLineColor == newLineColor) { return; } myLineColor = newLineColor; emit lineColorChanged(); } const QColor& DataPaintedItem::getGridLineColor() const { return myGridLineColor; } void DataPaintedItem::setGridLineColor(const QColor& newGridLineColor) { if (myGridLineColor == newGridLineColor) return; myGridLineColor = newGridLineColor; emit gridLineColorChanged(); } const QFont& DataPaintedItem::getLineFont() const { return myLineFont; } void DataPaintedItem::setLineFont(const QFont& newLineFont) { if (myLineFont == newLineFont) { return; } myLineFont = newLineFont; emit lineFontChanged(); } const QFont& DataPaintedItem::getGridLineFont() const { return myGridLineFont; } void DataPaintedItem::setGridLineFont(const QFont& newGridLineFont) { if (myGridLineFont == newGridLineFont) { return; } myGridLineFont = newGridLineFont; emit gridLineFontChanged(); } int DataPaintedItem::getGridLineWidth() const { return myGridLineWidth; } void DataPaintedItem::setGridLineWidth(int newGridLineWidth) { if (myGridLineWidth == newGridLineWidth) { return; } myGridLineWidth = newGridLineWidth; emit gridLineWidthChanged(); } int DataPaintedItem::getLineWidth() const { return myLineWidth; } void DataPaintedItem::setLineWidth(int newLineWidth) { if (myLineWidth == newLineWidth) { return; } myLineWidth = newLineWidth; emit lineWidthChanged(); } bool DataPaintedItem::getLabelsVisible() const { return myLabelsVisible; } void DataPaintedItem::setLabelsVisible(bool newLabelsVisible) { if (myLabelsVisible == newLabelsVisible) { return; } myLabelsVisible = newLabelsVisible; emit labelsVisibleChanged(); } const QFont& DataPaintedItem::getLabelsFont() const { return myLabelsFont; } void DataPaintedItem::setLabelsFont(const QFont& newLabelsFont) { if (myLabelsFont == newLabelsFont) { return; } myLabelsFont = newLabelsFont; emit labelsFontChanged(); } const QColor& DataPaintedItem::getLabelsColor() const { return myLabelsColor; } void DataPaintedItem::setLabelsColor(const QColor& newLabelsColor) { if (myLabelsColor == newLabelsColor) { return; } myLabelsColor = newLabelsColor; emit labelsColorChanged(); } void DataPaintedItem::drawTitle(QPainter* painter) { if (!myTitle.isEmpty()) { QFontMetrics metrics(myTitleFont, painter->device()); int titleHeight = metrics.height() + 10; myYStart += titleHeight; painter->save(); QPen pen = painter->pen(); pen.setColor(myTitleColor); painter->setPen(pen); painter->setFont(myTitleFont); painter->drawText(0, 0, width(), titleHeight, Qt::AlignCenter, myTitle); painter->restore(); } } void DataPaintedItem::drawLabels(QPainter* painter) { if (myLabelsVisible) { painter->save(); QPen pen = painter->pen(); pen.setColor(myLabelsColor); painter->setPen(pen); painter->setFont(myLabelsFont); const QRect rc = painter->boundingRect(myXStart, myYStart, myXEnd - myXStart, myYEnd - myYStart , Qt::AlignLeft, QString::number(myYMaxValue) + "W"); auto fnDrawLabel = [&](const QRect& rc, int val) { painter->drawText(rc, Qt::AlignCenter, QString::number(val)); }; QRect labelRect{myXStart, myYStart, rc.width(), rc.height()}; fnDrawLabel(labelRect, myYMaxValue); int valueSpace = (myYMaxValue - myYMinValue) / myYTickCount; int curValue = myYMaxValue - valueSpace; int yLineSpace = (myYEnd - myYStart) / myYTickCount; for (int i = 1; i < myYTickCount; ++i) { labelRect.setY(myYStart + i * yLineSpace * 2 - rc.height()); fnDrawLabel(labelRect, curValue); curValue -= valueSpace; } labelRect.setRect(myXStart, myYEnd - rc.height(), rc.width(), rc.height()); fnDrawLabel(labelRect, myYMinValue); myXStart += rc.width(); painter->restore(); } } void DataPaintedItem::drawGridLine(QPainter* painter) { QPainterPath linePath; if (myXLineVisible) { int xLineSpace = (myXEnd - myXStart) / myXTickCount; for (int i = 1; i < myXTickCount; ++i) { int xPos = myXStart + xLineSpace * i; linePath.moveTo(xPos, myYStart); linePath.lineTo(xPos, myYEnd); } } if (myYLineVisible) { int yLineSpace = (myYEnd - myYStart) / myYTickCount; for (int i = 1; i < myYTickCount; ++i) { int yPos = myYStart + yLineSpace * i; linePath.moveTo(myXStart, yPos); linePath.lineTo(myXEnd, yPos); } } if (!linePath.isEmpty()) { painter->save(); QPen pen = painter->pen(); pen.setColor(myGridLineColor); pen.setStyle(Qt::CustomDashLine); pen.setDashPattern(QVector<qreal>{8, 10}); pen.setWidth(myGridLineWidth); painter->setPen(pen); painter->drawPath(linePath); painter->setFont(myGridLineFont); painter->restore(); } } void DataPaintedItem::drawLine(QPainter* painter) { if (!myPoints.isEmpty()) { QPainterPath ptPath; ptPath.moveTo(transformPoint(myPoints.at(0))); if (myCurrentPointIndex > 0) { int i = 0; for (; i < myCurrentPointIndex; ++i) { ptPath.lineTo(transformPoint(myPoints.at(i))); } i = myCurrentPointIndex + mySpaceCount; if (i < myPoints.size()) { ptPath.moveTo(transformPoint(myPoints.at(i))); for (; i < myPoints.size(); ++i) { ptPath.lineTo(transformPoint(myPoints.at(i))); } } } else { for (int i = 1; i < myPoints.size(); ++i) { ptPath.lineTo(transformPoint(myPoints.at(i))); } } painter->save(); painter->setRenderHint(QPainter::Antialiasing, true); QPen pen = painter->pen(); pen.setColor(myLineColor); pen.setWidth(myLineWidth); painter->setPen(pen); painter->drawPath(ptPath); painter->setFont(myLineFont); painter->restore(); } } QPointF DataPaintedItem::transformPoint(const QPointF& pt) const { int pointCount = myPointCount > 0 ? myPointCount : 1; int w = myXEnd - myXStart; auto x = pt.x() * w / pointCount + myXStart; int h = myYEnd - myYStart; auto y = h - pt.y() * h / (myYMaxValue - myYMinValue) + myYStart; if (y < myYStart) { y = myYStart; } else if (y > myYEnd) { y = myYEnd; } return QPointF{x, y}; }