我正在学习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_INVOKABLE
、SLOT
s 和 SIGNAL
s 将它们连接到 QML,应该可以实现大多数用例。