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

QT QML/C++ Hybrid Application Best Practices



  • Hello,

    I'm new to the QT world, and I'm attempting to learn the QT framework through a basic chat client. However, I'm having trouble finding a good method to structure my code.

    My original thought was to have every controller instantiated via MainController. To do this, I needed to call engine.load() early, so that the root context was not nullptr. If I move engine.load() after I attempt to set the rootContext, then I get a segfault because the parent of of MainController is nullptr. If I want MainController to instantiate all other controllers, then I need it to have the rootContext as the parent, so I can call findChild, which is demonstrated in the bottom-most function.

    I'm now running into the issue where I want to add a custom model with. I've followed the examples, but I receive the error ReferenceError: cppChatModel is not defined. This has lead to me posts saying that the context needs to be set before calling engine.load(). However, this brings me to the problem described in the previous paragraph.

    I think my issue is that I don't know the best practice with structuring code for QT.

    1. Is it good practice to have a single entry point for the controllers? I like the idea of the main controller instantiating other controllers, but this might be a bad assumption.

    2. Should the first objects created have the rootContext as the parent? This is practice is demonstrated in the example below, and stems from my assumption in 1).

    3. What is the best approach for this example?
      Most examples I've found were single use where these types of questions weren't encountered.

    I'm using QT 6 for Desktop.

    Here is my entry point: client_main.cpp

    #include <QGuiApplication>
    #include <QQmlApplicationEngine>
    #include <QQmlContext>
    
    #include <controllers/main_controller.h>
    #include <controllers/settings_controller.h>
    #include <controllers/messageController.h>
    
    #include <models/activeChatModel.h>
    
    int main(int argc, char* argv[])
    {
        QGuiApplication app(argc, argv);
    
        // register the custom C++ types
        qmlRegisterType<MainController>("MainController", 1, 0, "MainController");
        qmlRegisterType<SettingsController>("SettingsController", 1, 0, "SettingsController");
        qmlRegisterType<MessageController>("MessageController", 1, 0, "MessageController");
        qmlRegisterType<Message>("Message", 1, 0, "Message");
        qmlRegisterType<ActiveChatModel>("ActiveChatModel", 1, 0, "ActiveChatModel");
    
    
        // create the engine
        QQmlApplicationEngine engine;
        const QUrl url(QStringLiteral("qrc:/views/MainView.qml"));
        engine.load(url);
    
        // grab the root object so that we can use it as the parent for the main controller
        auto rootObject = engine.rootObjects().first();
    
        // create the main controller
        MainController mainController(rootObject);
        engine.rootContext()->setContextProperty("mainController", &mainController);
    
    
        ActiveChatModel chatModel(rootObject);
        engine.rootContext()->setContextProperty("cppChatModel", &chatModel);
    
        // I don't know what this does yet.
        QObject::connect(
          &engine, &QQmlApplicationEngine::objectCreated, &app, [url](QObject* obj, const QUrl& objUrl) {
              if (!obj && url == objUrl)
                  QCoreApplication::exit(-1);
          },
          Qt::QueuedConnection);
    
        return app.exec();
    }
    

    MainView.qml

    ListView {
                    id: messageListView
                    Layout.fillHeight: true
                    width: parent.width
                    anchors.fill: parent
                    anchors.margins: 20
                    spacing: 20
                    clip: true
                
                    model: cppChatModel // says it is undefined
                    delegate: messageBox
                }
    

    MainController ()

    MainController::MainController(QObject* parent)
      : QObject(parent),
        settingsController(new SettingsController(this)),
        messageController(new MessageController(this))
    {
        QObject* settingsView = parent->findChild<QObject*>("settingsView");
        QObject* sendButton = parent->findChild<QObject*>("sendButton");
    
        if (sendButton == nullptr) {
            qDebug() << "messageModel is sadly nullptr";
            return;
        }
    
        QObject::connect(settingsView, SIGNAL(saveSettings()),              settingsController, SLOT(onSaveButton()));
        QObject::connect(sendButton,   SIGNAL(addMessageToModel(QString)),  this,               SLOT(onAddMessageToModel(QString)));
        QObject::connect(sendButton, SIGNAL(addMessageToModel(QString)), messageController, SLOT(onMessageSend(QString)));
    }
    

  • Moderators

    @druepy said in QT QML/C++ Hybrid Application Best Practices:

    qmlRegisterType<MainController>("MainController", 1, 0, "MainController");

    You attach your controller as mainController already - this means you don't need to register it with QML like this. Remove those lines! Otherwise you may instantiate multiple main controllers and get into many weird bugs.

    // grab the root object so that we can use it as the parent for the main controller
    auto rootObject = engine.rootObjects().first();

    // create the main controller
    MainController mainController(rootObject);
    

    This is completely unnecessary. Your MainController does not need any parent - it is a variable created on stack, it will be deleted automatically when your app finishes.

    model: cppChatModel // says it is undefined

    This should fix it:

     model: cppChatModel === undefined ? 0 : cppChatModel
    

    // I don't know what this does yet.

    It exits the application when QML engine reports an error during loading of main QML file. Without this, app would continue running but only show empty window (if there was an error in your QML code).

    QObject::connect(settingsView, SIGNAL(saveSettings()), settingsController, SLOT(onSaveButton()));
    QObject::connect(sendButton, SIGNAL(addMessageToModel(QString)), this, SLOT(onAddMessageToModel(QString)));
    QObject::connect(sendButton, SIGNAL(addMessageToModel(QString)), messageController, SLOT(onMessageSend(QString)));

    Use new (functor-based) connect syntax: https://doc.qt.io/qt-5/signalsandslots-syntaxes.html



  • @sierdzio Within MainController::MainController(), I find the child objects and then connect the signals/slots. If I don't pass in the engine.rootObject() as the parent, how do I still find the child objects?

    Or, does this new syntax you linked to fix this problem?

    Thank you for the help,
    Drue


  • Moderators

    @druepy said in QT QML/C++ Hybrid Application Best Practices:

    @sierdzio Within MainController::MainController(), I find the child objects and then connect the signals/slots. If I don't pass in the engine.rootObject() as the parent, how do I still find the child objects?

    Why do you need to find them at all? Are they objects instantiated in QML? Then add a Q_PROPERTY for them in main controller, and in main QML file set this property to point to those objects.

    If they are instantiated in C++ - well then just instantiate them in main controller and expose them to QML (again - via Q_PROPERTY).

    Or, does this new syntax you linked to fix this problem?

    No.


  • Moderators

    @druepy said in QT QML/C++ Hybrid Application Best Practices:

    Within MainController::MainController(), I find the child objects and then connect the signals/slots. If I don't pass in the engine.rootObject() as the parent, how do I still find the child objects?

    where did you get the idea that the connects, between c++ and QML, have to be made on the c++ side ? My guess is from here :D

    public slots, or Q_INVOKABLE marked public functions can be called as a normal function call on QML side,

    for c++ signals, simply use :
    https://doc.qt.io/qt-5/qml-qtqml-connections.html

    with your c++ instance pointer (e.g cppChatModel) as target



  • I did not realize that. Thank you.

    When should you choose one method over the other?



  • Regarding the general topic of how to structure code for a hybrid QML/C++ application...

    The team I belong to has been extremely happy with an approach we usually refer to as an instance of the well-known MVVM pattern, but which I also argue is the same approach described in 2002 as "The Humble Dialog Box".

    The main points we follow are:

    • keep the QML "dumb" and declarative. avoid logic (loops, if/else) in the QML.
    • anything in the QML that should change (change text, change color, etc) will change based on a Q_PROPERTY controlled in C++
    • anything the application needs to "run" or "do" in response to clicks on the QML will be done in a Q_INVOKABLE (such that the logic is in a C++ function, but QML can call that function)

    In other words, we treat the QML more like HTML than like Javascript. We use QML for layout, not logic.

    Anything QML needs to read comes from a Q_PROPERTY.

    But we avoid having QML write to a Q_PROPERTY. Instead, QML can call an Q_INVOKABLE and that function can update any properties as needed. By treating the exposed properties as read-only from the QML side, this helps avoid binding loops. (If you haven't seen a binding loop error yet... just wait... you will.)

    By keeping the QML "dumb" (or "humble" in the terminology from 2002), all the business logic can be unit tested by writing C++ unit tests.

    You probably already suspected this level of testability and separation was possible, which might be what drew you to C++/QML. Kudos and welcome!

    In the 2002 article, the "ChainComposer" contains the business logic. I would write a ChainComposer in C++. The view (which for us nowadays is a QML view) can then call methods (Q_INVOKABLE methods) on this object. The ChainComposer has access to a "view" and calls setters on the view. In a C++/QML situation, I argue that a ChainComposer altering a Q_PROPERTY (and emitting a signal to tell QML this property changed) is analogous to the 2002 article advocating for the ChainComposer to call setters on the view.

    This is all a bit academic, but old-school fans of Michael Feathers (author of Humble Dialog Box paper) will find a lot to love.

    You can find working examples of this read-a-Q_PROPERTY/call-a-Q_INVOKABLE "humble QML" approach in these repositories:

    Having said all that, though, I still often struggle with binding to a model from QML. I find it much easier to bind to a plain boolean or string Q_PROPERTY than to bind to a full fledged model object.

    For learning patterns relating to model types in QML, you might want to peruse the Qt examples:

    (I realize my links are for Qt5 and you want Qt6, but I don't think these parts have changed that much.)



  • @J-Hilk That's exactly where I got that idea from. Thank you for the clairty.



  • @KH-219Design

    Do you consider formatting logic safe?
    I understand business logic being handled in C++. Is logic within the QML only for conditional formatting okay?

    And thanks for all the information.



  • @druepy Good question about logic for formatting.

    If you are thinking of things like printf and how many decimal places to show in a floating point number and things like that, I personally tend to still do all that in C++.

    But I am a pragmatist, not a fanatic nor a purist. There are definitely a handful of QML/Javascript functions in most of my projects. (one example) (a second example)

    The mantra of "no logic in the QML" is a guiding philosophy, but not a hard line in the sand.

    When I do put logic in the QML, I tend to ask myself:

    • what is my comfort level with having ZERO automated test coverage of this logic?
    • what is the worst outcome for the UI (and therefore for the user) if this logic fails unexpectedly?

    If the logic is complex enough to risk putting the UI into an utterly unusable state, then I will feel nervous enough to move the logic into a tested C++ unit. But if the only risk of the QML logic is that something might get rendered in the wrong color, or a possibly sub-optimal font size, then I would not worry.


  • Moderators

    @KH-219Design these are splendid hints, and very well described. Many thanks!


Log in to reply