专门的 QValidator 和 QML UI 更改

Specialized QValidator and QML UI changes

我正在学习 Qt 5.5 和 QML。

框架很强大,有时候做一件事有很多种方法。我认为有些可能比其他的更有效,我想了解何时以及为什么使用一个而不是另一个
我想要一个可以解释所做选择的答案。当我使用新代码时,如果在 C++ 方面有用,可以使用 C++ 11 和 C++ 14 语法。

要解决的问题是:
我有一个 TextField 链接到一个可以弹出 FileDialog 的按钮。我希望 TextField 中的文本在无效时为 red,否则保持不变(我将其设置为 green 因为我不知道如何获取 "default" 颜色)。 TextField 的值将在 C++ 端使用,并在应用程序退出时保留。

我使用自定义 QValidator 编写了一个版本,QML 端的一些属性,使用 onTextChanged:onValidatorChanged: 修改文本的颜色。文本颜色是根据从 C++ 端(在验证器中)设置的 QML 中的 属性 (valid) 设置的。要设置 属性,C++ 必须通过名称查找调用者(TextField 名为 directoryToSave),因为我还没有找到将对象本身作为参数传递的方法。

这里是MainForm.ui.qml中包含的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;
        }
    }

自定义验证码:

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();
};

与这些定义相关联:

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 是一个 QVariant 因为我希望最初引用 QML 实例而不是它的名称。

由于验证器只关心验证,因此它不包含有关它验证的数据的任何状态。
因为我必须获取 TextField 的值才能在应用程序中执行某些操作,所以我构建了另一个 class 以在它更改时保存值:MyClass。我将其视为我的 控制器 。目前,我将数据直接存储在应用程序对象中,可以看作是我的 model。这将在未来改变。

class MyClass : public QObject
{
    Q_OBJECT
public:
    MyClass() {}

public slots:
    void cppSlot(const QString &string) {
       ((LAACApplication *) qApp)->setLocalDataDirectory(string);
    }
};

控制器 MyClass 和验证器 QDirectoryValidator 的实例是在应用程序初始化期间使用以下代码创建的:

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);

//delete 仅用于发现实例被删除时发生的情况。

main.qml 将事物联系在一起:

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)

    }

最后,MainForm.ui.qml 胶水:

Item {

    // ...
    property alias directoryToSavePlaceholderText: directoryToSave.placeholderText
    property alias directoryToSaveText: directoryToSave.text

    // ...
}

我不满意:

我能想到其他 5 个解决方案:

好吧,如您所见,过多的做事方式令人困惑,因此非常感谢前辈的建议。

完整的源代码可用 here

我不认为一个单一的答案可以解决你所有的问题,但我仍然认为一些关于应用程序结构的指南可以帮助你前进。

据我所知,没有集中讨论应用程序结构的地方。实际上,QML 中也没有关于 UI 结构的建议(例如参见 [​​=60=] 讨论)。也就是说,我们可以确定 QML 应用程序中的一些常见模式和选择,我们将在下面进一步讨论。

在开始之前,我想强调一个重要的方面。 QML 与 C++ 相差无几。 QML 基本上是 QObject 派生对象的对象树,其生命周期由 QMLEngine 实例控制。从这个意义上说,一段代码像

TextField {
    id: directoryToSave
    placeholderText: qsTr("placeHolder")
    validator: IntValidator { }
}

它与用普通命令式 C++ 语法编写的 QLineEditValidator 没有什么不同。余生如说。鉴于此,用纯 C++ 实现验证器是错误的:验证器是 TextField 的一部分,并且应该具有与其一致的生命周期。在这种特定情况下,registering a new type 是最好的方法。生成的代码更易于阅读和维护。

这个案例比较特殊。 validator 属性 接受从 Validator 派生的对象(参见声明 here and some usages here, here and here)。因此,不是简单地定义一个 Object 派生类型,我们可以定义一个 QValidator 派生类型并用它代替 IntValidator(或其他 QML 验证类型)。

我们的 DirectoryValidator 头文件如下所示:

#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

实现文件是这样的:

#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;
}

现在您可以像这样在 main 中注册新类型:

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();
}

您的 QML 代码可以这样重写:

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"
        }
    }
}

如您所见,用法变得更加直接。 C++ 代码更简洁,但 QML 代码也更简洁。您不需要在自己周围传递数据。这里我们使用与 TextField 完全相同的 acceptableInput,因为它是由与其关联的 Validator 设置的。

通过注册不是从 Validator 派生的另一种类型可以获得相同的效果 - 失去与 acceptableInput 的关联。看下面的代码:

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"
    }
}

此处 ValidationType 可以用两个 Q_PROPERTY 元素定义:

  • a QString 作为 textToCheck
  • 暴露于 QML
  • a bool 属性 暴露为 valid 到 QML

当绑定到 first.text 时,属性 被设置并在 TextField 文本更改时重置。在更改时,您可以检查文本,例如使用完全相同的代码,然后更新 valid 属性。参见 this 回答或上面的注册link以获取有关Q_PROPERTY更新的详细信息。我把这种方法的实现留给你,作为练习。

最后,当谈到 services-like/global objects/types 时,使用非实例化/单例类型可能是正确的方法。在这种情况下,我会让 documentation 为我说话:

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类型。

还有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.

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.

总而言之,使用 objectName 并从 C++ 访问 QML 是可能且可行的,但通常不鼓励(参见 QML Camera 类型的警告 here). Also, when necessary and possible, QML/C++ integration is favoured via dedicated properties, see for instance the mediaObject 属性。使用(单例)注册类型、上下文属性并通过 Connections 类型、Q_INVOKABLESLOTs 和 SIGNALs 将它们连接到 QML 应该可以启用大多数用例.