QML dynamic build from virtual Dom in QSyncable

PDF | DARKHTML

Идея “React” конкретной реализациии компонентов для qml движка.

  • “ускоряет создание библиотек крутых компонентов на QML” ,
  • “драйвит” QML в массы на все платформы
  • повышает скорость создания интерфейсов
  • повышает качество разрабатываемых QML приложений

Прост текст - просто описание идеи.

Типа “проблема”

на qml мы не имеем ничего даже близко похожего на react

  • Имеется проект react-qml но в нем
    1. JSX это XML… Это странно для QML мира , но нормально для React мира (не о низ речь) Proof
    2. JS много делает, хотя в qml мире есть С++
    3. нет mobx, а если бы был - то это тоже был бы JS. (см. п.2)

Короче:

  • Проект https://github.com/longseespace/react-qml мне НЕ нравится своей “javascript нутостью”. там js инструменты разрабтки, js делает парсинг , интерпретация … - все на js.
  • Проект https://github.com/grassator/react-qml - делает, то что требуется, но он
    • заброшен
    • работает на JS “яваскриптнутый” . много всякого кода - можно упростить.

В то же время в проекте QSyncable :

  • интеграция с C++ объектами через Qt Variant (он кстати легковесный)
  • быстрый c++ DIFF для инкрементальной модификации моделей, patch. proof
  • Но всё ещё надо писать на QML в обычном стиле, привязывая всякие Repeater к этим моделям… (не React style, но hi QML)
  • QML интерпретируется, а не строится на лету из кода

Самые очевидные преимущества React философии / технологии

React технология (я буду называть это технологией) этим и хорош, что

  • описание - декларативное, исполнение императивное, с примесью “фокусов” реактивности… - компоненты соединены с логикой, встроены в нее, но выглядит всё отдельным
  • нет “промежуточные перестроенией”. т.е. нет всяких аналогов Repeater , которые
    • получают “состояние1” верхнего уровня, подписываются на него
    • реагированием создают “состояние2” + создают элементы , уничтожают, двигают… т.е. получаются элементы которые им подконтрольны, но не управляются больше никак.
    • состояние 1 - меняет qml дерево, и меняют “состояние2” , потом “состояние2” меняет qml дерево, том возможно внутри возникает еще одно “сотояние3” если там есть циклы.
    • всякие if ... else .. endif в react системе тоже отсутствуют. а в qml часто реализуются видимостью одного из вариантов. это плохо тем, что все эти ветки реально есть в дереве и мутируют, хотя и не видны (лишняя работа и большое число проверок условий итд)

Пример как делается условие и цикл в “React JSX way”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const products = [{
name: "onion",
price: ".99",
id: 1
}, {
name: "pepper",
price: "1.25",
id: 2
}, {
name: "broccoli",
price: "3.00",
id: 3
}];
// function. return jsx "model" (jsx code handle by spec prerprocessor and convert to model (vdom) builder)
const TableRow = ({row}) => (
<tr>
<td key={row.name}>{row.name}</td>
<td key={row.id}>{row.id}</td>
<td key={row.price}>{row.price}</td>
</tr>
)
// function. return jsx "model"
const Table = ({data}) = (
<table>
{data.map(row => {
<TableRow row={row} />
})}
</table>
)
ReactDOM.render(
<Table data={products} />,
document.getElementById("root")
);

Лямбда map это - цикл. там же может быть и filter сразу:

1
2
3
{data.map(row => {
<TableRow row={row} />
})}

if - будет тернарный оператор , который возвращает один или второй (или пустой) варианты. т.е. нет управления видимостью и отображения ненужного.

Идея

Представьте себе модуль типа такого . Он упрощен - нет Repeater. Вместо него есть две строки комментария.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
import QtQuick.Window 2.2

Window {
id: wnd
objectName : "wnd" // from QObject
width: 400
height: 500
visible: true

ColumnLayout {
id: colLay
objectName : "colLay" // from QObject
width: 200
x: (Window.width - width) / 2
y: (Window.height - height) / 2

Text {
id: t1
objectName : "t1" // from QObject
text: "Welcome to QML"
Layout.fillWidth: true
font.pointSize: 20
}
// Тут должны быть повторяющиеся элементы,
// которые обычно генерируются Repeater по какой-то модели
Text {
id: t2
objectName : "t2" // from QObject
text: "To get started, edit App.qml"
color: "#333333"
Layout.fillWidth: true
}
}
}

В данном контексте нет внешних обращений.

Я предлагаю два режима

  1. разработка компонентов и наладка их работы (с mock моделями данных)
  2. использование компонентов (с настоящими моделями)

Режимы можно совмещать : почти все приложение в режиме 2, только один компонент в режиме 1, который разрабатываем.

Режим разработки компонента

Пишем такой код файла с разметкой:

Тут CC это “специальное имя” , эта строка в рабочем режиме пропадет, а СС будет замена в контексте на “магию”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// file : In.qml
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
import QtQuick.Window 2.2

import "Ract.js" as React
import "someJsFile.js" as CC // Component Context

Window {
id: wnd
objectName : "wnd" // from QObject
width: 400
height: 500
visible: true

ColumnLayout {
id: colLay
objectName : "colLay" // from QObject
width: 200
x: (Window.width - width) / 2
y: (Window.height - height) / 2

Text {
id: t1
objectName : "t1" // from QObject
text: "Welcome to QML"
Layout.fillWidth: true
font.pointSize: 20
}

// Тут должны быть повторяющиеся элементы,
// которые обычно генерируются Repeater по какой-то модели
React.Placeholer {
id: ph1
func: CC.blockBetwenT1T2
}

Text {
id: t2
objectName : "t2" // from QObject
text: "To get started, edit App.qml"
color: "#333333"
Layout.fillWidth: true
}
}
}

Файл “someJsFile.js”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// file : someJsFile.js
text = Qt.binding( function(){return "Welcome to QML"} )
color = Qt.binding( function(){return "#333333"} )
function setT2Width(width){
print('width', width)
}
function t2Clicked(){
print('clicked')
}
function blockBetwenT1T2(){
// createObject from qml with Repeater or just some items or just
return [ Qt.createQmlObject('import QtQuick 2.0; Rectangle {color: "red"; width: 20; height: 20}',
parentItem,
"dynamicSnippet1") ] ;
}

React.Placeholer

  1. get self content items from function
  2. get to self parent and find self children index by self id check
  3. insert siblings before self
  4. React.Placeholer - not remove self from parent, but set visible=false and set 0x0 size

React.Placeholer usage examples

1
2
3
4
5
6
7
8
9
10
// load mock elemets from other module
React.Placeholer {
id: ph1
func: React.LoadPlaceholderFrom("some.qml")
}
// gen mock element from Context function
React.Placeholer {
id: ph1
func: CC.blockBetwenT1T2
}

Это покрывает более 80% сценариев. + если функция вернет [] это как бы условная if ... endif логика.

Такой код легко отлаживать.

Режим нормальной работы

Файл In.qml обрабатывается утилитой превращаясь в In_compiled.qml . Импорт js файла с сонтекстом CC удаляется. _compiled файл добавляется со случайным именем.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// file : In_compiled.qml
// THIS FILE ARE AUTO GENERATED. DO NOT CHANGE IT BY HAND.
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
import QtQuick.Window 2.2

import "Ract.js" as React
// for internbal Component Logic
import "In.qml_logic.js" as CCWORK
// for QSyncable To QML createObject "draw logic"
import "In.qml_compiled.js" as SGHG

Window {
id: wnd
objectName : "wnd" // from QObject
width: 400
height: 500
visible: true

// reserved prop name
property var prop ;

Component.onCompleted: {
SGHG.initProp(prop);
// init internal conext of component
CCWORK.init(wnd, prop, SGHG);
SGHG.reactRender();
}
}

Внутри JS файла “In.qml_compiled.js” :

  1. функции для инициализации модели через задание свойств. и только. никаких левух эффектов.
  2. Функции реагирования на модель, которые перестараивают instance, состояние которого функции хранят в самом элементе.
  3. ожидается наличие калбеков для получения кодом из этого файла элементов для некоторых своих частей. по умолчнию - пустой список.

Внутрянка компонента

Файл “In.qml_logic.js” пишем руками определяя внутренний контекст и все калбеки для внутреннего реагирования. Это одна функция - запускается при инициализации инстанса компонента. Настраивает все калбеки для внутренней логики контрола и сайд эффекты. перестранивает модель данных контекста.

Итого там доступно две модели

  1. модель контекста которая приехала из режима разработки - это то что становится свойствами компонента
  2. модель виртуального DOM, так сказать, если так можно сказать. (не знаю зачем, для хаков наверное. “правильные пацаны” смогут написать так, чтобы туда не пришлось лазать)

Использование компонента:

1
2
3
4
5
6
7
8
9
10
11
12
13
In_compiled {
prop: {
text: 'tttt',
color: 'red',
setT2Width: function(width){
print('width', width)
},
t2Clicked : function(){
print('clicked')
},
blockBetwenT1T2: some.Path.To.CPP.Model
}
}

Как оно внутри работает ?

Есть преобразователь QSyncable список в особого вида в “настроенные элементы”. QSyncable модели внутри используются как промужуточное представление (виртуальный DOM) для перестроения интерфейса (надо отнаследоваться конечно):

  • когда qml компилируется утилитой в _compiled это создает код для работы с QSyncable моделью этой структуры.
  • запуск функции SGHG.reactRender();
    • вычисляет эту структуру (как из json) и по ней строит QML дерево элементов, к которому привязывает каллбеки для сигналов, которые нужны
    • отображаемая структура кешируется
    • функции меняют эту структуру, происходит сравнение, применяется патч, и QML дерево с процессе этого перестраивается

Что это все даёт ? GOALS

  1. Всё ещё можно разрабатывать блоки и компоненты на QML обычным образом имея мощные модели которые легко стыкуются с с++
  2. Не нужны: репитеры, уловки с видимостью,
  3. можно описывать qml компоненты в стиле реакт на хуках имея, возможность включить в моделях mobx поведение - это очень круто, наглядно, чисто, …. много плюсов.
  4. можно менять управлять UI из C++ & JS через изменение модели в стиле UI QT Designer как бы меняя виджеты. Это устаревший подход, но он более безопасен т.к. управление происходит моделью UI а не самими элементами внутри engine.
  5. При этом можно сделать все изменения и потом применитиь их все разом - очень повышается производительность.
  6. У теперь настоящих компонентов, появляются настоящие “свойства”. Не требуется ничего “пробрасывать”, не требуется ничего перекрывать.

Драйвер создания массы крутых компонентов.

Наикрутейший потенциал для технологии во всех таргетах: мобайл, веб, десктоп…. новые мобильные и аппаратные платформы.

Я бы даже инвесторов поискал. ;-) хз.

Блииин….. понимаю, что очень крупная замута.

Как ее уменьшить и побить на куски? ключевые куски

  1. по наследнику QSyncable модели (назвать к примеру QSybcableQMLModel ) построить динамически QML объект.
    1. [1w] загрузить нужные модули и сделать компоненты доступными
    2. [2w] построить сам объект
    3. [2w] доработать наследника так, чтобы он был Patchable но чтобы управляемый QML при патчинге тоже перестраивался
  2. [1w] распарсить qml в json
  3. [2w] уже по этому json(qml) сгенерировать JS + данные для соответствующей QSybcableQMLModel модели в внутренние функции для работы компонента.
  4. [2+w] Главный JS модуль ReactQml с хелперами для связывания всего этого в целостное приложение. (соединения нескольких )

Минимальная Трудоёмкость (точность + 50%-100%) 1+2+2+1+2+2=10 недель. реально 20 недель.

При этом придется “упороться”, нужна мотивация “интересная задача, хочу сделать!”

И конкурент уже есть, походу https://proton-native.js.org/#/?id=proton-native .

Мнение по этому конкуренту:

у них, по моему, плохо, что:

  • JS
  • не вполне QT , больше выглядит как развитие “React Native”

Хорошо:

  • wxWidgets поддерживать пытаются. т.е. это другой бекенд (не QT)

Итого по ним и сравнение с моей идеей:

  • они воспринимают JSX XML как единственно верный синтаксис для компонентов в мире для любых бекендов, я считаю это неестественным.
  • Они считают, что JS - язык описания логики UI , я считаю, что важнее иметь модель виртуального DOM которая будет доступна с разных языков, с++ в том числе.
  • я более таргетирован в QML и QT , хочу в этом бекенде “навести порядок”. Они сосредоточились на внешней форме компонентов, в первую очередь, так, чтобы предоставить для JS сообщества нативные бекенды отрисовки (распознаю это как новый тренд, который на волне успеха React Native?)
    • Это значит что цели у нас все таки разные….
    • ЦА у них : JS програмисты , компании , расширяющие сферу заказов с веб на native
    • ЦА у меня: QT сообщество, компании, расширяющие сверу заказов с Qt на веб и другие таргеты

___Всякое

Правильный входной код компонента должен быть НЕ (Хотя, то, что ниже будет уже очень круто. Минус только , что XML и много JS внутри)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from "react";
import { render, Window, ColumnLayout, Text } from "react-qml";

const styles = {
window: {
width: 400,
height: 500
},
title: {
fontSize: 20
},
subtitle: {
color: "#333"
}
};

const App = () => (
<Window visible style={styles.window}>
<ColumnLayout width={200} anchors={{ centerIn: "parent" }}>
<Text
text="Welcome to React QML"
Layout={{ fillWidth: true }}
style={styles.title}
/>
<Text
text="To get started, edit index.js"
Layout={{ fillWidth: true }}
style={styles.subtitle}
/>
</ColumnLayout>
</Window>
);

export default () => render(<App />);

а , лучше бы он был типа такого :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import QtQuick 2.4
import QtQuick.Controls 1.3
import "js/ReactQml.js" as React
import "In.qml.js" as InQml

ApplicationWindow {
id: root
title: qsTr("React QML")
width: 300
height: 300
visible: true

function reactRender(x, y) {
// from model
/* var props = {
x: 100,
y: 100,
width: 100,
height: 100,
color: '#000'
};*/

// from model
/* var childProps = {
x: 25,
y: 25,
width: 50,
height: 50,
color: '#fff'
};*/
var c1
var child = React.createElement(React.Rectangle, childProps);
React.render(React.createElement(React.Rectangle, props, child), root);
}

Component.onCompleted: {
reactRender();
}
}

Но код создания и управления элементами генерить из qml … Это то, что остаётся сделать.