Porting from Widgets to QML: suggestions needed on how objects interact
-
I'm evaluating porting a Linux desktop application from QWidgets to QML (Qt 5).
I cannot post here any code, hope next few lines describe the scenario with a sufficient level of detail. I may eventually prepare a minimal project as example, if needed.A single state machine is present that:
- Has no knowledge of the UI.
- Communicates with the context by generating/ receiving events.
The basic structure of the UI is based on a grid layout, made by 3 rows:
- The top and bottom rows, whose widgets are always visible.
- The center row, made by a single QStackedWidget, that shows the correct widget ('page') based on the current state of the application.
In this scenario, the UI is composed at runtime:
- All widgets ('pages') are programmatically added to the QStackedWidget at startup.
- A reference to the widgets is kept so that UI can be updated depending on state machine transitions and/or user interaction.
I've already ported applications from QWidgets to QML, but scenario was quite different, as interface was all described in a single .ui file, therefore creating a .qml file that could interact with its C++ counterpart was straightforward (basically, replacing calls to QWidget methods with property setting/getting did the job easily).
That said:
-
Before trying to replicate this design as-is with QML: is there some recommended approach that can be used? Links to any kind of example are more than welcome :)
Most relevant aspect is that I would like to have no business logic in the QML, other that the one strictly necessary to update the UI, as this would let me reuse as much C++ code as possible from current project, especially the state machine. -
In case current design can be ported to QML, I would appreciate some guidance on who would be responsible for objects creation:
a. Each C++ 'page' loads its own QML: how can I programmatically compose and update the UI? (that is, adding items to the stack from outside the QML).
b. Each QML 'page' instantiates its C++ counterpart: how can the underlying C++ instances interact one with the other?
Any help is greatly appreciated.
-
One thing that is not clear to me is how dynamic the "pages" are. For example, is there a finite set of possible pages known up front? Is the actual content of each page pre-determined, or is it driven by data? (By "content" I mean the set of widgets, etc., that is shown on each page.)
At a high level, I would tend to reach for
StackLayoutfor the basic stack management. The details of how you add and manage the child objects will really depend on the details of what they are. QML supports various types of "model-view" structure, where it is relatively easy to expose the model from C++. It sounds like that is the sort of thing you need. -
Let's see if I can make things more clear :)
- The set of possible pages is known in advance: they are all created at application startup and added to the QStackedWidget as its children.
- The structure of each page is fixed: there are no widgets that are added/removed on the fly at runtime depending on some external trigger.
- The actual data that is displayed by the widgets within each page can change (e.g., there are tables, images, labels, or other views that can be updated).
Depending on the events coming from the state machine, the underlying models that are driving the views pick from different data sets, therefore the content visible to the user is updated.
Some exampes:
- one of the pages is responsible for the selection of an entry from a table: the state machine just requests one entry of a specific type X, the UI triggers a model reload based on X and waits for the user to select the entry, than notifies to the state machine that entry has been selected.
- one page displays some message to the user asking for confirmation in order to proceed.
- one page keeps the user updated on how the interaction with some external system is progressing.
I'm glad you pointed to StackLayout, as it seems also to me to be more or less the QML equivalent of QStackedWidget, so maybe I can avoid a heavy refactoring.
Currently, I am trying to implement point a in my original question:
- Keep each page in a separated .qml file (as equivalent of the .ui)
- Make the current C++ 'page' classes, that are derived from QWidget, the interfaces of these QMLs.
- Create the QML items as QQmlComponents in the constructors of these interfaces (the old UI::<class> pointers are replaced with QQmlComponents ones).
- Find a way to attach the C++ models to these components.
- Find a way to add these components to the StackLayout in the main QML.
This seems to me to be a quite conservative approach, as in fact I am trying to replace QWidgets with QML at the end. Last two point are the ones where some guidance would probably avoid me doing silly errors. Maybe, and that's why I opened this thread, there was a simpler / more recommended way to proceed.
-
Let's see if I can make things more clear :)
- The set of possible pages is known in advance: they are all created at application startup and added to the QStackedWidget as its children.
- The structure of each page is fixed: there are no widgets that are added/removed on the fly at runtime depending on some external trigger.
- The actual data that is displayed by the widgets within each page can change (e.g., there are tables, images, labels, or other views that can be updated).
Depending on the events coming from the state machine, the underlying models that are driving the views pick from different data sets, therefore the content visible to the user is updated.
Some exampes:
- one of the pages is responsible for the selection of an entry from a table: the state machine just requests one entry of a specific type X, the UI triggers a model reload based on X and waits for the user to select the entry, than notifies to the state machine that entry has been selected.
- one page displays some message to the user asking for confirmation in order to proceed.
- one page keeps the user updated on how the interaction with some external system is progressing.
I'm glad you pointed to StackLayout, as it seems also to me to be more or less the QML equivalent of QStackedWidget, so maybe I can avoid a heavy refactoring.
Currently, I am trying to implement point a in my original question:
- Keep each page in a separated .qml file (as equivalent of the .ui)
- Make the current C++ 'page' classes, that are derived from QWidget, the interfaces of these QMLs.
- Create the QML items as QQmlComponents in the constructors of these interfaces (the old UI::<class> pointers are replaced with QQmlComponents ones).
- Find a way to attach the C++ models to these components.
- Find a way to add these components to the StackLayout in the main QML.
This seems to me to be a quite conservative approach, as in fact I am trying to replace QWidgets with QML at the end. Last two point are the ones where some guidance would probably avoid me doing silly errors. Maybe, and that's why I opened this thread, there was a simpler / more recommended way to proceed.
@meslor Thanks for the clarification.
As far as your plan is concerned, I wonder if you are creating more complication than is necessary. You first point is reasonable - create the pages as separate QML components, each in its own file. However I don't understand the motivation for creating them as
QQmlComponentsin the C++ layer.Why not simply add them to the
StackLayoutin QML? So for example if you havePage1.qml,Page2.qml, ..., you might have a "PageStack" component that has this sort of structure:StackLayout { currentIndex: ??? Page1 { ... } Page2 { ... } ... }Note that
currentIndexis what determines which item is currently shown, counting the page items in definition order. You will probably want to tie this into your C++ layer. Because I am used to it, my approach here would probably be to expose a C++ class as a "context property" to QML and this would expose a "current page" property that would be tied to thecurrentIndex. However, rather than use a "context property" (seeQQmlContext::setContextProperty) , I believe the modern advice to achieve the same goal is instead to useqmlRegisterSingletonTypeand expose the C++ functionality as a singleton class.I suspect the questions then will be about how to interface the pages' content to C++. As mentioned previously, a lot will depend on the details of what is in the pages. For example, if you already have instances of the various types of Qt model classes (
QAbstractItemModeland sub-classes) in the C++ layer, these are fairly easy to expose to QML. Otherwise the mentionedsetContextProperty/qmlRegisterSingletonTypeapproach is available. -
@meslor Thanks for the clarification.
As far as your plan is concerned, I wonder if you are creating more complication than is necessary. You first point is reasonable - create the pages as separate QML components, each in its own file. However I don't understand the motivation for creating them as
QQmlComponentsin the C++ layer.Why not simply add them to the
StackLayoutin QML? So for example if you havePage1.qml,Page2.qml, ..., you might have a "PageStack" component that has this sort of structure:StackLayout { currentIndex: ??? Page1 { ... } Page2 { ... } ... }Note that
currentIndexis what determines which item is currently shown, counting the page items in definition order. You will probably want to tie this into your C++ layer. Because I am used to it, my approach here would probably be to expose a C++ class as a "context property" to QML and this would expose a "current page" property that would be tied to thecurrentIndex. However, rather than use a "context property" (seeQQmlContext::setContextProperty) , I believe the modern advice to achieve the same goal is instead to useqmlRegisterSingletonTypeand expose the C++ functionality as a singleton class.I suspect the questions then will be about how to interface the pages' content to C++. As mentioned previously, a lot will depend on the details of what is in the pages. For example, if you already have instances of the various types of Qt model classes (
QAbstractItemModeland sub-classes) in the C++ layer, these are fairly easy to expose to QML. Otherwise the mentionedsetContextProperty/qmlRegisterSingletonTypeapproach is available.@Bob64 Thanks for the detailed feedback.
I am proceeding as per your suggestion:
- All QML pages are loaded at startup as part of a single 'MainWindow.qml' file.
- The MainWindow C++ class that previously implemented the interface to the MainWindow Widget now exposes invokables/slots/properties accessible from its .qml (will refactor this as next step, to date is fine).
For the moment, old C++ classes that previously implemented the Widget pages are exposed by the MainWindow class via Q_INVOKABLE methods so that the QML can directly access signals and slots as if it was an UI file.
By calling qRegisterMetatype("xxxPage*") I can directly invoke from QML their slots / emit their signals as if the 'old' user interface was present.To date, porting seems to be feasible, btw I encountered an issue I was not expecting.
All my C++ 'Page' classes are derived from a common AbsPage class that provides signals and slots.
It seems that I am not able to call the slots/signals that are exposed in the derived classes:- The AbsPage class relies on NVI pattern so that derived classes implement their own methods w/o overriding the slots.
- In the QML, the slots declared in the AbsPage class are not accessible ("TypeError: Property 'init' of object xxxPage_QMLTYPE_NN is not a function".
I've investigated a bit, but did not find any specific help on this, as all examples always present slots that are direct members of the C++ class that is accessed from QML.
Do you have any idea if this is achievable or not?
It seems strange to me that QML does not access slots that moc has already setup as members of base classes. -
@Bob64 Thanks for the detailed feedback.
I am proceeding as per your suggestion:
- All QML pages are loaded at startup as part of a single 'MainWindow.qml' file.
- The MainWindow C++ class that previously implemented the interface to the MainWindow Widget now exposes invokables/slots/properties accessible from its .qml (will refactor this as next step, to date is fine).
For the moment, old C++ classes that previously implemented the Widget pages are exposed by the MainWindow class via Q_INVOKABLE methods so that the QML can directly access signals and slots as if it was an UI file.
By calling qRegisterMetatype("xxxPage*") I can directly invoke from QML their slots / emit their signals as if the 'old' user interface was present.To date, porting seems to be feasible, btw I encountered an issue I was not expecting.
All my C++ 'Page' classes are derived from a common AbsPage class that provides signals and slots.
It seems that I am not able to call the slots/signals that are exposed in the derived classes:- The AbsPage class relies on NVI pattern so that derived classes implement their own methods w/o overriding the slots.
- In the QML, the slots declared in the AbsPage class are not accessible ("TypeError: Property 'init' of object xxxPage_QMLTYPE_NN is not a function".
I've investigated a bit, but did not find any specific help on this, as all examples always present slots that are direct members of the C++ class that is accessed from QML.
Do you have any idea if this is achievable or not?
It seems strange to me that QML does not access slots that moc has already setup as members of base classes.@meslor I wasn't sure about your slots inheritance question as I don't think it's something I have tried to do. However, I just tried it with a toy project and it seems to work for me.
Below is my code, based on a QtCreator-generated empty Qt Quick app. Note that I tried using two methods of exposing C++ to QML - a context property and using
qmlRegisterTypeand both seem to work . You mentionedqRegisterMetatype- I don't know if this was just a slip when you were writing it or whether it is what you really meant. If that is what you are using, I have to be honest and say that I have never made any use of that facility myself and am not sure how it relates to QML exposure, so I can't offer any advice.Note that my example doesn't do anything exciting - it just writes to debug output, so you need to look at application output to see it do anything.
base.h
#pragma once #include <QObject> class Base : public QObject { Q_OBJECT public: explicit Base(QObject *parent = nullptr); public slots: void doIt(); private: virtual void doItImpl() = 0; };base.cpp
#include "base.h" Base::Base(QObject *parent) : QObject(parent) {} void Base::doIt() { doItImpl(); }derived.h
#pragma once #include "base.h" class Derived : public Base { Q_OBJECT public: Derived(); private: void doItImpl(); };derived.cpp
#include "derived.h" #include <QtDebug> Derived::Derived() : Base(nullptr) {} void Derived::doItImpl() { qDebug() << "Here!"; }main.cpp
... QQmlApplicationEngine engine; // Note: two ways of exposing to QML tried: // 1. Context property Derived d; engine.rootContext()->setContextProperty("dctxt", &d); // 2. Registered type so instantiable from QML qmlRegisterType<Derived>("MyTypes", 1, 0, "Derived"); ...main.qml
import QtQuick 2.9 import QtQuick.Controls 2.2 import QtQuick.Window 2.2 import MyTypes 1.0 Window { id: root visible: true width: 640 height: 480 title: qsTr("Hello World") Button { text: "Hello" anchors.centerIn: parent onClicked: { dctxt.doIt(); // invoke on context property exposed object d.doIt(); // invoke on QML object instantiated below } } Derived { id: d } } -
Many thanks for the valuable feedback.
After testing some alternatives, I evaluated that exposing the C++ API to the QML (as per your suggestion) would quickly make the code less maintainable for complex projects. There are a lot of nested classes & widgets, and this approach requires to propagate these slots so that they are accessible from the context.
As far as I understand so far, the preferred path when QML and C++ are involved is that the the QML modules are the 'main controllers' and the C++ stuff is used 'as needed', there is why for example the singleton makes sense somehow.
The example you provided, where the Derived class is instantiated in the QML, make this very clear.What I am trying to achieve is the exact opposite: having C++ code that uses the QML.
The cleanest way I've found to keep the C++ classes as the 'main controllers' is to totally decouple the C++ from the QML:- One single QML file instantiates all the UI (as per your suggestion with the stack).
- All nested QML objects that need to be accessed by C++ are named, therefore the C++ can search for children by name.
- All QMLs exposes signals / properties related to the main UI.
In this way, each C++ class can still be directly managing the UI, as usually done when widgets are involved.
Instead of having a setupUi() call, some other class method searches for the QML objects by name and connects to signals.
This is where establishing connections from QML signals to C++ derived classes seems not to work.After some extensive test, my opinion is that the effort to manage the UI in this way is not convenient.
For mobile a simple approach with QML seems good and very fast, but for complex Desktop applications simple widgets still seem the right choice to me. This is indeed due also to my lack of experience with QML, for sure.Of course, any feedback is greatly appreciated on this :)
-
Starting from scratch using QML for complex desktop applications is perfectly fine.
The recommended way of exposing C++ to QML and not manipulating QML from C++ makes perfect sense and is not a drawback, quite the opposite.The QML code doesn't have to be the "main controller", it's just the first point of entry to it. You don't have to instantiate your business classes in QML like the
Derivedshown above. -
Starting from scratch using QML for complex desktop applications is perfectly fine.
The recommended way of exposing C++ to QML and not manipulating QML from C++ makes perfect sense and is not a drawback, quite the opposite.The QML code doesn't have to be the "main controller", it's just the first point of entry to it. You don't have to instantiate your business classes in QML like the
Derivedshown above.@GrecKo I am sure this is the case, I was not very clear as I was referring to porting already complex applications from QWidgets to QML.
Starting from scratch, I expect that thing are easier, also considering my limited knowledge on QML.
Is there any chance you are aware of some open source project that made this step, so that I can take a look at how this has been addressed? -
I don't know if it's worth me saying that my example was never intended to propose any particular architectural approach. It was purely about demonstrating that the slot mechanism works in derived classes.