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:
- a
QString
exposed to QML as textToCheck
- a
bool
property exposed as valid
to QML
When 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.