QML 自定义折线图

ChartView + LineSeries 虽然强大,但由于性能和效果显示上和期待的结果有偏差,仍无法满足需求,这种情况下,需要自定义绘图实现。

 

本例模拟实现 CT 仪器上面显示的患者的心电图,先上效果图:

QML 自定义折线图

 

 

 通过本例,可以学习到,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};
}

 

QML 自定义折线图

上一篇:Sentinel配置


下一篇:lightoj1004【基础DP】