How to expose property of QObject pointer to QML

zar picture zar · Jul 16, 2015 · Viewed 11.8k times · Source

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.

Answer

There were just a couple things wrong, related specifically to QML:

  1. The context properties must be set before the engine loads the qml file.

  2. 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.

screenshot of the example

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) }
        }
    }
}