专门的QValidator和QML UI更改



我正在学习Qt 5.5和QML。

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

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

我使用自定义QValidator、QML 端的一些属性、使用onTextChanged:onValidatorChanged:来修改文本的颜色编写了一个版本。文本颜色是根据QML中从C++端(在验证器中(设置的属性(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的值才能在应用程序中执行某些操作,因此我构建了另一个类来在更改时保存值:MyClass。我把它看作是我的控制器。目前,我直接将数据存储在应用程序对象中,这可以被视为我的模型。这将在未来改变.

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
    // ...
}

我不满意有:

  • 污垢在onValidatorChanged:,以确保使用正确的颜色初始化UI
  • 通过名称树搜索以查找调用者(看起来效率低下;可能不是(
  • C++的几个实例和部分QML之间的类似意大利面条的编码

我可以想到其他5种解决方案:

  • 摆脱自定义验证器,只保留onTextChanged:,因为我们无法摆脱 QML 方面的信令。大多数事情都是在MyClass完成的
  • 修补Qt以实现属性值写入拦截器,而不是Behavior(见这里(
  • 注册要附加到 QML 对象的C++类型。(看这里(
  • 注册一个类型并将其用作控制器和数据结构(类似 Bean(,以便稍后传递给模型(请参阅此处(
  • 手动使用信号,就像我已经对signalDirectoryChanged所做的那样

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

此处提供完整的源代码。

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

AFAIK 没有讨论应用程序结构的中心位置。实际上,也没有关于QML中UI结构的建议(例如,请参阅此讨论(。也就是说,我们可以在QML应用程序中识别一些常见的模式和选择,我们将在下面进一步讨论。

在到达那里之前,我想强调一个重要方面。QML离C++并不遥远。QML 基本上是一个由QObject派生对象组成的对象树,其生存期由QMLEngine实例控制。从这个意义上说,一段代码像

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

它与QLineEdit Validator用简单的命令式C++语法编写没有什么不同。除了寿命,如前所述。鉴于此,以简单C++的方式实现验证器是错误的:验证器是TextField的一部分,应该具有与之一致的生命周期。在这种特定情况下,注册新类型是最好的方法。生成的代码更易于阅读和维护。

现在,这个案例是特别的。validator 属性接受派生自Validator的对象(请参阅此处的声明以及此处、此处和此处的一些用法(。因此,我们可以定义一个QValidator派生类型,并用它来代替IntValidator(或其他 QML 验证类型(,而不是简单地定义一个Object派生类型。

我们的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元素来定义:

  • 暴露于QML的QString作为textToCheck
  • 作为 QML valid公开的bool属性

绑定到first.text时,将设置该属性,并在TextField文本更改时重置该属性。在更改时,您可以检查文本,例如使用相同的代码,并更新valid属性。见this回答或上面的注册链接,了解有关Q_PROPERTY更新的详细信息。我把这种方法的实施留给你们,作为练习。

最后,当涉及到类似服务/全局对象/类型时,使用非实例/单例类型可能是正确的方法。在这种情况下,我会让文档为我说话:

QObject

单例类型可以以类似于任何其他 QObject 或实例化类型的方式进行交互,只是只有一个(引擎构造和拥有的(实例存在,并且必须通过类型名称而不是 id 引用它。 QObject 单例类型的Q_PROPERTYs可以绑定到,并且可以在信号处理程序表达式中使用 QObject 模块 API Q_INVOKABLE函数。这使得单一实例类型成为实现样式或主题的理想方式,并且还可以使用它们代替".pragma 库"脚本导入来存储全局状态或提供全局功能

qmlRegisterSingletonType是首选的功能。这也是"快速预测"应用程序(即Digia展示应用程序(中使用的方法。请参阅main和相关ApplicationInfo类型。

上下文属性也特别有用。由于它们被添加到根上下文中(请参阅链接(,因此它们在所有 QML 文件中都可用,也可以用作全局对象。用于访问数据库的类、用于访问 Web 服务的类或类似类有资格添加为上下文属性。另一个有用的案例与模型有关:C++模型,如AbstractListModel,可以注册为上下文属性并用作视图的模型,例如ListView。请参阅此处提供的示例。

Connections类型可用于连接上下文属性和寄存器类型(显然也是单例类型(发出的信号。而信号、Q_INVOKABLE函数和SLOT可以直接从 QML 调用以触发其他C++槽,如此处部分讨论的那样。

总而言之,使用objectName并从C++访问QML是可能和可行的,但通常不鼓励这样做(请参阅此处的警告(。此外,在必要和可能的情况下,QML/C++ 集成通过专用属性进行,例如参见 QML Camera 类型的mediaObject属性。使用(单例(注册类型、上下文属性并通过Connections类型、Q_INVOKABLESLOT s 和 SIGNAL s 将它们连接到 QML,应该可以实现大多数用例。

相关内容

  • 没有找到相关文章

最新更新