5

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.

zar
  • 11,361
  • 14
  • 96
  • 178
  • Did you register your custom type via `qmlRegisterType`? Also when your class is inside a namespace, make sure you use something like `MyNameSpace::Fruit*` for the property type. Then it should work. – qCring Jul 16 '15 at 06:37
  • 3
    What does it mean "it doesn't work"? Which error do you receive? – skypjack Jul 16 '15 at 06:39
  • 1
    Sorry, but questions like these are very poor. As @skypjack already mentioned, "it doesn't work" is the typical, lazy phrase that will always get downvoted. qCring asked the inevitable question of "where is the rest of your code that we can't answer the question without". You have been here for four years... I just don't understand. – Mitch Jul 16 '15 at 08:32
  • @qCring Yes I do and that's why the `price' property works which is property of 'Fruit'. Than the 2nd property 'fruit' also returns fruit but its price property doens't work, by that I mean it doesn't display the value, there is no error message. – zar Jul 16 '15 at 13:29
  • @Mitch Sorry I understand I didn't include full code partly because question will get lengthy but also because I wanted to see of calling property in this way is fine so I don't make futile attempts. If they are than I will try my best to sort it out first. So no one has objected to this, I am going to look deeper and post again if I couldn't solve it. – zar Jul 16 '15 at 13:32
  • 2
    That's not true. If you had a minimal example (which is almost always possible to create), the main.cpp would be very small - they usually are for Qt Quick applications. – Mitch Jul 16 '15 at 13:49
  • @KubaOber and Mido thanks, you guys pretty much answered what I wanted to know, this means I have to use a different strategy to accomplish what I want to do. I will make minimum demo and post that perhaps in a new post. Thanks you saved me from debugging this issue further and make the impossible work. – zar Jul 16 '15 at 19:28
  • 1
    I seen the code, you have 3 errors: 1. `virtual Q_INVOKABLE void add(HardwareComponent * item);` should be `Q_INVOKABLE virtual void add(HardwareComponent * item);` 2. `Q_PROPERTY( HardwareComponent * cpu READ getCPU );` remove the ; at the end of the line. 3. `engine.rootContext()->setContextProperty("computer", computer);` should be putted before the `engine.load(QUrl(QStringLiteral("qrc:/main.qml")));` this will solve your warning issue. for the root question you should read how to do Grouped Properties in qt http://doc.qt.io/qt-5/qtqml-cppintegration-exposecppattributes.html – Mido Jul 20 '15 at 08:50
  • @Mido Thanks, I fixed those errors. The section of article you pointed out seems confusing. The description is right but the example class definition doesn't quite add up. For example inside the class `CMessage`, it defines `Message *m_author` but I think this should be `MessageAuthor *m_author` based on the description and the classes. This does't look right! – zar Jul 20 '15 at 14:33
  • 1
    It is very bad form to fix the code in the question according to the answers, since it makes the question seem not to make any sense anymore. I've reverted your edit. – Kuba hasn't forgotten Monica Jul 20 '15 at 15:05

2 Answers2

5

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) }
        }
    }
}
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • Awesome! I took `qmlRegisterUncreatableType` from your code and put it in mine and whalla..its working! Apparently this was all I needed. Thanks! – zar Jul 20 '15 at 15:31
  • Well the `price()` implementation in base class is because I am implementing composite design pattern, I will be be revisiting it again to make sure I am aligned correctly but every `HardwareComponent`does have a price info so why not define price in base class? – zar Jul 20 '15 at 16:30
  • I agree, in this demo I wanted to make the accessing of the properties work so I didn't care about methods implementation but I think `setPrice()` shouldn't not do anything in base class and that would be better. I think you are are suggesting the same which sounds good. – zar Jul 20 '15 at 16:53
  • @zadane I've updated the answer to show how to leverage the fact that `QObject` is a composite class. – Kuba hasn't forgotten Monica Jul 20 '15 at 17:34
  • Thanks, it's very helpful. I still have to absorb the `event` function but the updated answer is even better. – zar Jul 20 '15 at 19:59
  • @ zadane Kuba ober is right and his code is good, but it's not `qmlRegisterUncreatableType` that solved your issue, it's the move of the `setContextProperty` before loading the main.qml, cause if you put it after the main, it will not know what is computer and cpu also :) – Mido Jul 20 '15 at 20:16
  • @Mido It doesn't work without `qmlRegisterUncreatableType`. You get the following error: *QMetaProperty::read: Unable to handle unregistered datatype 'HardwareComponent*' for property 'HardwareComponent::cpu'* – Kuba hasn't forgotten Monica Jul 20 '15 at 20:29
  • @Mido The `cpu.price` will fail without type registration under Qt 5.3. – Kuba hasn't forgotten Monica Jul 20 '15 at 20:34
2

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)
    }
}
BaCaRoZzo
  • 7,502
  • 6
  • 51
  • 82
Mido
  • 1,092
  • 1
  • 9
  • 14
  • Thanks but my project is a little different. My `Fruit` class is pure virtual and Q_PROPERTY getters and setters are virtual functions. I have an `add` function which is Q_INVOKABLE (and also virtual) where I add a fruit and than I want to access the price of that fruit. Following this whole thing gets complicated that's why I just posted little code but I am going to post my code a little later today in the post. – zar Jul 16 '15 at 20:16
  • I just posed complete code. I created a fresh demo and sorry I changed the context to computer components which I think it makes a better point what I am trying to achieve. – zar Jul 20 '15 at 05:10