JS Array не подходит для QML моделей
Если мы возьмем js array модель, то Repeater по ней будет топо пересоздавать все свое содержимое, реагируя на смену модели, а не на ее состав.
На изменение состава модели он не умеет реагировать вообще.
По идее очень соблазнительно использовать PLAIN JavaScript Objects | Array | Function как объектыпервого класа для моделей, но похоже QML ожидает QOBJECT с правильными сигналами. Это проблема! правильно в этом случае использовать что-то QT шное.
QML ListModel не подходит НИ ДЛЯ ЧЕГО, как выяснилось
Она определена тут https://code.woboq.org/qt6/qtdeclarative/src/qmlmodels/qqmllistmodel_p_p.h.html#ListModel (и это еще новая! 6-я, 5-я еще хуже)
В с++ она определена так, что не найти никаких сигналов изменения данных.
Аналогично &QAbstractListModel туда же.
Недостатки:
состоит исключительно из ListElement
не допускает nested data декларативно (правда через js императивно туда можно затолкать, что угодно… нет , не всё.)
- Можно сделать JS список из только ListElement:
1 | ListModel { |
от этого мало отлка т.к. JS список `[]` не имеет сигнало в для оптимальной обработки
- нельзя встраивать в нее JS скрипты, за исключением анонимных функций.
- если я в js с ней буду работать, то не смогу сделать .connect смогу сделать коннект из кода на изменение элементов модели т.к. таковы элементы и сама эта модель
- При этом
listModel.setProperty(index, propName, val)
мог бы быть используемым, если бы в скрипте можно было подписаться на изменение модели и отбразить новые данные. - но этого нельзя сделать
Кажется достоинством:
- Repeater “каким то образом узнает о изменении данных в модели относительно оптимально” (удаляет и добавляет элементы). Каким образом ? надо реверсинжинирить, но я подохреваю, что через engine - внутри движка есть какие то связи, сам объект ListModel не сигнализирует ничего, чего было бы достаточно для оптимального реагирования из Repeater
больше достоинств нет.
Есть еще такое использование (императивная инициализация модели).
1 | ListView { |
Так теоретически можнореагировать на сигналы модели … и менять состав итемов модели вызывая сигналы. для этого надо в onCompleted подписаться на модель и задать там лямбды для обработки ситуаций…
Преимущество тут только в том, что Repeater нормально реагирует на этот тип модели.
Промежуточный Вывод (о состоянии дел “по умолчанию” в qml)
- Нужно найти Гибкую базовую модель для хотябы qml js части приложения. найти класс или способ
- Вероятно это придется делать создавая свой собственный Repeater
Тогда сюда надо TODO требования для вссего этого.
- не пытаться писать внутри QML логику приложения
- использовать QML как движок декларативного отображения данных - с этим он прекрасно справляется.
Это значит, что будет другой способ сгенерировать и изменить QML код, например как в React это происходит и не понадобится Repeater
Я за этот вариант.
Биндинги которые ломаются
Еще одна вешь. Биндинги к свойствам часто ломаются в сложных случаях,
особенно когда нужно привязать к аттаченому свойству типа Layout.minimumWidth: control.labelWidth
внутри Repeater.
в момент когда Repeater оздает элемент , то свойство устанавливается, и когда позже меняется свойство control.labelWidth
установка связи не происходит.
Если при этом сделать = Qt.binding(function(){ return control.labelWidth;})
то значение не прилетает (иногда прилетает). - плавающая ситуация.
в этом деже нет желания разбираться/терять время - ведь очевидно:
удобно просто иметь сигнал (или несколько) с которым связывается обновление тех или иных свойств (группами) чтобы дергать его для обновления. что то типа :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20signal someUpdateSignal( int data )
...
{
id: elem1
...
Component.onCompleted: {
JS.connect(someUpdateSignal, function(){
elem1.prop1 = ...
elem1.prop2 = ...
})
}
}
somewhere: function(){
...
/// update update update work
...
someUpdateSignal(uuid) ; // !!! <- profit
}это “костыль”. Проблема в самой реактивности моделей и связях с QML - это сделано не достаточно качественно.
Это фантазия… все равно не то. т.к.
доступ из function по ID (замыкание) может не прокатить если элементы генерируются в Repeater например.
Binding {}
есть еще это
1 | TextEdit { id: myTextField; text: "Please type here..." } |
Это декларативная связь для свойств, и она работает как .connet внутри для QT сигналов. (пулинга не ожидаю)
Но надо это делать из javascript ! :-(
Из JS такое динамически создавать (и удалять) в контексте Repeater’а - это уже 100% перебор ! (+ потенциально место для ошибок)
Опять же : с ним тоже вопрос в том в какой момент происходит связывание и пересвязывание (важно) если элемент пересоздан (цель или источник)
В поисках нормальных моделей
Я решил не фантазировать “мега проект” , а сначала понять “что мы имеем на входе” , какие возможности вообще есть в эту сторону
поэтому тут не пишу требования, но держу их в голове.
QVariant - Это первое о чем я подумал. - FAIL
1 | Item { |
https://doc.qt.io/qt-5/qml-variant.html
https://doc.qt.io/qt-5/qtqml-typesystem-basictypes.html
https://doc.qt.io/qt-5/qml-enumeration.html
Страшно: не описаны сигналы!
Итого : не то это !
QQmlPropertyMap - SoSo | FAIL
Выглядит многообещающе. https://doc.qt.io/qt-5/qqmlpropertymap.html
1 | сигнал |
уже кое что . хороший пример https://stackoverflow.com/a/64879716/2355077
с таким сигналом, в котором есть параметры key & value ЖИТЬ МОЖНО. Это хорошая основа для простой модели (плоской). т.к. изменяя се вложенные в виде значения QQmlPropertyMap нужно уже отдельно коннектить, хотя это и не существенная проблема в принципе.
*Одобряю эту модель ЧАСТИЧНО
Однако проблемный он тоже
Note: It is not possible to remove keys from the map; once a key has been added, you can only modify or clear its associated value.
А можно ли на ней сделать список ? ключи в виде [ "1", "2", "3"]
…. - неверное НЕЛЬЗЯ. В этом случае:
- вставка в конец O(1)
- вставка в середину O(n) ,
- удаление в конце O(1)
- удаление в конце O(n)
- Изменение любого значения O(1)
- фильтрация , перегруппировка O(n)
=> относительно простого списка почти нет выигрыша… небольшой есть выигрыш, если элементов очень много + надо изменять их по одному (что-то редактировать), но НЕ удалять и добавлять - в этом случае выигрыша нет (наоборот - есть сложность реализации)
Но нужно быть осторожным с QList<QVariantMap> to QML
Hi! I think you can only return certain built-in QList
‘s. With a quick look at the docs I think you can actually only use:
QList<int>
QList<qreal>
QList<bool>
QList<QString>
andQStringList
QList<QUrl>
QVector<int>
QVector<qreal>
QVector<bool>
and
QList<QObject*>
. Not even sure aboutQVector<QObject*>
.For any other
QList<Something>
you’d use aQVariantList
as yourQ_PROPERTY
/ as return value from aQ_INVOKABLE
.
QVariantList = QList<QVariant > QVariantMap = QMap<QString, QVariant>. QVariantHash = QHash<QString, QVariant>. - FAIL
Сам QVariant не имеет полезных сигналов ! и это не позволяет следить за его изменением.
Плохо, но. это и не контейнер - это значение. так и надо к нему относится.
Есть ли сигналы у контейнеров для него?
это просто потрясающе! хреново
typedef QJSValueList - FAIL
Все JSValue* плохи по той же причине , что и QList - они “плохо реактивные”.
Сам QJSValue тоже не содержит сигналов об изменении содержимого. - как за значениями в QML следить - не понятно
QML - CPP интеграция
Возлагаем надежду на https://doc.qt.io/qt-5/qtqml-cppintegration-data.html .
Any QObject-derived class may be used as a type for the exchange of data between QML and C++, providing the class has been registered with the QML type system.
MAY ! а не обязательно - ок. Но далее статья ведет вдаль от решения проблемы, а начало было норм.
Сказано , что есть автоматическая конвертация ЗНАЧЕНИЙ в/из JavaScript вида :
The QML engine provides automatic type conversion between QVariantList and JavaScript arrays, and between QVariantMap and JavaScript objects.
Это может быть полезно, однако придется ЗАМЕНЯТЬ одни значения другими а не “вставлять в списки” новые элементы для быстрого обновления UI.
приводится примерчик
1 | // C++ |
тут ,кстати, важно , что в JS мы теряем доступ к QVariantMap т.к. это конвертируется в QJSValue , а значит это уже НЕ QObject “фокусы” не доступны.
1 | // MyItem.qml |
фигня это все. два объекта передать - это не история. в смысле : статья , которая должна решать проблему инграции “qml - c++” этого не делает как надо
Есть даже цитата, что “объекты придётся менять целиком”. я об этом и толкую - ужасное решение
Mind that QVariantList and QVariantMap properties of C++ types are stored as values and cannot be changed in place by QML code. You can only replace the whole map or list, but not manipulate its contents.
Это нам не подходит!
Полезная информация
In particular, QML currently supports:
QList<int>
QList<qreal>
QList<bool>
QList<QString>
andQStringList
QVector<QString>
std::vector<QString>
QList<QUrl>
QVector<QUrl>
std::vector<QUrl>
QVector<int>
QVector<qreal>
QVector<bool>
std::vector<int>
std::vector<qreal>
std::vector<bool>
and all registered QList, QVector, QQueue, QStack, QSet, QLinkedList, std::list, std::vector that contain a type marked with Q_DECLARE_METATYPE.
QVector - же бесполезен отсутствием сигналов о изменении структуры и данных, от std::*
этого и ждать не приходится.
Q_PROPERTY( … ) - OK
у некоторых зарагистрированных штук есть такие всякие свойства. у них есть сигналы, и это похоже единственный вариант сделать изменяемую модель, которая может менять UI - очень полезная фича, но она родом из c++ … но это лучше чем ничего.
в простейшем случае (в примере без сигнала, но мы добавим) это выглядит так
1 | class Actor |
The usual pattern is to use a gadget class as the type of a property, or to emit a gadget as a signal argument. In such cases, the gadget instance is passed by value between C++ and QML (because it’s a value type). If QML code changes a property of a gadget property, the entire gadget is re-created and passed back to the C++ property setter. In Qt 5, gadget types cannot be instantiated by direct declaration in QML. In contrast, a QObject instance can be declared; and QObject instances are always passed by pointer from C++ to QML.
так, что чтобы из JS порождать их надо видимо еще поколдовать…
ENUM for QML-C++ описан тут https://doc.qt.io/qt-5/qtqml-cppintegration-data.html#enumeration-types тоже значение с простым порождением в js
so… в чем удобство? типа что определено только в одном месте? - ерунда. такая примитивная структура в одиночку не живет и обрастает всякими условиями и switch ами …. лучше определить нормальную модель с объектами и идентификаторами и ей манипулировать .
ENUM - баловство “от бедности”
Динамические свойства - ОК or SoSo
bool QObject::setProperty(const char *name, const QVariant &value)
Sets the value of the object’s name property to value.
If the property is defined in the class using Q_PROPERTY then true is returned on success and false otherwise. If the property is not defined using Q_PROPERTY, and therefore not listed in the meta-object, it is added as a dynamic property and false is returned.
Information about all available properties is provided through the metaObject() and dynamicPropertyNames().
Dynamic properties can be queried again using property() and can be removed by setting the property value to an invalid QVariant. Changing the value of a dynamic property causes a QDynamicPropertyChangeEvent to be sent to the object.
Note: Dynamic properties starting with “q“ are reserved for internal purposes.
See also property(), metaObject(), dynamicPropertyNames(), and QMetaProperty::write().
Получается можно ловить “от всего” QDynamicPropertyChangeEvent https://doc.qt.io/qt-5/qdynamicpropertychangeevent.html
Dynamic property change events are sent to objects when properties are dynamically added, changed or removed using QObject::setProperty().
See also QObject::setProperty() and QObject::dynamicPropertyNames(). -
но они не доступны из скрипта!
Cобытие бросается “таким способом” link - очень то удобно, но, похоже, это то, чего не хватает QVariant*
моделям - реактивности.
1 | QDynamicPropertyChangeEvent ev(name); |
Для большинства QML объектов setProperty
сделана приватной и в js это undefined
. но это нормально иначе хаков (читай воркэраундов) было бы миллион.
Как перехватить событие с помощью connect?
QDynamicPropertyChangeEvent наследуется от QEvent , которое = Q_GADGET т.е. его можно частично читать в qml … если оно зарагистрировано (не выяснял).
Но ! эта хрень НЕ INVOKABLE
1 | inline QByteArray propertyName() const { return n; } |
Важно: Это рубит на корню идею обойтись JS обертками - придется на с++ кодить и эту часть.
Тут описана самам событийная модель QT https://doc.qt.io/qt-5/eventsandfilters.html
Пример как можно обработать event
1 | class MyObject : public SomeQObjectSubclass |
Обработка может быть в том, чтобы, будучи Q_OBJECT и имея сигналы и несколько Q_PROPRTY, привязанные к ним - вызвать “разумные” сигналы , которые видно на qml стороне (внутри проверяются имя сигнала и значения свойств на факт изменения. кешировать?).
Аналогично люди уже думали, вот нашел “единомышленника”. он в 2016 предложил
1 | class DynamicObject : public QObject |
Так, идея хорошая - я в ту же сторону думал. Но :
- зря он пытается из qml в с++ пробрасывать события - это идеологически не правильно. да и гонки состояний появятся.
- надо делать только в сторону “с++ -> qml” т.к. поток данных однонаправленный должен быть и даже это не правильно.
- не нужно соединять с++ объекты с qml объектами - нужно сделать только модель на с++ для того , чтобы в qml она выглядела реактивной. Этта задача данным методом решается отлично.
Почему не надо соединять модель с++ с qml стороной?
Ответ:
- Эта задача не так решается.
- Не достаточно просто соединить модели.
- Фокус в том, что qml модели это результат обработки с++ моделей в комплексном приложении. Модели для qml могут в общем случае иметь даже совсем другую структуру и не совпадать с моделями с++ структурно и тогда нечего будет соединять.
- Нужно превратить эту задачу в задачу такого вида:
- в с++ приходит сигнал о требуемых изменениях
- с++ после работы с с++ моделями понимает что обработка завершена и данные из с++ части надо показывать.
- из с++ моделей формируется промежуточная структура (модель для qml), которая описывает требуемую / будущую модель для qml но НЕ заменяет её напрямую
- отдельный код сравнивает qml модели которе отображаются сейчас и те, которые должны отображаться и меняет те, которе отображаются , приводя их в соответствие с теми , которе должны отображаться минимальным числом шагов. Потом Новые модели забываются на с++ стороне.
- qml движок реагирует на измененные модели и минималым числом изменений перерисовывает интерфейс
Поэтому нет задачи “свзяать с++ и qml” в принципе !
TODO Изучить проект туда же копает https://github.com/SkySparky/QuickProperties
QuickProperties тоже работает в эту сторону….
QuickContainers - Еще один проект коорый делает реактивные контейнеры для моделей
https://github.com/cneben/QuickQanava/tree/master/QuickContainers
QuickContainers
expose a generic container (QVector or std::vector) of items to an observale Qt item model.
QuickContainers
support to following containers with a unique interface:
- Qt containers: QVector, QList and QSet.
- Std containers: std::vector
With any combinations of:
- std::weak_ptr
- std::unique_ptr
- QPointer
- Raw QObject pointers.
- Any pod type.
Нормально так. - надо рабиратся
Учимся создавать QML (~QObject) из JS (динамически)
Микро дока https://doc.qt.io/qt-5/qtqml-javascript-dynamicobjectcreation.html
Это та самая возможность, которая и приведет к реакту для qml :-)
Возможности по сути: Чтобы создать QmlObjeсt (аналог document.createElement
) есть две возможности :
- Из компонента (аналог JS модуля или … блок кода, который или в файле определен или отдельный файл qml - это компонент)
- из строки qml текста, в которой кроме самого объекта будет импорты итд - по сути компонент задан + объект со свойствами
Функции :
- Qt.createComponent() - можно синхронно или нет
- Qt.createQmlObject() to create an object from a string of QML
- Once you have a Component, you can call its createObject() method to create an instance of the component. This function can take one or two arguments:
- The first is the parent for the new object. The parent can be a graphical object (i.e. of the Item type) or non-graphical object (i.e. of the QtObject or C++ QObject type). Only graphical objects with graphical parent objects will be rendered to the Qt Quick visual canvas. If you wish to set the parent later you can safely pass
null
to this function. - The second is optional and is a map of property-value pairs that define initial any property values for the object. Property values specified by this argument are applied to the object before its creation is finalized, avoiding binding errors that may occur if particular properties must be initialized to enable other property bindings. Additionally, there are small performance benefits when compared to defining property values and bindings after the object is created.
- The first is the parent for the new object. The parent can be a graphical object (i.e. of the Item type) or non-graphical object (i.e. of the QtObject or C++ QObject type). Only graphical objects with graphical parent objects will be rendered to the Qt Quick visual canvas. If you wish to set the parent later you can safely pass
Объект из qml строки
1 | var newObject = Qt.createQmlObject('import QtQuick 2.0; Rectangle {color: "red"; width: 20; height: 20}', |
- dynamicSnippet1 - можно не задавать, это если что то повалится - ошибки в консоли будут показаны из “файла” = “dynamicSnippet1”
- код объекта - понятно
- parentItem - куда в
children
добавлять “новоявленного”
Все синхронно интерпретируется. движок идет по коду строит дерево элементов и задаёт им свойства.
Подгружаем компонент откуда нибудь
1 | var component = Qt.createComponent("Button.qml"); |
существует на самом деле понятия QmlDocument https://doc.qt.io/qt-5/qtqml-documents-topic.html
Тут есть еще параметры, кроме URL с кодом.
Полный формат вызова:
1 | createComponent(url, mode, parent) |
mode
- про синхроность загрузки (правило: думай, что загрузка всегда асинхронная если не уверен в обратном)
If the optional mode parameter is set to
Component.Asynchronous
, the component will be loaded in a background thread. The Component::status property will beComponent.Loading
while it is loading. The status will change toComponent.Ready
if the component loads successfully, orComponent.Error
if loading fails. This parameter defaults toComponent.PreferSynchronous
if omitted.If mode is set to
Component.PreferSynchronous
, Qt will attempt to load the component synchronously, but may end up loading it asynchronously if necessary. Scenarios that may cause asynchronous loading include, but are not limited to, the following:
- The URL refers to a network resource
- The component is being created as a result of another component that is being loaded asynchronously
Идея :
URI - для можно придумать свой протокол и провайдер и обрабатывать , возвращая на свое усмотрение, получим лучшее из всех миров
qrc:/
,http:/
,file:/
,memory:/
итд
Объект порождаем из компонента
Компонент не обязательно подгружать откуда то. можно к нему обратиться , если он уже часть текущего модуля, по id
1 | Component { |
Или есть еще inline
компоненты . Так что модуль - это всё таки отдельный файл с qml, а компоненты - как бы класс, а объект - как бы экземпляр класса.
Можно “подгрузив компнент” (лучше бы тут было “модуль”), скомандовать ему сделать объект (qml Элемент) component.createObject
1 | var component = Qt.createComponent("Button.qml"); |
parent
- родитель- далее
{...}
- свойства (тут сразу важно сразу задать биндинги, по возможности т.к. свойства устанавливаются до финализщации объекта)
т.к. загрузка компонента может быть и асинхронная, то лучше создавать их по такому примеру
1 | var component; |
Как всё таки делать “нормальные модели”
- на c++ надо зарегистрировать Q_OBJECT для QML с Q_PROPERTY так, чтобы из JS можно было создавать их а не только использовать как значения.
- дальше “дело в шляпе”, если обернуть это нормальными js обертками.
Требования
Какие модели потребуются
- список с сигналами
- вставка, куда, индекс
- удаление откуда, индекс
- изменение длинны
- изменение внутри элемента (не уверен, что требуется)
- замена элемента
- значение объект - похоже на объект со свойствами. сигналы:
- установка поля
- удаление и добавление поля имя
- значение базового простого типа,
- свойства а) Тип значения и б) Само Значение
- с сигналами о их изменении, сигнал, что значение пустое
- таблица (уже композит? предыдущих моделей). несколько полей с разными. сигналы:
- список полей с синалами списка (поле добавлено , удалено, …)
- список строк с сигналами списка (строка добавлена… )
- / видимо это композит уже
Идеи WIP
- Значение объект -
- вариант 1: это простой
QObject
, надо использовать “динамические свойства” и сигналы к ним. JS Хелперы помогают подписаться на соответствующие сигналы т.к. напрямую возможно ошибиться с их именем., но это максимально простой и гибкий путь получить реактивную модель и отобразить её. - вариант 2: сделать простого наследника от QObject как контейнер полей, но с нормальными сигалами. число полей должно уметь уменьшаться, чигналы параматризированные простыми типами параметров
- вариант 1: это простой
Значение объект (2й вариант) -только растет - ключи не удалить, да и создавать его из JS не проще.QQmlPropertyMap
- Список - модель для списков
- вариант 1 : наследник
QList | QArray
(QVariantList?
) но добавлены сингалы и переопределены некоторые методы, которые эти сигналы генерируют так, чтобы можно было подписаться явно - вариант 2: Просто QList
- вариант 1 : наследник
Нормальный вариант - для классического, но современного пути
QSyncable ( https://github.com/benlau/qsyncable ) - вполне нормальное решение , покрывает 80% задач, но это не плагин к QT !
- Задача : надо сделать это плагином! и немного доработать, чтобы это можно было использовать
- не только в сценарии “с++ управляет qml”
- QML может с помощью этой модели сам оптимально управлять моделью (diff runner сделать доступным в QML)
- Добавить туда “C++ Qt обертки” для тех моделей , которые не удобные (просто добавить им сигналов для удобства взаимодействия)