Search code examples
c++validationqmlqt5qtquick2

Specialized QValidator and QML UI changes


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 :

  • dirt in onValidatorChanged: to be sure to initialize the UI with the correct colour
  • byname tree searching to find the caller (looks unefficient ; may not be)
  • spaghetti-like coding among several instances of C++ and parts of QML

I can think of 5 other solutions :

  • getting rid of the custom validator, and keeping only onTextChanged: because we cannot get rid of signaling from QML side. Most things are done in MyClass
  • patching Qt in order to implement a property value write interceptor for something else than Behavior (see here)
  • registering a C++ type to attach to the QML Object. (see here)
  • registering a type and using it as both a controller and a data structure (bean-like), to pass onto the model afterwards (see here)
  • using signals manually as I already do with signalDirectoryChanged

Well, as you see, the plethoric ways of doing things is confusing, so senpai advice is appreciated.

Complete source code available here.


Solution

  • 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 SLOTs 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, SLOTs and SIGNALs should enable the most of the use cases.