Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Exposing C++ Object list to Qml - objects not displayed when modeling list using Repeater



  • Hi

    I have just started to learn Qt Qml, by following Nicholas Sheriffs' book: Build modern, responsive cross-platform desktop applications with Qt, C++, and QML from which the whole idea come.

    Following authors' idea, I'm trying to implement context sensitive buttons that would be displayed in different views.

    In order to do that, I have implemented Command class that represents single button:

    #ifndef COMMAND_H
    #define COMMAND_H
    
    #include <functional>
    
    #include <QObject>
    #include <QScopedPointer>
    #include <QString>
    
    #include <cms-lib_global.h>
    
    namespace cms {
    namespace framework {
    
    class CMSLIBSHARED_EXPORT Command : public QObject
    {
        Q_OBJECT
        Q_PROPERTY(QString ui_iconCharacter READ iconCharacter CONSTANT)
        Q_PROPERTY(QString ui_description READ description CONSTANT)
        Q_PROPERTY(bool ui_canExecute READ canExecute NOTIFY canExecuteChanged)
    
    public:
        explicit Command(QObject* parent = nullptr,
                         const QString& iconCharacter = "",
                         const QString& description = "",
                         std::function<bool()> canExecute = [](){ return true; });
        ~Command();
    
        const QString& iconCharacter() const;
        const QString& description() const;
        bool canExecute() const;
    
    signals:
        void canExecuteChanged();
        void executed();
    
    private:
        class Implementation;
        QScopedPointer<Implementation> implementation;
    };
    
    }
    }
    
    
    #endif // COMMAND_H
    
    
    
    //command.cpp
    
    #include "command.h"
    
    namespace cms {
    namespace framework {
    
    class Command::Implementation{
    public:
        QString iconCharacter;
        QString description;
        std::function<bool()> canExecute;
    
        Implementation(const QString& _iconCharacter,
                       const QString& _description,
                       std::function<bool()> _canExecute)
            : iconCharacter(_iconCharacter),
              description(_description),
              canExecute(_canExecute) {}
    
    };
    
    Command::Command(QObject* parent, const QString& iconCharacter,
                     const QString& description, std::function<bool()> canExecute)
        : QObject(parent){
        implementation.reset(new Implementation(iconCharacter, description, canExecute));
    }
    
    Command::~Command()
    {
    }
    
    const QString& Command::iconCharacter() const
    {
        return implementation->iconCharacter;
    }
    
    const QString& Command::description() const
    {
        return implementation->description;
    }
    
    bool Command::canExecute() const
    {
        return implementation->canExecute();
    }
    
    }
    }
    
    
    

    Command Class is handled withing CommandController class:

    #ifndef COMMANDCONTROLLER_H
    #define COMMANDCONTROLLER_H
    
    
    #include <QObject>
    #include <QtQml/QQmlListProperty>
    
    #include <cms-lib_global.h>
    #include <framework/command.h>
    
    
    namespace cms {
    namespace controllers {
    
    class CMSLIBSHARED_EXPORT CommandController : public QObject{
        Q_OBJECT
        Q_PROPERTY(QQmlListProperty<cms::framework::Command>
                   ui_createPatientViewContextCommands READ
                   ui_createPatientViewContextCommands CONSTANT)
    public:
        explicit CommandController(QObject* _parent = nullptr);
        ~CommandController();
    
        QQmlListProperty<cms::framework::Command> ui_createPatientViewContextCommands();
    public slots:
        void onCreateClientSaveExecuted();
    
    private:
        class Implementation;
        QScopedPointer<Implementation> implementation;
    };
    
    }
    }
    
    #endif // COMMANDCONTROLLER_H
    
    //commandcontroller.cpp
    
    #include "commandcontroller.h"
    
    #include <QList>
    #include <QDebug>
    
    using namespace cms::framework;
    
    namespace cms{
    namespace controllers{
    
    class CommandController::Implementation{
    
    public:
        //Controller
        CommandController* commandController {nullptr};
        //Commands
        QList<Command*> createClientViewContextCommands{};
    
    
        Implementation(CommandController* _commandController) :
            commandController(_commandController)
        {
    
            Command* createClientSaveCommand = new Command(commandController,
                                                           QChar( 0xf0c7 ),
                                                           "Save");
    
            QObject::connect(createClientSaveCommand,
                             &Command::executed,
                             commandController,
                             &CommandController::onCreateClientSaveExecuted);
    
            createClientViewContextCommands.append(createClientSaveCommand);
    
        }
    
    
    };
    
    CommandController::CommandController(QObject* parent) : QObject(parent){
        implementation.reset(new Implementation(this));
    }
    
    CommandController::~CommandController(){
    
    }
    
    QQmlListProperty<Command> CommandController::ui_createPatientViewContextCommands(){
        return QQmlListProperty<Command>(this, implementation->createClientViewContextCommands);
    }
    
    void CommandController::onCreateClientSaveExecuted(){
        qDebug() << "You executed the Save command!";
    }
    
    
    }
    }
    
    
    

    With that in place, i registered both created types within Qml inside main.cpp :

     qmlRegisterType<cms::framework::Command>("CMS", 1, 0, "Command");
     qmlRegisterType<cms::controllers::CommandController>("CMS", 1, 0, "CommandController");
    

    I created 2 component Qml files and successfully (atleast i think so) exported them in my components module:

    //CommandBar.qml
    import QtQuick 2.12
    import assets 1.0
    
    Item {
        property alias commandList: commandRepeater.model
    
        anchors {
            left: parent.left
            bottom: parent.bottom
            right: parent.right
        }
    
        height: Style.heightCommandBar
    
        Rectangle {
            anchors.fill: parent
            color: Style.colorCommandBarBackground
    
            Row {
                anchors {
                    top: parent.top
                    bottom: parent.bottom
                    right: parent.right
                }
    
                Repeater {
                    id: commandRepeater
                    delegate: CommandButton {
                        command: modelData
                    }
                }
            }
        }
    }
    
    
    //CommandButton.qml
    
    import QtQuick 2.12
    import CMS 1.0
    import assets 1.0
    
    Item {
        property Command command
        width: Style.widthCommandButton
        height: Style.heightCommandButton
    
        Rectangle {
            id: background
            anchors.fill: parent
            color: Style.colorCommandBarBackground
    
            //Command button icon
            Text {
                id: textIcon
    
                anchors {
                    centerIn: parent
                    verticalCenterOffset: -10
                }
                font {
                    family: Style.fontAwesomeSolid
                    pixelSize: Style.pixelSizeCommandBarIcon
                }
    
                color: command.ui_canExecute ? Style.colorCommandBarFont : Style.colorCommandBarFontDisabled
    
                text: command.ui_iconCharacter
                horizontalAlignment: Text.AlignHCenter
            }
    
            //Command button description
            Text {
                id: textDescription
    
                anchors {
                    top: textIcon.bottom
                    bottom: parent.bottom
                    left: parent.left
                    right: parent.right
                }
    
                font.pixelSize: Style.pixelSizeNavigationBarText
    
                color: command.ui_canExecute ? Style.colorCommandBarFont : Style.colorCommandBarFontDisabled
                text: command.ui_description
                horizontalAlignment: Text.AlignHCenter
                verticalAlignment: Text.AlignVCenter
            }
    
            MouseArea {
                anchors.fill: parent
                cursorShape: Qt.PointingHandCursor
                hoverEnabled: true
                onEntered: background.state = "hover"
                onExited: background.state = ""
                onClicked: if(command.ui_canExecute) {
                               command.executed();
                           }
            }
    
            states: [
                State {
                    name: "hover"
                    PropertyChanges {
                        target: background
                        color: Qt.darker(Style.colorCommandBarBackground)
                    }
                }
    
            ]
        }
    }
    
    

    I used created components inside CreatePatientView.qml

    import QtQuick 2.12
    import assets 1.0
    import components 1.0
    
    Item {
    
        Rectangle {
            anchors.fill: parent
            color: Style.colorBackground
            Text {
                anchors.centerIn: parent
                text: "Create patient view"
            }
        }
    
        CommandBar {
            commandList: masterController.ui_commandController.ui_createClientViewContextCommands
        }
    
    }
    

    And used the CreatePatientView inside MasterView.qml

    For the beginning, im trying to display single "save" button (icon + description) onto my CreatePatientView, before I implement more buttons.

    Sadly, the icon + description command button is not displayed, mouse hover area isn't present neither. Im new to Qml and UI, so I cannot figure out what might be the problem.

    Sorry for lots of code, but I simply do not know where should I look for source of problem.

    Best regards.



  • @bwylegly said in Exposing C++ Object list to Qml - objects not displayed when modeling list using Repeater:

    ui_createClientViewContextCommands

    Should this be ui_createPatientViewContextCommands? I don't see ui_createClientViewContextCommands in any of the code you posted. That is where I had trouble. I could not see where those 3 items were defined in the code.



  • After doing some digging, I have found the following:

    • if the CommandButton is declared explicitly outside of CommandBar, and initialized using aliases, the button is populated and displayed correctly - therefore, I think that my problem is connected to populating CommandButton's using Repeater OR displaying it within row in CommandBar.qml

    • I have placed simple onCompleted console logs inside CommandBar and CommandButton root Item's - if i DO NOT use any explicit CommandButtons but those retrievied from Command metaData, the CommandBar qml is being read, while CommandButton is not.

    Can somebody point out what am I doing wrong?





  • Hi @fcarney,
    im not sure if I understood that mechanism correctly, but I thought that I provided model for repeater this way:

    1. CommandController provides getter:
    QQmlListProperty<cms::framework::Command> ui_createPatientViewContextCommands();
    

    which is registered as Q_PROPERTY:

     Q_PROPERTY(QQmlListProperty<cms::framework::Command>
                   ui_createPatientViewContextCommands READ
                   ui_createPatientViewContextCommands)
    
    1. CommandBar.qml contains Repeater inside it's hierarchy (as posted in original post)
    Repeater {
                    id: commandRepeater
                    delegate: CommandButton {
                        command: modelData
                    }
                }
    

    With that in place, i use alias inside CommandBar.qml:

    property alias commandList: commandRepeater.model
    
    1. Inside CreatePatientView.qml I include CommandBar component, populating it using getter from CommandController:
    CommandBar {
            commandList: masterController.ui_commandController
            .ui_createClientViewContextCommands
    
        }
    

    What's wrong in that approach?

    Thanks in advance.



  • Okay, I see what you are doing there with the alias. I do not see how the value you set is defined:

    CommandBar {
            commandList: masterController.ui_commandController.ui_createClientViewContextCommands
    
            Component.onCompleted: {
                console.log(masterController)
                console.log(masterController.ui_commandController)
                console.log(masterController.ui_commandController.ui_createClientViewContextCommands)
            }
        }
    

    That will tell you if those are valid objects. Where are those defined?



  • For now, as its only button for testing purposes i want to keep it as simple as possible, so it's defined inside constructor for CommandController (also mentioned in original post):

    Implementation(CommandController* _commandController) :
            commandController(_commandController)
        {
    
            Command* createClientSaveCommand = new Command(commandController,
                                                           QChar( 0xf0c7 ),
                                                           "Save");
    
            QObject::connect(createClientSaveCommand,
                             &Command::executed,
                             commandController,
                             &CommandController::onCreateClientSaveExecuted);
    
            createClientViewContextCommands.append(createClientSaveCommand);
    
        }
    

    createClientViewContextCommands is defined as:

    QList<Command*> createClientViewContextCommands{};
    

    which is used inside getter I have mentioned before:

    QQmlListProperty<Command> CommandController::ui_createPatientViewContextCommands(){
        return QQmlListProperty<Command>(this, implementation->createClientViewContextCommands);
    }
    


  • @fcarney following your suggestion, after placing console logs i got the following output:

    qml: cms::controllers::MasterController(0x70fe18)
    qml: cms::controllers::CommandController(0x8e00d0)
    qml: undefined
    

    I see what's the problem, but could u provide any tips where should I look for the reason of this?
    I can't find any errors in my code.



  • @bwylegly said in Exposing C++ Object list to Qml - objects not displayed when modeling list using Repeater:

    ui_createClientViewContextCommands

    Should this be ui_createPatientViewContextCommands? I don't see ui_createClientViewContextCommands in any of the code you posted. That is where I had trouble. I could not see where those 3 items were defined in the code.



  • I simply cannot belive that, 2 days taken out of life because of that!
    Now it works as intended. Thank you every much @fcarney !

    I will leave this post here in case somebody looks for similar approach to implement buttons.



  • Don't feel bad. I spent 3 weeks working on a solution that I eventually had to scrap and do a different way. I was really mad about it, but it was a learning experience. So in this case just check assumptions. I always have to remind myself of this.