Смотрю на форумах рунета, люди начинают писать на 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)
Antervis
16.08.2016 13:46+1В принципе, отличный пример. Q_GADGET действительно хорошо подходят для обертывания структур и использования их в QML. Ключевой недостаток Q_GADGET'ов в том, что Q_PROPERTY без сигналов (об изменении значения) в лучшем случае неудобны, а в худшем — бесполезны
п.с. В моделях всё-таки лучше использовать не одну роль, возвращающую объект, а много ролей, возвращающих простые значения. Производительнее и нагляднее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" ] }
то создавать роли может быть неудобно. Тут как всегда, нужно смотреть по ситуации.
Roumed1
16.08.2016 14:57+1В этом случае оборачивание в QVariant повлечет за собой:
- написание Q_GADGET wrapper-а с пачкой Q_PROPERTY, которые будет также неудобно описывать (если я правильно Вас понял.);
- копирование данных при выполнении QVariant::fromValue(), что при большой структуре отразится на производительности;
- необходимость в qml-коде обращаться к конкретному свойству через объект:
model.object.property
(сахар, конечно, но это будет раздражать).
С другой стороны, сохранение значения
model.object
позволит минимизировать обращение к QAbstractItemModel::data() со всеми вытекающими.
Так что соглашусь: нужно смотреть по ситуации.
Antervis
16.08.2016 16:58Наоборот, чем больше структура, тем накладнее её постоянное копирование с целью вытащить из модели значение всего одного поля…
RPG18
16.08.2016 17:19Преждевременная оптимизация — корень всех зол.
Д. Кнут.
Часть типов в Qt implicitly shared, а значит копирование приводит к увеличению атомарных счетчиков, а не выделение и перемещение(std::memmove) памяти. А если уж хочется минимезировать инкриментирование атомарных счетчиков, то можно pimpl применить:
pimplclass 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; }
Antervis
17.08.2016 06:02написание обвязки для одной роли модели требует меньше кода, чем оборачивание этого же значения в Q_PROPERTY
vitaly_KF
16.08.2016 21:39Я для такого пилил обертку, чтобы rolenames() и data() автоматом генерились и вообще не надо было заморачиваться с внутренностями модели. Просто получаешь списочный json, скармливаешь в модель и пользуешь где нужно.
Roumed1
Хорошая статья, важное замечание.
Предложил бы еще так же сказать пару слов о макросе Q_DECLARE_OPAQUE_POINTER и смежных.
Не совсем согласен с утверждением:
Утверждаю, что следующий код будет работать в 5.6.1:
RPG18
Отлично! Теперь буду знать.