I'm learning Qt 5.5 and QML.
The framework is powerful, and there are sometimes many ways to do one thing. I think that some are probably more efficient than others, and I'd like to understand when and why using one rather than another.
I'd like an answer that can explain the choices made. As I'm working with new code, C++ 11 and C++ 14 syntax can be used if useful on the C++ side.
To problem to solve is :
I've got a TextField
linked to a button that can pop a FileDialog
. I want the text in the TextField
to be red
when it is invalid, and left unchanged otherwise (I set it to green
because I don't know how to get the "default" colour). The value of the TextField
is to be used in the C++ side, and is persisted when application exits.
I've coded a version using a custom QValidator
, some properties on the QML side, an using onTextChanged:
and onValidatorChanged:
to modify the colour of the text. The text colour is set according to a property (valid
) in the QML that is set from the C++ side (in the validator). To set the property, the C++ has to find by name the caller (TextField
named directoryToSave
) because I've not yet found a way to pass the object itself as argument.
Here is the QML code contained in MainForm.ui.qml
:
TextField {
property bool valid: false
id: directoryToSave
objectName: 'directoryToSave'
Layout.fillWidth:true
placeholderText: qsTr("Enter a directory path to save to the peer")
validator: directoryToSaveValidator
onTextChanged: if (valid) textColor = 'green'; else textColor = 'red';
onValidatorChanged:
{
directoryToSave.validator.attachedObject = directoryToSave.objectName;
// forces validation
var oldText = text;
text = text+' ';
text = oldText;
}
}
The custom validator code :
class QDirectoryValidator : public QValidator
{
Q_OBJECT
Q_PROPERTY(QVariant attachedObject READ attachedObject WRITE setAttachedObject NOTIFY attachedObjectChanged)
private:
QVariant m_attachedObject;
public:
explicit QDirectoryValidator(QObject* parent = 0);
virtual State validate(QString& input, int& pos) const;
QVariant attachedObject() const;
void setAttachedObject(const QVariant &attachedObject);
signals:
void attachedObjectChanged();
};
Associated with these definitions :
QVariant QDirectoryValidator::attachedObject() const
{
return m_attachedObject;
}
void QDirectoryValidator::setAttachedObject(const QVariant &attachedObject)
{
if (attachedObject != m_attachedObject)
{
m_attachedObject = attachedObject;
emit attachedObjectChanged();
}
}
QValidator::State QDirectoryValidator::validate(QString& input, int& pos) const
{
QString attachedObjectName = m_attachedObject.toString();
QObject *rootObject = ((LAACApplication *) qApp)->engine().rootObjects().first();
QObject *qmlObject = rootObject ? rootObject->findChild<QObject*>(attachedObjectName) : 0;
// Either the directory exists, then it is _valid_
// or the directory does not exist (maybe the string is an invalid directory name, or whatever), and then it is _invalid_
QDir dir(input);
bool isAcceptable = (dir.exists());
if (qmlObject) qmlObject->setProperty("valid", isAcceptable);
return isAcceptable ? Acceptable : Intermediate;
}
m_attachedObject
is a QVariant
because I wanted the QML instance to be referenced instead of its name, initially.
As the validator is only concerned about validation it does not contain any state about the data it validates.
As I must get the value of the TextField
in order to do something in the application, I've built another class to save the value when it changes : MyClass
. I see it as my controller. Currently, I store data directly in the application object, that can be seen as my model. That will change in the future.
class MyClass : public QObject
{
Q_OBJECT
public:
MyClass() {}
public slots:
void cppSlot(const QString &string) {
((LAACApplication *) qApp)->setLocalDataDirectory(string);
}
};
The instances of controller MyClass
and validator QDirectoryValidator
are created during application initialization with the following code :
MyClass * myClass = new MyClass;
QObject::connect(rootObject, SIGNAL(signalDirectoryChanged(QString)),
myClass, SLOT(cppSlot(QString)));
//delete myClass;
QValidator* validator = new QDirectoryValidator();
QVariant variant;
variant.setValue(validator);
rootObject->setProperty("directoryToSaveValidator", variant);
The //delete
only serves the purpose to discover what happens when the instance is deleted or not.
The main.qml
ties things together :
ApplicationWindow {
id: thisIsTheMainWindow
objectName: "thisIsTheMainWindow"
// ...
property alias directoryToSaveText: mainForm.directoryToSaveText
property var directoryToSaveValidator: null
signal signalDirectoryChanged(string msg)
// ...
FileDialog {
id: fileDialog
title: "Please choose a directory"
folder: shortcuts.home
selectFolder: true
onAccepted: {
var url = fileDialog.fileUrls[0]
mainForm.directoryToSaveText = url.slice(8)
}
onRejected: {
//console.log("Canceled")
}
Component.onCompleted: visible = false
}
onDirectoryToSaveTextChanged: thisIsTheMainWindow.signalDirectoryChanged(directoryToSaveText)
}
And, at last, the MainForm.ui.qml glue :
Item {
// ...
property alias directoryToSavePlaceholderText: directoryToSave.placeholderText
property alias directoryToSaveText: directoryToSave.text
// ...
}
I'm not satisfied having :
onValidatorChanged:
to be sure to initialize the UI with the correct colourI can think of 5 other solutions :
onTextChanged:
because we cannot get rid of signaling from QML side. Most things are done in MyClass
Behavior
(see here)signalDirectoryChanged
Well, as you see, the plethoric ways of doing things is confusing, so senpai advice is appreciated.
Complete source code available here.
I don't think a single answer can address all your questions but I still think that some guidelines about application structure can help getting you going.
AFAIK there's no central place that discuss application structure. Actually, there's also no recommendation about UI structure in QML (see this discussion for instance). That said, we can identify some common patterns and choices in a QML application which we are going to discuss further below.
Before getting there I would like to stress an important aspect. QML is not so distant from C++. QML is basically an object tree of QObject
-derived objects whose lifetime is controlled by a QMLEngine
instance. In this sense a piece of code like
TextField {
id: directoryToSave
placeholderText: qsTr("placeHolder")
validator: IntValidator { }
}
it's not that different from a QLineEdit
with a Validator
written in plain imperative C++ syntax. Apart from the lifetime, as said. Given that, implementing your validator in plain C++ is wrong: the validator is part of the TextField
and should have a lifetime consistent with it. In this specific case registering a new type is the best way to go. The resulting code is easier to read and easier to maintain.
Now, this case is particular. The validator
property accepts objects that derives from Validator
(see declaration here and some usages here, here and here). Hence, instead of simply define a Object
-derived type, we can define a QValidator
-derived type and use it in place of IntValidator
(or the other QML validation types).
Our DirectoryValidator
header file looks like this:
#ifndef DIRECTORYVALIDATOR_H
#define DIRECTORYVALIDATOR_H
#include <QValidator>
#include <QDir>
class DirectoryValidator : public QValidator
{
Q_OBJECT
public:
DirectoryValidator(QObject * parent = 0);
void fixup(QString & input) const override;
QLocale locale() const;
void setLocale(const QLocale & locale);
State validate(QString & input, int & pos) const override;
};
#endif
The implementation file is like this:
#include "directoryvalidator.h"
DirectoryValidator::DirectoryValidator(QObject *parent): QValidator(parent)
{
// NOTHING
}
void DirectoryValidator::fixup(QString & input) const
{
// try to fix the string??
QValidator::fixup(input);
}
QLocale DirectoryValidator::locale() const
{
return QValidator::locale();
}
void DirectoryValidator::setLocale(const QLocale & locale)
{
QValidator::setLocale(locale);
}
QValidator::State DirectoryValidator::validate(QString & input, int & pos) const
{
Q_UNUSED(pos) // change cursor position if you like...
if(QDir(input).exists())
return Acceptable;
return Intermediate;
}
Now you can register the new type in your main
like this:
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
qmlRegisterType<DirectoryValidator>("DirValidator", 1, 0, "DirValidator");
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
and your QML code could be rewritten like this:
import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.1
import DirValidator 1.0 // import the new type
Window {
id: main
visible: true
width: 600
height: 600
DirValidator { // external declaration
id: dirVal
}
Column {
anchors.fill: parent
TextField {
id: first
validator: dirVal
textColor: acceptableInput ? "black" : "red"
}
TextField {
validator: DirValidator { } // declaration inline
textColor: acceptableInput ? "black" : "red"
}
TextField {
validator: DirValidator { } // declaration inline
textColor: acceptableInput ? "black" : "red"
}
}
}
As you can see the usage become more straightforward. The C++ code is cleaner but also the QML code is cleaner. You don't need to pass data around yourself. Here we use the very same acceptableInput
of TextField
since it is set by the Validator
associated with it.
The same effect could have been obtained by registering another type which is not derived from Validator
- losing the association with acceptableInput
. Look at the following code:
import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import ValidationType 1.0
Window {
id: main
visible: true
width: 600
height: 600
ValidationType {
id: validator
textToCheck: first.text
}
TextField {
id: first
validator: dirVal
textColor: validator.valid ? "black" : "red" // "valid" used in place of "acceptableInput"
}
}
Here ValidationType
could be defined with two Q_PROPERTY
elements:
QString
exposed to QML as textToCheck
bool
property exposed as valid
to QMLWhen bound to first.text
the property is set and reset when the TextField
text changes. On change you can check the text, for instance with your very same code, and update valid
property. See this
answer or the registration link above for details about Q_PROPERTY
updates. I leave the implementation of this approach to you, as exercise.
Finally, when it comes to services-like/global objects/types, using non instanciable / singleton types could be the right approach. I would let the documentation talk for me in this case:
A QObject singleton type can be interacted with in a manner similar to any other QObject or instantiated type, except that only one (engine constructed and owned) instance will exist, and it must be referenced by type name rather than id. Q_PROPERTYs of QObject singleton types may be bound to, and Q_INVOKABLE functions of QObject module APIs may be used in signal handler expressions. This makes singleton types an ideal way to implement styling or theming, and they can also be used instead of ".pragma library" script imports to store global state or to provide global functionality.
qmlRegisterSingletonType
is the function to prefer. It's the approach used also in the "Quick Forecast" app i.e. the Digia showcase app. See the main
and the related ApplicationInfo
type.
Also context properties are particularly useful. Since they are added to the root context (see the link) they are available in all the QML files and can be used also as global objects. Classes to access DBs, classes to access web services or similar are eligible to be added as context properties. Another useful case is related to models: a C++ model, like an AbstractListModel
can be registered as a context property and used as the model of a view, e.g. a ListView
. See the example available here.
The Connections
type can be used to connect signals emitted by both context properties and register types (obviously also the singleton one). Whereas signals, Q_INVOKABLE
functions and SLOT
s can be directly called from QML to trigger other C++ slots like partially discussed here.
Summing up, using objectName
and accessing QML from C++ is possible and feasible but is usually discouraged (see the warning here). Also, when necessary and possible, QML/C++ integration is favoured via dedicated properties, see for instance the mediaObject
property of QML Camera
type. Using (singleton) registered types, context properties and connecting them to QML via the Connections
type, Q_INVOKABLE
, SLOT
s and SIGNAL
s should enable the most of the use cases.