How to display QML view( with scroll) to load and display multiline Text around 10 million lines from file [duplicate] - qt

I have a Qt application and I'd like to show some log. I use a TextArea. However, if the log is large or the events come too fast the GUI can't draw Textarea fast enough.
I have analyzed this problem with Qt Creator (QML Profiler) and if the log is large it takes 300 ms to draw the GUI. I use this software on a Raspberry Pi2.
Any ideas how to solve this? Should I use other QML controls? Thanks.
QML code:
TextArea {
text: appHandler.rawCommunication
readOnly: true
}
C++ code:
Q_PROPERTY(QString rawCommunication READ rawCommunication WRITE setrawCommunication NOTIFY rawCommunicationChanged)
void setrawCommunication(QString val)
{
val.append("\n");
val.append(m_rawCommunication);
m_rawCommunication = val;
emit rawCommunicationChanged(m_rawCommunication);
}

Use a view, like ListView. They instantiate their delegates as needed, based on which data the view says it needs to show depending on the position the user is at in the list. This means that they perform much better for visualising large amounts of data than items like TextArea, which in your case is going to keep a massive, ever-growing string in memory.
Your delegate could then be a TextArea, so you'd have one editable block of text per log line. However, if you don't need styling, I'd recommend going with something a bit lighter, like TextEdit. Taking it one step further: if you don't need editable text, use plain old Text. Switching to these might not make much of a difference, but if you're still seeing slowness (and have lots of delegates visible at a time), it's worth a try.

I tried the ListView suggestion but it has several drawbacks:
No easy way to keep the view positioned at the bottom when new output is added
No selection across lines/delegates
So I ended up using a cached TextArea, updating once every second:
TextArea {
id: outputArea_text
wrapMode: TextArea.Wrap
readOnly: true
font.family: "Ubuntu Mono, times"
function appendText(text){
logCache += text + "\n";
update_timer.start();
}
property string logCache: ""
Timer {
id: update_timer
// Update every second
interval: 1000
running: false
repeat: false
onTriggered: {
outputArea_text.append(outputArea_text.logCache);
outputArea_text.logCache = "";
}
}
Component.onCompleted: {
my_signal.connect(outputArea_text.appendText)
}
}

try this approach:
create a c++ logger class
append all logs to this class
and print them using some action, example a button click
this will solve your performance issue
Example of code:
Logger.h
#ifndef LOGGER_H
#define LOGGER_H
#include <QQmlContext>
#include <QObject>
#include <QStringList>
#include <QQmlEngine>
#include <QString>
#include <QtCore>
#include <QDebug>
class Logger : public QObject
{
Q_OBJECT
public:
explicit Logger(QObject *parent = 0);
~Logger();
Q_INVOKABLE QStringList *getLogStream();
Q_INVOKABLE void printLogStream();
Q_INVOKABLE void appendLog(QString log);
Q_INVOKABLE void log(QString log="");
Q_INVOKABLE void log(QString fileName, QString log);
signals:
public slots:
private:
QStringList* stringStream_;
};
#endif // LOGGER_H
Logger.cpp
#include "logger.h"
Logger::Logger(QObject *parent) :
QObject(parent),
stringStream_(new QStringList)
{
}
~Logger(){
if(stringStream_ != NULL)
{
delete stringStream_;
stringStream_ = NULL;
}
}
QStringList* Logger::getLogStream(){
return stringStream_;
}
void Logger::printLogStream()
{
QStringListIterator itr(*stringStream_);
while (itr.hasNext())
qDebug()<< itr.next()<<"\n";
}
void Logger::appendLog(QString log){
stringStream_->push_back(log) ;
}
void Logger::log(QString fileName,QString log)
{
#ifdef ENABLElogs
fileName.push_front(" [");
if(!fileName.contains(".qml"))
{
fileName.append(".qml]:");
}
qDebug()<<fileName<<log;
#else
Q_UNUSED(log);
Q_UNUSED(fileName);
#endif
}
void Logger::log(QString log)
{
#ifdef ENABLElogs
qDebug()<<log;
#else
Q_UNUSED(log);
#endif
}
main.cpp
#include <QtGui/QGuiApplication>
#include "qtquick2applicationviewer.h"
#include "logger.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QtQuick2ApplicationViewer *viewer = new QtQuick2ApplicationViewer;
Logger* stream = new Logger;
viewer->rootContext()->setContextProperty("Stream",stream);
viewer->setMainQmlFile(QStringLiteral("qml/project/main.qml"));
viewer->showExpanded();
return app.exec();
}
main.qml
import QtQuick 2.0
import QtQuick.Controls 1.1
Rectangle {
width: 800
height: 480
Text {
text: qsTr("Hello World")
anchors.centerIn: parent
Component.onCompleted: Stream.appendLog("Text object is completed")
}
Column{
x:300
Button{
text:"append"
onClicked: {
Stream.appendLog("MouseArea object clicked")
}
Component.onCompleted: Stream.appendLog("Button object is completed")
}
Button{
text:"logger"
onClicked: {
Stream.printLogStream()
}
Component.onCompleted: Stream.appendLog("Button logger object is completed")
}
}
TextArea{
text:"blablabla"
Component.onCompleted: Stream.appendLog("TextArea object is completed")
}
Component.onCompleted: Stream.appendLog("the main object is completed")
}
project.pro
#add this line
# comment it, run qmake and recompile to disable logs
DEFINES += ENABLElogs
using this approch you can stop all logs with a single line change when you want to release your soft

However, I have included complete code, using "QAbstractListModel" for a Logging heavy data to QML
listmodel.h
#ifndef LISTMODEL_H
#define LISTMODEL_H
#include <QAbstractListModel>
class ListModel: public QAbstractListModel
{
Q_OBJECT
public:
ListModel();
// Q_PROPERTY(QStringList logs READ name WRITE nameChanged)
int rowCount(const QModelIndex & parent = QModelIndex()) const;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
Q_INVOKABLE QVariant activate(int i);
private:
QStringList m_list;
};
#endif // LISTMODEL_H
listmodel.cpp
#include "listmodel.h"
#include <QFile>
#include <QHash>
ListModel::ListModel()
{
QFile file("/home/ashif/LogFile");
if(!file.open(QIODevice::ReadOnly))
{
qDebug( "Log file open failed" );
}
bool isContinue = true;
do
{
if(file.atEnd())
{
isContinue = false;
}
m_list.append(file.readLine());
}
while( isContinue);
}
int ListModel::rowCount(const QModelIndex & parent ) const
{
return m_list.count();
}
QVariant ListModel::data(const QModelIndex & index, int role ) const
{
if(!index.isValid()) {
return QVariant("temp");
}
return m_list.value(index.row());
}
QVariant ListModel::activate(int i)
{
return m_list[i];
}
main.qml
import QtQuick 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
Window {
visible: true
ListView
{
width: 200; height: 250
anchors.centerIn: parent
model:mylistModel
delegate: Text
{
text:mylistModel.activate(index)
}
}
}
main.cpp
#include <QGuiApplication>
#include <QQmlContext>
#include <QQmlApplicationEngine>
#include "logger.h"
#include "listmodel.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Logger myLogger;
ListModel listModel;
engine.rootContext()->setContextProperty("mylistModel", &listModel);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}

Related

Text gets missaligned when using Qt TextTable with TextArea using QtQuick.Controls 1.12

I have a problem where I am using a TextArea in Qml. A C++ Model holds a reference to that TextArea.
When I insert a QTextTable in the C++ model it all fine until the user enters some text. After the user manually edits a few cells and writes some text in it, it gets all messed up. Does anyone know anything on how to solve it?
I also have other functions with are working perfectly. So I would guess there is nothing wrong with the connection between the c++ model and the textarea.
Here is the Documenthandler.h
#include <QQuickTextDocument>
#include <QtGui/QTextCharFormat>
#include <QtCore/QTextCodec>
#include <qqmlfile.h>
QT_BEGIN_NAMESPACE
class QTextDocument;
QT_END_NAMESPACE
class DocumentHandler : public QObject
{
Q_OBJECT
Q_ENUMS(HAlignment)
Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged)
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
public:
DocumentHandler();
Q_INVOKABLE void createTable(int columns ,int rows);
QQuickItem *target() { return m_target; }
void setTarget(QQuickItem *target);
QString text() const;
public Q_SLOTS:
void setText(const QString &arg);
Q_SIGNALS:
void targetChanged();
void textChanged();
void error(QString message);
private:
QTextCursor textCursor() const;
QQuickItem *m_target;
QTextDocument *m_doc;
QString m_text;
};
Here is the documenthandler.cpp
in the "createTable" function I create the table
#include "documenthandler.h"
#include <QtGui/QTextDocument>
#include <QtGui/QTextList>
#include <QtGui/QTextTable>
#include <QtGui/QTextCursor>
#include <QtGui/QFontDatabase>
#include <QtCore/QFileInfo>
DocumentHandler::DocumentHandler()
: m_target(0)
, m_doc(0)
{
}
void DocumentHandler::setTarget(QQuickItem *target)
{
m_doc = 0;
m_target = target;
if (!m_target)
return;
QVariant doc = m_target->property("textDocument");
if (doc.canConvert<QQuickTextDocument*>()) {
QQuickTextDocument *qqdoc = doc.value<QQuickTextDocument*>();
if (qqdoc)
m_doc = qqdoc->textDocument();
}
emit targetChanged();
}
void DocumentHandler::setText(const QString &arg)
{
if (m_text != arg) {
m_text = arg;
emit textChanged();
}
}
QString DocumentHandler::text() const
{
return m_text;
}
QTextCursor DocumentHandler::textCursor() const
{
if (!m_doc)
return QTextCursor();
QTextCursor cursor = QTextCursor(m_doc);
return cursor;
}
void DocumentHandler::createTable(int columns , int rows)
{
QTextCursor cursor = textCursor();
if (cursor.isNull())
return;
cursor.insertTable(rows,columns);
}
Here is the main qml
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 1.6
import DocumentHandler 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Button
{
id:btn
text:"test"
onClicked: document.createTable(5,5);
}
TextArea {
Accessible.name: "document"
id: tooltip_area
selectByMouse: true
anchors.left:parent.left
anchors.right:parent.right
anchors.top: btn.bottom
anchors.bottom: parent.bottom
baseUrl: "qrc:/"
text: document.text
textFormat: Qt.RichText
wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
Component.onCompleted: forceActiveFocus()
}
DocumentHandler{
id: document
target: tooltip_area
}
}
here is the main function, there is nothing special except I register the QML Type
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "documenthandler.h"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/main.qml"));
qmlRegisterType<DocumentHandler>("DocumentHandler",1,0,"DocumentHandler");
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
This is how it looks after filling up some cells
So I solved it by just selecting everything and deselecting when the text changes. Theoretically I only need to do it when I am in a Table Block.
onTextChanged:
{
if(recoursionlock)
{
recoursionlock =false;
var curpos = tooltip_area.cursorPosition;
var select_start = tooltip_area.selectionStart;
var select_end = tooltip_area.selectionEnd;
selectAll();
deselect();
if(curpos !== -1 && curpos >= tooltip_area.text.lenght)
tooltip_area.cursorPosition = curpos;
else
tooltip_area.select(select_start,select_end)
recoursionlock = true;
}
}

How to get 2D array from UART to QList<QString> Qt and set text on QML

I receive a 2D array from UART that sent from Arduino.
I can show it in debug, but I can not save it in a QList variant to set text for matrix of rectangle in QML.
I want to show text on QML each rectangle.
How I can do?
This is Arduino code. I send 2d array 17x17
void setup(){
pinMode(LED_BUILTIN,OUTPUT);
analogWrite(LED_BUILTIN,255);
Serial.begin(115200);
void loop(){
double data[17][17];
if(Serial.available()){
delay(100);
for(int i=0; i<17; i++){
for(int j=0; j<17; j++){
data[i][j] = i+j+0.01;
sendData(data[i][j]);
delay(10);
}
}
}
return;
}
void sendData(double data){
Serial.print((data));
}
This is readSerial function:
void serial::readSerial(){
serialData = arduino.readAll();
qDebug()<< serialData <<"\n";
}
QML file:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 17*square_size
height: 17*square_size
title: qsTr("Hello World")
property int square_size: 30
Grid {
id: grid
columns: 17
Repeater{
id: rpt
model: 17*17
Rectangle{
width: square_size
height: square_size
border.color: "black"
border.width: 1
Text {
anchors.centerIn: parent
text: Serial.model_data[index]
}
}
}
}
}
serial.h:
#include <QQmlApplicationEngine>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QtDebug>
class serial: public QObject
{
Q_OBJECT
Q_PROPERTY(QList<QString> text READ text NOTIFY textChanged)
public:
explicit serial(QQmlApplicationEngine *engine, QObject *parent = nullptr);
~serial();
void setupSerial();
Q_INVOKABLE QList<QString> text(){
return m_text;
}
private slots:
void readSerial();
private:
QQmlApplicationEngine* m_engine;
/* Varian of Arduino*/
QSerialPort arduino;
bool arduino_is_avaiable;
QString portName;
QByteArray serialData;
/*Varian of text*/
QList<QString> m_text;
signals:
void textChanged();
public slots:
};
#endif // SERIAL_H
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "serial.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
serial myserial(&engine);
engine.rootContext()->setContextProperty("Serial", &myserial);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
I think you are missing the property on the Serial class to read the data: text: Serial.model_data[index]. The fastest solution to get something on screen would be to add that property and fill it as follows:
Serial.h (I left some parts out)
class Serial : public QObject
{
Q_OBJECT
Q_PROPERTY(QVariantList model_data READ model_data NOTIFY modelDataChanged)
public:
explicit Serial(QObject *parent = nullptr);
QVariantList model_data() const { return model_data_; }
signals:
void modelDataChanged();
private slots:
void readSerial();
private:
QVariantList model_data_;
};
The readSerial() function would look like this:
void Serial::readSerial()
{
QByteArray serialData = QByteArray(17 * 17 * 8, 0); //shortcut for testing
double *arr = reinterpret_cast<double*>(serialData.data());
model_data_.clear();
for(int i=0;i<17*17;i++)
{
model_data_.append(arr[i]);
}
emit modelDataChanged();
}
What happens is that the QVariant list is filled with the doubles, which have been reinterpreted from byte to doubles. Note that this has to be done every time the serialData changes.
However, this might not be ideal in case you want to go any further with this. When changing dimensions, this setup will fail, both on arduino sending side, arduino reading side and on qml side. You should consider sending the dimensions of the array, reading those back and acting on it. One idea might be QAbstractListModel, which already has the notion of rows/columns; another, simpler idea might be to add properties to the Serial class stating the dimensions (which where read in readSerial())

QML Slot not displaying double value from serial input

I'm struggling to get my serial input "analogread2" converted to a double, to display in QML.
Ive added the Q_property, the root context in the main.cpp, and I can add the property in the qml, but I cant get it to display in text format.
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
SerialPort serialport;
//engine.rootContext()->setContextProperty("serialport", &serialport);
qmlRegisterType<SerialPort>("SerialPortlib", 1, 0, "SerialPort");
engine.load(QUrl(QStringLiteral("qrc:/main.qml")))
header.
Q_PROPERTY(double newValue MEMBER m_oil_pressure_volt WRITE set_oil_pressure_volt NOTIFY oil_pressure_volt_Changed )
qml
Window {
visible: true
width: 640
height: 480
id: gauge
SerialPort{
// what would I put in here to display the text value (double)
}
}
any help is much appreciated.
To do the test I am assuming that you are sending the data from the arduino in one of the following ways:
Serial.println(data);
Serial.print(data);
Assuming the above has been implemented the serialport class, this has a function called openDefault that scans the devices that are connected through serial and searches for the word "Arduino" within description or manufacturer and if one finds it connects to it.
The complete code can be found at: https://github.com/eyllanesc/stackoverflow/tree/master/Arduino-QML
serialport.h
#ifndef SERIALPORT_H
#define SERIALPORT_H
#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
class SerialPort : public QObject
{
Q_OBJECT
Q_PROPERTY(double oil_pressure_volt READ get_oil_pressure_volt WRITE set_oil_pressure_volt NOTIFY oil_pressure_volt_Changed )
public:
SerialPort(QObject *parent = 0);
~SerialPort();
double get_oil_pressure_volt() const;
void set_oil_pressure_volt(double newValue);
public slots:
void onReadData();
signals:
void oil_pressure_volt_Changed(double newValue);
private:
QSerialPort *arduino;
double mOil_pressure_volt;
QSerialPortInfo portInfo;
void openDefault();
};
serialport.cpp
#include "serialport.h"
#include <QDebug>
SerialPort::SerialPort(QObject *parent):QObject(parent)
{
arduino = new QSerialPort(this);
connect(arduino, &QSerialPort::readyRead, this, &SerialPort::onReadData);
openDefault();
}
SerialPort::~SerialPort()
{
delete arduino;
}
void SerialPort::set_oil_pressure_volt(double newValue)
{
if (mOil_pressure_volt == newValue)
return;
mOil_pressure_volt = newValue;
emit oil_pressure_volt_Changed(mOil_pressure_volt);
}
void SerialPort::onReadData()
{
if(arduino->bytesAvailable()>0){
QByteArray data = arduino->readAll();
QString value = QString(data).trimmed();
qDebug()<< value;
bool ok;
double val = value.toDouble(&ok);
if(ok)
set_oil_pressure_volt(val);
}
}
void SerialPort::openDefault()
{
for(auto info: QSerialPortInfo::availablePorts()){
qDebug()<<info.portName()<<info.description()<<info.manufacturer();
if(!info.isBusy() && (info.description().contains("Arduino") || info.manufacturer().contains("Arduino"))){
portInfo = info;
break;
}
}
if(portInfo.isNull()){
return;
}
arduino->setPortName(portInfo.portName());
arduino->setBaudRate(QSerialPort::Baud115200);
arduino->setDataBits(QSerialPort::Data8);
arduino->setParity(QSerialPort::NoParity);
arduino->setStopBits(QSerialPort::OneStop);
arduino->setFlowControl(QSerialPort::NoFlowControl);
if(arduino->open(QSerialPort::ReadWrite))
qDebug()<<"Connected to "<< portInfo.manufacturer()<< " on " << portInfo.portName();
else
qCritical()<<"Serial Port error: " << arduino->errorString();
}
double SerialPort::get_oil_pressure_volt() const
{
return mOil_pressure_volt;
}
Example:
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "serialport.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
qmlRegisterType<SerialPort>("SerialPortLib", 1, 0, "SerialPort");
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
main.qml
import QtQuick 2.6
import QtQuick.Window 2.2
import SerialPortLib 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Test")
SerialPort{
onOil_pressure_voltChanged: {
tx.text = "%1".arg(oil_pressure_volt);
}
}
Text {
id: tx
anchors.fill: parent
font.family: "Helvetica"
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}

Update TextArea in QML from C++

My question is pretty simple I think. Nevertheless I was not able to figure it out. I have a TextArea defined in my .qml file, which needs to be updated dynamically from the C++ code.
Unfortunately, I don't know how to access/update the TextArea from within the imserver.cpp class.
Can anyone help me out please?
Here is the .qml file:
import QtQuick 2.2
import QtQuick.Controls 1.1
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("IMServer")
menuBar: MenuBar {
Menu {
title: qsTr("File")
MenuItem {
text: qsTr("Exit")
onTriggered: Qt.quit();
}
}
}
TextArea {
id: serverInformation
x: 0
y: 0
width: 247
height: 279
}
}
My main.cpp:
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQmlEngine>
#include <QtQml>
#include "imserver.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
IMServer server(2000);
qmlRegisterUncreatableType<IMServer>("App", 1, 0, "IMServer", "");
engine.rootContext()->setContextProperty("imserver", &server);
server.startServer();
return app.exec();
}
imserver.h
#ifndef IMSERVER_H
#define IMSERVER_H
#include <QTcpServer>
#include <QTcpSocket>
#include <QAbstractSocket>
#include <QThreadPool>
class IMServer : public QTcpServer {
Q_OBJECT
Q_PROPERTY(QString text WRITE setText NOTIFY textChanged)
public:
explicit IMServer(int port, QObject *parent = 0);
void startServer();
void setText(const QString &txt);
signals:
void textChanged();
public slots:
protected:
void incomingConnection(qintptr fd);
private:
int port;
QThreadPool *pool;
QString m_text;
};
#endif // IMSERVER_H
imserver.cpp:
#include "imserver.h"
#include "clienthandler.h"
IMServer::IMServer(int port, QObject *parent) : QTcpServer(parent) {
this->pool = new QThreadPool(this);
this->pool->setMaxThreadCount(100);
this->port = port;
}
void IMServer::startServer() {
setText("TEST");
if (!this->listen(QHostAddress::Any, this->port)) {
qDebug() << "Server could not be started";
} else {
qDebug() << "Server started, listening...";
}
}
void IMServer::setText(const QString &txt) {
m_text = txt;
emit textChanged();
}
void IMServer::incomingConnection(qintptr fd) {
ClientHandler *client = new ClientHandler();
client->setAutoDelete(true);
client->fd = fd;
this->pool->start(client);
}
There are several ways. Here is how I'd do it.
First you should register your IMServer class:
qmlRegisterUncreatableType<IMServer>("App", 1, 0, "IMServer", "");
Then you add your IMServer instance to QML:
enigne.rootContext()->setContextProperty("imserver", &server);
Your IMServer class then needs a signal, that your TextArea can be connected to, or even better add a property (you need to add the getText() function and textChanged() signal here as well, for a read-only property):
Updated:
Q_PROPERTY(QString text READ getText NOTIFY textChanged)
In the TextArea you can then create a binding:
TextArea {
text: imserver.text
}
Then whenever you emit textChanged in your IMServer class, the TextArea's text will be updated.
For more information: http://doc.qt.io/qt-5/qtqml-cppintegration-topic.html

How to expose property of QObject pointer to QML

I am giving very brief (and partial) description of my class to show my problem. Basically I have setup two properties.
class Fruit : public QObject
{
Q_OBJECT
....
public:
Q_PROPERTY( int price READ getPrice NOTIFY priceChanged)
Q_PROPERTY(Fruit * fruit READ fruit WRITE setFruit NOTIFY fruitChanged)
}
In my QML if I access the price property it works well and good. But if I access the fruit property which obviously returns Fruit and then try to use its price property, that doesn't work. Is this not supposed to work this way?
Text {
id: myText
anchors.centerIn: parent
text: basket.price // shows correctly
//text: basket.fruit.price // doesn't show
}
The 2nd one returns Fruit which also is a property and it has price property but it doesn't seem to access that property? Should this work?
Updated
I am including my source code. I created a new demo with HardwareComponent, it just makes more sense this way. I tried to make it work based on answer I received but no luck.
HardwareComponent class is the base class for Computer and CPU.
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
class HardwareComponent : public QObject
{
Q_OBJECT
public:
HardwareComponent() : m_price(0) {}
virtual int price() = 0;
virtual Q_INVOKABLE void add(HardwareComponent * item) { m_CPU = item; }
HardwareComponent * getCPU() const { return m_CPU; }
Q_SLOT virtual void setPrice(int arg)
{
if (m_price == arg) return;
m_price = arg;
emit priceChanged(arg);
}
Q_SIGNAL void priceChanged(int arg);
protected:
Q_PROPERTY(HardwareComponent * cpu READ getCPU);
Q_PROPERTY(int price READ price WRITE setPrice NOTIFY priceChanged)
HardwareComponent * m_CPU;
int m_price;
};
class Computer : public HardwareComponent
{
Q_OBJECT
public:
Computer() { m_price = 500; }
int price() { return m_price; }
void setprice(int arg) { m_price = arg; }
};
class CPU : public HardwareComponent
{
Q_OBJECT
public:
CPU() { m_price = 100; }
int price() { return m_price; }
void setprice(int arg) { m_price = arg; }
};
int main(int argc, char *argv[])
{
HardwareComponent * computer = new Computer;
CPU * cpu = new CPU;
computer->add( cpu );
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
engine.rootContext()->setContextProperty("computer", computer);
return app.exec();
}
#include "main.moc"
main.qml
import QtQuick 2.3
import QtQuick.Window 2.2
Window {
visible: true
width: 360
height: 360
Text {
anchors.centerIn: parent
text: computer.price // works
//text: computer.cpu.price // doesn't work, doesn't show any value
}
}
This is the complete source code for all my project files.
When I run it I get this output:
Starting C:\Users\User\Documents\My Qt
Projects\build-hardware-Desktop_Qt_5_4_0_MSVC2010_OpenGL_32bit-Debug\debug\hardware.exe...
QML debugging is enabled. Only use this in a safe environment.
qrc:/MainForm.ui.qml:20: ReferenceError: computer is not defined
Even though it gives a warning at line 20 (computer.price) line, it still works and shows the price of computer (=500). If change it computer.cpu.price, the same warning is reported but the price no longer shows - it doesn't seem to work.
The problem is since price is a virtual property, it works! but if I use this property on another hardware component inside the computer component, it doesn't work! The code/answer posted by Mido gives me hope there could be a solution to this, it looks quite close! I hope I can make this work.
There were just a couple things wrong, related specifically to QML:
The context properties must be set before the engine loads the qml file.
You have not registered the HardwareComponent type. See Defining QML Types from C++ for more information.
Apart from the QML side of things, it appears that you wish the hardware to be a composite with a tree structure. Since a QObject is already a composite, you can leverage this to your advantage. The tree of hardware items can be a tree of objects. The QObject notifies the parents when the children are added or removed: it's thus easy to keep the price current as the children are added and removed.
Each component has a unitPrice: this is the price of the component in isolation. The price is a total of the component's unit price, and the prices of its children.
Instead of traversing the entire tree to obtain the total price, you could cache it and only update it when the unit price changes, or the child prices change. Similarly, the current CPU could be cached, you could enforce a single CPU at any given time, etc. The example is functional, but only a minimal sketch.
I've also shown how to change the CPU types in the Computer. There are notifications for both the price changes and cpu changes. The QML UI reacts appropriately when the cpu property gets changed, as well as when any price property changes.
main.cpp
#include <QGuiApplication>
#include <QtQml>
class HardwareComponent : public QObject {
Q_OBJECT
Q_PROPERTY(QString category MEMBER m_category READ category CONSTANT)
Q_PROPERTY(HardwareComponent * cpu READ cpu WRITE setCpu NOTIFY cpuChanged)
Q_PROPERTY(int price READ price NOTIFY priceChanged)
Q_PROPERTY(int unitPrice MEMBER m_unitPrice READ unitPrice WRITE setUnitPrice NOTIFY unitPriceChanged)
QString m_category;
int m_unitPrice;
bool event(QEvent * ev) Q_DECL_OVERRIDE {
if (ev->type() != QEvent::ChildAdded && ev->type() != QEvent::ChildRemoved)
return QObject::event(ev);
auto childEvent = static_cast<QChildEvent*>(ev);
auto child = qobject_cast<HardwareComponent*>(childEvent->child());
if (! child) return QObject::event(ev);
if (childEvent->added())
connect(child, &HardwareComponent::priceChanged,
this, &HardwareComponent::priceChanged, Qt::UniqueConnection);
else
disconnect(child, &HardwareComponent::priceChanged,
this, &HardwareComponent::priceChanged);
emit priceChanged(price());
if (child->category() == "CPU") emit cpuChanged(cpu());
return QObject::event(ev);
}
public:
HardwareComponent(int price, QString category = QString(), QObject * parent = 0) :
QObject(parent), m_category(category), m_unitPrice(price) {}
HardwareComponent * cpu() const {
for (auto child : findChildren<HardwareComponent*>())
if (child->category() == "CPU") return child;
return 0;
}
Q_INVOKABLE void setCpu(HardwareComponent * newCpu) {
Q_ASSERT(!newCpu || newCpu->category() == "CPU");
auto oldCpu = cpu();
if (oldCpu == newCpu) return;
if (oldCpu) oldCpu->setParent(0);
if (newCpu) newCpu->setParent(this);
emit cpuChanged(newCpu);
}
Q_SIGNAL void cpuChanged(HardwareComponent *);
virtual int price() const {
int total = unitPrice();
for (auto child : findChildren<HardwareComponent*>(QString(), Qt::FindDirectChildrenOnly))
total += child->price();
return total;
}
Q_SIGNAL void priceChanged(int);
int unitPrice() const { return m_unitPrice; }
void setUnitPrice(int unitPrice) {
if (m_unitPrice == unitPrice) return;
m_unitPrice = unitPrice;
emit unitPriceChanged(m_unitPrice);
emit priceChanged(this->price());
}
Q_SIGNAL void unitPriceChanged(int);
QString category() const { return m_category; }
};
struct Computer : public HardwareComponent {
Computer() : HardwareComponent(400) {}
};
class FluctuatingPriceComponent : public HardwareComponent {
QTimer m_timer;
int m_basePrice;
public:
FluctuatingPriceComponent(int basePrice, const QString & category = QString(), QObject * parent = 0) :
HardwareComponent(basePrice, category, parent),
m_basePrice(basePrice) {
m_timer.start(250);
connect(&m_timer, &QTimer::timeout, [this]{
setUnitPrice(m_basePrice + qrand()*20.0/RAND_MAX - 10);
});
}
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Computer computer;
HardwareComponent memoryBay(40, "Memory Bay", &computer);
HardwareComponent memoryStick(60, "Memory Stick", &memoryBay);
FluctuatingPriceComponent cpu1(100, "CPU", &computer);
HardwareComponent cpu2(200, "CPU");
qmlRegisterUncreatableType<HardwareComponent>("bar.foo", 1, 0, "HardwareComponent", "");
engine.rootContext()->setContextProperty("computer", &computer);
engine.rootContext()->setContextProperty("cpu1", &cpu1);
engine.rootContext()->setContextProperty("cpu2", &cpu2);
engine.load(QUrl("qrc:/main.qml"));
return app.exec();
}
#include "main.moc"
main.qml
import QtQuick 2.0
import QtQuick.Controls 1.2
import QtQuick.Window 2.0
Window {
visible: true
Column {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
Text {
text: "Computer price: " + computer.price
}
Text {
text: "CPU price: " + (computer.cpu ? computer.cpu.price : "N/A")
}
Button {
text: "Use CPU 1";
onClicked: { computer.setCpu(cpu1) }
}
Button {
text: "Use CPU 2";
onClicked: { computer.setCpu(cpu2) }
}
Button {
text: "Use no CPU";
onClicked: { computer.setCpu(undefined) }
}
}
}
I created a Fruit class like in your example and it's working fine. I created a new instance of Fruit and I can get the price value. Here's my code:
fruit.h
#ifndef FRUIT_H
#define FRUIT_H
#include <QObject>
#include <QQuickView>
class Fruit : public QObject
{
Q_OBJECT
Q_PROPERTY( int price READ getPrice WRITE setPrice NOTIFY priceChanged)
Q_PROPERTY(Fruit * fruit READ fruit WRITE setFruit NOTIFY fruitChanged)
public:
Fruit();
~Fruit();
Fruit *fruit();
void setFruit(Fruit * value);
int getPrice();
void setPrice(int value);
signals:
void fruitChanged();
void priceChanged(int price);
private:
int _price;
Fruit *_fruit;
};
#endif // FRUIT_H
fruit.cpp
#include "fruit.h"
Fruit::Fruit() :
_price(1),_fruit(this)
{
}
Fruit::~Fruit()
{
if(_fruit)
{
delete _fruit;
_fruit = NULL;
}
}
Fruit *Fruit::fruit()
{
//Uncomment this line to test the set
//_fruit = new Fruit;
return _fruit;
}
void Fruit::setFruit(Fruit *value)
{
_fruit = value;
emit fruitChanged();
}
int Fruit::getPrice()
{
return _price;
}
void Fruit::setPrice(int value)
{
_price = value;
emit priceChanged(_price);
}
main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include <QQmlContext>
#include "fruit.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQuickView view;
view.rootContext()->setContextProperty("basket", new Fruit);
view.setSource(QStringLiteral("qml/main.qml"));
view.show();
return app.exec();
}
main.qml
import QtQuick 2.2
Rectangle {
width:800
height: 480
property var obj
color: "yellow"
Text{
text: basket.fruit.price
font.pixelSize: 20
}
Component.onCompleted: {
basket.price = 20
console.log(basket.fruit.price)
}
}

Resources