Изменения в специализированном интерфейсе QValidator и QML
Я изучаю Qt 5.5 и QML.
Каркас является мощным, и иногда есть много способов сделать одну вещь. Я думаю, что некоторые, вероятно, более эффективны, чем другие, и я хотел бы понять, когда и почему использовать один, а не другой.
Я хотел бы получить ответ, который может объяснить сделанный выбор. Поскольку я работаю с новым кодом, синтаксис C++ 11 и C++ 14 можно использовать, если он полезен на стороне C++.
Чтобы решить проблему это:
у меня есть TextField
связан с кнопкой, которая может выдвинуть FileDialog
, Я хочу текст в TextField
быть red
когда он недействителен, и остался без изменений в противном случае (я установил его green
потому что я не знаю, как получить цвет по умолчанию). Значение TextField
должен использоваться на стороне C++ и сохраняется при выходе из приложения.
Я закодировал версию, используя кастом QValidator
некоторые свойства на стороне QML, использование onTextChanged:
а также onValidatorChanged:
изменить цвет текста. Цвет текста устанавливается в соответствии со свойством (valid
) в QML, который устанавливается со стороны C++ (в валидаторе). Чтобы установить свойство, C++ должен найти по имени вызывающегоTextField
названный directoryToSave
) потому что я еще не нашел способ передать сам объект в качестве аргумента.
Вот код QML, содержащийся в 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;
}
}
Пользовательский код валидатора:
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:
чтобы быть уверенным, чтобы инициализировать интерфейс с правильным цветом - поиск по имени дерева для поиска вызывающего абонента (выглядит неэффективно; может и не быть)
- спагетти-подобное кодирование среди нескольких экземпляров C++ и частей QML
Я могу думать о 5 других решениях:
- избавиться от пользовательского валидатора и сохранить только
onTextChanged:
потому что мы не можем избавиться от сигнализации со стороны QML. Большинство вещей сделано вMyClass
- исправление Qt для реализации перехватчика записи значения свойства для чего-то другого, кроме
Behavior
(см. здесь) - регистрация типа C++ для присоединения к объекту QML. (см. здесь)
- регистрация типа и использование его в качестве контроллера и структуры данных (в виде бобов) для последующей передачи на модель (см. здесь)
- используя сигналы вручную, как я уже делаю с
signalDirectoryChanged
Ну, как вы видите, плеторические способы делать вещи сбивают с толку, поэтому совет сэмпай ценится.
Полный исходный код доступен здесь.
1 ответ
Я не думаю, что один ответ может ответить на все ваши вопросы, но я все же думаю, что некоторые рекомендации по структуре приложения могут помочь вам в работе.
AFAIK нет центрального места, где обсуждают структуру приложения. На самом деле, в QML также нет рекомендаций относительно структуры пользовательского интерфейса (см. Это обсуждение, например). Тем не менее, мы можем определить некоторые общие шаблоны и варианты в приложении QML, которые мы обсудим ниже.
Прежде чем попасть туда, я хотел бы подчеркнуть важный аспект. QML не так далек от C++. QML в основном дерево объектов QObject
объекты, чье время жизни контролируется QMLEngine
пример. В этом смысле кусок кода, как
TextField {
id: directoryToSave
placeholderText: qsTr("placeHolder")
validator: IntValidator { }
}
это не так уж отличается от QLineEdit
с Validator
написано в простом императивном синтаксисе C++. Помимо жизни, как сказано. Учитывая это, реализация вашего валидатора в простом C++ неверна: валидатор является частью TextField
и должен иметь продолжительность жизни в соответствии с этим. В этом конкретном случае регистрация нового типа - лучший способ. Полученный код легче читать и легче поддерживать.
Теперь этот случай особенный. validator
свойство принимает объекты, производные от Validator
(см. декларацию здесь и некоторые использования здесь, здесь и здесь). Следовательно, вместо того, чтобы просто определить 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 { // use it
id: dirVal
}
Column {
anchors.fill: parent
TextField {
id: first
validator: dirVal
textColor: acceptableInput ? "black" : "red"
}
TextField {
validator: dirVal
textColor: acceptableInput ? "black" : "red"
}
TextField {
validator: DirValidator { } // use it
textColor: acceptableInput ? "black" : "red"
}
}
}
Как вы можете видеть, использование стало более простым. Код C++ чище, но и код QML чище. Вам не нужно передавать данные вокруг себя. Здесь мы используем то же самое acceptableInput
из TextField
так как это установлено 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
элементы:
-
QString
подвергается QML какtextToCheck
-
bool
собственность выставлена какvalid
в QML
Когда связано с first.text
свойство устанавливается и сбрасывается, когда TextField
текстовые изменения. При изменении вы можете проверить текст, например, с тем же кодом, и обновить valid
имущество. Увидеть this
ответ или регистрационную ссылку выше для деталей о Q_PROPERTY
Обновления. Я оставляю реализацию этого подхода вам, как упражнение.
Наконец, когда дело доходит до сервисных / глобальных объектов / типов, использование неинстанцируемых / одноэлементных типов может быть правильным подходом. Я бы позволил документации говорить за меня в этом случае:
С одноэлементным типом QObject можно взаимодействовать аналогично любому другому типу QObject или экземпляру, за исключением того, что будет существовать только один экземпляр ( созданный и принадлежащий движку), и на него должна ссылаться имя типа, а не идентификатор. Q_PROPERTY с одноэлементными типами QObject могут быть связаны, а функции Q_INVOKABLE API-интерфейсов модуля QObject могут использоваться в выражениях обработчика сигналов. Это делает одноэлементные типы идеальным способом реализации стилей или создания тем, и их также можно использовать вместо импорта скриптов ".pragma library" для хранения глобального состояния или для обеспечения глобальной функциональности.
qmlRegisterSingletonType
является функцией предпочтения. Этот подход используется также в приложении "Быстрый прогноз", то есть в демонстрационном приложении Digia. Увидеть main
и связанные ApplicationInfo
тип.
Также контекстные свойства особенно полезны. Поскольку они добавляются в корневой контекст (см. Ссылку), они доступны во всех файлах QML и могут использоваться также в качестве глобальных объектов. Классы для доступа к БД, классы для доступа к веб-службам или аналогичные могут быть добавлены в качестве свойств контекста. Еще один полезный случай связан с моделями: модель C++, такая как AbstractListModel
может быть зарегистрирован как свойство контекста и использоваться в качестве модели представления, например ListView
, Смотрите пример, доступный здесь.
Connections
Тип может использоваться для подключения сигналов, излучаемых как свойствами контекста, так и типами регистров (очевидно, также одноэлементным). Принимая во внимание, что сигналы, Q_INVOKABLE
функции и SLOT
s можно напрямую вызывать из QML для запуска других слотов C++, как это частично обсуждалось здесь.
Подводя итоги, используя objectName
и доступ к QML из C++ возможен и выполним, но обычно не рекомендуется (см. предупреждение здесь). Кроме того, когда это необходимо и возможно, интеграция с QML/C++ поддерживается с помощью специальных свойств, см., Например, mediaObject
свойство QML Camera
тип. Использование (единичных) зарегистрированных типов, свойств контекста и подключение их к QML через Connections
тип, Q_INVOKABLE
, SLOT
с и SIGNAL
s должны включить большинство случаев использования.