Привет, Хабровчане!

Последнее время часто встречаю проекты для desktop, которые необходимо портировать с QWidget на QML. Кто-то хочет написать с нуля, кто-то перенести старые наработки. В любом из сценариев это популяризация QML, чему я очень рад. Я решил тоже побыть полезным и поделиться своими наработками и ситуациями из личной практики. Возможно, некоторые из них покажутся вам простыми и понятными, однако если я что-то упустил, буду рад почитать об этом в комментариях.

Кратко о QML и QWidget


QML — это декларативный язык программирования. Он позволяет разработчикам и дизайнерам создавать удобные, плавно анимированные и визуально привлекательные приложения. QML предлагает читаемый декларативный JSON-подобный синтаксис с поддержкой императивных выражений JavaScript в сочетании с динамическими привязками свойств.

QWidget — это предшествующая QML технология для создания пользовательского интерфейса, основанная на использовании заранее созданных desktop-style компонентов и предоставляющая API для взаимодействия с ними.

Пользовательский опыт


Около года назад я нашел прекрасный инструмент для работы с гитом gitAhead. Он написан на QWidget. В целом я доволен большей частью функционала и он мне часто помогает. Но на этом примере прекрасно видно, что виджетам не хватает гибкости и функционала. Из-за этого невозможно покрыть больше пользовательских сценариев и сделать более дружелюбный интерфейс. В итоге вместо отличного дерева коммитов и дифов мы имеем огромное количество ограничений.

Стандартное окно(кликабельно)


Дизайн приложения и его влияние на портирование


Классически Qt нам советует придерживаться паттернов MVC\MVP, где всё взаимодействие с UI вынесено в отдельные сущности, что позволяет нам изменять их, не затрагивая остальные части приложений и бизнес-логику. Абстрагирование элементов и возможность встраивать QML-компонент в стандартный QWidget позволяет гибко подойти к плану дальнейшей работы. Если вы сейчас на этом пункте, то я бы хотел вам посоветовать продумать требования и функциональность будущего приложения, нарисовать его макет, посоветоваться с дизайнерами, опросить пользователей или заказчика. В целом решить как вы будете жить ближайшее время, и какие вопросы будете решать. Ниже представлены варианты, по которым может пойти проект.

План А «С чистого листа»


Мы убираем весь старый UI, виджеты, зависимости, legacy код и оставляем только требования и функциональность, которые нам необходимо будет восстановить. Хорошая возможность улучшить архитектуру, подготовить почву для новых изменений. При подготовке необходимо учитывать следующие вещи, которые изменяются при переходе от одной библиотеки к другой и лучше продумать их заранее:

  1. Библиотека элементов. Создайте свою начальную базу элементов, где пропишите базовые настройки для элементов, их вид и состояния по умолчанию. Если есть возможность, оберните их тестами. В одной из конфигураций проекта сделайте их копируемыми в папку проекта, а не компилируемыми в бинарный*.qrc, это позволит вам править эти сборки в runtime без дополнительных компиляций и подключать qmllive и его аналоги.
  2. Отсутствие QAction. В приложениях на QWidget частым гостем является и QAction, расширяющий возможности перехвата действий пользователя как по нажатию кнопки на экране, так и перехватом горячих клавиш. К сожалению в QML QAction переехал в несколько ином виде и позволяет только из QML обертки перехватывать заранее определенные горячие клавиши, что накладывает ограничения на использование его внутри C++ или создавать динамически. Здесь вы можете написать свою реализацию, использовать event-filter от qobject или использовать сторонние проекты, такие как QHotkey .

В остальном здесь всё типично для приложения на C++ и QML.

План Б «У нас типо agile»


Так как для Agile одной из главной ценностью является работающий продукт, то мы должны делать переезд поэтапно, без регресса функционала. Для этого мы делим весь UI на логические сегменты с минимальным количеством взаимозависимостей и реализуем их отдельно. На примере gitAhead можно рассмотреть следующий вариант(выделил зеленым):

Разделение UI на сегменты(кликабельно)


Для каждой из областей мы создадим родительский виджет, в который и положим корневой QML элемент. Это можно сделать двумя способами.

Создать QQuickWidget

QQuickWidget *view = new QQuickWidget;
view->setSource(QUrl::fromLocalFile("myqmlfile.qml"));
view->show();

Или вызывать метод QWidget::createWindowContainer(...)

QQuickView *view = new QQuickView();
...
QWidget *container = QWidget::createWindowContainer(view);
container->setMinimumSize(...);
container->setMaximumSize(...);
...
widgetLayout->addWidget(container);

Разница между предложенными способами в том что QQuickWidget является более гибкой альтернативой чем QWidget::createWindowContainer(...), ведя себя больше как обычный QWidget. При этом большая функциональность достигается из-за незначительных потерь в производительности.

Дальнейшая работа производится как с обычным QQuickView, за исключением следующих особенностей:

  1. Ограниченный контекст. У каждого родительского QWidget будет свой QML-контекст, что в свое очередь накладывает ограничения на область видимости. Свои property, функции, объекты и т.д. Каждый widget по сути становится независимой «песочницей»
  2. Ограничение по размеру виджета. В ходе создания виджета, можно создать правило чтобы размеры виджета следовали за размерами контента внутри и по сути это правильно. Но это распространяется только на корневые элементы внутри этого QML, а все временные объекты с размерами превышающими виджет, будут обрезаться границами. Хорошим примером тут может служить всплывающая подсказка для кнопок, если кнопки маленькие, при это есть большое описание, то часть подсказки скроется за границей виджета. Если размеры виджета-контейнера поставлены не по размеру контента, то это так же может привести к проблемам при масштабировании интерфейса. Частично эта проблема решается элементами из lab.

    Пример выхода за границы и неправильной верстки
    image

  3. 3D графика. Здесь всё усложнено и полно ограничений, рекомендуется использовать QQuickWidget и есть не маленькая вероятность что именно ваш сценарий либо потребует «костылей», либо больших трудовложений, поэтому ознакомьтесь с ограничениями заранее, либо сразу реализуйте под использование в QML

Стоит напомнить что приведенные выше ограничения распространяются только на тот период, пока вы переносите свою элементную базу. Описанные же в плане А ограничения распространяются на всё приложение. Последним элементом будет являться базовое окно приложения, где необходимо будет всё расположить в соответствии с дизайном, работой фокуса и др. требованиями. Останется удалить последние зависимости приложения от QWidget библиотеки, убрать ее из поставки и всё, работа закончена. Вы молодцы!

Итоги


Над gitAhead мне предстоит еще много работы, и не только по UI, но приблизительно как это происходит я описал. Если вам понравилась идея и вам хочется поучаствовать, то буду рад сотрудничеству. Мой репозиторий будет тут.