Сучасний підхід до розробки на Qt/QML та MVC

💡 Усі статті, обговорення, новини про дизайн — в одному місці. Приєднуйтесь до Design спільноти!

Привіт, спільното! Мене звати Олег, і я працюю вже два роки в компанії Canyon Development, яка займається розробкою різноманітних девайсів. Моя основна діяльність — розробка графічних застосунків для embedded-систем компанії.

Сьогодні поговоримо про Qt. У більшості випадків, коли потрібно створити складний інтерфейс, розробники обирають Qt завдяки його швидкості, функціональності та кросплатформеності. Це дозволяє легко розробляти та переносити рішення між різними платформами, такими як MCU, MPU, Desktop, Android тощо.

Останнім часом я все частіше стикаюся з проєктами новачків, які тільки починають знайомство з Qt, і, чесно кажучи, деякі з них виглядають доволі хаотично. Тому я вирішив поділитися своїм досвідом та віковою мудрістю. Ця стаття допоможе вам розібратися в актуальних технологіях і практиках роботи з Qt, щоб ваші проєкти були більш якісними.

QML introduction

Почнемо здалеку, тобто з початку. Перший реліз Qt вийшов у 1995 році, і в ті часи вимоги до графічного інтерфейсу були значно простішими, якщо він взагалі був.

З роками вимоги зростали, старі підходи застарівали, і в 2009 році з’явилася перша версія QML (Qt Modeling Language). Нова мова запропонувала новий декларативний підхід до створення інтерфейсів, заснований на компонентах. Крім того, QML значно покращив роботу з анімаціями та ефектами.

Перші версії QtQuick/QML орієнтувалися переважно на мобільні платформи, оскільки на той час Qt належав Nokia (якщо хтось ще її пам’ятає). Через це QML мав певні труднощі із запуском на десктопних системах. Можливо, саме тому в головах багатьох людей він закріпився як щось суто «для мобілок», а для серйозних проєктів нібито й далі слід використовувати QtWidgets — бо так робили наші пращури.

Наші предки створюють свою першу багатовіконну програму

Більшість цих проблем було вирішено з випуском QtQuick 2 та Qt 5. Сьогодні актуальна версія — Qt 6, і QML став одним із найзручніших способів створення кросплатформенних застосунків із графічним інтерфейсом.

Ну добре, скажете ви. Зрозуміло, що QML — це БАЗА, але що далі? Що це взагалі таке і як із ним працювати?

Про це я зараз і розповім. QML (Qt Meta Language або Qt Modeling Language) — це декларативна мова програмування, створена спеціально для розробки графічних інтерфейсів. Цей інструмент дозволяє описувати структуру UI зрозумілою та компактною мовою, спрощуючи створення динамічних і анімованих інтерфейсів.

Типовий код на QML виглядає приблизно так:

import QtQuick
import QtQuick.Controls
Window {
    id: window
    width: 647
    height: 700
    title: "Example"
    property int clickCounter: 0
    Button {
        id: button
        x: 220
        y: 517
        width: 207
        height: 91
        text: qsTr("Button")
        onClicked: {
            clickCounter++
            console.log(clickCounter)
        }
    }
    Text {
        id: _text
        y: 167
        text: clickCounter
        font.pixelSize: 47
        anchors.horizontalCenter: parent.horizontalCenter
    }
}

Це звичайний лічильник натискань кнопки.

Приклад роботи програми:

Розберемо написане. Цей код містить декларативну та імперативну частини.

Грубо кажучи, імперативний код — це набір інструкцій, які виконуються послідовно, визначаючи, як саме досягти певного результату. Натомість декларативний код описує очікуваний результат, не вказуючи, яким чином його отримати.

Якщо ви раніше не працювали з таким підходом, різниця може здатися неочевидною. Давайте розглянемо це більш детально.

        onClicked: {
            clickCounter++
            console.log(clickCounter)
        }

Це обробка сигналу clicked() для нашої кнопки. Все, що знаходится в {...}, є імперативним кодом на JavaScript і виконується поступово (построчно).

А от все інше тут це декларативний код (крім імпортів).

Text {
        id: _text
        y: 167
        text: clickCounter
        font.pixelSize: 47
        anchors.horizontalCenter: parent.horizontalCenter
    }

Ось тут, наприклад, описується текстове поле. Зверніть увагу на рядок text: clickCounter — тут відбувається binding («прив’язка») змінної clickCounter до властивості text. Це одна з ключових концепцій у QML.

Простіше кажучи, якщо в ході виконання програми значення clickCounter зміниться, всі місця, де воно використовується в декларативній частині, автоматично оновляться. Це дуже зручно, оскільки позбавляє необхідності вручну працювати з сигналами та хендлерами.

Тепер поглянемо на сам блок Text { ... }. У QML подібна конструкція називається компонентом. Якщо коротко пояснити, що таке компонент у QML, то це щось середнє між класом і екземпляром цього класу.

Якщо ви звикли до імперативного підходу, спочатку може бути складно зрозуміти, як це працює. Однак, якщо ви колись користувалися Figma, то вже знайомі з принципами роботи компонентів — у QML вони працюють дуже схоже. Також компонентний підхід широко використовується у вебфреймворках, таких як React, що робить його сучасним і зручним способом створення UI.

Оскільки QML декларативний, ми можемо вказувати property та створювати компоненти у будь якому порядку:

Window {
    id: window
    width: 647
    height: 700
    title: "Example"
    Button {
        ........
    }
    Text {
        ........
    }
    property int clickCounter: 0
}

І це все одно працюватиме, адже QML-код не виконується построково, як традиційні імперативні мови.

Структура цього коду може нагадати вам XML або HTML. І це не випадково, адже вони теж є декларативними.

Можливо, дехто з вас згадає таку програму, як Qt Designer (не плутати з Qt Design Studio — про нього розповім далі). Вона дозволяє створювати інтерфейси на основі Qt Widgets за допомогою графічного редактора, а на виході формує .ui файли, які, по суті, є звичайними XML-файлами з іншою назвою.

Далі ці .ui -файли можна:

  • автоматично конвертувати в C++ класи (що є найпоширенішим підходом);
  • завантажувати напряму в програму (але це може впливати на продуктивність, адже .ui-файли все одно потрібно парсити під час запуску).

Ті, хто працював із Qt Designer, знають, що він має чимало обмежень:

  • кастомізація віджетів досить обмежена — доводиться використовувати StyleSheet, які написані на якомусь псевдо-CSS;
  • створення власних віджетів — це окрема проблема. Їх доводиться писати вручну та підключати до програми самостійно (вони не відображатимуться в preview);
  • є можливість створення власних плагінів для Qt Designer, але це вже зовсім складний і заплутаний процес, який вимагає багато часу та зусиль.

Також варто зазначити, що хоча Qt Designer все ще підтримується розробниками, важливих оновлень чи покращень він не отримував вже дуже давно.

На зміну йому прийшов Qt Design Studio (далі — Qt DS) — сучасний інструмент, орієнтований на роботу з QML. І найкраще те, що в ньому вирішили всі згадані раніше проблеми.

Qt Design Studio

Крім того, Qt DS приніс багато нових інструментів, які були б неможливі у Qt Designer. Серед них:

  • state View — для зручного управління станами інтерфейсу;
  • робота з 3D-графікою — інтеграція 3D-об’єктів прямо в QML;
  • графічне керування прив’язками та сигналами — тепер можна налаштовувати їх безпосередньо у візуальному редакторі;
  • редактор коду — дозволяє одночасно бачити та редагувати згенерований QML-код;
  • timeline та Curves View — для зручного створення анімацій.

Крім того, Qt DS активно підтримується розробниками, постійно виходять нові оновлення з виправленнями багів та розширенням функціоналу. Наприклад, нещодавно була додана підтримка шейдерних ефектів.

Ця стаття не ставить за мету навчити вас користуватися Qt DS або писати на QML. Все, що було описано вище, наведено для ознайомлення з можливостями QML.

Якщо хочете дізнатися більше, ви знайдете відповіді у Qt Academy або в офіційній книзі по QML.

Головна думка цього розділу: QML — це сучасніший та зручніший підхід, ніж QtWidgets.

Звісно, залишаються ситуації, де старий підхід все ще виправданий (не варто забувати і про легасі), але з кожною новою версією таких ситуацій стає все менше і менше.

Тепер, коли ми розібралися з інструментами, пропоную перейти до більш високорівневих тем.

Архітектура MVC

Раніше ми говорили про БАЗУ, яку потрібно знати, щоб писати ефективний та структурований код на Qt, зараз же ми переходимо до цілого ГЕНШТАБУ, до MVC архітектури. Її також інколи називають патерном, хоча різниця між архітектурою, патерном проєктування та архітектурним патерном може бути предметом дискусій. Сеньйори-помідори, прошу в коментарі!

MVC була розроблена наприкінці 1970-х років у компанії Xerox для створення графічних інтерфейсів комп’ютера Xerox Alto, який, по суті, став першим пристроєм із сучасним користувацьким досвідом — із мишею, вікнами та іншими знайомими елементами. Згодом від MVC відокремилися інші архітектурні підходи (MVP, MVA, HMVC, MVVM), але її основні ідеї виявилися настільки вдалими, що використовуються й до сьогодні.

Що ж таке MVC? Це концепція, яка передбачає розділення коду на три основні частини:

  1. Model — зберігає дані, з якими працює програма (модель — це не база даних!);
  2. View — відображає дані з моделі та реагує на їхні зміни;
  3. Controller — містить бізнес-логіку програми та змінює модель.

Звучить складно? Так? 🤓 Перед тим, як перейти до роз’яснень, хочу підкреслити: архітектура — це не щось жорстко фіксоване, а радше загальна абстрактна концепція. Її конкретна реалізація може відрізнятися залежно від мови програмування та фреймворку. Я лише покажу свою реалізацію MVC з Qt.

Свого часу мені ці визначення теж здавалися незрозумілими, поки я нарешті не збагнув...

Простими словами, основна ідея MVC — відокремлення коду, який відповідає за графічну частину програми (View) від бізнес-логіки (Controller). Модель (Model) же служить для зручної взаємодії між ними.

Ще простіше: код, який відповідає за вигляд кнопки — її колір, розташування та анімацію, —це View. А код, що визначає, що ця кнопка робить (відправляє дані, виконує розрахунки чи перевіряє умови), —це Controller.

Повертаючись до нашого прикладу на QML.

Цю частину коду можна вважати нашим контролером:

        onClicked: {
            clickCounter++
            console.log(clickCounter)
        }

Property clickCounter зберігає дані нашої програми, тому його можна вважати місцевою моделлю.

    property int clickCounter: 0

Весь інший код це View, оскільки він відповідальний за графічну частину.

І що, це все? Так просто? Ми вже написали програму з архітектурою MVC? Ну, не зовсім.

Проблема в тому, що handler і clickCounter, які ніби є нашим контролером і моделлю, прив’язані до конкретних компонентів. У невеликій програмі це не критично, але якщо програма розростається, отримати доступ до них і керувати станом стає проблематично.

Щоб вирішити цю проблему, зазвичай контролер і модель пишуть на C++ або Python і реєструють у View. Таким чином, вони існують незалежно від конкретних компонентів і можуть використовуватися в різних частинах програми.

Ось приклад. Я створив Qt-проєкт через Qt Creator у тій же папці, що й Qt DS. Весь код проєкту ви можете знайти тут.

Так виглядає наша модель у файлі model.h:

#ifndef MODEL_H
#define MODEL_H
#include <QObject>
class Model : public QObject {
    Q_OBJECT
    Q_PROPERTY(int counter READ getCounter WRITE setCounter NOTIFY counterChanged)
signals:
    void counterChanged(int newCounter);
public:
    explicit Model(QObject *parent = nullptr): QObject(parent) {
    };
    int getCounter() {
        return m_counter;
    };
    void setCounter(int counter) {
        m_counter = counter;
        emit counterChanged(m_counter);
    };
private:
    int m_counter = 0;
};
#endif //MODEL_H

А це наш контролер:

#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <QObject>
#include "Model/model.h"
class Controller : public QObject {
   Q_OBJECT
public:
   explicit Controller(Model *myModel, QObject *parent = nullptr): QObject(parent), model(myModel) {
    };
public slots:
   void incrementCounter() {
       model->setCounter(model->getCounter() + 1);
    };
private:
   Model *model;
};
#endif //CONTROLLER_H

Зверніть увагу: всі об’єкти, які ми будемо використовувати в QML, повинні успадковуватися від QObject і бути позначені макросом Q_OBJECT.

Крім того, щоб зробити змінні та методи доступними в QML, потрібно реєструвати їх за допомогою макросів:

  • Q_PROPERTY — для властивостей, які ми хочемо використовувати в QML.
  • Q_INVOKABLE — для методів, які можна викликати безпосередньо з QML (альтернативний варіант—реєстрація через public slots:).

Зверніть увагу на рядки Q_PROPERTY(int counter READ getCounter WRITE setCounter NOTIFY counterChanged) та model->setCounter(model->getCounter() + 1);: саме сюди тепер перемістилася наша змінна counter і функція її інкременту.

Тепер, щоб мати можливість викликати нашу модель і контролер з QML, їх потрібно зареєструвати як ContextProperty під відповідними іменами. Це робиться так:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "Controller/controller.h"
#include "Model/model.h"
int main(int argc, char *argv[]) {
    QGuiApplication app(argc, argv);
    Model myModel;
    Controller myController(&myModel);
    QQmlApplicationEngine myView;
    
    //Registration in View
    //       ||
    //       ||
    //       \/
    myView.rootContext()->setContextProperty("myModel",&myModel);
    myView.rootContext()->setContextProperty("myController",&myController);
    QObject::connect(
        &myView, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);
    myView.loadFromModule("Example", "App");
    return app.exec();
}

Тепер ми можемо переписати наш App.qml

import QtQuick
import QtQuick.Controls
Window {
    id: window
    width: 647
    height: 700
    visible: true
    title: "Example"
    Button {
        id: button
        x: 220
        y: 517
        width: 207
        height: 91
        text: qsTr("Button")
        onClicked: {
            myController.incrementCounter()
        }
    }
    Text {
        id: _text
        y: 167
        text: myModel.counter
        font.pixelSize: 47
        anchors.horizontalCenter: parent.horizontalCenter
    }
}

Результат виглядатиме так само, як і раніше, але тепер ми можемо справедливо назвати це повноцінною MVC-архітектурою. На перший погляд може здатися, що я навмисно ускладнив код і що все можна було б зробити простіше. «І нащо мені твій QML з MVC? Буду і далі писати все в купу на віджетах і горя не знати.»

Ну дивись. Наведу такий схематичний приклад.

Приклад реалізації на QtWidgets

Допустимо, ми вирішили розширити проєкт і тепер нам потрібен QTabWidget, де:

  • Перша вкладка містить усе, що ми вже реалізували.
  • Друга вкладка є розширеним варіантом першої, але з додатковими функціями.

У реальних проєктах такий підхід зустрічається досить часто, наприклад, у «Advanced Settings», де базовий і розширений режими мають спільні елементи, але різний набір можливостей.

Розширена версія програми

Швидше за все, ти захочеш створити базовий віджет MyWidget, щоб не дублювати весь код дизайну. Але вже на цьому етапі виникає проблема: кожен новий екземпляр MyWidget створює свою власну counter-змінну та метод counterIncrement().

У результаті кожен counterIncrement() змінює свій власний counter, а не спільний для всієї програми. Щоб цього уникнути, доведеться винести counter у спільний батьківський клас і звертатися до нього через parent. Вийде щось на кшталт: window().tabWidget.counter. Але навіть якщо ми це зробимо, залишається ще одна проблема: зміни counter потрібно відобразити у двох QLabel. У великій програмі це може бути доволі складно, але в нашому простому випадку можна скористатися сигналом, який буде спрацьовувати щоразу, коли змінюється counter.

Ну добре, цю проблему нам вдалося вирішити відносно безболісно. Давай тепер ускладнимо ситуацію.

Допустимо, що одна з функцій у Some Advanced Settings повинна змінювати поведінку MyWidget на другій вкладці. Тепер наша кнопка більше не інкрементує значення counter, а декрементує його, тобто віднімає одиницю.

Зміна поведінки програми

Є кілька способів реалізувати цю програму.

  1. Додати два стани й дві поведінки в базовий клас. Наприклад, ввести додатковий bool-флаг, від якого залежатиме, чи виконується counterIncrement(), чи counterDecrement().
  2. Створити новий клас, який успадковує MyWidget і розширює його функціонал методом counterDecrement().

На перший погляд другий варіант здається більш правильним. Але насправді обидва підходи мають проблеми.

У реальних складних проєктах можуть бути десятки подібних changeBehavior() із counter property, які ще й взаємопов’язані. У root-віджеті хаотично накопичуються всі property і сигнали, а разом із ними — й безліч інших функцій, які повинен виконувати root-віджет.

Зрештою, всі ці changeBehavior(), counterIncrement() і counterDecrement() переплітаються у величезний клубок, у якому постійно щось ламається і який ніхто не зможе розплутати. Саме такі проблеми й вирішує MVC-архітектура.

Як я вже казав, архітектура — це абстрактне рішення, яке можна інтерпретувати по-різному. Навіть у QWidgets можна побудувати грамотну архітектуру, але в QML це зробити набагато простіше. Чому?

  • у QML є прив’язки та компоненти, що дозволяють зручно розділяти логіку;
  • частина програми пишеться іншою мовою (C++/Python), що природно відокремлює Controller від View;
  • обмежені можливості JavaScript у QML не дозволяють написати занадто складну й заплутану бізнес-логіку.

Далі я наведу приклад реалізації ситуації, схожої на описану вище.

Приклад роботи на QML:

Розберемо зміни в коді:

  • В моделі ми додали додаткове decrementEnabled Property.
class Model : public QObject {
    Q_OBJECT
    Q_PROPERTY(int counter READ getCounter WRITE setCounter NOTIFY counterChanged)
    Q_PROPERTY(bool decrementEnabled READ getDecrementEnabled WRITE setDecrementEnabled NOTIFY decrementEnabledChanged)
signals:
    void counterChanged(int newCounter);
    void decrementEnabledChanged(bool newDecrementEnabled);
public:
    explicit Model(QObject *parent = nullptr): QObject(parent) {
    };
    int getCounter() {
        return m_counter;
    };
    void setCounter(int counter) {
        m_counter = counter;
        emit counterChanged(m_counter);
    };
    bool getDecrementEnabled() {
        return m_decrementEnabled;
    }
    void setDecrementEnabled(bool decrementEnabled) {
        m_decrementEnabled = decrementEnabled;
        emit decrementEnabledChanged(m_decrementEnabled);
    }
private:
    int m_counter = 0;
    bool m_decrementEnabled = false;
};
  • В контролері додали функцію changeBehavior() та поміняли incrementCounter() на універсальну buttonAction().
class Controller : public QObject {
    Q_OBJECT
public:
    explicit Controller(Model *myModel, QObject *parent = nullptr): QObject(parent), model(myModel) {
    };
public slots:
    void buttonAction() {
        if (model->getDecrementEnabled()) {
            model->setCounter(model->getCounter() - 1);
        }
        else {
            model->setCounter(model->getCounter() + 1);
        }
    }
    void changeBehavior() {
        model->setDecrementEnabled(!model->getDecrementEnabled());
    }
private:
    Model *model;
};
  • Ми винесли віджет в окремий файл My_item.qml. У QML саме так створюються власні кастомні компоненти. Тепер цей компонент можна використовувати в коді під ім’ям, яке збігається з назвою файлу, тобто просто My_item.
// My_item.qml
import QtQuick
import QtQuick.Controls
Item {
    id: _item
    width: 647
    height: 700
    Button {
        id: button
        x: 220
        y: 517
        width: 207
        height: 91
        text: qsTr("Button")
        onClicked: {
            myController.buttonAction()
        }
    }
    Text {
        id: _text
        x: 106
        y: 167
        text: myModel.counter
        font.pixelSize: 47
        anchors.horizontalCenter: parent.horizontalCenter
    }
}
  • У головному файлі App.qml ми створили дві сторінки та модифікували одну з них, додавши додаткову кнопку і індикатор поведінки.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Window {
    id: window
    width: 647
    height: 700
    visible: true
    title: "Example"
    TabBar {
        id: bar
        width: parent.width
        TabButton {
            text: qsTr("Page1")
        }
        TabButton {
            text: qsTr("Page2")
        }
    }
    StackLayout {
        width: parent.width
        currentIndex: bar.currentIndex
        My_item {
            id: page1
        }
        My_item {
            id: page2
            Text {
                id: buttonActionLabel
                text: myModel.decrementEnabled ? "-1" : "+1"
                anchors.bottom: changeBehaviorButton.top
                anchors.bottomMargin: 20
                anchors.horizontalCenter: parent.horizontalCenter
            }
            Button {
                id: changeBehaviorButton
                text: qsTr("Change Behavior")
                anchors.bottom: parent.bottom
                anchors.bottomMargin: 200
                anchors.horizontalCenter: parent.horizontalCenter
                onClicked: {
                    myController.changeBehavior()
                }
            }
        }
    }
}

З таким підходом написати заплутаний код набагато складніше. Далі я розкажу ще декілька нюансів про роботу з MVC-архітектурою.

Recommendations

Structure of the model

У нашому випадку програма дуже проста, тому модель не потребує складної структури. Але в великих проєктах її краще розділяти. Я зазвичай використовую деревовидну структуру моделі, де property може бути: простою змінною, що містить дані, або об’єктом класу, якому належать інші змінні.

Приклад структури моделі

Такий підхід особливо зручний при використанні NoSQL баз даних (наприклад, MongoDB), оскільки структура моделі збігатиметься зі структурою даних у базі, що значно спрощує розробку.

QML Threading

Тепер View (GUI Thread) та Controller працюють у різних потоках. Це означає, що потрібно стежити за синхронізацією між View і Controller. Наприклад, якщо в контролері викликати довготривалу функцію, інтерфейс буде чекати відповіді, а разом із ним зависне користувач — з’являться фризи.

Проблема синхронізації

Цю проблему можна вирішити, виконавши функцію в окремому потоці.

В Qt є власні механізми асинхронності, які необхідно використовувати, щоб коректно працювали сигнали, таймери та інші механізми. Серед них:

  • QThread — для створення окремих потоків;
  • QThreadPool — для керування групою потоків;
  • QtConcurrent — для простого запуску асинхронних завдань без необхідності створювати потоки вручну.

Однак може виникнути ще одна проблема — якщо завдання має повернути результат після виконання. Щоб правильно обробити цю ситуацію, можна використати патерн Promise. Його суть у тому, що перед запуском завдання ми одразу повертаємо об’єкт, який містить:

  • порожнє поле для результату,
  • сигнал, що спрацьовує після завершення роботи.

View підписується на цей сигнал і очікує, поки з’явиться результат. На діаграмі це виглядало б приблизно так:

Приклад асинхронного виконання

Це, в принципі, все, що я хотів розповісти про QML та MVC. Якщо у вас залишилися питання — пишіть у коментарях, із радістю відповім!

👍ПодобаєтьсяСподобалось14
До обраногоВ обраному10
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Хорошая статья, хотелось бы добавить только 2 детали о которых не знают могие новички и синьеры-помидоры которых я встречал.
1 — у QML есть свой код-стайл документ который, к сожалению, даже сами Qt часто игнорируют. Но на мой взгляд он вполне удачный и очень помогает особенно в больших проектах с большим кол-вом разработчиков.
2 — В самом QML есть опции для использования асинхронных вычислений, без прибегания к C++. рекомндую обратить внимание на asynchronous свойство у Loader и Image, WorkerScript, Animation vs Animator.

Great work!
Thanks for author.

Велика дяка шановному пану автору статті за збагачення знань нашої ІТ спільноти по QML та MVC.

Підкажіть, будь ласка, чи був у вас досвід написання 3D UI на Qt?

У мене був досвід роботи з OpenGL та ImGui. У QML я пробував працювати з 3D, але поки що не було підходящої задачі — лише для експериментів. 3D в інтерфейсах поки що не надто корисне, хіба що для візуалізації даних і створення красивих «вундервафель».

Виходячи з вашого досвіду, які саме технології ви б порадили обрати для написання 3D UI?

Не знаю, що вам саме потрібно. Якщо використовувати OpenGL та ImGui, це дасть більше контролю над 3D-частиною, але сам інтерфейс ImGui не надто пристосований для кастомізації й більше орієнтований на те, щоб бути простим оверлеєм.

QML надає більше можливостей у 2D і менше у 3D, проте варто зазначити, що в Qt Design Studio є вбудовані функції для роботи з 3D, що значно полегшують розробку.

Обирайте залежно від ваших потреб.

А якщо орієнтуватися на програмування навігації по просторовому графу?

QML наче були якісь можливості для 3D візуалізації наукової графіки. Думаю їх повинно вистачити

Дякую.
Було б також цікаво почитати про ColumnLayout, RowLayout та GridLayout. Якось в них неочевидно відбувається розміщення елементів та обчислення їх розмірів.

Мені здається, що там усе зрозуміло. Вказуєш spacing, виставляєш прив’язки для anchors і все. А ось sizePolicy у Qt Widgets була справді дивною, де Minimum і Maximum робили протилежне до їхніх назв.

мені теж так здавалось. в реальності зробити лейаут з кількох колонок та рядків і розмістити на них контроли та кнопки так, щоб вони були там де треба і воно все було б адаптивним до зміни розмуру... ну довелось постраждати. Для порівняння, такий самий лейаут в Avalonia UI (аналог WPF) на XAML я зробив менше ніж за годину.

Підписатись на коментарі