Смотрю на форумах рунета, люди начинают писать на C++&Qt Quick и используют наследников от QObject, для так называемых типов значений(Value Type). Мартин Фаулер их называет Value Object. Хотя есть макрос Q_GADGET позволяющий использовать QMetaObject c некоторыми ограничениями, но без наследования от QObject. Все что будет описано ниже результат экспериментов с Qt Quick. Буду рад узнать что-то новое из комментариев.


Пример таких типов QPoint, QGeoCoordinate и т.д. Наследоваться от QObject и использовать макрос Q_OBJECT неудобно для таких типов:


  • QObject защищен от копирования;
  • нужно возвращать значение по указателю. Приходится задумываться о CppOwnership/JavaScriptOwnership из перечисления QQmlEngine::ObjectOwnership.

Q_GADGET позволяет нам использовать:


  • Q_ENUM;
  • Q_PROPERTY;
  • Q_INVOKABLE.

Ограничение:


  • Отсутствие поддержки сигналов и слотов.

Если наше приложение просто отображает то, что пришло с сервера, то можно завести структуру:


struct PlayItem
{
private:
    Q_GADGET
    Q_PROPERTY(int episode MEMBER episode)
    Q_PROPERTY(QString mp4Url MEMBER mp4Url)
    Q_PROPERTY(QString name MEMBER name)

public:

    int episode;
    QString mp4Url;
    QString name;

    static PlayItem fromJson(const QJsonObject& jobj);

};

Q_DECLARE_METATYPE(PlayItem)

Q_DECLARE_METATYPE здесь используется для регистрации типа в QVariant. Зачем он тут нужен, об этом будет позже.


Такие типы можно использовать в свойствах других объектов:


class Size
{
    Q_GADGET

public:

    Q_INVOKABLE quint16 rows() const noexcept;
    Q_INVOKABLE quint16 column() const noexcept;
    Q_INVOKABLE bool isNull() const noexcept;

//..
};

class Crossword: public QObject
{
    Q_OBJECT
    Q_PROPERTY(Size size READ size)

public:

    Crossword(QObject* parent = nullptr);

    Size size() const noexcept;
}

И спокойно работаем в js:


var csize = crossword.size;
//...
rows = csize.rows();
column = csize.column();

Q_GADGET и Q_INVOKABLE


Почему то мы не можем использовать ValueType в методах помеченными Q_INVOKABLE. За то можно возвращать QVariant с ValueType! И так же использовать его в js! Это очень удобно в моделях, заместо множества ролей и switch:


QVariant BucketModel::data(const QModelIndex &index, int role) const
{
    switch (role)
    {
        case Bucket:
            return QVariant::fromValue(m_buckets[index.row()]);
        default:
            return QVariant();
    }

}

QHash<int, QByteArray> BucketModel::roleNames() const
{
    static const QHash<int, QByteArray> roles = {
        {Bucket, "bucket" }
    };

    return roles;
};

В делегате как обычно:


delegate: ItemDelegate {
    width: parent.width
    text: bucket.name

    Image{
        visible: bucket.id === b2App.settings.bucketId

        anchors{
            right:parent.right
            verticalCenter: parent.verticalCenter
            margins: 8
        }

        source: "qrc:/icons/tick/tick.png"
    }

Item и property


Такие типы можно использовать как свойства и делать на них привязки. Это осуществляется через общий тип(generic type):


Item {
    property var film

    //...
    Label {
        text: film.year
        //...
    }

    Label {
        text: film.countries
        //...
    }
  //...
}

Так как до инстанцирования тип неизвестен, то во время выполнения ругается(но не падает): TypeError: Cannot read property 'year' of undefined.


Убрать эту ругань можно инициализировав свойство, каким-нибудь экземпляром:


QQmlApplicationEngine engine;

Film film;
engine.rootContext()->setContextProperty("emptyFilm", QVariant::fromValue(film));

Item {
    property var film: emptyFilm

    //...
    Label {
        text: film.year
        //...
    }

    Label {
        text: film.countries
        //...
    }
 //...
}

Это оказывается очень удобно, когда используется StackView, на одном экране выводишь модель с минимум информацией, а на следующем экране более подробно:




По-моему личному мнению, такие value type очень удобные.

Поделиться с друзьями
-->

Комментарии (12)


  1. Roumed1
    16.08.2016 13:01
    +2

    Хорошая статья, важное замечание.

    Предложил бы еще так же сказать пару слов о макросе Q_DECLARE_OPAQUE_POINTER и смежных.

    Не совсем согласен с утверждением:

    Почему то мы не можем использовать ValueType в методах помеченными Q_INVOKABLE.

    Утверждаю, что следующий код будет работать в 5.6.1:
    // record.h
    class Record
    {
      Q_GADGET
    ...
    }
    
    // listener.h
    class Listener: public QObject
    {
      Q_OBJECT
    ...
      Q_INVOKABLE Record foo ();
    }
    
    //main.cpp
    ...
    qRegisterMetaType<Record>();
    qmlRegisterType<Listener>("MyModule", 1, 0, "Listener");
    ...
    


    // main.qml
    ...
    Component.onCompleted: {
        var  r = someListener.foo();
    
        print(r)
        print(typeof r)
        print(Qt.isQtObject(r))
    
        r.text = "hooray";
        print(r.text)
    }
    ...
    
    // output:
    qml:  Record()
    qml:  object
    qml:  false
    qml:  hooray
    
    



    1. RPG18
      16.08.2016 13:03

      Отлично! Теперь буду знать.


  1. Antervis
    16.08.2016 13:46
    +1

    В принципе, отличный пример. Q_GADGET действительно хорошо подходят для обертывания структур и использования их в QML. Ключевой недостаток Q_GADGET'ов в том, что Q_PROPERTY без сигналов (об изменении значения) в лучшем случае неудобны, а в худшем — бесполезны

    п.с. В моделях всё-таки лучше использовать не одну роль, возвращающую объект, а много ролей, возвращающих простые значения. Производительнее и нагляднее


    1. RPG18
      16.08.2016 14:17

      Например если делать клиент для сервиса(на скриншоте клиент futuron.tv), то сервер использует пагинацию, что уменьшает число элементов, то вопрос производительности особо не стоит. А вот когда количество полей у структуры очень много:


      {
            "id" : 1708722,
            "type" : 0,
            "rate" : 3,
            "creationTime" : 1460376607027,
            "downloads" : 0,
            "rusName" : "Пришельцы 3: Взятие Бастилии",
            "engName" : "Les Visiteurs: La Revolution",
            "releaseYear" : 2016,
            "description" : "В третьей части граф с оруженосцем отправляются в самое сложное для Франции время ? период Французской революции.",
            "youtubeId" : "FmpaphvPiTY",
            "imdbRate" : "4.4",
            "kpRate" : "5.8",
            "duration" : "1ч.50мин.",
            "officialSite" : "http://lesvisiteurs-lefilm.com/,http://vk.com/lesvisiteurs",
            "video3d" : false,
            "nextSeazonId" : 0,
            "rusAgeRate" : "12+",
            "studies" : "Cine, TF1 Films Production, Canal+ [fr], Nexus Factory, Gaumont, Gaumont, The Czech Republic State Fund for Support and Development of Cinematography, La Wallonie, Okko Productions, Ouille Productions, BNP Paribas Fortis Film Finance",
            "directors" : "Жан-Мари Пуаре",
            "actors" : "Сильви Тестю, Кристиан Клавье, Жан Рено, Эри Абиттан, Карин Вьяр, Лоран Дойч, Франк Дюбоск, Фредерик Бель, Мари-Анн Шазель, Алекс Лутс, Вероника Буланжер, Стефани де Крэенкур",
            "countries" : "Франция",
            "genres" : "Комедии",
            "translationTypes" : "Проф. многоголосый",
            "translationAuthors" : "",
            "videoQuality" : "HD",
            "audioQuality" : "чистый звук",
            "languages" : "",
            "coverURL" : "http://static-gw.futuron.name/static/mobile/71/CF/71CF47394B5BA22ADDA68C66F29019E5.jpeg",
            "coverMaxiURL" : "http://static-gw.futuron.name/static/mobile/71/CF/71CF47394B5BA22ADDA68C66F29019E5.jpeg",
            "posterURL" : "http://static-gw.futuron.name/static/torrents/DA/69/DA69912E9EE01738090984132A70DEBA.jpg",
            "screenshotsURL" : [ "http://static-gw.futuron.name/static/torrents/32/5A/325A0ADEABE689251170180309FFF167.jpg", "http://static-gw.futuron.name/static/torrents/2D/2E/2D2E3DC8B83DA2FF7764BB6AE4E50398.jpg", "http://static-gw.futuron.name/static/torrents/D7/71/D771B2DA86EDE8EC444D80E7E2D6BA1B.jpg", "http://static-gw.futuron.name/static/torrents/F0/E6/F0E6AB1DCA3366E0E1C3BE3C5A09E11B.jpg", "http://static-gw.futuron.name/static/torrents/6F/C0/6FC0895E1E265C0D3DBF4671FD330595.jpg", "http://static-gw.futuron.name/static/torrents/D3/05/D3059FCE4D46B868ADB88FD4FC1C0241.jpg" ]
          }

      то создавать роли может быть неудобно. Тут как всегда, нужно смотреть по ситуации.


      1. Roumed1
        16.08.2016 14:57
        +1

        В этом случае оборачивание в QVariant повлечет за собой:


        • написание Q_GADGET wrapper-а с пачкой Q_PROPERTY, которые будет также неудобно описывать (если я правильно Вас понял.);
        • копирование данных при выполнении QVariant::fromValue(), что при большой структуре отразится на производительности;
        • необходимость в qml-коде обращаться к конкретному свойству через объект: model.object.property (сахар, конечно, но это будет раздражать).

        С другой стороны, сохранение значения model.object позволит минимизировать обращение к QAbstractItemModel::data() со всеми вытекающими.


        Так что соглашусь: нужно смотреть по ситуации.


      1. Antervis
        16.08.2016 16:58

        Наоборот, чем больше структура, тем накладнее её постоянное копирование с целью вытащить из модели значение всего одного поля…


        1. RPG18
          16.08.2016 17:19

          Преждевременная оптимизация — корень всех зол.

          Д. Кнут.


          Часть типов в Qt implicitly shared, а значит копирование приводит к увеличению атомарных счетчиков, а не выделение и перемещение(std::memmove) памяти. А если уж хочется минимезировать инкриментирование атомарных счетчиков, то можно pimpl применить:


          pimpl
          class Film
          {
              Q_GADGET
          
              Q_PROPERTY(quint64 id READ id)
              Q_PROPERTY(QString endName  READ engName CONSTANT FINAL)
              Q_PROPERTY(QString rusName  READ rusName CONSTANT FINAL)
              Q_PROPERTY(QUrl coverUrl READ coverUrl CONSTANT FINAL)
              Q_PROPERTY(QUrl coverMaxiURL READ coverMaxiURL CONSTANT FINAL)
              Q_PROPERTY(QStringList screenshotsURL READ screenshotsURL CONSTANT FINAL)
              Q_PROPERTY(QString duration READ duration CONSTANT FINAL)
              Q_PROPERTY(QString description READ description CONSTANT FINAL)
              Q_PROPERTY(quint32 year READ year CONSTANT FINAL)
              Q_PROPERTY(QString countries READ countries CONSTANT FINAL)
              Q_PROPERTY(QString actors READ actors CONSTANT FINAL)
              Q_PROPERTY(QString genres READ genres CONSTANT FINAL)
              Q_PROPERTY(bool isValid READ isValid CONSTANT FINAL)
          
          public:
          
              typedef QList<QUrl> Urls;
          
              Film();
              Film(Film&& obj);
              Film(const Film& obj) = default;
          
              static Film formJson(const QJsonObject& obj);
          
              quint64  id() const noexcept ;
              const QString& engName() const noexcept;
              const QString& rusName() const noexcept;
          
              const QUrl& coverUrl() const noexcept;
              const QUrl& coverMaxiURL() const noexcept;
          
              const QStringList& screenshotsURL() const noexcept;
              const QString& duration() const noexcept;
          
              const QString& description() const noexcept;
          
              quint32 year() const noexcept;
          
              const QString& countries() const noexcept;
              const QString& actors() const noexcept;
              const QString& genres() const noexcept;
          
              bool isValid() const noexcept;
              bool operator==(const Film& film) const noexcept;
          
          private:
          
              struct FilmImpl;
              std::shared_ptr<FilmImpl> m_impl;
          };

          #include <QtCore/QDebug>
          #include <QtCore/QVariant>
          #include <QtCore/QJsonArray>
          #include <QtCore/QJsonObject>
          
          #include "Film.h"
          
          struct Film::FilmImpl
          {
              bool m_isValid = false;
              quint64 m_id   = 0;
          
              QString m_engName;
              QString m_russName;
              QUrl m_coverURL;
              QUrl m_coverMaxiURL;
              QStringList m_screenshotsURL;
              QString m_duration;
              QString m_description;
              QString countries;
              QString actors;
              QString genres;
              quint32 year;
          };
          
          Film::Film()
              : m_impl(std::make_shared<FilmImpl>())
          {
          
          }
          
          Film::Film(Film&& obj):
              m_impl(std::move(obj.m_impl))
          {
          }
          
          quint64 Film::id() const noexcept
          {
              return m_impl->m_id;
          }
          
          const QStringList& Film::screenshotsURL() const noexcept
          {
              return m_impl->m_screenshotsURL;
          }
          
          const QString& Film::rusName() const noexcept
          {
              return m_impl->m_russName;
          }
          
          const QString& Film::engName() const noexcept
          {
              return m_impl->m_engName;
          }
          
          const QUrl& Film::coverUrl() const noexcept
          {
              return m_impl->m_coverURL;
          }
          
          const QUrl& Film::coverMaxiURL() const noexcept
          {
              return m_impl->m_coverMaxiURL;
          }
          bool Film::isValid() const noexcept
          {
              return m_impl->m_isValid;
          }
          
          const QString& Film::duration() const noexcept
          {
              return m_impl->m_duration;
          }
          
          const QString& Film::description() const noexcept
          {
              return m_impl->m_description;
          }
          
          bool Film::operator==(const Film& film) const noexcept
          {
              return m_impl->m_id == film.m_impl->m_id;
          }
          
          Film Film::formJson(const QJsonObject &obj)
          {
              Film film;
              film.m_impl->m_isValid      = true;
              film.m_impl->m_id           = obj.value("id").toVariant().toULongLong();
              film.m_impl->m_russName     = obj.value("rusName").toString();
              film.m_impl->m_engName      = obj.value("engName").toString();
              film.m_impl->m_description  = obj.value("description").toString();
              film.m_impl->countries      = obj.value("countries").toString();
          
              film.m_impl->m_coverURL     = QUrl(obj.value("coverURL").toString());
              film.m_impl->m_coverMaxiURL = QUrl(obj.value("coverMaxiURL").toString());
              film.m_impl->m_duration     = obj.value("duration").toString();
          
              film.m_impl->year           = obj.value("releaseYear").toVariant().toUInt();
              film.m_impl->actors         = obj.value("actors").toString();
              film.m_impl->genres         = obj.value("genres").toString();
          
              for(const auto& val : obj.value("screenshotsURL").toArray())
              {
                  film.m_impl->m_screenshotsURL.push_back(val.toString());
              }
          
              return film;
          }
          quint32 Film::year() const noexcept
          {
              return m_impl->year;
          }
          const QString& Film::countries() const noexcept
          {
              return m_impl->countries;
          }
          
          const QString& Film::actors() const noexcept
          {
              return m_impl->actors;
          }
          
          const QString& Film::genres() const noexcept
          {
              return m_impl->genres;
          }


          1. Antervis
            17.08.2016 06:02

            написание обвязки для одной роли модели требует меньше кода, чем оборачивание этого же значения в Q_PROPERTY


      1. vitaly_KF
        16.08.2016 21:39

        Я для такого пилил обертку, чтобы rolenames() и data() автоматом генерились и вообще не надо было заморачиваться с внутренностями модели. Просто получаешь списочный json, скармливаешь в модель и пользуешь где нужно.


  1. Roumed1
    16.08.2016 14:55

    удали-меня


  1. kolayuk
    16.08.2016 22:53

    Биндинги я так понимаю не обновляются (раз сигналов нет)?


    1. RPG18
      16.08.2016 23:05

      Нет, но можно создать свойство с var и при изменение значения, значение в контролах поменяются.