6

I'm trying to adapt the Qt5.9 QML Oscilloscope example to have the graph data pushed from c++ rather than requested from QML. Below are the pertinent sections from the QML Oscilloscope example.

datasource.h:

#ifndef DATASOURCE_H
#define DATASOURCE_H

#include <QtCore/QObject>
#include <QtCharts/QAbstractSeries>

QT_BEGIN_NAMESPACE
class QQuickView;
QT_END_NAMESPACE

QT_CHARTS_USE_NAMESPACE

class DataSource : public QObject
{
    Q_OBJECT
public:
    explicit DataSource(QQuickView *appViewer, QObject *parent = 0);

Q_SIGNALS:

public slots:
    void generateData(int type, int rowCount, int colCount);
    void update(QAbstractSeries *series);

private:
    QQuickView *m_appViewer;
    QList<QVector<QPointF> > m_data;
    int m_index;
};

#endif // DATASOURCE_H

datasource.cpp:

#include "datasource.h"
#include <QtCharts/QXYSeries>
#include <QtCharts/QAreaSeries>
#include <QtQuick/QQuickView>
#include <QtQuick/QQuickItem>
#include <QtCore/QDebug>
#include <QtCore/QtMath>

QT_CHARTS_USE_NAMESPACE

Q_DECLARE_METATYPE(QAbstractSeries *)
Q_DECLARE_METATYPE(QAbstractAxis *)

DataSource::DataSource(QQuickView *appViewer, QObject *parent) :
    QObject(parent),
    m_appViewer(appViewer),
    m_index(-1)
{
    qRegisterMetaType<QAbstractSeries*>();
    qRegisterMetaType<QAbstractAxis*>();

    generateData(0, 5, 1024);
}

void DataSource::update(QAbstractSeries *series)
{
    if (series) {
        QXYSeries *xySeries = static_cast<QXYSeries *>(series);
        m_index++;
        if (m_index > m_data.count() - 1)
            m_index = 0;

        QVector<QPointF> points = m_data.at(m_index);
        // Use replace instead of clear + append, it's optimized for performance
        xySeries->replace(points);
    }
}

void DataSource::generateData(int type, int rowCount, int colCount)
{
    // Remove previous data
    m_data.clear();

    // Append the new data depending on the type
    for (int i(0); i < rowCount; i++) {
        QVector<QPointF> points;
        points.reserve(colCount);
        for (int j(0); j < colCount; j++) {
            qreal x(0);
            qreal y(0);
            switch (type) {
            case 0:
                // data with sin + random component
                y = qSin(3.14159265358979 / 50 * j) + 0.5 + (qreal) rand() / (qreal) RAND_MAX;
                x = j;
                break;
            case 1:
                // linear data
                x = j;
                y = (qreal) i / 10;
                break;
            default:
                // unknown, do nothing
                break;
            }
            points.append(QPointF(x, y));
        }
        m_data.append(points);
    }
}

main.cpp:

#include <QtWidgets/QApplication>
#include <QtQml/QQmlContext>
#include <QtQuick/QQuickView>
#include <QtQml/QQmlEngine>
#include <QtCore/QDir>
#include "datasource.h"

int main(int argc, char *argv[])
{
    // Qt Charts uses Qt Graphics View Framework for drawing, therefore 
QApplication must be used.
    QApplication app(argc, argv);

    QQuickView viewer;

    // The following are needed to make examples run without having to install the module
    // in desktop environments.
#ifdef Q_OS_WIN
    QString extraImportPath(QStringLiteral("%1/../../../../%2"));
#else
    QString extraImportPath(QStringLiteral("%1/../../../%2"));
#endif
    viewer.engine()->addImportPath(extraImportPath.arg(QGuiApplication::applicationDirPath(),
                                  QString::fromLatin1("qml")));
    //QObject::connect(viewer.engine(), &QQmlEngine::quit, &viewer, &QWindow::close);

    viewer.setTitle(QStringLiteral("QML Oscilloscope"));

    DataSource dataSource(&viewer);
    viewer.rootContext()->setContextProperty("dataSource", &dataSource);

    viewer.setSource(QUrl("qrc:/qml/qmloscilloscope/main.qml"));
    viewer.setResizeMode(QQuickView::SizeRootObjectToView);
    viewer.setColor(QColor("#404040"));
    viewer.show();

    return app.exec();
}

ScopeView.qml:

import QtQuick 2.0
import QtCharts 2.1

ChartView {
    id: chartView
    animationOptions: ChartView.NoAnimation
    theme: ChartView.ChartThemeDark
    property bool openGL: true
    property bool openGLSupported: true
    onOpenGLChanged: {
        if (openGLSupported) {
            series("signal 1").useOpenGL = openGL;
        }
    }
    Component.onCompleted: {
        if (!series("signal 1").useOpenGL) {
            openGLSupported = false
            openGL = false
        }
    }

    ValueAxis {
        id: axisY1
        min: -1
        max: 4
    }

    ValueAxis {
        id: axisX
        min: 0
        max: 1024
    }

    LineSeries {
        id: lineSeries1
        name: "signal 1"
        axisX: axisX
        axisY: axisY1
        useOpenGL: chartView.openGL
    }

    Timer {
        id: refreshTimer
        interval: 1 / 60 * 1000 // 60 Hz
        running: true
       repeat: true
        onTriggered: {
            dataSource.update(chartView.series(0));
        }
    }
}

Rather than using the Timer in QML, I'd like to use an existing Timeout in a c++ class to push new data to the QML ChartView. I have two questions:

  1. How would I achieve this for the QML Oscilloscope example posted above?
  2. What format would be most suitable for the c++ data to facilitate this? I'm thinking a QVector of some sort; the data will be an integer or float with a vector index.
jars121
  • 1,127
  • 2
  • 20
  • 35
  • Do you want to send it to update from C ++ for example with a QTimer instead of the QML Timer? – eyllanesc Dec 09 '17 at 00:20
  • Using the Oscilloscope files as an example, I'd like to have a Timeout function in DataSource.cpp which calls DataSource::update(), rather than the QML Timer calling update(). – jars121 Dec 09 '17 at 00:22
  • You could place this file to work with this example. – eyllanesc Dec 09 '17 at 00:23
  • I'm sorry @eyllanesc I don't quite follow? – jars121 Dec 09 '17 at 00:25
  • What do you mean by `Oscilloscope files`? – eyllanesc Dec 09 '17 at 00:26
  • The example files I've included in my original post/question. How would I replace the QML Timer function with a c++ QTimer function in the datasource.cpp excerpt included in my original question? The source files for each of the excerpts I've included is linked in the first sentence in my question. – jars121 Dec 09 '17 at 00:27
  • Okay I understand, I thought you wanted to use the data generated by a real oscilloscope to take as a source for the graph. – eyllanesc Dec 09 '17 at 00:29
  • My apologies, I didn't make that particularly clear :) – jars121 Dec 09 '17 at 00:30
  • and could you explain your second question? – eyllanesc Dec 09 '17 at 00:30
  • Why do not you want to use `QVector` as the example shows? – eyllanesc Dec 09 '17 at 00:33
  • I'm more than happy to use QVector, I just wanted to confirm that this was a suitable/efficient approach. – jars121 Dec 09 '17 at 00:34
  • `QXYSeries` accepts that type of data as input, even if you have other types of data you will have to convert it. – eyllanesc Dec 09 '17 at 00:36
  • Ok understood. The question then becomes, how can I call update() at the end of generateData(), given that it requires a QAbstractSeries to be passed to it? – jars121 Dec 09 '17 at 00:39

1 Answers1

4

As you say in a comment you need to pass a series, then we create a method that receives the series and saves it in a member of the C ++ class, We also create a QTimer, and we do the same to update the interval:

*.h

public:
    Q_INVOKABLE void setSeries(QAbstractSeries *series);
    Q_INVOKABLE void setInterval(int interval);
    [...]
private:
    QXYSeries *mSeries;
    QTimer *timer;
    [...]

*.cpp

void DataSource::setSeries(QAbstractSeries *series)
{
    if (series) {
        mSeries = static_cast<QXYSeries *>(series);
    }
}

Then we remove the update argument and use mSeries:

DataSource::DataSource(QQuickView *appViewer, QObject *parent) :
    QObject(parent),
    m_appViewer(appViewer),
    m_index(-1)
{
    [...]
    timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, &DataSource::update);
    timer->start(1 / 60 * 1000 );
}

void DataSource::update()
{
    if (mSeries) {
        m_index++;
        if (m_index > m_data.count() - 1)
            m_index = 0;

        QVector<QPointF> points = m_data.at(m_index);
        // Use replace instead of clear + append, it's optimized for performance
        mSeries->replace(points);
    }
}

void DataSource::setInterval(int interval)
{
    if(timer){
        if(timer->isActive())
            timer->stop();
        timer->start(interval);
    }
}

*.qml

Component.onCompleted: {
    dataSource.setSeries(chartView.series(0));
    if (!series("signal 1").useOpenGL) {
        openGLSupported = false
        openGL = false
    }
}
[...]
function changeRefreshRate(rate) {
    dataSource.setInterval(1 / Number(rate) * 1000);
    //refreshTimer.interval = 1 / Number(rate) * 1000;
}

You can find the complete example in the following link.

Aleksey Kontsevich
  • 4,671
  • 4
  • 46
  • 101
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you very much! I don't receive any errors when running your version, but I don't receive a plot either (blank ChartView)? The update() function is being called, but it appears to fail the if (mSeries) logic every time. – jars121 Dec 09 '17 at 01:30
  • How weird. although I had a small error that I just corrected it should work. What version of Qt are you using? – eyllanesc Dec 09 '17 at 01:35
  • You could check if setSeries is called. Are you using my code? – eyllanesc Dec 09 '17 at 01:37
  • I'm using 5.9.2; yes I'm using your code. I just re-tried and it appears to be working perfectly. Thanks again! I'll go through it now in detail to understand exactly how it works. – jars121 Dec 09 '17 at 01:39