Изменения в специализированном интерфейсе 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 должны включить большинство случаев использования.

Другие вопросы по тегам