Текст
                    Компания Qt

Юрген Боклаге-Рианнель
Сирил Лорке
Йохан Телин


Оглавление Добро пожаловать! Благодарности Авторы Qt и Qt Quick Строительные блоки Qt Введение в Qt 6 Быстрый старт Установка Qt 6 SDK Hello World Типы приложений Резюме Qt Creator IDE Пользовательский интерфейс Регистрация комплекта Qt Управление проектами Использование редактора Локатор Отладка Краткое описание Быстрый стартер Синтаксис QML Основные элементы Компоненты Простые преобразования Элементы позиционирования Элементы макета Входные элементы Передовые методы Элементы жидкостей Анимация Государства и переходы Передовые методы Средства управления пользовательским интерфейсом Введение в систему управления Средство просмотра изображений Общие закономерности Стиль Imagine Резюме Модель-Вид-Делегат Concept Базовые модели Динамические представления Делегат Продвинутая техника 5 7 10 14 16 20 29 30 32 36 50 51 52 54 55 57 58 60 61 63 64 74 84 89 94 102 106 115 116 117 144 151 152 153 162 184 207 214 215 216 218 226 245 262
Резюме Элемент холста Удобство API Градиенты Тени Изображения Трансформация Режимы композиции Пиксельные буферы Краски для холста Перенос с HTML5 Canvas Формы Базовая форма Пути строительства Заполняющие формы Анимированные фигуры Резюме Эффекты в QML Концепция частиц Простое моделирование Параметры частиц Направленные частицы Воздействие на частицы Группы частиц Художники по частицам Графические шейдеры Элементы шейдеров Фрагментные шейдеры Волновой эффект Вершинный шейдер Эффект занавеса Резюме Мультимедиа Игровые носители Звуковые эффекты Видеопотоки Захват изображений Резюме Qt Quick 3D Основы Работа с активами Материалы и свет Анимация Смешивание 2D и 3D контента Резюме Сетевое взаимодействие 284 286 290 292 294 296 298 300 303 306 309 318 319 323 328 336 339 340 342 344 348 351 358 365 377 379 381 385 392 395 409 415 416 417 427 432 434 441 442 443 454 465 473 480 483 485
Обслуживание пользовательского интерфейса через HTTP Шаблоны HTTP-запросы Локальные файлы REST API Аутентификация с использованием OAuth Веб-сокеты Резюме Хранение Настройки Локальное хранилище - SQL Динамический QML Динамическая загрузка компонентов Создание и уничтожение объектов Отслеживание динамических объектов Резюме JavaScript Браузер/HTML против Qt Quick/QML Язык JS Объекты JS Создание JS-консоли Qt и C++ Шаблонное приложение Объект QObject Build Systems Общие классы Qt Модели в C++ Расширение QML с помощью C++ Понимание времени выполнения QML Содержание плагина Создание плагина Реализация FileIO Использование FileIO Резюме Qt для Python Введение Установка Построение приложения Ограничения Резюме Qt для MCU Настройка Hello World - для микроконтроллеров Интеграция с С++ Работа с моделями Резюме 486 494 496 502 506 516 536 544 545 546 549 555 556 565 572 577 578 581 583 586 589 596 599 607 611 618 628 645 646 652 655 658 661 672 674 675 677 679 699 700 701 702 705 713 721 728
Добро пожаловать! Добро пожаловать в The Qt 6 Book - книгу о QML. Этот текст поможет вам разобраться в QML, языке Qt для создания динамических пользовательских интерфейсов. Я считаю, что возможность создавать декларативные, реактивные, аппаратно ускоренные пользовательские интерфейсы, работающие с собственной производительностью на всех основных (и некоторых не очень основных) платформах, - это переломный момент. Когда я начинал работать с Qt, мне казалось, что у меня есть секретное оружие для быстрого создания программного обеспечения. QML выводит это на новый уровень. Чем эта книга отличается от документации по Qt? Я слышу ваш вопрос. Намерение состоит в том, чтобы создать дополнение. Эта книга предназначена для чтения от начала до конца, где каждая глава опирается на то, что вы узнали ранее. Но она также может быть использована и опытным читателем для того, чтобы сориентироваться в новой теме. Каждая глава посвящена определенной теме и знакомит с концепциями из Qt и QML. Тем не менее, документация Qt всегда дает полную картину и является отличным справочником для поиска подробностей обо всех элементах, свойствах, перечислениях и многом другом. Желаю Вам приятного чтения! Йохан Телин Структура Можно сказать, что книга разделена на три части. Это разделение не настолько четкое, чтобы мотивировать строгое деление на главы, а скорее ориентир, которому мы старались следовать при ее написании.
Первые несколько глав, скажем, где-то до 5 - 7 главы, можно считать введением. Если вы хотите изучить QML, то вам следует обязательно прочитать эти главы. Следующие главы, 6-14, можно рассматривать как достаточно самостоятельные главы, раскрывающие независимые темы, хотя модели из главы 7 используются во многих других местах. Не стесняйтесь погружаться в них в том порядке, который вам удобен, и изучайте те темы, которые вам интересны. Остальная часть книги посвящена более сложным темам, таким как детали JavaScript, смешение C++ и QML, а также привязка Qt для Python и QML. Это очень важные темы, и я очень хочу, чтобы вы их прочитали. Для создания полноценного приложения на QML необходимо понимать эти темы, но основное внимание в них уделено не QML. Непрекращающаяся работа Работа над книгой Qt 6 ведется постоянно. Мы приветствуем участников и планируем открыть нашу инфраструктуру для того, чтобы вы могли вносить свой вклад, сообщая о проблемах, исправлениях и новых материалах. Конечная цель - представить вам печатную книгу, когда Материал достиг уровня зрелости, который нас устраивает, но мы хотим поделиться им с вами уже сейчас и узнать из ваших отзывов, что нужно улучшить, а что добавить.
Благодарности Создание этой книги было бы невозможным без любезной спонсорской поддержки со стороны компании The Qt Company. Для меня большая честь работать над подобным проектом, и их помощь была неоценима. Я хотел бы особо отметить (в алфавитном порядке): empenzes Fabian K Luca Di sera magoldst-qt Морис Калиновски Митч Кертис nezticle Тино Пюссисало Ульф Херманн Владимир Миненко Вкладчики Эта книга стала возможной благодаря замечательным участникам сообщества. Я хотел бы поблагодарить их всех (в алфавитном порядке): алексен арки DavidAdamsПериметр delvianv гуочи ниттв итт
oleksis LorenDB paulmasri QtSCH ruudschouten task-jp topecongiro VideoCarp wangchunlin5013 История Эта книга основана на книге The QML Book (https://qmlbook.github.io/), первоначально написанной для Qt 5. Я хотел бы поблагодарить всех авторов этой книги (в алфавитном порядке): aamirglb alexeRadu andreabedini amura11 bakku cibersheep dbelyaev danielbaak DocWicking empyrical Ge0 гиллесфернан дес гиттербарсук гсантнер hckr iitaka1142
jiakuan justinfx maggu2810 марко пикколино мариопаль mark-summerfield mhubig micdoug Mihaylov93 moritzsternemann RossRogers Swordfish90 sycy600 тележк а 29jm Я также хотел бы особо отметить Pelagicore, The Qt Company и Felgo за помощь в развитии The QML Book, спонсируя нашу работу и в целом оказывая замечательную обратную связь и поддержку.
Авторы Книга Qt 6 написана коллективом авторов. К ним относятся: Йохан Телин Йохан работает системным архитектором, создавая решения для автомобильной промышленности. За его плечами более чем двадцатилетний опыт создания устройств на базе Linux, Qt
и т.д. Он пишет для различных изданий и блогов, выступает на многочисленных конференциях и дает советы по созданию программного обеспечения и программных организаций. Будучи приверженцем свободных решений с открытым исходным кодом, он основал и организует конференцию foss-north (https://foss-north.se) . Более подробную информацию о Йохане можно найти на сайте LinkedIn (https://www.linkedin.com/in/johanthelin), его блог (http://www.thelins.se/johan/blog/) , и его домашняя страница (http://e8johan.se) . Юрген Боклаге-Ряннель Юрген является которая генеральным представляет собой директором инструмент компании для ApiGear, совместного проектирования машинных интерфейсов, позволяющий командам совместно разрабатывать программные интерфейсы с использованием автоматизированных решений для мониторинга и моделирования. Он был соучредителем компании Pelagicore AG и отвечал там в качестве главного архитектора пользовательского интерфейса за ранние версии Daimler MBUX. В настоящее время он специализируется на API-ориентированном рабочем процессе для проектирования и создания интерфейсов между
пользовательским опытом и базовыми сервисами для
различные платформы. Более подробную информацию о Юргене можно получить на сайте LinkedIn (https://www.linkedin.com/in/jryannel/) . Сирил Лорке Соучредитель и генеральный директор бельгийской компании Eunoia Studio (https://www.eunoia.be), Сирил помогает организациям превратить их ноу-хау в программные продукты. С 2009 года он работает над программными продуктами в различных областях (строительство, здравоохранение, гидрология, маркетинг...).
Несколько из них связаны с Qt. Инженер-программист по своей сути, он увлекается процессами проектирования, разработкой программного обеспечения и управлением изменениями. Более подробную информацию о Кирилле можно получить на сайте LinkedIn (https://www.linkedin.com/in/cyrillorquet) .
Qt и Qt Quick В этой книге подробно рассматриваются различные аспекты разработки приложений с использованием нового Qt 6. Основное внимание уделяется технологии Qt Quick, но также дается необходимая информация о написании бэкэндов и расширений на языке C++ для Qt Quick. В этой главе представлен высокоуровневый обзор Qt 6. В ней показаны различные модели приложений, доступные разработчикам, а также демонстрационное приложение в качестве предварительного просмотра. Кроме того, глава призвана дать широкий обзор содержания Qt и рассказать о том, как связаться с создателями Qt компанией Qt. Qt 6 Focus Qt 5 был выпущен много лет назад и представил новый декларативный способ написания потрясающих пользовательских интерфейсов. С тех пор многое изменилось в окружающем нас мире. Qt 6 будет продолжением того, что было сделано в Qt 5, и не должен стать помехой для большинства пользователей. Что делает Qt ценным для пользователей? Кроссплатформенность Масштабируемость API и документация мирового класса Ремонтопригодность, стабильность и совместимость Большая экосистема разработчиков Qt 6 развивает продукт Qt, выводя его на новые рынки, сохраняя при этом близость к ценностям пользователей.
Рынок настольных систем является корнем предложения Qt. Именно здесь большинство пользователей впервые сталкиваются с Qt, и именно здесь формируется база для инструментария Qt и его успеха. Ожидается, что наибольший рост Qt 6 получит на рынке встраиваемых и подключаемых устройств - от высокопроизводительных устройств, близких к настольным, до низкопроизводительных устройств, таких как микроконтроллеры. Количество сенсорных экранов в этих устройствах будет расти по экспоненте. Многие из этих устройств будут иметь относительно простую функциональность, но нуждаться в отполированном и гладком пользовательском интерфейсе. На другом конце спектра существует спрос на более сложные и интегрированные в 2D/3D пользовательские интерфейсы. Распространенными будут 3D-контент с интерфейсами на основе 2Dэлементов, а также использование дополненной и виртуальной реальности. Рост числа подключенных устройств и повышение требований к плавности пользовательских интерфейсов требуют упрощения рабочего процесса при создании приложений и устройств. Интеграция UX-дизайнеров в рабочий процесс разработки - одна из целей серии Qt 6. Qt 6 предоставляет нам: Следующее поколение QML Следующее поколение графики Унифицированный и согласованный инструментарий Расширенные API-интерфейсы Qts C++ Рынок компонентов
Qt Building Blocks Qt 6 состоит из большого количества модулей. В общем случае модуль - это библиотека, которую может использовать разработчик. Некоторые модули являются обязательными для платформы с поддержкой Qt и образуют набор, называемый Qt Essentials Modules. Другие модули являются необязательными и образуют набор Qt AddOn Modules. Большинству разработчиков последние могут и не понадобиться, но знать о них полезно, так как они дают неоценимые решения часто встречающихся проблем. Модули Qt Модули Qt Essentials являются обязательными для любой платформы с поддержкой Qt. Они дают основу для разработки современных приложений Qt 6 с использованием Qt Quick 2. Полный список модулей доступен в списке модулей документации Qt (https://doc.qt.io/qt-6/qtmodules.html#qt-essentials) . Основные модули Минимальный набор модулей Qt 6 для начала программирования на QML. Qt Core - основные неграфические классы, используемые другими модулями. Qt D-BUS - Классы для межпроцессного взаимодействия по протоколу D-Bus в linux. Qt GUI - Базовые классы для компонентов графического интерфейса пользователя (GUI). Включает OpenGL. Qt Network - Классы для упрощения и переносимости сетевого программирования. Qt QML - Классы для языков QML и JavaScript.
Qt Quick - Декларативный фреймворк для создания высокодинамичных приложений с пользовательскими интерфейсами. Qt Quick Controls - Предоставляет легковесные QML-типы для создания производительных пользовательских интерфейсов для настольных, встроенных и мобильных устройств. Эти типы используют простую архитектуру стилей и очень эффективны. Qt Quick Layouts - Макеты - это элементы, которые используются для расположения элементов на основе Qt Quick 2 в пользовательском интерфейсе. Qt Quick Test - фреймворк модульного тестирования для QMLприложений, в котором тестовые случаи записываются в виде JavaScript-функций. Qt Test - Классы для модульного тестирования приложений и библиотек Qt. Qt Widgets - Классы для расширения графического интерфейса Qt с помощью виджетов на языке C++. QtQuickTest QtQuickLayout QtQuickControls QtQuick QtNetwork QtGui QtQml QtTest QtCore Дополнительные модули Qt Помимо основных модулей, Qt предлагает дополнительные модули, ориентированные на конкретные цели. Многие дополнительные модули либо являются полнофункциональными и существуют для обратной совместимости, либо применимы только для определенных платформ. Здесь приведен список некоторых из доступных дополнительных модулей, но обязательно ознакомьтесь с ними в
списке дополнений документации Qt (https://doc.qt.io/qt6/qtmodules.html#qt-add-ons) и в приведенном ниже списке.
Сеть: Qt Bluetooth / Qt Network Авторизация Компоненты пользовательского интерфейса: Qt Quick 3D / Qt Quick Timeline / Qt Charts / Qt Data Visualization / Qt Lottie Animation / Qt Virtual Keyboard Графика: Qt 3D / Qt Image Formats / Qt OpenGL / Qt Shader Tools / Qt SVG / Qt Wayland Compositor Помощники: Qt 5 Core Compatibility APIs / Qt Concurrent / Qt Help / Qt Print Support / Qt Quick Widgets / Qt SCXML / Qt SQL / Qt State Machine / Qt UI Tools / Qt XML СОВЕТ Поскольку эти модули не являются частью релиза, состояние каждого из них может отличаться в зависимости от того, насколько активен контрибьютор и насколько хорошо он протестирован. Поддерживаемые платформы Qt поддерживает множество платформ, включая все основные настольные и встраиваемые платформы. Благодаря Qt Platform Abstraction теперь как никогда легко перенести Qt на свою собственную платформу, если это необходимо. Тестирование Qt 6 на платформах требует много времени. Проект Qt выбрал подмножество платформ для создания набора эталонных платформ. Эти платформы тщательно проверяются в ходе системного тестирования, чтобы обеспечить наилучшее качество . Однако следует помнить, что ни один код не может быть безошибочным. Проект Qt Из Qt Wiki (http://wiki.qt.io/) : "Qt Wiki" - это сообщество, основанное на меритократическом
консенсусе и заинтересованное в Qt. Любой, кто разделяет этот интерес, может присоединиться к сообществу, участвовать
в процессах принятия решений и вносить свой вклад в развитие Qt". Qt Wiki - это место, где пользователи и разработчики Qt делятся своими знаниями. Она является основой для вклада других пользователей. Самым крупным участником является компания The Qt Company, которая также владеет коммерческими правами на Qt. Qt имеет аспект открытого кода и коммерческий аспект для компаний. Коммерческий аспект предназначен для компаний, которые не могут или не хотят следовать лицензиям open-source. Без коммерческого аспекта эти компании не смогли бы использовать Qt, и это не позволило бы компании The Qt Company внести такой большой объем кода в проект Qt. В мире существует множество компаний, которые зарабатывают на жизнь консультированием и разработкой продуктов с использованием Qt на различных платформах. Существует множество проектов с открытым исходным кодом и разработчиков, которые используют Qt в качестве основной библиотеки для разработки. Приятно быть частью этого яркого сообщества и работать с этими замечательными инструментами и библиотеками. Делает ли это вас лучше? Возможно:-) Внести вклад можно здесь: http://wiki.qt.io/
Qt 6 Введение Qt Quick Qt Quick - это зонтичный термин для обозначения технологии пользовательского интерфейса, используемой в Qt 6. Она была представлена в Qt 4 и теперь расширена для Qt 6. Сам Qt Quick представляет собой набор из нескольких технологий: QML - язык разметки пользовательских интерфейсов JavaScript - язык динамических сценариев Qt C++ - хорошо переносимая библиотека с расширенными возможностями языка c++ Подобно HTML, QML является языком разметки. Он состоит из тегов, называемых в Qt Quick типами, которые заключены в фигурные скобки: Item {} . Он был разработан с нуля для создания пользовательских интерфейсов, скорости и облегчения чтения для разработчиков. Пользовательский интерфейс может быть доработан с помощью JavaScript-кода. Qt Quick легко расширяется за счет собственной функциональности с использованием Qt C++. В двух словах декларативный пользовательский интерфейс называется front-end
а "родная" часть называется back-end. Это позволяет отделить вычислительную часть приложения от пользовательского интерфейса. В типичном проекте фронтэнд разрабатывается на QML/JavaScript. На сайте Код back-end, который взаимодействует с системой и выполняет основную работу, разрабатывается с использованием Qt C++. Это позволяет естественным образом разделить разработчиков, ориентированных на дизайн, и функциональных разработчиков. Как правило, back-end тестируется с помощью Qt Test, фреймворка для модульного тестирования Qt, и экспортируется для использования разработчиками front-end. Усвоение пользовательского интерфейса Давайте создадим простой пользовательский интерфейс с помощью Qt Quick, который демонстрирует некоторые аспекты языка QML. В итоге у нас получится бумажная ветряная мельница с вращающимися лопастями. Мы начинаем с пустого документа под названием main.qml . Все наши QML-файлы будут иметь суффикс .qml . Как язык разметки (подобно HTML), QML-документ должен иметь один и только один корневой тип. В нашем случае это
Тип изображения, ширина и высота которого определяются геометрией фонового изображения: import QtQuick Image { id: root source: "images/background.png" } Поскольку QML не ограничивает выбор типа для корневого типа, мы используем в качестве корневого типа тип Image со свойством source, установленным на наше фоновое изображение.
СОВЕТ Каждый тип имеет свойства. Например, изображение имеет свойства width и height , каждое из которых содержит количество пикселей. У него также есть другие свойства, например, source . Поскольку размер типа image автоматически определяется из размера изображения, нам не нужно самостоятельно задавать свойства width и height. Наиболее стандартные типы находятся в модуле QtQuick, который становится доступным благодаря оператору import в начале файла .qml. id - это специальное необязательное свойство, содержащее идентификатор, который может быть использован для ссылки на связанный с ним тип в других местах документа. Важно: свойство id не может быть изменено после его установки и не может быть установлено во время выполнения. Использование свойства root в качестве идентификатора корневого типа - это соглашение, используемое в данной книге для того, чтобы сделать ссылки на самый верхний тип предсказуемыми в больших документах QML. Элементы переднего плана, представляющие собой столб и колесо в пользовательском интерфейсе, включены как отдельные изображения.
Мы хотим разместить столб горизонтально в центре фона, но со смещением по вертикали к низу. И мы хотим разместить колесо в центре фона. Хотя в этом примере для начинающих используются только типы изображений, по мере продвижения вы будете создавать более сложные пользовательские интерфейсы, состоящие из множества различных типов. Image { id: root ... Image { id: pole anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom source: "images/pole.png" } Image { id: wheel anchors.centerIn: parent source: "images/pinwheel.png" } ... } Чтобы поместить колесо в центр, мы используем комплексное свойство якорь Якорение позволяет задать геометрические соотношения между
родительских и сиблинговых объектов. Например, поместите меня в центр другого типа ( anchors.centerIn: parent ). С обеих сторон имеются отношения left, right, top, bottom, centerIn, fill, verticalCenter и horizontalCenter. Естественно, что при совместном использовании двух и более якорей они должны дополнять друг друга: не имеет смысла, например, привязывать левую часть типа к верхней части другого типа. В случае с маховиком для крепления требуется только один простой анкер. СОВЕТ Иногда требуется внести небольшие корректировки, например, немного сместить тип от центра. Это можно сделать с помощью anchors.horizontalCenterOffset или anchors.verticalCenterOffset . Аналогичные свойства настройки доступны и для всех остальных якорей. Полный список свойств якорей см. в документации. СОВЕТ Размещение изображения в качестве дочернего типа нашего корневого типа (Image ) иллюстрирует важную концепцию декларативного языка. Вы описываете визуальный вид пользовательского интерфейса в порядке слоев и группировки, где самый верхний слой (наше фоновое изображение) рисуется первым, а дочерние слои рисуются поверх него в локальной системе координат содержащего типа. Чтобы сделать демонстрацию более интересной, давайте сделаем сцену интерактивной. Идея заключается в том, чтобы вращать колесо, когда пользователь нажимает на мышь в каком-либо месте сцены. Мы используем тип MouseArea и сделаем так, чтобы он покрывал всю область нашего корневого типа.
Image { id: root ... MouseArea { anchors.fill: parent onClicked: wheel.rotation += 90 } ... } Область мыши издает сигналы, когда пользователь щелкает мышью внутри области, которую она покрывает. Подключиться к этому сигналу можно, переопределив функцию onClicked. Подключение сигнала означает, что функция (или функции), которой он соответствует, вызывается всякий раз, когда этот сигнал подается. В данном случае мы говорим, что при щелчке мыши в области мыши тип, идентификатором которого является wheel (т.е. изображение pinwheel), должен поворачиваться на +90 градусов. СОВЕТ Эта техника работает для каждого сигнала, причем в качестве именования используется on + SignalName в заглавном регистре. Кроме того, все свойства издают сигнал при изменении своего значения. Для этих сигналов соглашение об именовании выглядит следующим образом: js `on${property}Changed` Например, если изменяется свойство width, то это можно наблюдать с помощью функции onWidthChanged: print(width) . Теперь колесо будет вращаться при каждом щелчке пользователя, но вращение будет происходить одним скачком, а не плавно перемещаться во времени. Добиться плавного движения можно с помощью анимации. Анимация определяет, как свойство
изменение происходит в течение некоторого времени. Для этого мы используем свойство типа Animation под названием Behavior . Свойство Behavior задает анимацию для определенного свойства при каждом изменении этого свойства. Другими словами, при каждом изменении свойства запускается анимация. Это лишь один из многих способов реализации анимации в QML. Image { id: root Image { id: wheel Behavior on rotation { NumberAnimation { duration: 250 } } } } Теперь при каждом изменении свойства вращения колеса оно будет анимироваться с помощью NumberAnimation с длительностью 250 мс. Таким образом, каждый поворот на 90 градусов будет занимать 250 мс, создавая приятный плавный поворот.
СОВЕТ На самом деле вы не увидите, что колесо размыто. Оно просто указывает на вращение. (Размытое колесо находится в папке assets, если вы хотите поэкспериментировать с ним). Теперь колесо выглядит гораздо лучше и ведет себя хорошо, а также дает очень краткое представление об основах программирования на Qt Quick.
Быстрый старт В этой главе мы познакомим вас с разработкой на Qt 6. Мы покажем, как установить Qt SDK и как создать, а также запустить простое приложение hello world с помощью среды разработки Qt Creator.
Установка Qt 6 SDK Qt SDK включает в себя инструменты, необходимые для создания настольных и встраиваемых приложений. Последнюю версию можно получить с домашней страницы компании Qt (https://qt.io). Существует автономный и онлайновый инсталлятор. Лично автор предпочитает онлайн-установщик, так как он позволяет устанавливать и обновлять несколько релизов Qt. С него и рекомендуется начинать. Сам SDK имеет инструмент сопровождения, который позволяет обновить SDK до последней версии. Qt SDK легко устанавливается и поставляется с собственной средой разработки Qt Creator. IDE является высокопроизводительной средой для кодирования Qt и рекомендуется всем читателям. Однако многие разработчики используют Qt из командной строки, и вы можете свободно пользоваться редактором кода по своему усмотрению. При установке SDK следует выбрать вариант по умолчанию и убедиться, что включена версия не ниже Qt 6.2. После этого все готово к работе. Обновление Qt Qt SDK поставляется с собственным инструментом сопровождения, расположенным в папке ${install_dir} . Он позволяет добавлять и/или обновлять компоненты Qt SDK. Сборка из источника Для сборки Qt из исходных текстов можно следовать руководству из Qt Wiki (https://wiki.qt.io/Building_Qt_6_from_Git) .

Hello World Для тестирования установки мы создадим небольшое приложение hello world. Откройте Qt Creator и создайте проект Qt Quick UI Project ( File ‣ New File или Project ‣ Other Project ‣ Qt Quick UI Prototype ) и назовите проект HelloWorld . СОВЕТ IDE Qt Creator позволяет создавать различные типы приложений. Если не указано иное, мы всегда используем проект прототипа Qt Quick UI. Для создания производственного приложения часто предпочтительнее использовать проект на основе CMake, но для быстрого создания прототипов этот тип подходит лучше. СОВЕТ Типичное приложение Qt Quick состоит из среды выполнения, называемой QmlEngine, которая загружает исходный QML-код. Разработчик может зарегистрировать в среде выполнения типы C++ для взаимодействия с "родным" кодом. Эти типы C++ также могут быть собраны в подключаемый модуль и затем динамически загружены с помощью оператора import. Инструмент qml - это готовая среда выполнения, которая используется напрямую. Для начала мы не будем рассматривать нативную сторону разработки и сосредоточимся только на QML-аспектах Qt 6. Поэтому мы начнем с проекта-прототипа. Qt Creator создает для вас несколько файлов. Файл HelloWorld.qmlproject - это файл проекта, в котором хранится соответствующая конфигурация проекта. Этот файл
управляется Qt Creator, поэтому не стоит редактировать его самостоятельно. Другой файл, HelloWorld.qml, представляет собой код нашего приложения. Откройте его и попытайтесь понять, что делает приложение, прежде чем читать дальше. // HelloWorld.qml import QtQuick import QtQuick.Window Window { width: 640 height: 480 visible: true title: qsTr("Hello World") } Программа HelloWorld.qml написана на языке QML. Более подробно мы рассмотрим язык QML в следующей главе. QML описывает пользовательский интерфейс как дерево иерархических элементов. В данном случае это окно размером 640 x 480 пикселей с заголовком "Hello World". Чтобы запустить приложение самостоятельно, нажмите клавишу Инструмент Run с левой стороны или выберите в меню Build > Run. В фоновом режиме Qt Creator запускает qml и передает ваш QMLдокумент в качестве первого аргумента. Приложение qml разбирает документ и запускает пользовательский интерфейс. Вы должны увидеть нечто подобное:
Qt 6 работает! Это означает, что мы готовы продолжать. СОВЕТ Если вы являетесь системным интегратором, то вам необходимо установить Qt SDK, чтобы получить последний стабильный выпуск Qt, а также версию Qt, скомпилированную из исходных текстов для конкретного устройства.
СОВЕТ Построение с нуля Если вы хотите собрать Qt 6 из командной строки, то сначала вам нужно получить копию репозитория кода и собрать его. Посетите вики Qt, чтобы получить актуальное объяснение того, как собирать Qt из git. После успешной компиляции (и двух чашек кофе) Qt 6 будет доступен в папке qtbase. Для этого подойдет любой напиток, однако для достижения наилучших результатов мы рекомендуем пить кофе. Если вы хотите проверить свою компиляцию, то теперь можете запустить пример с помощью стандартной среды выполнения, поставляемой с Qt 6: $ qtbase/bin/qml
Приложение Типы В этом разделе мы рассмотрим различные типы приложений, которые можно написать с помощью Qt 6. Он не ограничивается представленной здесь подборкой, но даст вам лучшее представление о том, чего можно достичь с помощью Qt 6 в целом. Консольное приложение Консольное приложение не предоставляет графического интерфейса пользователя и обычно вызывается в составе системной службы или из командной строки. Qt 6 поставляется с рядом готовых компонентов, которые помогают очень эффективно создавать кроссплатформенные консольные приложения. Например, API сетевых файлов, обработка строк, эффективный парсер командной строки. Поскольку Qt - это высокоуровневый API поверх C++, скорость программирования сочетается со скоростью исполнения. Не думайте, что Qt - это просто набор инструментов для работы с пользовательским интерфейсом, он может предложить гораздо больше! Обработка строк Первый пример демонстрирует, как можно добавить 2 константные строки. Конечно, это не очень полезное приложение, но оно дает представление о том, как может выглядеть нативное приложение на языке Си++ без цикла событий. // включение модуля или класса #include <QtCore // текстовый поток имеет текстовый кодек QTextStream cout(stdout, QIODevice::WriteOnly);
int main(int argc, char** argv) { // avoid compiler warnings Q_UNUSED(argc) Q_UNUSED(argv) QString s1("Paris"); QString s2("London"); // string concatenation QString s = s1 + " " + s2 + "!"; cout << s << Qt::endl; } Классы контейнеров Этот пример добавляет в приложение список и итерацию списка. Qt поставляется с большой коллекцией контейнерных классов, которые просты в использовании и имеют те же парадигмы API, что и другие классы Qt. QString s1("Hello"); QString s2("Qt"); QList<QString> list; // stream into containers list << s1 << s2; // Java and STL like iterators QListIterator<QString> iter(list); while(iter.hasNext()) { cout << iter.next(); if(iter.hasNext()) { cout << " "; } } cout << "!" << Qt::endl; Здесь представлена более продвинутая функция списка, которая позволяет объединить список строк в одну строку. Это очень удобно, когда необходимо выполнить построчную обработку
текстовый ввод. Обратное действие (преобразование строки в список) также возможно с помощью функции Функция QString::split(). QString s1("Hello"); QString s2("Qt"); // convenient container classes QStringList list; list << s1 << s2; // join strings QString s = list.join(" ") + "!"; cout << s << Qt::endl; Файловый ввод-вывод В следующем фрагменте мы считываем CSV-файл из локальной директории и в цикле перебираем строки, извлекая ячейки из каждой строки. Таким образом, мы получаем данные таблицы из CSV-файла примерно за 20 строк кода. При чтении файла мы получаем поток байтов, для преобразования которого в корректный текст Unicode нам необходимо использовать текстовый поток и передавать файл как поток нижнего уровня. Для записи CSV-файлов достаточно открыть файл в режиме записи и передать строки в текстовый поток. QList<QStringList> data; // file operations QFile file("sample.csv"); if(file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); // loop forever macro forever { QString line = stream.readLine(); // test for null string 'String()' if(line.isNull()) { break; } // test for empty string 'QString("")'
if(line.isEmpty()) { continue; } QStringList row; // for each loop to iterate over containers foreach(const QString& cell, line.split(",")) { row.append(cell.trimmed()); } data.append(row); } } На этом мы завершаем раздел, посвященный консольным приложениям на Qt. ВиджетноеQприложение Консольные приложения очень удобны, но иногда требуется графический интерфейс пользователя (GUI). Кроме того, приложениям с графическим интерфейсом, скорее всего, потребуется внутренняя часть для чтения/записи файлов, взаимодействия по сети В или хранения данных в контейнере. В этом первом фрагменте для приложений, основанных на виджетах, мы делаем ровно столько, сколько нужно для создания окна и его отображения. В Qt виджет, не имеющий родителя, является виджетом window. Для обеспечения удаления виджета при выходе указателя за пределы области видимости мы используем скопированный указатель. Объект приложения инкапсулирует среду выполнения Qt, и мы запускаем цикл событий с помощью вызова exec(). С этого момента приложение реагирует только на события, инициируемые пользовательским вводом (например, мышью или клавиатурой) или другими поставщиками событий, такими как сетевые или файловые IO. Выход из приложения происходит только при завершении цикла обработки событий. Это происходит путем вызова quit() или закрытия окна. При выполнении кода появится окно размером 240 x 120 пикселей. Это все.

include <QtGui> int main(int argc, char** argv) { QApplication app(argc, argv); QScopedPointer<QWidget> widget(new CustomWidget()); widget->resize(240, 120); widget->show(); return app.exec(); } Пользовательские виджеты При работе над пользовательскими интерфейсами может возникнуть необходимость в создании пользовательских виджетов. Как правило, виджет представляет собой область окна, заполненную вызовами рисования. Кроме того, виджет обладает внутренними знаниями о том, как обрабатывать ввод с клавиатуры и мыши, а также как реагировать на внешние триггеры. Для этого в Qt необходимо получить производную от QWidget и переписать несколько функций для рисования и обработки событий. #pragma once include <QtWidgets> class CustomWidget : public QWidget { Q_OBJECT public: explicit CustomWidget(QWidget *parent = 0); void paintEvent(QPaintEvent *event); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); private: QPoint m_lastPos; };
В реализации мы рисуем небольшую границу нашего виджета и небольшой прямоугольник по последнему положению мыши. Это очень типично для низкоуровневого пользовательского виджета. События мыши и клавиатуры изменяют внутреннее состояние виджета и вызывают обновление рисунка. Мы не будем слишком подробно останавливаться н а этом коде, но полезно знать, что у вас есть такая возможность. Qt поставляется с большим набором готовых виджетов рабочего стола, поэтому, скорее всего, вам не придется этим заниматься. include "customwidget.h" CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) { } void CustomWidget::paintEvent(QPaintEvent *) { QPainter painter(this); QRect r1 = rect().adjusted(10,10,-10,-10); painter.setPen(QColor("#33B5E5")); painter.drawRect(r1); QRect r2(QPoint(0,0),QSize(40,40)); if(m_lastPos.isNull()) { r2.moveCenter(r1.center()); } else { r2.moveCenter(m_lastPos); } painter.fillRect(r2, QColor("#FFBB33")); } void CustomWidget::mousePressEvent(QMouseEvent *event) { m_lastPos = event->pos(); update(); }
void CustomWidget::mouseMoveEvent(QMouseEvent *event) { m_lastPos = event->pos(); update(); } Виджеты для рабочего стола Разработчики Qt уже сделали все это за вас и предоставили набор виджетов рабочего стола, имеющих свой собственный вид в различных операционных системах. Ваша задача состоит в том, чтобы расположить эти различные виджеты в контейнере виджетов в более крупные панели. Виджет в Qt также может быть контейнером для других виджетов. Это достигается с помощью отношений "родитель-ребенок". Это означает, что нам необходимо сделать наши готовые виджеты, такие как кнопки, флажки, радиокнопки, списки и сетки, дочерними по отношению к другим виджетам. Один из способов решения этой задачи показан ниже. Здесь представлен заголовочный файл для так называемого контейнера виджетов. class CustomWidget : public QWidget { Q_OBJECT public: explicit CustomWidget(QWidget *parent = 0); private slots: void itemClicked(QListWidgetItem* item); void updateItem(); private: QListWidget *m_widget; QLineEdit *m_edit; QPushButton *m_button; }; В реализации мы используем макеты для более удобного расположения виджетов. Менеджеры макетов перестраивают расположение виджетов в соответствии с некоторыми политиками размеров, когда
виджет-контейнер изменяет свои размеры. В данном примере мы имеем список, строку редактирования и кнопку, которые расположены вертикально и позволяют пользователю редактировать список городов. Для связи объектов-отправителей и объектов-получателей мы используем сигналы и слоты Qt. CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); m_widget = new QListWidget(this); layout->addWidget(m_widget); m_edit = new QLineEdit(this); layout->addWidget(m_edit); m_button = new QPushButton("Quit", this); layout->addWidget(m_button); setLayout(layout); QStringList городов; cities << "Paris" << "London" << "Munich"; foreach(const QString& city, cities) { m_widget->addItem(city); } connect(m_widget, SIGNAL(itemClicked(QListWidgetItem*)), th connect(m_edit, SIGNAL(editingFinished()), this, SLOT(updat connect(m_button, SIGNAL(clicked()), qApp, SLOT(quit())); } void CustomWidget::itemClicked(QListWidgetItem *item) { Q_ASSERT(item); m_edit->setText(item->text()); } void CustomWidget::updateItem() {
QListWidgetItem* item = m_widget->currentItem(); if(item) { item->setText(m_edit->text()); } } Рисование фигур Некоторые проблемы лучше визуализировать. Если рассматриваемая проблема отдаленно напоминает геометрические объекты, то графическое представление Qt является хорошим кандидатом. Графическое представление организует простые геометрические фигуры в сцене. Пользователь может взаимодействовать с этими фигурами, либо они располагаются с помощью алгоритма. Для наполнения графического представления необходимо графическое представление и графическая сцена. Сцена прикрепляется к представлению и заполняется графическими элементами. Приведем небольшой пример. Сначала заголовочный файл с объявлением вида и сцены. class CustomWidgetV2 : public QWidget { Q_OBJECT public: explicit CustomWidgetV2(QWidget *parent = 0); private: QGraphicsView *m_view; QGraphicsScene *m_scene; }; }; В реализации сцена сначала прикрепляется к представлению. Представление является виджетом и размещается в нашем виджетеконтейнере. В конце мы добавляем в виджет небольшой прямоугольник в сцене, который затем отображается на экране.
include "customwidgetv2.h" CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) { m_view = new QGraphicsView(this); m_scene = new QGraphicsScene(this); m_view->setScene(m_scene); QVBoxLayout *layout = new QVBoxLayout(this); layout->setMargin(0); layout->addWidget(m_view); setLayout(layout); QGraphicsItem* rect1 = m_scene->addRect(0,0, 40, 40, Qt::No rect1->setFlags(QGraphicsItem::ItemIsFocusable|QGraphicsIte } Адаптация данных До сих пор мы рассматривали в основном основные типы данных, а также использование виджетов и графических представлений. В приложениях часто требуется больший объем структурированных данных, которые, возможно, также необходимо хранить постоянно. Наконец, данные также необходимо отображать. Для этого в Qt используются модели. Простой моделью является модель списка строк, которая заполняется строками и затем присоединяется к представлению списка. m_view = new QListView(this); m_model = new QStringListModel(this); view>setModel(m_model); QList<QString> города; cities << "Munich" << "Paris" << "London"; m_model->setStringList(cities);
Другим популярным способом хранения и получения данных является SQL. Qt поставляется со встроенным SQLite, а также имеет поддержку других движков баз данных (например, MySQL и PostgreSQL). Сначала необходимо создать базу данных, используя схему, например, так: CREATE TABLE city (name TEXT, country TEXT); INSERT INTO city VALUES ("Мюнхен", "Германия"); INSERT INTO city VALUES ("Париж", "Франция"); INSERT INTO city VALUES ("Лондон", "Великобритания"); Для использования SQL нам необходимо добавить модуль SQL в наш файл .pro QT += sql А затем мы можем открыть нашу базу данных с помощью языка C++. Сначала нам необходимо получить новый объект базы данных для указанного движка базы данных. С помощью этого объекта базы данных мы открываем базу данных. Для SQLite достаточно указать путь к файлу базы данных. Qt предоставляет несколько высокоуровневых моделей баз данных, одной из которых является модель таблицы. Модель таблицы использует идентификатор таблицы и объект необязательное условие where для выбора данных. Полученную модель можно присоединить к представлению списка, как и в случае с другими моделями. QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("cities.db"); if(!db.open()) { qFatal("не удалось открыть базу данных"); } m_model = QSqlTableModel(this); m_model->setTable("city"); m_model->setHeaderData(0, Qt::Horizontal, "City"); m_model->setHeaderData(1, Qt::Horizontal, "Country");
view->setModel(m_model); m_model->select(); Для более высокого уровня работы с моделями Qt предоставляет прокси-модель сортировки файлов, которая позволяет сортировать, фильтровать и преобразовывать модели. QSortFilterProxyModel* proxy = new QSortFilterProxyModel(this); proxy->setSourceModel(m_model); view->setModel(proxy); view->setSortingEnabled(true); Фильтрация осуществляется на основе фильтруемого столбца и строки в качестве аргумента фильтра. proxy->setFilterKeyColumn(0); proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); proxy->setFilterFixedString(QString) Модель фильтрующего прокси гораздо мощнее, чем показано здесь. Пока достаточно помнить о ее существовании. !!! нота Это был обзор различных видов классических приложений Далее: Qt Quick на помощь. Быстрое приложение Qt В современной разработке программного обеспечения существует внутреннее противоречие. Пользовательский интерфейс развивается гораздо быстрее, чем наши back-end сервисы. В традиционной технологии вы разрабатываете так называемый front-end в том же темпе, что и
back-end. Это приводит к конфликтам, когда заказчики хотят изменить пользовательский интерфейс в ходе проекта или разработать идею пользовательского интерфейса в ходе проекта. Для реализации гибких проектов требуются гибкие методы. Qt Quick предоставляет декларативную среду, в которой пользовательский интерфейс (front-end) объявляется как HTML, а back-end находится в родном коде C++. Это позволяет получить лучшее из двух миров. Ниже представлен простой Qt Quick UI import QtQuick Rectangle { width: 240; height: 240 Rectangle { width: 40; height: 40 anchors.centerIn: parent color: '#FFBB33' } } Язык деклараций называется QML, и для его выполнения необходима среда выполнения. Qt предоставляет стандартную среду выполнения под названием qml . Можно также написать собственную среду выполнения. Для этого нам нужно быстрое представление и установить основной QML-документ в качестве источника из C++. Затем можно показать пользовательский интерфейс. #include <QtGui> #include <QtQml> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine("main.qml"); return app.exec(); }
Вернемся к нашим предыдущим примерам. В одном из примеров мы использовали модель города на языке C++. Было бы здорово, если бы мы могли использовать эту модель внутри нашего декларативного QML-кода. Для того чтобы это сделать, мы сначала кодируем наш фронтенд, чтобы увидеть, как мы хотим использовать модель города. В данном случае фронтенд ожидает объект с именем cityModel, который мы можем использовать внутри представления списка. import QtQuick Rectangle { width: 240; height: 120 ListView { width: 180; height: 120 anchors.centerIn: parent model: cityModel delegate: Text { text: model.city } } } Чтобы включить модель cityModel, мы можем в основном использовать нашу предыдущую модель и добавить свойство context к нашему корневому контексту. Корневой контекст - это другой корневой элемент основного документа. m_model = QSqlTableModel(this); ... // some magic code QHash<int, QByteArray> roles; roles[Qt::UserRole+1] = "city"; roles[Qt::UserRole+2] = "country"; m_model->setRoleNames(roles); engine.rootContext()->setContextProperty("cityModel", m_model);
Резюме Мы рассмотрели, как установить Qt SDK и как создать наше первое приложение. Затем мы рассмотрели различные типы приложений, чтобы дать общее представление о Qt и продемонстрировать некоторые возможности, которые Qt предлагает для разработки приложений. Надеюсь, у вас сложилось хорошее впечатление, что Qt это очень богатый инструментарий пользовательского интерфейса и предлагает все, на что может рассчитывать разработчик приложений, и даже больше. Тем не менее, Qt не привязывает вас к конкретным библиотекам, поскольку вы всегда можете использовать другие библиотеки или даже самостоятельно расширять Qt. Богат он и тем, что поддерживает различные модели приложений: консоль, классический интерфейс настольного компьютера и сенсорный интерфейс.
Qt Creator IDE Qt Creator - это интегрированная среда разработки Qt по умолчанию. Она написана разработчиками Qt для разработчиков Qt. IDE доступна на всех основных настольных платформах, например, Windows/Mac/Linux. Мы уже видели заказчиков, использующих Qt Creator на встраиваемых устройствах. Qt Creator имеет простой и эффективный пользовательский интерфейс, который позволяет разработчику работать продуктивно. Qt Creator можно использовать не только для запуска пользовательского интерфейса Qt Quick, но и для компиляции кода на языке c++, как для хост-системы, так и для другого устройства с помощью кросс-компилятора. ВНИМАНИЕ Обновление скриншотов!
Пользовательский интерфейс При запуске Qt Creator вы попадаете на экран приветствия. На нем вы найдете наиболее важные подсказки по дальнейшей работе в Qt Creator и недавно использованные проекты. Вы также увидите список сессий, который может быть для вас пустым. Сессия - это набор проектов и конфигураций, сохраненных для быстрого доступа. Это очень удобно, когда у вас несколько клиентов с большими проектами. С левой стороны вы увидите селектор режимов. Селекторы режимов поддерживают типичные этапы рабочего процесса разработчика. Режим приветствия: Для ориентации. Режим редактирования: Фокусировка на коде Режим проектирования: Сосредоточьтесь на дизайне пользовательского интерфейса Режим отладки: Получение информации о работающем приложении Режим проектов: Изменение конфигурации запуска и сборки проектов Режим анализа: Для обнаружения утечек памяти и профилирования Режим справки: Удобный доступ к документации по Qt Ниже селекторов режимов находится собственно селектор конфигурации проекта и селектор запуска/отладки
Большую часть времени вы будете находиться в режиме редактирования с редактором кода в центральная панель. Время от времени вы будете заходить в режим Projects, когда потребуется настроить проект. Затем вы нажимаете кнопку Run . Qt Creator достаточно умен, чтобы убедиться, что ваш проект полностью собран, прежде чем запускать его. В нижней части расположены панели вывода проблем, сообщений приложения, сообщений компиляции и других сообщений.
Регистрация комплекта Qt Qt Kit - это, пожалуй, самый сложный аспект при работе с Qt Creator на начальном этапе. Qt Kit - это набор из версии Qt, компилятора и устройства, а также некоторых других параметров. Он используется для уникальной идентификации комбинации инструментов для сборки проекта. Типичный набор для настольного компьютера содержит компилятор C++, версию Qt (например, Qt 6.xx.yy) и устройство ("Desktop"). После создания проекта необходимо присвоить проекту набор, прежде чем Qt Creator сможет собрать проект. Перед созданием набора сначала необходимо установить компилятор и зарегистрировать версию Qt. Регистрация версии Qt осуществляется путем указания пути к исполняемому файлу qmake. Затем Qt Creator запрашивает у qmake информацию, необходимую для идентификации версии Qt. Это справедливо и для Qt 6, где предпочтительным инструментом сборки является CMake. Добавление комплекта и регистрация версии Qt осуществляется в разделе Настройки ‣ Комплекты entry. Там же можно увидеть, какие компиляторы зарегистрированы. СОВЕТ Сначала проверьте, зарегистрирована ли в вашем Qt Creator правильная версия Qt, а затем убедитесь, что указан комплект для вашей комбинации компилятора, Qt и устройства. Без комплекта невозможно собрать проект.
Управление проектами Qt Creator управляет исходным кодом в проектах. Создать новый проект можно с помощью команды File ‣ New File или Project . При создании проекта имеется множество вариантов шаблонов приложений. Qt Creator способен создавать настольные, встраиваемые, мобильные приложения и даже проекты на языке python с использованием Qt for Python. Есть шаблоны для приложений, использующих виджеты или Qt Quick, или даже "голые" проекты, использующие только консоль. Новичку трудно сделать выбор, поэтому мы выберем для вас три типа проектов. Другие проекты / QtQuick UI Prototype: Отлично подходит для игры с QML, так как не требует сборки на C++. В основном подходит только для создания прототипов. Приложения (Qt Quick) / Qt Quick Application (Empty): Создает "пустой" проект на языке C++ с поддержкой cmake и основным документом QML для отрисовки пустого окна. Это типичная стартовая точка по умолчанию для всех нативных QMLприложений. Библиотеки / Qt Quick 2.0 Extension Plug-in: С помощью этого мастера можно создать заглушку подключаемого модуля для пользовательского интерфейса Qt Quick. Подключаемый модуль используется для расширения Qt Quick собственными элементами. В идеале это позволяет создать многократно используемую библиотеку Qt Quick. Приложения (Qt) / Qt Widgets Application: Создает отправную точку для настольного приложения, использующего виджеты Qt. Это будет отправной точкой, если вы планируете создать традиционное приложение на основе виджетов на языке C++.
Приложения (Qt) / Консольное приложение Qt: Создает начальную точку для настольного приложения без пользовательского интерфейса. Это будет отправной точкой, если вы планируете создать традиционный инструмент командной строки на языке C++ с использованием Qt C++. СОВЕТ В первых частях книги мы будем использовать в основном тип QtQuick UI Prototype или Qt Quick Application, в зависимости от того, будем ли мы также использовать некоторый код на C++ с Qt Quick. В дальнейшем для описания некоторых аспектов работы с С++ мы будем использовать тип Qt Console Application. Для расширения Qt Quick собственными подключаемыми модулями мы будем использовать тип Qt Quick 2.0 Extension Plug-in wizard.
Использование редактора При открытии проекта или только что созданного проекта Qt Creator переключится в режим редактирования. Вы должны увидеть слева файлы проекта, а в центральной области - редактор кода. Выделение файлов слева приведет к их открытию в редакторе. Редактор обеспечивает подсветку синтаксиса, завершение кода и быстрые исправления. Кроме того, он поддерживает несколько команд для рефакторинга кода. При работе с редактором создается ощущение, что все реагирует мгновенно. Это происходит благодаря разработчикам Qt Creator, которые сделали инструмент очень быстрым.
Локатор Локатор является центральным компонентом Qt Creator. Он позволяет разработчикам быстро переходить к определенным местам в исходном коде или в справке. Чтобы открыть локатор, нажмите Ctrl+K . Слева внизу появится всплывающее окно со списком опций. Если вы просто ищете файл внутри вашего проекта, просто нажмите первую букву из имени файла. Локатор также принимает символы подстановки, поэтому \ * m a i n .qml также будет работать. В противном случае можно также использовать префикс поиска для поиска конкретного типа контента.
Пожалуйста, попробуйте это сделать. Например, чтобы открыть справку по QML-элементу Rectangle, откройте локатор и введите ? rectangle . Пока вы набираете текст, локатор будет обновлять предложения, пока вы не найдете нужную ссылку.
Отладка Qt Creator - это простая в использовании и хорошо продуманная IDE для разработки проектов на Qt C++ и QML. Она обладает первоклассной поддержкой CMake и предварительно настроена для разработки Qt C++. Благодаря отличной поддержке C++ она также может быть использована для любых других проектов на ванильном C++. СОВЕТ Хм, я только что понял, что не часто использовал отладку. Надеюсь, это хороший знак. Нужно попросить кого-нибудь помочь мне в этом. А пока посмотрите документацию по Qt Creator (http://http://doc.qt.io/qtcreator/index.html) .
Ярлыки Ярлыки - это разница между приятным в использовании редактором и профессиональный редактор. Как профессионал вы проводите сотни часов за своим приложением. Каждый ярлык, позволяющий ускорить работу, имеет значение. К счастью, разработчики Qt Creator думают так же и добавили в приложение буквально сотни ярлыков. Для начала работы мы собрали несколько основных сочетаний клавиш (в нотации Windows): Ctrl+B - Построить проект Ctrl+R - Запустить проект Ctrl+Tab Ctrl+K - Переключение между открытыми документами - Открыть локатор - вернуться назад (нажмите несколько раз, и вы снова окажетесь в редакторе) Esc F2 - Следовать за символом под курсором F4 - Переключение между заголовком и исходным текстом (полезно только для кода на языке c++) Список ярлыков Qt Creator (http://doc.qt.io/qtcreator/creator-keyboardshortcuts.html) из документации. Настройка ярлыков Настроить ярлыки можно изнутри создателя с помощью диалога настроек.

Quick Starter В этой главе представлен обзор QML, декларативного языка пользовательского интерфейса, используемого в Qt 6. Мы рассмотрим синтаксис QML, который представляет собой дерево элементов, а затем сделаем обзор наиболее важных базовых элементов. Затем мы кратко рассмотрим, как создавать собственные элементы, называемые компонентами, и как преобразовывать элементы с помощью манипуляций со свойствами. В конце мы рассмотрим, как расположить элементы в макете, и, наконец, рассмотрим элементы, в которых пользователь может осуществлять ввод.
Синтаксис QML QML - это декларативный язык, используемый для описания взаимосвязи объектов между собой. QtQuick - это фреймворк, построенный на QML для создания пользовательского интерфейса вашего приложения. Он разбивает пользовательский интерфейс на более мелкие элементы, которые могут быть объединены в компоненты. QtQuick описывает внешний вид и поведение этих элементов пользовательского интерфейса. Это описание пользовательского интерфейса может быть дополнено кодом JavaScript для обеспечения простой, но также и более сложной логики. С этой точки зрения, он повторяет схему HTML-JavaScript, но QML и QtQuick с самого начала были разработаны для описания пользовательских интерфейсов, а не текстовых документов. В своей простейшей форме QtQuick позволяет создавать иерархию элементов. Дочерние элементы наследуют систему координат от родительского. Координаты x,y всегда относятся к родительскому элементу. СОВЕТ QtQuick базируется на языке QML. Язык QML знает только элементы, свойства, сигналы и привязки. QtQuick - это фреймворк, построенный на основе QML. Используя свойства по умолчанию, иерархия элементов QtQuick может быть построена элегантным образом.
Начнем с простого примера QML-файла, чтобы пояснить различия в синтаксисе. // RectangleExample.qml import QtQuick // The root element is the Rectangle Rectangle { // name this element root id: root // properties: <name>: <value> width: 120; height: 240 // color property color: "#4A4A4A" // Declare a nested element (child of root) Image { id: triangle // reference the parent x: (parent.width - width)/2; y: 40 source: 'assets/triangle_red.png'
} // Another child of root Text { // un-named element // reference element by id y: triangle.y + triangle.height + 20 // reference root element width: root.width color: 'white' horizontalAlignment: Text.AlignHCenter text: 'Triangle' } } Оператор import импортирует модуль. Может быть добавлена необязательная версия в виде <major>.<minor>. Комментарии могут быть сделаны с помощью // для однострочных комментариев или /* */. для многострочных комментариев. Так же, как в C/C++ и JavaScript Каждый QML-файл должен иметь ровно один корневой элемент, как в HTML Элемент объявляется по его типу, за которым следует { } Элементы могут иметь свойства, которые имеют вид имя: значение Доступ к произвольным элементам внутри QML-документа можно получить, используя их id (идентификатор без кавычек) Элементы могут быть вложенными, то есть родительский элемент может иметь дочерние элементы. Доступ к родительскому элементу можно получить с помощью ключевого слова parent С помощью оператора import вы импортируете QML-модуль по имени. В Qt5 необходимо было указать мажорную и минорную версию (например, 2.15 ), теперь это делается так необязательный в Qt6. Для содержания книги мы отказываемся от этого необязательного номера версии
поскольку обычно вы автоматически хотите выбрать самую новую версию, доступную из выбранного вами комплекта Qt. СОВЕТ Часто требуется обращаться к определенному элементу по id или к родительскому элементу, используя ключевое слово parent. Поэтому хорошей практикой является присвоение корневому элементу имени "root" с использованием id: root . Тогда вам не придется думать о том, как именовать корневой элемент в вашем QML-документе. СОВЕТ Запустить пример с помощью среды исполнения Qt Quick из командной строки из вашей ОС можно следующим образом: $ $QTDIR/bin/qml RectangleExample.qml Где необходимо заменить $QTDIR на путь к вашей установке Qt. Исполняемый файл qml инициализирует среду выполнения Qt Quick и интерпретирует предоставленный QML-файл. В Qt Creator можно открыть соответствующий файл проекта и запустить документ RectangleExample.qml . Свойства Элементы объявляются с помощью имени элемента, а определяются с помощью его свойств или путем создания собственных свойств. Свойство - это простая пара ключ-значение, например, width: 100 , text: 'Greetings' , color: '#FF0000' . Свойство имеет четко определенный тип и может иметь начальное значение.
Text { // (1) identifier id: thisLabel // (2) set x- and y-position x: 24; y: 16 // (3) bind height to 2 * width height: 2 * width // (4) custom property property int times: 24 // (5) property alias property alias anotherTimes: thisLabel.times // (6) set text appended by value text: "Greetings " + times // (7) font is a grouped property font.family: "Ubuntu" font.pixelSize: 24 // (8) KeyNavigation is an attached property KeyNavigation.tab: otherLabel // (9) signal handler for property changes onHeightChanged: console.log('height:', height) // focus is need to receive key events focus: true // change color based on focus value color: focus ? "red" : "black" } Рассмотрим различные особенности свойств:
(1) id - это очень специальное свойство-значение, которое используется для ссылки на элементы внутри QML-файла (в QML он называется "документ"). id - это не строковый тип, а идентификатор и часть синтаксиса QML. id должен быть уникальным внутри документа, и его нельзя сбросить в другое значение, а также нельзя запросить (он ведет себя подобно ссылке в Мир C++). (2) Свойство может быть установлено в значение, зависящее от его типа. Если для свойства не задано значение, то будет выбрано начальное значение. Для получения дополнительной информации о начальном значении свойства необходимо обратиться к документации конкретного элемента. (3) Свойство может зависеть от одного или многих других свойств. Это называется связыванием. Связанное свойство обновляется при изменении зависимых свойств. Это работает как контракт, в данном случае высота всегда должна быть в два раза больше ширины. (4) Добавление новых свойств к элементу осуществляется с помощью квалификатора property, за которым следуют тип, имя и необязательное начальное значение ( property <type> <name> : <value> ). Если начальное значение не указано, то выбирается начальное значение по умолчанию. СОВЕТ Можно также объявить одно свойство свойством по умолчанию, используя ключевое слово default. Если внутри элемента создается другой элемент, не привязанный явно к какому-либо свойству, то он привязывается к свойству по умолчанию. Например, это используется при добавлении дочернего элемента elements. Дочерние элементы автоматически добавляются в список свойств типа children по умолчанию, если они являются видимыми элементами.
(5) Другим важным способом объявления свойств является использование ключевого слова alias ( property alias <имя>: <ссылка> ). Ключевое слово alias позволяет переслать свойство объекта или сам объект
изнутри типа во внешнюю область видимости. Этот прием мы будем использовать позже при определении компонентов для экспорта внутренних свойств или идентификаторов элементов на корневой уровень. Псевдоним свойства не нуждается в типе, он использует тип ссылаемого свойства или объекта. (6) Свойство text зависит от пользовательского свойства times типа int. Значение типа int строковый автоматически преобразуется в тип. Само выражение является еще одним примером связывания и приводит к тому, что текст обновляется каждый раз, когда изменяется свойство times. (7) Некоторые свойства являются сгруппированными свойствами. Это свойство используется, когда свойство является более структурированным и связанные свойства должны быть сгруппированы вместе. Другой способ записи сгруппированных свойств - font { family: "Ubuntu"; pixelSize: 24 } . (8) Некоторые свойства принадлежат самому классу элемента. Это делается для элементов глобальных настроек, которые появляются в приложении только один раз (например, ввод с клавиатуры). Запись имеет вид <Element>.<property>: <value> . (9) Для каждого свойства можно предоставить обработчик сигнала. Этот обработчик вызывается после изменения свойства. Например, здесь мы хотим получать уведомление при изменении высоты и использовать встроенную консоль для вывода сообщения в систему.
ВНИМАНИЕ Идентификатор элемента должен использоваться только для ссылки на элементы внутри вашего документа (например, текущего файла). В QML реализован механизм "динамической привязки", при котором документы, загруженные позже, перезаписывают идентификаторы элементов из ранее загруженных документов. Это позволяет ссылаться на идентификаторы элементов из ранее загруженных документов, если они еще не были перезаписаны. Это похоже на создание глобальных переменных. К сожалению, на практике это часто приводит к очень плохому коду, когда программа зависит от порядка выполнения. К сожалению, это нельзя отключить. Пожалуйста, используйте это с осторожностью, а еще лучше - не используйте этот механизм вообще. Лучше экспортировать элемент, который вы хотите предоставить внешнему миру, используя свойства корневого элемента вашего документа. Скриптинг QML и JavaScript (также известный как ECMAScript) - лучшие друзья. В главе, посвященной JavaScript, мы более подробно рассмотрим этот симбиоз. Сейчас мы просто хотим, чтобы вы знали об этих отношениях. Text { id: label x: 24; y: 24 // custom counter property for space presses property int spacePresses: 0 text: "Space pressed: " + spacePresses + " times" // (1) handler for text changes. Need to use function to ca onTextChanged: function(text) {

console.log("text changed to:", text) } // need focus to receive key events focus: true // (2) handler with some JS Keys.onSpacePressed: { increment() } // clear the text on escape Keys.onEscapePressed: { label.text = '' } // (3) a JS function function increment() { spacePresses = spacePresses + 1 } } (1) Обработчик изменения текста onTextChanged печатает текущий текст каждый раз, когда текст изменился из-за нажатия клавиши пробела. Поскольку мы используем параметр, передаваемый сигналом, нам необходимо использовать здесь синтаксис функции. Можно использовать и стрелочную функцию ( (text) => {} ), но нам кажется, ч т о function(text) {} более читабельна. (2) Когда текстовый элемент получает клавишу пробела (потому что пользователь нажал пробел на клавиатуре), мы вызываем JavaScript-функцию increment() . (3) Определение функции JavaScript в виде function <name> (<parameters>) { ... } spacePresses. , которая увеличивает наш счетчик При каждом увеличении spacePresses б у д у т обновляться и связанные свойства.
Переплет Разница между QML : (привязка) и JavaScript = (присваивание) заключается в том, что привязка является контрактом и сохраняет свое значение на протяжении всего времени существования привязки, в то время как JavaScript-присваивание ( = ) является однократным присвоением значения. Время жизни привязки заканчивается, когда для свойства устанавливается новая привязка или даже когда свойству присваивается значение JavaScript. Например, обработчик ключа, устанавливающий свойство text в пустую строку, уничтожит наше отображение инкремента: Keys.onEscapePressed: { label.text = '' } После нажатия escape нажатие клавиши пробела больше не будет обновлять отображение, так как предыдущая привязка свойства text (text: "Space pressed: " + spacePresses + " times") была разрушена. Когда у вас есть противоречивые стратегии изменения свойства, как в данном случае (текст обновляется при изменении инкремента свойства через привязку и текст очищается при присваивании JavaScript), то вы не можете использовать привязки! Необходимо использовать присваивание на обоих путях изменения свойства, так как привязка будет уничтожена присваиванием (нарушен контракт!).
Основные элементы Элементы можно разделить на визуальные и невизуальные. Визуальный элемент (например, Rectangle) имеет геометрию и обычно представляет собой область на экране. Невизуальный элемент (например, таймер) обеспечивает общую функциональность, обычно используемую для манипулирования визуальными элементами. В настоящее время мы сосредоточимся на основных визуальных элементах, таких как Item , Rectangle , Text , Image и MouseArea . Однако, используя модуль Qt Quick Controls 2, можно создавать пользовательские интерфейсы, построенные из стандартных компонентов платформы, таких как кнопки, ярлыки и ползунки. Элемент Item является базовым элементом для всех визуальных элементов, поэтому все остальные визуальные элементы наследуются от Item. Сам по себе он ничего не рисует, но определяет все свойства, которые являются общими для всех визуальных элементов: Геометрия - x и y д л я ширина определения верхне-левого положения, и высота для расширения элемента, и z для порядка укладки, чтобы поднять или опустить элементы вверх или вниз от их естественного порядка. Работа с макетом - якоря (левый, правый, верхний, нижний, вертикальный и горизонтальный центр) для позиционирования элементов относительно других элементов с необязательными полями. Работа с клавишами - подключены свойства Key и KeyNavigation для управления работой с клавишами, а также свойство focus для включения работы с клавишами в первую очередь.
Трансформация - преобразование масштаба и поворота и общее преобразование список свойств transform для преобразования x,y,z, а также
transformOrigin point. Визуальные - opacity для управления прозрачностью, visible для отображения/скрытия элементов, clip для ограничения операций рисования границами элементов и smooth для улучшения качества рендеринга. Определение состояния - свойство списка состояний с поддерживаемым списком состояний, свойство текущего состояния и свойство списка переходов для анимации изменений состояния. Для лучшего понимания различных свойств мы постараемся представить их на протяжении всей этой главы в контексте представленного элемента. Следует помнить, что эти фундаментальные свойства доступны для каждого визуального элемента и работают одинаково для всех этих элементов. СОВЕТ Элемент Item часто используется в качестве контейнера для других элементов, подобно элементу div в HTML. Прямоугольный элемент Rectangle расширяет Item и добавляет ему цвет заливки. Кроме того, он поддерживает границы, определяемые border.color и border.width. Для создания закругленных прямоугольников можно использовать свойство radius. Rectangle { id: rect1 x: 12; y: 12 width: 76; height: 96 color: "lightsteelblue" } Rectangle { id: rect2 x: 112; y: 12
width: 76; height: 96 border.color: "lightsteelblue" border.width: 4 radius: 8 } СОВЕТ Допустимыми значениями цвета являются цвета из имен цветов SVG (см. http://www.w3.org/TR/css3-color/#svg-color (http://www.w3.org/TR/css3-color/#svg-color) ). Цвета в QML можно задавать различными способами, но наиболее распространенным способом является строка RGB ('#FF4444') или имя цвета (например, 'white'). Произвольный цвет может быть создан с помощью некоторого JavaScript: color: Qt.rgba( Math.random(), Math.random(), Math.rando Помимо цвета заливки и границы, прямоугольник также поддерживает пользовательские градиенты: Rectangle { id: rect1 x: 12; y: 12 width: 176; height: 96 gradient: Gradient { GradientStop { position: 0.0; color: "lightsteelblue" }
GradientStop { position: 1.0; color: "slategray" } } border.color: "slategray" } Градиент определяется серией градиентных остановок. Каждая остановка имеет позицию и цвет. Позиция обозначает положение на оси y (0 = верх, 1 = низ). Цвет остановки градиента о б о з н а ч а е т цвет в данной позиции. СОВЕТ Прямоугольник, у которого не заданы ширина/высота, не будет виден. Такое часто случается, когда несколько прямоугольников по ширине (высоте) зависят друг от друга и что-то пошло не так в логике композиции. Так что будьте внимательны! СОВЕТ Создать градиент под углом не представляется возможным. Для этого лучше использовать предопределенные изображения. Один из вариантов - просто повернуть прямоугольник с градиентом, но следует помнить, что геометрия повернутого прямоугольника не изменится, что приведет к путанице, поскольку геометрия элемента не совпадает с видимой областью. С точки зрения автора, в этом случае действительно лучше использовать проектные градиентные изображения.
Текстовый элемент Для отображения текста можно использовать элемент Text. Его наиболее заметным свойством является свойство text типа string . Элемент вычисляет свои начальные ширину и высоту в зависимости от заданного текста и используемого шрифта. На шрифт можно влиять с помощью группы свойств font (например, font.family , font.pixelSize , ...). Для изменения цвета текста достаточно использовать свойство color. Text { text: "The quick brown fox" color: "#303030" font.family: "Ubuntu" font.pixelSize: 28 } Текст может быть выровнен по сторонам и по центру с помощью свойств horizontalAlignment и verticalAlignment. Для дополнительного улучшения визуализации текста можно использовать свойства style и styleColor, которые позволяют визуализировать текст в контурном, рельефном и утопленном виде. Для длинного текста часто требуется определить позицию разрыва, например, для очень длинного текста это можно сделать с помощью свойства elide. Свойство elide позволяет задать позицию разрыва слева, справа или посередине текста.
Если вы не хотите, чтобы отображался '...' режима ускорения, но при этом хотите видеть полный текст, вы можете обернуть текст, используя свойство wrapMode (работает только при явном задании ширины): Text { width: 40; height: 120 text: 'A very long text' // '...' shall appear in the middle elide: Text.ElideMiddle // red sunken text styling style: Text.Sunken styleColor: '#FF4444' // align text to the top verticalAlignment: Text.AlignTop // only sensible when no elide mode // wrapMode: Text.WordWrap } Элемент Text отображает только заданный текст, а все остальное пространство, которое он занимает, является прозрачным. Это означает, что он не отображает никаких фоновых декораций, поэтому при желании можно создать приемлемый фон. СОВЕТ Следует помнить, что начальная ширина элемента Text зависит от заданного шрифта и текстовой строки. Элемент Text без заданной ширины и без текста не будет виден, так как его начальная ширина будет равна 0.
СОВЕТ Часто при компоновке элементов Text необходимо различать выравнивание текста внутри границы элемента Text и выравнивание самой границы элемента. В первом случае необходимо использовать свойства horizontalAlignment и verticalAlignment, а во втором - манипулировать геометрией элемента или использовать якоря. Элемент изображения Элемент Image способен отображать изображения различных форматов (например, PNG , JPG , GIF , BMP , WEBP ). Полный список поддерживаемых форматов изображений можно найти в документации Qt (https://doc.qt.io/qt6/qimagereader.html#supportedImageFormats) . Кроме исходного текста свойство для указания URL-адреса изображения, оно содержит параметр fillMode, который управляет поведением изменения размера. Image { x: 12; y: 12 // width: 72 // height: 72 source: "assets/triangle_red.png" } Image { x: 12+64+12; y: 12 // width: 72 height: 72/2 source: "assets/triangle_red.png" fillMode: Image.PreserveAspectCrop clip: true }
СОВЕТ URL может представлять собой локальный путь с прямыми косыми чертами ( "./images/home.png" ) или web-ссылку (например, " h t t p : / / e x a m p l e . o r g / h o m e . p n g (http://example.org/home.png) "). СОВЕТ Элементы изображения, использующие PreserveAspectCrop, должны также включать обрезку, чтобы избежать вывода данных изображения за границы Image. По умолчанию обрезка отключена ( clip: false ). Необходимо включить обрезку ( clip: true ), чтобы ограничить рисование ограничивающим прямоугольником элемента. Это может быть использовано для любого визуального элемента, но применять его следует осторожно (https://doc.qt.io/qt- 6/qtquickperformance.html#clipping). СОВЕТ Используя язык C++, вы можете создать свой собственный провайдер изображений с помощью QQuickImageProvider . Это позволяет создавать изображения "на лету" и использовать потоковую загрузку изображений. Элемент MouseArea Для взаимодействия с этими элементами часто используется область MouseArea . Это прямоугольный невидимый элемент, в котором можно перехватывать события мыши. Сайт
Область мыши часто используется вместе с видимым элементом для выполнения команд при взаимодействии пользователя с визуальной частью. Rectangle { id: rect1 x: 12; y: 12 width: 76; height: 96 color: "lightsteelblue" MouseArea { id: area width: parent.width height: parent.height onClicked: rect2.visible = !rect2.visible } } Rectangle { id: rect2 x: 112; y: 12 width: 76; height: 96 border.color: "lightsteelblue" border.width: 4 radius: 8 }
СОВЕТ Это важный аспект Qt Quick: обработка ввода отделена от визуального представления. Это позволяет показать пользователю элемент интерфейса, где фактическая область взаимодействия может быть больше. СОВЕТ Для более сложного взаимодействия см. раздел Qt Quick Input Handlers (https://doc.qt.io/qt-6/qtquickhandlers-index.html) . Они предназначены для использования вместо таких элементов, как MouseArea и Flickable, и обеспечивают больший контроль и гибкость. Идея заключается в том, чтобы обрабатывать один аспект взаимодействия в каждом экземпляре обработчика вместо того, чтобы централизовать обработку всех событий от данного источника в одном элементе, как это было раньше.
Компоненты Компонент - это многократно используемый элемент. QML предоставляет различные способы создания компонентов. В настоящее время мы рассмотрим только самую простую форму компонент на основе файла. Компонент на основе файла создается путем помещения QML-элемента в файл и присвоения файлу имени элемента (например, Button.qml ). Компонент можно использовать, как и любой другой элемент из модуля Qt Quick. В нашем случае в коде это будет выглядеть так: Button { ... } . Например, создадим прямоугольник, содержащий текстовый компонент и область мыши. Это напоминает простую кнопку и для наших целей не требует усложнения. Rectangle { // our inlined button ui id: button x: 12; y: 12 width: 116; height: 26 color: "lightsteelblue" border.color: "slategrey" Text { anchors.centerIn: parent text: "Start" } MouseArea { anchors.fill: parent onClicked: { status.text = "Button clicked!" } } } Text { // text changes when button was clicked
id: status x: 12; y: 76 width: 116; height: 26 text: "waiting ..." horizontalAlignment: Text.AlignHCenter } Пользовательский интерфейс будет выглядеть примерно так. На первом изображении пользовательский интерфейс находится в начальном состоянии, а на втором - кнопка уже нажата. Теперь наша задача - извлечь пользовательский интерфейс кнопки в многократно используемый компонент. Для этого необходимо продумать возможный API для нашей кнопки. Это можно сделать, представив, как кто-то другой будет использовать вашу кнопку. Вот что я придумал: // minimal API for a button Button { text: "Click Me" onClicked: { /* do something */ } } Я хотел бы установить текст с помощью свойства text и реализовать собственный обработчик нажатия. Кроме того, я хотел бы, чтобы кнопка имела разумное начальное значение
размер, который я могу перезаписать (например, с помощью width: 240). Для этого мы создаем файл Button.qml и копируем в него наш пользовательский интерфейс кнопки. Кроме того, нам необходимо экспортировать свойства, которые пользователь может захотеть изменить на корневом уровне. // Button.qml import QtQuick Rectangle { id: root // export button properties property alias text: label.text signal clicked width: 116; height: 26 color: "lightsteelblue" border.color: "slategrey" Text { id: label anchors.centerIn: parent text: "Start" } MouseArea { anchors.fill: parent onClicked: { root.clicked() } } } Мы экспортировали свойство text и сигнал clicked на корневом уровне. Обычно мы называем наш корневой элемент root, чтобы упростить обращение к нему. Мы используем функцию псевдонимов в QML, которая позволяет экспортировать свойства внутри вложенных элементов QML на корневой уровень и сделать их доступными для элемента
внешний мир. Важно знать, что извне этого файла другие компоненты могут получить доступ только к свойствам корневого уровня. Чтобы использовать наш новый элемент Button, мы можем просто объявить его в нашем файле. Таким образом, предыдущий пример немного упростится. Button { // our Button component id: button x: 12; y: 12 text: "Start" onClicked: { status.text = "Button clicked!" } } Text { // text changes when button was clicked id: status x: 12; y: 76 width: 116; height: 26 text: "waiting ..." horizontalAlignment: Text.AlignHCenter } Теперь в пользовательском интерфейсе можно использовать сколько угодно кнопок, просто используя Button { ... } . Настоящая кнопка может быть более сложной, например, обеспечивать обратную связь при нажатии или показывать более красивое оформление.
СОВЕТ При желании можно даже пойти дальше и использовать элемент Item в качестве корневого элемента. Это не позволит пользователям изменить цвет кнопки, которую мы разработали, и обеспечит нам больший контроль над экспортируемым API. Цель должна заключаться в экспорте минимального API. Практически это означает, что нам нужно заменить корневой Rectangle на Item и сделать прямоугольник вложенным элементом в корневой элемент. Item { id: root width: 116; height: 26 property alias text: label.text signal clicked Rectangle { anchors.fill parent color: "lightsteelblue" border.color: "slategrey" } ... } С помощью этой техники легко создать целую серию многократно используемых компонентов.
Простыепреобразования Трансформация управляет геометрией объекта. Объекты QML, как правило, могут быть переведены, повернуты и масштабированы. Существует простая форма этих операций и более сложная. Начнем с простых преобразований. В качестве отправной точки приведем нашу сцену. Простой перевод осуществляется путем изменения положения x,y. Поворот осуществляется с помощью свойства rotation. Значение задается в градусах (0 ... 360). Масштабирование выполняется с помощью свойства scale, причем значение <1 означает уменьшение масштаба элемента, а >1 - увеличение. Вращение и масштабирование не изменяют геометрию элемента: x,y и не меняются; преобразуются только инструкции рисования. Прежде чем мы покажем пример, я хотел бы представить вам небольшого помощника: элемент ClickableImage. ClickableImage это просто изображение с областью мыши. В связи с этим возникает полезное правило: если вы скопировали кусок кода три раза, извлеките его в компонент. // ClickableImage.qml // Simple image which can be clicked import QtQuick Image { id: root signal clicked
MouseArea { anchors.fill: parent onClicked: root.clicked() } } Мы используем наше кликабельное изображение для представления трех объектов (коробка, круг, треугольник). Каждый объект при щелчке выполняет простое преобразование. Щелчок на фоне приводит к сбросу сцены. // TransformationExample.qml import QtQuick Item { // set width based on given background width: bg.width height: bg.height Image { // nice background image id: bg source: "assets/background.png" } MouseArea { id: backgroundClicker // needs to be before the images as order matters // otherwise this mousearea would be before the other e
// and consume the mouse events anchors.fill: parent onClicked: { // reset our little scene circle.x = 84 box.rotation = 0 triangle.rotation = 0 triangle.scale = 1.0 } } ClickableImage { id: circle x: 84; y: 68 source: "assets/circle_blue.png" antialiasing: true onClicked: { // increase the x-position on click x += 20 } } ClickableImage { id: box x: 164; y: 68 source: "assets/box_green.png" antialiasing: true onClicked: { // increase the rotation on click rotation += 15 } } ClickableImage { id: triangle x: 248; y: 68 source: "assets/triangle_red.png" antialiasing: true onClicked: { // several transformations
rotation += 15 scale += 0.05 } } // ... Окружность увеличивает положение x при каждом щелчке, а рамка поворачивается при каждом щелчке. Треугольник будет поворачивать и масштабировать изображение при каждом щелчке, демонстрируя комбинированное преобразование. Для операций масштабирования и вращения мы установили значение antialiasing: true, чтобы включить сглаживание, которое отключено (так же, как и свойство обрезки clip ) по соображениям производительности. В собственной работе, когда в графике появляются растрированные края, сглаживание, вероятно, следует включить. СОВЕТ Для достижения лучшего качества изображения при масштабировании рекомендуется масштабировать не вверх, а вниз. Увеличение масштаба изображения с большим коэффициентом масштабирования приведет к появлению артефактов масштабирования (размытости изображения). При масштабировании изображения следует использовать smooth: true, что позволяет использовать фильтр более высокого качества за счет снижения производительности.
Фоновая MouseArea занимает весь фон и сбрасывает значения объектов. СОВЕТ Элементы, которые появляются раньше в коде, имеют более низкий порядок укладки (так называемый z-порядок). Если долго щелкать на круге, то можно увидеть, что он перемещается ниже бокса. Порядком z можно также управлять с помощью свойства z элемента. Это связано с тем, что box появляется позже в коде. То же самое относится и к областям мыши. Область мыши, расположенная позже в коде, будет перекрывать (и, следовательно, захватывать события мыши) область мыши, расположенную раньше в коде. Помните: порядок расположения элементов в документе имеет значение.
Позиционированиеэлементов Существует ряд QML-элементов, используемых для позиционирования элементов. Они называются позиционерами, из которых модуль Qt Quick предоставляет следующие: Row , Column , Grid и Flow . На рисунке ниже можно увидеть, как они отображают одно и то же содержимое. СОВЕТ Прежде чем перейти к деталям, позвольте представить некоторые вспомогательные элементы: красный, синий, зеленый, более светлый и более темный квадраты. Каждый из этих компонентов содержит раскрашенный прямоугольник размером 48x48 пикселей. В качестве справочного материала здесь приведен исходный код квадрата RedSquare : // RedSquare.qml import QtQuick Rectangle { width: 48 height: 48 color: "#ea7025" border.color: Qt.lighter(color) } Обратите внимание на использование Qt.lighter(color) для получения более светлого цвета границы на основе цвета заливки. Мы будем использовать эти помощники в следующих примерах, чтобы сделать исходный код более компактным и читабельным. Помните, что каждый прямоугольник изначально имеет размер 48x48 пикселей.
Элемент Column организует дочерние элементы в колонку, укладывая их друг на друга. Свойство spacing может быть использовано для расстояния между дочерними элементами. // ColumnExample.qml import QtQuick DarkSquare { id: root width: 120 height: 240 Column { id: column anchors.centerIn: parent spacing: 8 RedSquare { } GreenSquare { width: 96 } BlueSquare { } } } Элемент Row размещает свои дочерние элементы рядом друг с другом, либо слева направо, либо справа налево, в зависимости от
свойство layoutDirection. Д л я разделения дочерних элементов снова используется интервал. // RowExample.qml import QtQuick BrightSquare { id: root width: 400; height: 120 Row { id: row anchors.centerIn: parent spacing: 20 BlueSquare { } GreenSquare { } RedSquare { } } } Элемент Grid располагает свои дочерние элементы в виде сетки. Задавая свойства rows и columns, можно ограничить количество строк или столбцов. Если не задавать ни одно из них, то другое вычисляется из количества дочерних элементов. Например, если установить значение rows равным 3 и добавить 6 дочерних элементов, то получится 2 столбца. Свойства flow и layoutDirection используются для управления порядком расположения элементов добавляется в сетку, а spacing управляет количеством пространства, разделяющего дочерние элементы.
// GridExample.qml import QtQuick BrightSquare { id: root width: 160 height: 160 Grid { id: grid rows: 2 columns: 2 anchors.centerIn: parent spacing: 8 RedSquare { } RedSquare { } RedSquare { } RedSquare { } } } Последним позиционером является Flow. Он добавляет свои дочерние элементы в поток. Направление потока контролируется с помощью параметров flow и layoutDirection . Он может идти сбоку или сверху вниз. Он также может идти слева направо или в противоположном направлении. По мере добавления элементов в поток они заворачиваются, образуя при необходимости новые строки или столбцы. Для того чтобы поток работал, он должен
имеют ширину или высоту. Они могут быть заданы как непосредственно, так и с помощью макетов якорей. // FlowExample.qml import QtQuick BrightSquare { id: root width: 160 height: 160 Flow { anchors.fill: parent anchors.margins: 20 spacing: 20 RedSquare { } BlueSquare { } GreenSquare { } } } Элементом, часто используемым с позиционерами, является повторитель (Repeater). Он работает как цикл for и выполняет итерации по модели. В простейшем случае модель - это просто значение, задающее количество циклов.
// RepeaterExample.qml import QtQuick DarkSquare { id: root width: 252 height: 252 property variant colorArray: ["#00bde3", "#67c111", "#ea702 Grid{ anchors.fill: parent anchors.margins: 8 spacing: 4 Repeater { model: 16 delegate: Rectangle { required property int index property int colorIndex: Math.floor(Math.random width: 56; height: 56 color: root.colorArray[colorIndex] border.color: Qt.lighter(color) Text { anchors.centerIn: parent
color: "#f0f0f0" text: "Cell " + parent.index } } } } } В этом примере ретранслятора мы используем новую магию. Мы определяем собственное свойство colorArray, которое представляет собой массив цветов. Повторитель создает серию прямоугольников (16, как определено в модели). Для каждого цикла он создает прямоугольник, определенный дочерним свойством ретранслятора. В прямоугольнике мы выбираем цвет с помощью математических функций JS: Math.floor(Math.random()*3) . Это дает нам случайное число в диапазоне от 0...2, которое мы используем для выбора цвета из нашего массива цветов. Как уже отмечалось, JavaScript является основной частью Qt Quick, и поэтому нам доступны стандартные библиотеки. В ретранслятор вводится свойство index. Оно содержит текущий индекс цикла. (0,1,..15). Мы можем использовать его для принятия собственных решений на основе индекса или, в нашем случае, для визуализации текущего индекса с помощью элемента Text. СОВЕТ Хотя свойство index динамически вводится в Rectangle, для облегчения читаемости и помощи в работе с инструментами рекомендуется объявлять его как обязательное свойство. Для этого используется обязательное свойство int index line.
СОВЕТ Более сложная работа с большими моделями и кинетическими представлениями с динамическими делегатами рассматривается в отдельной главе, посвященной моделям-представлениям. Ретрансляторы лучше всего использовать при небольшом количестве статических данных, которые необходимо представить.
Макет Элементы QML предоставляет гибкий способ компоновки элементов с помощью якорей. Концепция привязки является фундаментальной для Item , и доступна для всех визуальных элементов QML. Якоря действуют подобно контракту и являются более сильными, чем конкурирующие изменения геометрии. Якоря - это выражение относительности; для привязки всегда нужен связанный элемент. Элемент имеет 6 основных линий привязки (top, bottom, left, right, horizontalCenter, verticalCenter). имеется базовая линия п р и в я з к и Кроме того, в элементах Text для текста. Каждая линия привязки имеет свое смещение. В случае привязок сверху, снизу, слева и справа они называются полями. Для привязок horizontalCenter, verticalCenter смещениями. и baseline о н и н а з ы в а ю т с я
Элемент заполняет родительский элемент. GreenSquare { BlueSquare { width: 12 anchors.fill: parent anchors.margins: 8 text: '(1)' } } Элемент выравнивается по левому краю относительно родительского. GreenSquare { BlueSquare { width: 48 y: 8 anchors.left: parent.left anchors.leftMargin: 8 text: '(2)' } } Левая сторона элемента выравнивается по правой стороне родительского элемента.
GreenSquare { BlueSquare { width: 48 anchors.left: parent.right text: '(3)' } } Элементы, выровненные по центру. Blue1 горизонтально центрирован на родительском элементе. Blue2 также выровнен по горизонтали, но относительно Blue1, и его верхняя часть выровнена по нижней линии Blue1. GreenSquare { BlueSquare { id: blue1 width: 48; height: 24 y: 8 anchors.horizontalCenter: parent.horizontalCenter } BlueSquare { id: blue2 width: 72; height: 24 anchors.top: blue1.bottom anchors.topMargin: 4 anchors.horizontalCenter: blue1.horizontalCenter text: '(4)' } } Элемент центрируется по родительскому элементу GreenSquare { BlueSquare { width: 48 anchors.centerIn: parent
text: '(5)' } } Элемент центрируется со смещением влево относительно родительского элемента с помощью горизонтальных и вертикальных центрирующих линий GreenSquare { BlueSquare { width: 48 anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenterOffset: -12 anchors.verticalCenter: parent.verticalCenter text: '(6)' } } Скрытые драгоценные камни Наши квадраты были волшебным образом усовершенствованы, чтобы обеспечить возможность перетаскивания. Попробуйте воспользоваться примером и перетащить несколько квадратов. Вы увидите, что (1) нельзя перетащить, так как он закреплен со всех сторон (хотя родителя (1) можно перетащить, так как он вообще не закреплен). (2) можно перетащить по вертикали, так как привязана только левая сторона. То же самое относится и к (3). (4) можно перетаскивать только по вертикали, так как оба квадрата центрированы по горизонтали. (5) центрирован на родителе и поэтому не может быть перетащен. То же самое относится и к (6). Перетаскивание элемента означает изменение его положения x,y. Поскольку привязка сильнее, чем задание свойств x,y, перетаскивание ограничивается привязанными линиями. Этот эффект мы увидим позже, когда будем обсуждать анимацию.
Элементы ввода Мы уже использовали MouseArea качестве элемента ввода данных с помощью мыши. Далее мы сосредоточимся на вводе с клавиатуры. Начнем с элементов редактирования текста: TextInput и TextEdit . TextInput позволяет пользователю ввести строку текста. Элемент поддерживает такие ограничения ввода, как validator, inputMask и echoMode. // textinput.qml import QtQuick Rectangle { width: 200 height: 80 color: "linen" TextInput { id: input1 x: 8; y: 8 width: 96; height: 20 focus: true text: "Text Input 1" } TextInput { id: input2 x: 8; y: 36 width: 96; height: 20
text: "Text Input 2" } } Пользователь может щелкнуть внутри TextInput, чтобы изменить фокус. Для поддержки переключения фокуса с клавиатуры мы можем использовать прикрепленное свойство KeyNavigation. // textinput2.qml import QtQuick Rectangle { width: 200 height: 80 color: "linen" TextInput { id: input1 x: 8; y: 8 width: 96; height: 20 focus: true text: "Text Input 1" KeyNavigation.tab: input2 } TextInput { id: input2 x: 8; y: 36 width: 96; height: 20 text: "Text Input 2" KeyNavigation.tab: input1
} } Присоединенное свойство KeyNavigation поддерживает предварительный набор навигационных клавиш, к которым привязывается идентификатор элемента, переключающий фокус при нажатии заданной клавиши. Элемент ввода текста не имеет никакого визуального оформления, кроме мигающего курсора и вводимого текста. Для того чтобы пользователь мог распознать элемент как элемент ввода, ему необходимо некоторое визуальное оформление, например, простой прямоугольник. При размещении TextInput внутри элемента необходимо убедиться в том, что вы экспортируете основные свойства, которые должны быть доступны другим пользователям. Мы перенесем этот кусок кода в наш собственный компонент под названием TLineEditV1 для повторного использования. // TLineEditV1.qml import QtQuick Rectangle { width: 96; height: input.height + 8 color: "lightsteelblue" border.color: "gray" property alias text: input.text property alias input: input TextInput { id: input anchors.fill: parent anchors.margins: 4 focus: true } } }
СОВЕТ Если вы хотите экспортировать TextInput полностью, то можно экспортировать элемент с помощью псевдонима свойства input: input . Первый вход - это имя свойства, а второй - идентификатор элемента. Затем мы переписываем наш пример KeyNavigation с новым TLineEditV1 компонент. Rectangle { ... TLineEditV1 { id: input1 ... } TLineEditV1 { id: input2 ... } } Попробуйте использовать клавишу табуляции для навигации. Вы увидите, что фокус не переключается на input2. Простого использования focus: true недостаточно. Проблема заключается в том, что при передаче фокуса на элемент input2 элемент верхнего уровня внутри TlineEditV1 (наш Rectangle ) получил фокус и не передал его на TextInput . Для предотвращения этого QML предлагает функцию FocusScope .
FocusScope Область видимости фокуса объявляет, что последний дочерний элемент с focus: true получает фокус, когда область видимости фокуса получает фокус. Таким образом, она пересылает фокус последнему запрашивающему фокус дочернему элементу. Создадим вторую версию нашего компонента TLineEdit под названием TLineEditV2, используя в качестве корневого элемента focus scope. // TLineEditV2.qml import QtQuick FocusScope { width: 96; height: input.height + 8 Rectangle { anchors.fill: parent color: "lightsteelblue" border.color: "gray" } property alias text: input.text property alias input: input TextInput { id: input anchors.fill: parent anchors.margins: 4 focus: true } } Теперь наш пример выглядит следующим образом:
Rectangle { ... TLineEditV2 { id: input1 ... } TLineEditV2 { id: input2 ... } } Теперь нажатие клавиши tab успешно переключает фокус между двумя компонентами, и фокусируется нужный дочерний элемент внутри компонента. TextEdit TextEdit очень похож н а TextInput и поддерживает многострочное поле редактирования текста. У него нет свойств ограничения текста, так как это зависит от запроса размера содержимого текста (contentHeight , contentWidth ). Мы также создаем собственный компонент под названием TTextEdit для обеспечения фона редактирования и использования области фокусировки для более точной переадресации фокуса. // TTextEdit.qml import QtQuick FocusScope { width: 96; height: 96 Rectangle { anchors.fill: parent color: "lightsteelblue" border.color: "gray"
} property alias text: input.text property alias input: input TextEdit { id: input anchors.fill: parent anchors.margins: 4 focus: true } } Вы можете использовать его подобно компоненту TLineEdit // textedit.qml import QtQuick Rectangle { width: 136 height: 120 color: "linen" TTextEdit { id: input x: 8; y: 8 width: 120; height: 104 focus: true text: "Text Edit" } }
Элемент "Ключи Присоединенное свойство Keys позволяет выполнять код, основанный на нажатии определенных клавиш. Например, для перемещения и масштабирования квадрата мы можем подключиться к клавишам "вверх", "вниз", "влево" и "вправо", чтобы переместить элемент, и к клавишам "плюс" и "минус", чтобы масштабировать элемент. // keys.qml import QtQuick DarkSquare { width: 400; height: 200 GreenSquare { id: square x: 8; y: 8 } focus: true Keys.onLeftPressed: square.x -= 8 Keys.onRightPressed: square.x += 8 Keys.onUpPressed: square.y -= 8 Keys.onDownPressed: square.y += 8 Keys.onPressed: function (event) { switch(event.key) { case Qt.Key_Plus: square.scale += 0.2 break; case Qt.Key_Minus: square.scale -= 0.2
break; } } }
Advanced Techniques Производительность QML QML и Javascript являются интерпретируемыми языками. Это означает, что перед выполнением они не должны обрабатываться компилятором. Вместо этого они выполняются внутри механизма исполнения. Однако, поскольку интерпретация является дорогостоящей операцией, для повышения производительности используются различные приемы. Для повышения производительности движок QML использует компиляцию "точно в срок" (JIT). Он также кэширует промежуточные результаты, чтобы избежать необходимости повторной компиляции. Для разработчика это работает без проблем. Единственным признаком этого является то, что рядом с исходными файлами можно найти файлы, заканчивающиеся на qmlc и jsc. Если вы хотите избежать штрафа за начальный запуск, вызванного начальным разбором, вы также можете предварительно скомпилировать QML и Javascript. Это требует помещения кода в файл ресурсов Qt и подробно описано в главе "Компиляция QML с опережением времени" (https://doc.qt.io/qt-6/qtquickdeployment.html#ahead-of-time-compilation) в документации по Qt.
Жидкость Элементы До сих пор мы рассматривали в основном некоторые простые графические элементы и способы их расположения и манипулирования ими. Эта глава посвящена тому, как сделать эти изменения более интересными, анимировав их. Анимация является одной из ключевых основ современных и удобных пользовательских интерфейсов и может быть использована в пользовательском интерфейсе с помощью состояний, переходов и анимации. Каждое состояние определяет набор изменений свойств и может быть объединено с анимацией изменения состояния. Эти изменения описываются как переход из одного состояния в другое. Кроме того, что анимация используется при переходах, она может применяться и как самостоятельный элемент, запускаемый по какимлибо записанным событиям.
Анимация Анимация применяется к изменениям свойств. Анимация определяет кривую интерполяции от одного значения к другому при изменении значения свойства. Эти анимационные кривые создают плавные переходы от одного значения к другому. Анимация определяется рядом целевых свойств, которые необходимо анимировать, кривой смягчения интерполяционной кривой и длительностью. Все анимации в Qt Quick управляются одним и тем же таймером и поэтому синхронизированы. Это позволяет повысить производительность и визуальное качество анимации. Анимация управляет изменением свойств с помощью интерполяции значений Это фундаментальная концепция. QML основан на элементах, свойствах и сценариях. Каждый элемент предоставляет десятки свойств, и каждое свойство ждет, когда вы его оживите. В книге вы увидите, что это захватывающее игровое поле. Глядя на некоторые анимации, вы будете просто восхищаться их красотой, а заодно и своим творческим гением. Помните, что анимация управляет изменением свойств, а каждый элемент имеет в своем распоряжении десятки свойств. Разблокируйте силу!
// AnimationExample.qml import QtQuick Image { id: root source: "assets/background.png" property int padding: 40 property int duration: 4000 property bool running: false Image { id: box x: root.padding; y: (root.height-height)/2 source: "assets/box_green.png" NumberAnimation on x { to: root.width - box.width - root.padding duration: root.duration running: root.running } RotationAnimation on rotation { to: 360 duration: root.duration running: root.running } } MouseArea { anchors.fill: parent onClicked: root.running = true
} } В приведенном примере показана простая анимация, примененная к свойствам x и rotation. Длительность каждой анимации составляет 4000 миллисекунд (мс). Анимация по x постепенно перемещает координату x объекта на 240px. Анимация вращения выполняется от текущего угла до 360 градусов. Обе анимации выполняются параллельно и запускаются при нажатии на MouseArea. С анимацией можно поиграть, меняя значения to и или добавить еще одну анимацию (например, на непрозрачности или даже на масштабе). Комбинируя их, можно создать впечатление, что объект исчезает в глубоком космосе. Попробуйте! Элементы анимации Существует несколько типов анимационных элементов, каждый из которых оптимизирован для конкретного случая использования. Ниже приведен список наиболее распространенных видов анимации: PropertyAnimation NumberAnimation ColorAnimation - Анимация изменения значений свойств - Анимация изменений значений типа qreal - Анимация изменения значений цвета RotationAnimation - Анимация изменения значений поворота Помимо этих базовых и широко используемых элементов анимации, Qt Quick предоставляет и более специализированные анимации для конкретных случаев использования: PauseAnimation - Обеспечивает паузу для анимации SequentialAnimation последовательно - Позволяет запускать анимацию
ParallelAnimation - Позволяет запускать анимацию параллельно AnchorAnimation - Анимация изменения значений якорей ParentAnimation - Анимация изменения родительских значений SmoothedAnimation - Позволяет свойству плавно отслеживать значение - Позволяет свойству отслеживать значение в SpringAnimation пружинящем движении - Анимация элемента вдоль траектории движения PathAnimation Vector3dAnimation - Анимация изменений значений QVector3d Позже мы узнаем, как создать последовательность анимаций. При работе над более сложными анимациями иногда возникает необходимость изменить какое-либо свойство или запустить сценарий во время текущей анимации. Для этого Qt Quick предлагает элементы действия, которые можно использовать везде, где можно использовать другие элементы анимации: PropertyAction - задает немедленное изменение свойств во время анимации ScriptAction - Определяет сценарии, запускаемые во время анимации Основные виды анимации будут рассмотрены в этой главе на небольших концентрированных примерах. Применение анимации Анимация может применяться несколькими способами: Анимация на свойстве - запускается автоматически после полной загрузки элемента
Поведение при изменении свойства - выполняется автоматически при изменении значения свойства . Автономная анимация - запускается, когда анимация явно запущена с помощью функции start() или для параметра running установлено значение true (например, с помощью привязки свойства) Позже мы также увидим, как анимация может быть использована внутри переходов состояний. Кликабельное изображение V2 Для демонстрации использования анимации мы повторили наш компонент ClickableImage из предыдущей главы и дополнили его текстовым элементом. // ClickableImageV2.qml // Simple image which can be clicked import QtQuick Item { id: root width: container.childrenRect.width height: container.childrenRect.height property alias text: label.text property alias source: image.source signal clicked Column { id: container Image { id: image } Text { id: label width: image.width horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap color: "#ececec"
} } MouseArea { anchors.fill: parent onClicked: root.clicked() } } Для организации элемента под изображением мы использовали позиционер Column и вычислили ширину и высоту на основе свойства childrenRect колонки. Мы открыли свойства источника текста и изображения, а также сигнал щелчка. Мы также хотели, чтобы текст был такой же ширины, как и изображение, и чтобы он обворачивался. Для этого мы используем свойство wrapMode элемента Text. Геометрическая зависимость "родитель/ребенок Из-за инверсии геометрической зависимости (родительская геометрия зависит от дочерней) мы не можем задать ширину/высоту для ClickableImageV2, так как это нарушит нашу привязку ширины/высоты. Предпочтительнее, чтобы геометрия дочернего элемента зависела от геометрии родительского элемента, если элемент является скорее контейнером для других элементов и должен адаптироваться к геометрии родительского элемента. Объекты по возрастанию
Три объекта находятся в одном и том же положении по оси y ( y=200 ). Им всем нужно добраться до y=40, причем каждый из них использует свой метод с различными побочными эффектами и возможностями. Первый объект Первый объект перемещается с использованием стратегии Animation on <property>. Анимация начинается немедленно. ClickableImageV2 { id: greenBox x: 40; y: root.height-height source: "assets/box_green.png" text: qsTr("animation on property") NumberAnimation on y { to: 40; duration: 4000 } } При щелчке на объекте его положение по оси y сбрасывается в начальное положение, причем это происходит со всеми объектами. Для первого объекта сброс не имеет значения
пока работает анимация, не имеет никакого эффекта. Это может быть визуально неприятно, так как за долю секунды до начала анимации y-позиция устанавливается в новое значение. Таких конкурирующих изменений свойства следует избегать. Второй объект Второй объект путешествует, используя поведение анимации. Это поведение указывает свойству, что оно должно анимировать каждое изменение значения. Поведение можно отключить, установив для элемента Behavior значение enabled: false. ClickableImageV2 { id: blueBox x: (root.width-width)/2; y: root.height-height source: "assets/box_blue.png" text: qsTr("behavior on property") Behavior on y { NumberAnimation { duration: 4000 } } onClicked: y = 40 // random y on each click // onClicked: y = 40 + Math.random() * (205-40) } Объект начнет перемещаться, когда вы щелкнете на нем (его положение по оси y будет установлено на 40). Повторный щелчок не оказывает никакого влияния, так как положение уже установлено. Можно попробовать использовать случайное значение (например, 40 + (Math.random()\* (205- 40) ) для позиции y. Вы увидите, что объект всегда будет анимироваться в новое положение и адаптировать свою скорость, чтобы соответствовать 4 секундам до места назначения, определенным длительностью анимации.
Третий объект Третий объект использует автономную анимацию. Анимация определяется как собственный элемент и может находиться практически в любом месте документа. ClickableImageV2 { id: redBox x: root.width-width-40; y: root.height-height source: "assets/box_red.png" onClicked: anim.start() // onClicked: anim.restart() text: qsTr("standalone animation") NumberAnimation { id: anim target: redBox properties: "y" to: 40 duration: 4000 } } Щелчок запускает анимацию с помощью функции start() анимации. Каждая анимация имеет функции start(), stop(), resume() и restart(). Сама анимация содержит гораздо больше информации, чем другие типы анимации, рассмотренные ранее. Нам необходимо определить target - элемент, который будет анимирован, а также имена свойств, которые мы хотим анимировать. Также необходимо определить значение to и, в данном случае, значение from, которое позволяет перезапустить анимацию.
Щелчок на фоне приводит к сбросу всех объектов в исходное положение. Первый объект не может быть перезапущен, кроме как путем повторного запуска программы, вызывающей повторную загрузку элемента. Другие способы управления анимацией Другим способом запуска/остановки анимации является привязка свойства к выполняющемуся свойству анимации. Это особенно удобно, когда пользовательский ввод управляет свойствами: NumberAnimation { // [...] // animation runs when mouse is pressed running: area.pressed } MouseArea { id: area }
Смягчение кривых Изменением значения свойства можно управлять с помощью анимации. Атрибуты Easing позволяют влиять на интерполяционную кривую изменения свойства. Все анимации, которые мы определили к настоящему времени, используют линейную интерполяцию, поскольку исходным типом смягчения анимации является Easing.Linear . Лучше всего это представить на небольшом графике, где ось y - анимируемое свойство, а ось x - время (длительность). Линейная интерполяция проведет прямую линию от значения from в начале анимации до значения to в конце анимации. Таким образом, тип easing определяет кривую изменения . Типы смягчения должны быть тщательно подобраны, чтобы поддерживать естественное соответствие движущемуся объекту. Например, когда страница выдвигается, она должна сначала выдвигаться медленно, а затем набирать обороты и в конце концов выдвигаться с большой скоростью, подобно перелистыванию страницы книги. Не следует злоупотреблять анимацией. Как и в других аспектах дизайна пользовательского интерфейса, анимация должна быть тщательно продумана, чтобы поддерживать поток пользовательского интерфейса, а не доминировать над ним. Глаз очень чувствителен к движущимся объектам, и анимация может легко отвлечь пользователя. В следующем примере мы попробуем использовать несколько кривых смягчения. Каждая кривая смягчения отображается в виде кликабельного изображения и при нажатии на нее устанавливает новый тип смягчения для анимации квадрата, а затем запускает функцию restart() для запуска анимации с новой кривой.
Код для этого примера был немного усложнен. Сначала мы создаем сетку типов EasingTypes и бокс, который управляется типами easing. Тип смягчения просто отображает кривую, которую бокс должен использовать для своей анимации. Когда пользователь щелкает на кривой смягчения, бокс перемещается в направлении, соответствующем кривой смягчения. Сама анимация представляет собой отдельную анимацию с целью, установленной на коробку, и настроенной на анимацию свойства x с длительностью 2 секунды. СОВЕТ Внутреннее устройство EasingType выводит кривую в реальном времени, и заинтересованный читатель может посмотреть это в примере EasingCurves. // EasingCurves.qml import QtQuick import QtQuick.Layouts Rectangle { id: root width: childrenRect.width height: childrenRect.height
color: '#4a4a4a' gradient: Gradient { GradientStop { position: 0.0; color: root.color } GradientStop { position: 1.0; color: Qt.lighter(root.co } ColumnLayout { Grid { spacing: 8 columns: 5 EasingType { easingType: Easing.Linear title: 'Linear' onClicked: { animation.easing.type = easingType box.toggle = !box.toggle } } EasingType { easingType: Easing.InExpo title: "InExpo" onClicked: { animation.easing.type = easingType box.toggle = !box.toggle } } EasingType { easingType: Easing.OutExpo title: "OutExpo" onClicked: { animation.easing.type = easingType box.toggle = !box.toggle } } EasingType { easingType: Easing.InOutExpo title: "InOutExpo" onClicked: { animation.easing.type = easingType
box.toggle = !box.toggle } } EasingType { easingType: Easing.InOutCubic title: "InOutCubic" onClicked: { animation.easing.type = easingType box.toggle = !box.toggle } } EasingType { easingType: Easing.SineCurve title: "SineCurve" onClicked: { animation.easing.type = easingType box.toggle = !box.toggle } } EasingType { easingType: Easing.InOutCirc title: "InOutCirc" onClicked: { animation.easing.type = easingType box.toggle = !box.toggle } } EasingType { easingType: Easing.InOutElastic title: "InOutElastic" onClicked: { animation.easing.type = easingType box.toggle = !box.toggle } } EasingType { easingType: Easing.InOutBack title: "InOutBack" onClicked: { animation.easing.type = easingType
box.toggle = !box.toggle } } EasingType { easingType: Easing.InOutBounce title: "InOutBounce" onClicked: { animation.easing.type = easingType box.toggle = !box.toggle } } } Item { height: 80 Layout.fillWidth: true Box { id: box property bool toggle x: toggle ? 20 : root.width - width - 20 anchors.verticalCenter: parent.verticalCenter gradient: Gradient { GradientStop { position: 0.0; color: "#2ed5 GradientStop { position: 1.0; color: "#2467 } Behavior on x { NumberAnimation { id: animation duration: 500 } } } } } } Пожалуйста, поиграйте с примером и понаблюдайте за изменением скорости при анимации. Некоторые анимации кажутся более естественными для объекта, а некоторые вызывают раздражение.
Помимо свойств duration и easing.type, существует возможность тонкой настройки анимации. Например, общий тип PropertyAnimation (от которого наследуется большинство анимаций) дополнительно поддерживает свойства easing.amplitude, easing.overshoot и easing.period, позволяющие тонко настраивать поведение определенных кривых ослабления. Не все кривые ослабления поддерживают эти параметры. Чтобы проверить, влияет ли тот или иной параметр смягчения на кривую смягчения, обратитесь к таблице смягчения (http://doc.qt.io/qt-6/qmlqtquick-propertyanimation.html#easing- prop) из документации PropertyAnimation. Выбор правильной анимации Выбор правильной анимации для элемента в контексте пользовательского интерфейса имеет решающее значение для результата. Помните, что анимация должна поддерживать работу пользовательского интерфейса, а не раздражать его. Сгруппированные анимации Часто анимация оказывается более сложной, чем просто анимация одного свойства. Возможно, потребуется запустить несколько анимаций одновременно или одну за другой, или даже выполнить сценарий между двумя анимациями. Для этого можно использовать сгруппированные анимации. Как следует из названия, анимации можно группировать. Группировка может осуществляться двумя способами: параллельным или последовательным. Для этого можно использовать элемент SequentialAnimation или ParallelAnimation, которые выступают в роли контейнеров анимации для других элементов анимации. Эти сгруппированные анимации сами являются анимациями и могут использоваться именно как таковые.
Параллельные анимации При запуске параллельной анимации все ее прямые дочерние анимации выполняются параллельно. Это позволяет одновременно анимировать различные свойства. // ParallelAnimationExample.qml import QtQuick BrightSquare { id: root property int duration: 3000 property Item ufo: ufo width: 600 height: 400 Image { anchors.fill: parent source: "assets/ufo_background.png" } ClickableImageV3 { id: ufo x: 20; y: root.height-height text: qsTr('ufo') source: "assets/ufo.png" onClicked: anim.restart() }
ParallelAnimation { id: anim NumberAnimation { target: ufo properties: "y" to: 20 duration: root.duration } NumberAnimation { target: ufo properties: "x" to: 160 duration: root.duration } } } Последовательная анимация При последовательной анимации каждая дочерняя анимация запускается в том порядке, в котором она объявлена: сверху вниз. // SequentialAnimationExample.qml import QtQuick BrightSquare { id: root property int duration: 3000 property Item ufo: ufo
width: 600 height: 400 Image { anchors.fill: parent source: "assets/ufo_background.png" } ClickableImageV3 { id: ufo x: 20; y: root.height-height text: qsTr('rocket') source: "assets/ufo.png" onClicked: anim.restart() } SequentialAnimation { id: anim NumberAnimation { target: ufo properties: "y" to: 20 // 60% of time to travel up duration: root.duration * 0.6 } NumberAnimation { target: ufo properties: "x" to: 400 // 40% of time to travel sideways duration: root.duration * 0.4 } } } }
Вложенные анимации Сгруппированные анимации также могут быть вложенными. Например, последовательная анимация может иметь две параллельные анимации в качестве дочерних анимаций и т.д. Мы можем представить это на примере футбольного мяча. Идея состоит в том, чтобы бросить мяч слева направо и анимировать его поведение. Для понимания анимации необходимо расчленить ее на целостные преобразования объекта. Необходимо помнить, что анимация анимирует изменения свойств. Вот различные преобразования: Х-трансляция слева направо ( X1 ) Перемещение по оси Y снизу вверх (Y1) с последующим перемещением сверху вниз (Y2) с некоторым подпрыгиванием
Вращение на 360 градусов в течение всего времени анимации ( ROT1 ) Вся продолжительность анимации должна занимать три секунды. Мы начинаем с пустого элемента в качестве корневого элемента шириной 480 и высотой 300. import QtQuick Item { id: root property int duration: 3000 width: 480 height: 300 // [...] }
Мы определили общую продолжительность анимации в качестве эталона для лучшей синхронизации частей анимации. Следующий шаг - добавление фона, который в нашем случае представляет собой 2 прямоугольника с зеленым и синим градиентами. Rectangle { id: sky width: parent.width height: 200 gradient: Gradient { GradientStop { position: 0.0; color: "#0080FF" } GradientStop { position: 1.0; color: "#66CCFF" } } } Rectangle { id: ground anchors.top: sky.bottom anchors.bottom: root.bottom width: parent.width gradient: Gradient { GradientStop { position: 0.0; color: "#00FF00" } GradientStop { position: 1.0; color: "#00803F" } } } Верхний синий прямоугольник занимает 200 пикселей высоты, а нижний привязан к нижней части неба и к нижней части корневого элемента.
Давайте перенесем футбольный мяч на зеленую площадку. Мяч представляет собой изображение, хранящееся в папке "assets/soccer_ball.png". Для начала мы хотим расположить его в левом нижнем углу, рядом с краем. Image { id: ball x: 0; y: root.height-height source: "assets/soccer_ball.png" MouseArea { anchors.fill: parent onClicked: { ball.x = 0 ball.y = root.height-ball.height ball.rotation = 0 anim.restart() } } } К изображению прикреплена область мыши. Если щелкнуть мышью по шарику, то его положение изменится и анимация будет перезапущена. Начнем с последовательной анимации для двух трансляций y. SequentialAnimation { id: anim NumberAnimation { target: ball
properties: "y" to: 20 duration: root.duration * 0.4 } NumberAnimation { target: ball properties: "y" to: 240 duration: root.duration * 0.6 } } Это означает, что 40% от общей продолжительности анимации занимает анимация подъема, а 60% - анимация опускания, причем каждая анимация выполняется последовательно. Трансформации анимируются по линейной траектории, но на данный момент кривые отсутствуют. Кривые будут добавлены позже с помощью кривых смягчения, в данный момент мы сосредоточены на анимации трансформаций. Далее необходимо добавить x-трансляцию. x-трансляция должна выполняться параллельно с y-трансляцией, поэтому нам необходимо заключить последовательность y-трансляций в параллельную анимацию вместе с x-трансляцией. ParallelAnimation { id: anim SequentialAnimation { // ... our Y1, Y2 animation } NumberAnimation { // X1 animation target: ball properties: "x" to: 400
duration: root.duration } } В итоге мы хотим, чтобы шар вращался. Для этого к параллельной анимации нужно добавить еще одну анимацию. Мы выбираем RotationAnimation, так как она специализирована для вращения. ParallelAnimation { id: anim SequentialAnimation { // ... our Y1, Y2 animation } NumberAnimation { // X1 animation // X1 animation } RotationAnimation { target: ball properties: "rotation" to: 720 duration: root.duration } } Вот и вся анимационная последовательность. Осталось только задать правильные кривые смягчения для движений шарика. Для анимации Y1 мы используем кривую Easing.OutCirc, так как это должно быть больше похоже на круговое движение. Y2 используется Easing.OutBounce, чтобы придать мячу отскок, причем отскок должен происходить в конце (попробуйте использовать Easing.InBounce, и вы увидите, что отскок начинается сразу).
Анимации X1 и ROT1 оставлены как есть, с линейной кривой. Ниже приведен окончательный код анимации для ознакомления: ParallelAnimation { id: anim SequentialAnimation { NumberAnimation { target: ball properties: "y" to: 20 duration: root.duration * 0.4 easing.type: Easing.OutCirc } NumberAnimation { target: ball properties: "y" to: root.height-ball.height duration: root.duration * 0.6 easing.type: Easing.OutBounce } } NumberAnimation { target: ball properties: "x" to: root.width-ball.width duration: root.duration } RotationAnimation { target: ball properties: "rotation" to: 720 duration: root.duration } }

Состояния и Переходы Часто части пользовательского интерфейса могут быть описаны в виде состояний. Состояние определяет набор изменений свойств и может быть вызвано определенным условием. Кроме того, к этим переключателям состояний может быть присоединен переход, который определяет, как эти изменения должны быть оживлены или какие дополнительные действия должны быть применены. Действия также могут применяться при переходе в то или иное состояние. Состояния В QML состояния определяются с помощью элемента State, который должен быть привязан к массиву states любого элемента. Состояние идентифицируется через имя состояния и в своей простейшей форме состоит из серии изменений свойств элементов. Состояние по умолчанию определяется начальными свойствами элемента и имеет имя "" (пустая строка). Item { id: root states: [ State { name: "go" PropertyChanges { ... } }, State { name: "stop" PropertyChanges { ... } } ] }
Изменение состояния осуществляется путем присвоения нового имени состояния свойству state элемента, в котором определены состояния. Состояния управления с использованием when Другим способом управления состояниями является использование свойства when элемента State. Свойство when может быть установлено в выражение, которое оценивается как true, когда состояние должно быть применено. Item { id: root states: [ ... ] Button { id: goButton ... onClicked: root.state = "go" } } Например, светофор может иметь два сигнальных огня. Верхний из них сигнализирует об остановке красным цветом, а нижний зеленым. В данном примере оба огня не должны светить одновременно. Давайте посмотрим на диаграмму состояний.
При включении система автоматически переходит в режим останова, который является состоянием по умолчанию. В состоянии останова индикатор1 переключается в красный цвет, а индикатор2 - в черный (выключен). Теперь внешнее событие может вызвать переключение состояния в состояние light1 "идти". В состоянии "идти" мы меняем цвет светофора на черный (выключен), а светофора light2 - на зеленый, чтобы показать, что теперь пешеходы могут переходить дорогу. Для реализации этого сценария мы начнем рисовать эскиз пользовательского интерфейса для двух светильников. Для простоты мы используем 2 прямоугольника с радиусом, равным половине ширины (а ширина равна высоте, то есть это квадрат). Rectangle { id: light1 x: 25; y: 15 width: 100; height: width radius: width / 2 color: root.black border.color: Qt.lighter(color, 1.1) } Rectangle { id: light2 x: 25; y: 135 width: 100; height: width radius: width/2 color: root.black border.color: Qt.lighter(color, 1.1) }
Как определено на диаграмме состояний, мы хотим иметь два состояния: "движение" и "остановка", в каждом из которых светофор меняет свой цвет на красный или зеленый. Мы устанавливаем свойство state в значение stop, чтобы гарантировать, что начальным состоянием нашего светофора будет состояние stop. Исходное состояние Мы могли бы добиться того же эффекта только с состоянием "go" и без явного состояния "stop", установив цвет light1 красным, а цвет light2 - черным. Начальное состояние "", определяемое начальными значениями свойств, в этом случае выступало бы в качестве состояния " стоп". state: "stop" states: [ State { name: "stop" PropertyChanges { target: light1; color: root.red } PropertyChanges { target: light2; color: root.black } }, State { name: "go" PropertyChanges { target: light1; color: root.black } PropertyChanges { target: light2; color: root.green } } ] Использование PropertyChanges { target: light2; color: "black" } в данных примерах не требуется, так как начальный цвет light2 уже черный. В состоянии необходимо только описать, как свойства должны измениться по сравнению с их состоянием по умолчанию (а не с предыдущим состоянием).
Смена состояния происходит с помощью области мыши, которая охватывает весь светофор и при нажатии переключается между состояниями "горит" и "стоп". MouseArea { anchors.fill: parent onClicked: parent.state = (parent.state == "stop" ? "go" : } Теперь мы можем успешно изменять состояние светофора. Чтобы сделать пользовательский интерфейс более привлекательным и естественным, необходимо добавить несколько переходов с эффектами анимации. Переход может быть вызван изменением состояния. Использование сценариев Аналогичную логику можно создать, используя сценарии, а не состояния QML. Однако QML является лучшим языком, чем JavaScript, для описания пользовательских интерфейсов. По возможности старайтесь писать декларативный, а не императивный код.
Переходы К каждому элементу может быть добавлена серия переходов. Переход выполняется при изменении состояния. С помощью свойств from: и to: можно определить, при каком изменении состояния может быть применен тот или иной переход. Эти два свойства действуют как фильтр: когда фильтр равен true, переход будет применен. Можно также использовать подстановочный знак "*", который означает "любое состояние". Например, from: "*"; to: "*" означает "из любого состояния в любое другое состояние" и является значением по умолчанию для from и to. Это означает, что переход будет применен к каждому переключателю состояний. В данном примере мы хотим анимировать изменение цвета при переключении состояния с "go" на "stop". Для другого обратного изменения состояния ("stop" на "go") мы хотим сохранить немедленное изменение цвета и не применять переход. Мы ограничиваем переход с помощью свойств from и to, чтобы фильтровать только изменение состояния от "go" до "stop". Внутри перехода мы добавляем две цветовые анимации для каждого света, которые должны анимировать изменения свойств, определенных в описании состояния. transitions: [ Transition { from: "stop"; to: "go" // from: "*"; to: "*" ColorAnimation { target: light1; properties: "color"; } ColorAnimation { target: light2; properties: "color"; } } ]
Изменить состояние можно щелчком мыши в пользовательском интерфейсе. Состояние применяется немедленно, а также изменяет состояние во время выполнения перехода. Поэтому попробуйте щелкнуть на пользовательском интерфейсе, когда состояние находится в состоянии перехода от "stop" к "go". Вы увидите, что изменение произойдет немедленно. С этим пользовательским интерфейсом можно поиграть, например, уменьшив масштаб неактивного света, чтобы выделить активный. Для этого необходимо добавить в состояния еще одно свойство изменения масштаба, а также обработать анимацию для свойства масштабирования в переходе. Другим вариантом может быть добавление состояния "внимание", когда индикаторы мигают желтым цветом. Для этого необходимо добавить к переходу последовательную анимацию, в которой одна секунда будет желтой (свойство "to" анимации и одна секунда черной). Возможно, вам также захочется изменить кривую ослабления, чтобы сделать ее более привлекательной.
Advanced Techniques Ничего продвинутого здесь нет �.
UI Controls В этой главе рассказывается об использовании модуля Qt Quick Controls. Qt Quick Controls используется для создания расширенных пользовательских интерфейсов, построенных из стандартных компонентов, таких как кнопки, ярлыки, ползунки и т.д. Элементы управления Qt Quick Controls могут быть организованы с помощью модуля layout и легко поддаются стилизации. Кроме того, мы рассмотрим различные стили для различных платформ, прежде чем перейти к пользовательским стилям.
Введение в систему управления Использование Qt Quick с нуля позволяет получить примитивные графические элементы и элементы взаимодействия, на основе которых можно строить пользовательские интерфейсы. При использовании Qt Quick Controls вы получаете несколько более структурированный набор элементов управления, из которых можно строить интерфейс. Элементы управления варьируются от простых текстовых надписей и кнопок до более сложных, таких как ползунки и циферблаты. Эти элементы удобны, если вы хотите создать пользовательский интерфейс, основанный на классических схемах взаимодействия, так как они являются хорошей основой, на которую можно опереться. Qt Quick Controls поставляются с несколькими стилями, представленными в таблице ниже. Стиль Basic - это базовый плоский стиль. Стиль Universal основан на рекомендациях Microsoft Universal Design Guidelines, Material - на рекомендациях Google Material Design Guidelines, а стиль Fusion - это стиль, ориентированный на рабочий стол. Некоторые из стилей могут быть изменены путем модификации палитр. Стиль Imagine основан на активах изображения, что позволяет графическому дизайнеру создавать новый стиль без написания какого-либо кода, даже без кодов цветов палитры. Основные
Fusion
macOS
Материал
Представьте себе
Windows
Универсальный
Qt Quick Controls 2 доступен из импорта QtQuick.Controls. Также представляют интерес следующие модули: QtQuick.Controls QtQuick.Templates - Основные элементы управления. - Предоставляет поведенческие, невизуальные базовые типы для элементов управления. - Поддержка тематического стиля Imagine. QtQuick.Controls.Imagine QtQuick.Controls.Material - Поддержка тематического стиля Material. QtQuick.Controls.Universal Universal. - Поддержка тематического стиля
Qt.labs.platform - Поддержка диалоговых окон, соответствующих платформе, для таких распространенных задач, как выбор файлов, цветов и т.д., а также значков системного трея и стандартных путей. Qt.Labs Обратите внимание, что модули Qt.labs являются экспериментальными, а это значит, что их API могут иметь разрывные изменения между версиями Qt.
Программа просмотра изображений Рассмотрим более крупный пример использования Qt Quick Controls. Для этого мы создадим простую программу просмотра изображений. Сначала мы создадим его для настольных компьютеров, используя стиль Fusion, затем рефакторим его для мобильных устройств, после чего посмотрим на финальную кодовую базу. Настольная версия Настольная версия основана на классическом окне приложения со строкой меню, панелью инструментов и областью документов. В действии это приложение можно увидеть ниже.
В качестве отправной точки мы используем шаблон проекта Qt Creator для пустого приложения Qt Quick. Однако элемент Window по умолчанию из шаблона мы заменим на ApplicationWindow из модуля QtQuick.Controls. В приведенном ниже коде показан файл main.qml, в котором создается само окно и задаются его размер и заголовок по умолчанию. import QtQuick import QtQuick.Controls import Qt.labs.platform ApplicationWindow { visible: true width: 640 height: 480 // ... } Окно ApplicationWindow состоит из четырех основных областей, как показано ниже. Строка меню, строка инструментов и строка состояния обычно заполняются экземплярами элементов управления MenuBar , ToolBar или TabBar, а область содержимого представляет собой куда помещаются дочерние элементы окна. Обратите внимание, что в приложении просмотра изображений нет строки состояния, поэтому она отсутствует как в приведенном здесь коде, так и на рисунке выше.
Поскольку мы ориентируемся на настольные компьютеры, мы принудительно используем стиль Fusion. Это можно сделать с помощью конфигурационного файла, переменных окружения, аргументов командной строки или программно в коде Си++. В последнем случае мы сделаем это, добавив в файл main.cpp следующую строку : QQuickStyle::setStyle("Fusion"); Затем мы начинаем строить пользовательский интерфейс в файле main.qml, добавляя в качестве содержимого элемент Image. Этот элемент будет содержать изображения, когда пользователь откроет их, поэтому пока он является просто заполнителем. Свойство background используется для того, чтобы предоставить окну элемент, который будет располагаться за содержимым. Он будет отображаться, когда изображение не загружено, а также в виде границ вокруг изображения, если соотношение сторон не позволяет ему заполнить область содержимого окна. ApplicationWindow { // ... background: Rectangle { color: "darkGray" }
Image { id: image anchors.fill: parent fillMode: Image.PreserveAspectFit asynchronous: true } // ... } Далее мы добавляем панель инструментов. Для этого используется свойство ToolBar окна. Внутри панели инструментов мы добавляем элемент Flow, который позволит содержимому заполнить всю ширину элемента управления, прежде чем оно перельется в новую строку. Внутри потока разместим кнопку ToolButton . Кнопка ToolButton имеет несколько интересных свойств. С текстом в с е понятно. Однако имя icon.name в з я т о из документа freedesktop.org Icon Naming Specification (https://specifications.freedesktop.org/iconnaming-spec/icon-naming-spec- latest.html) . В этом документе список стандартных иконок перечисляется по именам. Обратившись к такому имени, Qt выберет нужную иконку из текущей темы рабочего стола. В обработчике сигнала onClicked кнопки ToolButton находится последний фрагмент кода. Он вызывает метод open на элементе fileOpenDialog. ApplicationWindow { // ... header: ToolBar { Flow { anchors.fill: parent ToolButton {
text: qsTr("Open") icon.name: "document-open" onClicked: fileOpenDialog.open() } } } // ... } Элемент fileOpenDialog представляет собой элемент управления FileDialog из модуля Qt.labs.platform. Файловый диалог может использоваться для открытия или сохранения файлов. В коде мы начинаем с присвоения заголовка . Затем с помощью класса StandardsPaths задаем начальную папку. Класс StandardsPaths содержит ссылки на общие папки, такие как домашняя, документы и т.д. После этого мы задаем фильтр имен, который определяет, какие файлы пользователь может видеть и выбирать с помощью диалога. Наконец, мы доходим до обработчика сигнала onAccepted, где элемент Image, содержащий содержимое окна, устанавливается на отображение выбранного файла. Имеется также сигнал onRejected, но в приложении просмотра изображений его обрабатывать не нужно. ApplicationWindow { // ... FileDialog { id: fileOpenDialog title: "Select an image file" folder: StandardPaths.writableLocation(StandardPaths.Do nameFilters: [ "Image files (*.png *.jpeg *.jpg)",
] onAccepted: { image.source = fileOpenDialog.fileUrl } } // ... } Далее мы переходим к строке меню MenuBar . Для создания меню внутри строки меню размещаются элементы Menu, а затем каждое Menu заполняется элементами MenuItem. В приведенном ниже коде мы создаем два меню - Файл и Справка. В разделе File мы размещаем Open, используя ту же пиктограмму и действие, что и кнопка инструмента на панели инструментов. В разделе Help находится пункт About, который инициирует вызов метода open элемента aboutDialog. Обратите внимание, что амперсанд ("&") в свойстве title Menu и свойстве text MenuItem превращает следующий символ в комбинацию клавиш; например, вы попадаете в меню файла, нажав Alt+F, а затем Alt+O для вызова элемента open. ApplicationWindow { // ... menuBar: MenuBar { Menu { title: qsTr("&File") MenuItem { text: qsTr("&Open...") icon.name: "document-open" onTriggered: fileOpenDialog.open() }
} Menu { title: qsTr("&Help") MenuItem { text: qsTr("&About...") onTriggered: aboutDialog.open() } } } // ... } Элемент aboutDialog основан на элементе Dialog из модуля QtQuick.Controls, который является базой для пользовательских диалогов. Диалог, который мы собираемся создать, показан на рисунке ниже.
Код для aboutDialog можно разделить на три части. Сначала мы задаем диалоговому окну заголовок. Затем мы предоставляем содержимое диалога - в данном случае элемент управления Label. Наконец, для закрытия диалога мы используем стандартную кнопку Ok. ApplicationWindow { // ... Dialog { id: aboutDialog title: qsTr("About") Label { anchors.fill: parent text: qsTr("QML Image Viewer\nA part of the QmlBook horizontalAlignment: Text.AlignHCenter } standardButtons: StandardButton.Ok } // ... } В итоге получилось функциональное, хотя и простое, настольное приложение для просмотра изображений. Переход на мобильную связь Существует ряд отличий в том, как должен выглядеть и вести себя пользовательский интерфейс на мобильном устройстве по сравнению с настольным приложением. Самое большое отличие для нашего приложения - это способ доступа к действиям. Вместо панели меню и панели инструментов мы используем ящик, из которого пользователь может выбирать действия. Ящик можно пролистывать сбоку, но
мы также предлагаем кнопку "гамбургер" в заголовке. Полученное приложение с открытым ящиком можно увидеть ниже. Прежде всего, необходимо изменить стиль, заданный в файле main.cpp, с Сплав с материалом: QQuickStyle::setStyle("Material"); Затем мы приступаем к адаптации пользовательского интерфейса. Начнем с замены меню на ящик. В приведенном ниже коде компонент Drawer добавляется как
дочернего окна ApplicationWindow . Внутри ящика мы разместили ListView, содержащий экземпляры ItemDelegate. Он также содержит индикатор ScrollIndicator, используемый для отображения части длинного списка. Поскольку наш список состоит только из двух элементов, в данном примере индикатор не виден. ListView ящика заполняется из ListModel, где каждый ListItem соответствует пункту меню. При каждом щелчке на пункте меню в методе onClicked вызывается метод-триггер соответствующего ListItem. Таким образом, мы можем использовать один делегат для запуска различных действий. ApplicationWindow { // ... id: window Drawer { id: drawer width: Math.min(window.width, window.height) / 3 * 2 height: window.height ListView { focus: true currentIndex: -1 anchors.fill: parent delegate: ItemDelegate { width: parent.width text: model.text highlighted: ListView.isCurrentItem onClicked: { drawer.close() model.triggered() } }
model: ListModel { ListElement { text: qsTr("Open...") triggered: function() { fileOpenDialog.open } ListElement { text: qsTr("About...") triggered: function() { aboutDialog.open(); } } ScrollIndicator.vertical: ScrollIndicator { } } } // ... } Следующее изменение - в заголовке окна ApplicationWindow . Вместо панели инструментов в стиле рабочего стола мы добавляем кнопку для открытия ящика и ярлык для заголовка нашего приложения.
ToolBar содержит два дочерних элемента: ToolButton и Label . К н о п к а ToolButton открывает ящик. Соответствующая кнопка закрытия вызов находится в делегате ListView. После выбора элемента ящик закрывается. Иконка, используемая для кнопки ToolButton, взята со страницы Material Design Icons (https://material.io/tools/icons/? style=baseline) . ApplicationWindow {
// ... header: ToolBar { ToolButton { id: menuButton anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter icon.source: "images/baseline-menu-24px.svg" onClicked: drawer.open() } Label { anchors.centerIn: parent text: "Image Viewer" font.pixelSize: 20 elide: Label.ElideRight } } // ... } Наконец, мы сделаем фон панели инструментов красивым - или, по крайней мере, оранжевым. Для этого мы изменим свойство Material.background attached. Оно берется из модуля QtQuick.Controls.Material и влияет только на стиль Material. import QtQuick.Controls.Material ApplicationWindow { // ... header: ToolBar { Material.background: Material.Orange // ...
} С помощью этих небольших изменений мы преобразовали наш просмотрщик изображений для настольных компьютеров в версию , удобную для мобильных устройств. Общая кодовая база В предыдущих двух разделах мы рассмотрели программу просмотра изображений, разработанную для настольных компьютеров, а затем адаптировали ее для мобильных устройств. Если посмотреть на кодовую базу, то большая часть кода по-прежнему является общей. Те части, которые разделяются, в основном связаны с документом приложения, т.е. с изображением. Изменения учитывают различные паттерны взаимодействия на настольных и мобильных компьютерах. Естественно, мы хотим унифицировать эти кодовые базы. QML поддерживает это с помощью селекторов файлов. Селектор файлов позволяет заменять отдельные файлы в зависимости от того, какие селекторы активны. В документации по Qt список селекторов содержится в документации по классу QFileSelector (ссылка (https://doc.qt.io/qt- 5/qfileselector.html) ). В нашем случае мы сделаем настольную версию по умолчанию и будем заменять выбранные файлы при появлении селектора android. В процессе разработки можно установить переменную окружения QT_FILE_SELECTORS в значение android для имитации этого.
Селектор файлов Селекторы файлов работают, заменяя файлы на альтернативные, когда присутствует селектор. Создав каталог с именем +selector (где selector - имя селектора) в том же каталоге, что и заменяемые файлы, вы можете поместить в него файлы с тем же именем, что и заменяемый файл. При наличии селектора файл в каталоге будет выбран вместо исходного файла. Селекторы основаны на платформе: например, android, ios, osx, linux, qnx и т.д. Они также могут включать название используемого дистрибутива Linux (если он идентифицирован), например debian, ubuntu, fedora. Наконец, в них также указывается локаль, например en_US, sv_SE и т.д. Также возможно добавление собственных пользовательских селекторов. Первым шагом для выполнения этого изменения является изоляция общего кода. Для этого мы создаем элемент ImageViewerWindow, который будет использоваться вместо ApplicationWindow для обоих вариантов. Он будет состоять из диалоговых окон, элемента Image и фона. Для того чтобы сделать методы открытия диалогов доступными для кода, специфичного для платформы, необходимо раскрыть их через функции openFileDialog и openAboutDialog . import QtQuick import QtQuick.Controls import Qt.labs.platform ApplicationWindow { function openFileDialog() { fileOpenDialog.open(); } function openAboutDialog() { aboutDialog.open(); }
visible: true title: qsTr("Image Viewer") background: Rectangle { color: "darkGray" } Image { id: image anchors.fill: parent fillMode: Image.PreserveAspectFit asynchronous: true } FileDialog { id: fileOpenDialog // ... } Dialog { id: aboutDialog // ... } } Далее мы создаем новый файл main.qml для нашего стандартного стиля Fusion, т.е. для настольной версии пользовательского интерфейса. Здесь мы основываем пользовательский интерфейс на окне ImageViewerWindow вместо ApplicationWindow . Затем мы добавляем к нему специфические для данной платформы элементы, например, MenuBar и ToolBar. Единственным изменением в них является то, что вызовы для открытия соответствующих диалогов осуществляются не непосредственно к элементам управления диалогами, а к новым функциям.
import QtQuick import QtQuick.Controls ImageViewerWindow { id: window width: 640 height: 480 menuBar: MenuBar { Menu { title: qsTr("&File") MenuItem { text: qsTr("&Open...") icon.name: "document-open" onTriggered: window.openFileDialog() } } Menu { title: qsTr("&Help") MenuItem { text: qsTr("&About...") onTriggered: window.openAboutDialog() } } } header: ToolBar { Flow { anchors.fill: parent ToolButton { text: qsTr("Open") icon.name: "document-open" onClicked: window.openFileDialog() } } } }
Далее необходимо создать специфический для мобильных устройств файл main.qml . Он будет основан на теме Material. Здесь мы сохраняем Drawer и панель инструментов для мобильных устройств. Единственное изменение - это способ открытия диалогов. import QtQuick import QtQuick.Controls import QtQuick.Controls.Material ImageViewerWindow { id: window width: 360 height: 520 Drawer { id: drawer // ... ListView { // ... model: ListModel { ListElement { text: qsTr("Open...") triggered: function(){ window.openFileDialo } ListElement { text: qsTr("About...") triggered: function(){ window.openAboutDial } } // ... }
} header: ToolBar { // ... } } Два файла main.qml размещаются в файловой системе, как показано ниже. Это позволяет селектору файлов, который автоматически создается движком QML, выбрать нужный файл. По умолчанию загружается файл Fusion main.qml. Если присутствует селектор android, то вместо него загружается Material main.qml. До сих пор стиль задавался в файле main.cpp . Мы могли бы продолжать в том же духе и использовать выражения #ifdef для установки различных стилей для разных платформ. Вместо этого мы снова воспользуемся механизмом выбора файлов и зададим стиль с помощью конфигурационного файла. Ниже показан файл для стиля Material, но файл Fusion не менее прост. [Controls] Style=Material
В результате этих изменений мы получили объединенную кодовую базу, в которой весь код документа является общим, а различаются только шаблоны взаимодействия с пользователем. Это можно сделать разными способами, например, храня документ в отдельном компоненте , который включается в интерфейсы для каждой платформы, или, как в данном примере, создавая общую базу, которая расширяется для каждой платформы. Оптимальный подход лучше всего определить, когда вы знаете, как выглядит ваша конкретная кодовая база, и можете решить, как отделить общее от уникального. Родные диалоги При работе с программой просмотра изображений можно заметить, что в ней используется нестандартный диалог выбора файлов. Из-за этого он выглядит нестандартно. В решении этой проблемы нам может помочь модуль Qt.labs.platform. Он обеспечивает привязку QML к собственным диалоговым окнам, таким как диалог файла, диалог шрифта и диалог цвета. Он также предоставляет API для создания значков в системном трее, а также глобальных меню системы, которые располагаются поверх экрана (например, как в OS X). Ценой этого является зависимость от модуля QtWidgets, поскольку диалог на основе виджетов используется в качестве запасного варианта при отсутствии встроенной поддержки. Для того чтобы интегрировать в программу просмотра изображений собственный файловый диалог, нам необходимо импортировать модуль Qt.labs.platform. Поскольку этот модуль имеет несовпадение имен с модулем QtQuick.Dialogs, который он заменяет, важно удалить старый оператор import. В самом элементе файлового диалога необходимо изменить способ установки свойства folder и убедиться, что обработчик onAccepted использует свойство file, а не свойство fileUrl. За исключением этих деталей, использование идентично FileDialog из QtQuick.Dialogs .

import QtQuick import QtQuick.Controls import Qt.labs.platform ApplicationWindow { // ... FileDialog { id: fileOpenDialog title: "Select an image file" folder: StandardPaths.writableLocation(StandardPaths.Do nameFilters: [ "Image files (*.png *.jpeg *.jpg)", ] onAccepted: { image.source = fileOpenDialog.file } } // ... } Помимо изменений в QML, нам также необходимо изменить файл проекта программы просмотра изображений, чтобы включить в него модуль виджетов. QT += quick quickcontrols2 widgets И нам необходимо обновить файл main.qml, чтобы инстанцировать объект QApplication вместо объекта QGuiApplication. Это связано с тем, что класс QGuiApplication содержит минимальную среду, необходимую для работы графического приложения, а QApplication расширяет QGuiApplication функциями, необходимыми для поддержки QtWidgets. include <QApplication> // ...
int main(int argc, char *argv[]) { QApplication app(argc, argv); // ... } Благодаря этим изменениям программа просмотра изображений теперь будет использовать собственные диалоговые окна на большинстве платформ. Поддерживаются следующие платформы: iOS, Linux (с платформенной темой GTK+), macOS, Windows и WinRT. Для Android будет использоваться стандартный диалог Qt, предоставляемый модулем QtWidgets.
Распространенные шаблоны Существует ряд общих шаблонов пользовательского интерфейса, которые могут быть реализованы с помощью Qt Quick Controls. В этом разделе мы попытаемся продемонстрировать, как можно построить некоторые из наиболее распространенных шаблонов. Вложенные экраны Для данного примера мы создадим дерево страниц, на которые можно попасть с предыдущего уровня экранов. Структура изображена ниже. Ключевым компонентом в этом типе пользовательского интерфейса является StackView . Он позволяет размещать страницы в стеке, который затем может быть извлечен, когда пользователь захочет вернуться назад. В приведенном здесь примере мы покажем, как это можно реализовать. Начальный главный экран приложения показан на рисунке ниже.
Приложение начинается в файле main.qml, где у нас есть ApplicationWindow, содержащее ToolBar, Drawer, StackView и элемент домашней страницы Home. Ниже мы рассмотрим каждый из этих компонентов. import QtQuick import QtQuick.Controls ApplicationWindow { // ...
header: ToolBar { // ... } Drawer { // ... } StackView { id: stackView anchors.fill: parent initialItem: Home {} } } Домашняя страница Home.qml состоит из Page , который является nэлементом управления, поддерживающим верхние и нижние колонтитулы. В этом примере мы просто центрируем на странице Label с текстом Home Screen. Это работает потому, что содержимое StackView автоматически заполняет представление стека, поэтому страница будет иметь нужный размер для работы. import QtQuick import QtQuick.Controls Page { title: qsTr("Home") Label { anchors.centerIn: parent text: qsTr("Home Screen") } }
Возвращаясь к файлу main.qml, мы переходим к рассмотрению части, посвященной страницам. ящику. Именно Активными с частями нее начинается навигация пользовательского по интерфейса являются элементы МtemDelegate. В обработчике onClicked очередная страница помещается в stackView. Как показано в приведенном ниже коде, в стек можно поместить либо Компонент, либо ссылку на конкретный QML-файл. В любом случае создается новый экземпляр и помещается в стек. ApplicationWindow { // ... Drawer { id: drawer width: window.width * 0.66 height: window.height Column { anchors.fill: parent ItemDelegate { text: qsTr("Profile") width: parent.width onClicked: { stackView.push("Profile.qml") drawer.close() } } ItemDelegate { text: qsTr("About") width: parent.width onClicked: { stackView.push(aboutPage) drawer.close() } }
} } // ... Component { id: aboutPage About {} } // ... } Вторая половина головоломки - панель инструментов. Идея заключается в том, что кнопка "Назад" отображается, если в stackView содержится более одной страницы, в противном случае отображается кнопка меню. Логику этого можно увидеть в свойстве text, где строки "\\u..." представляют собой необходимые нам символы юникода. В обработчике onClicked мы видим, что если в стеке находится более одной страницы, то стек разворачивается, т.е. удаляется верхняя страница. Если в стеке находится только один элемент, т.е. главный экран, то открывается ящик. Ниже панели инструментов расположен элемент Label . Этот элемент отображает название каждой страницы в центре заголовка. ApplicationWindow { // ... header: ToolBar { contentHeight: toolButton.implicitHeight ToolButton { id: toolButton
text: stackView.depth > 1 ? "\u25C0" : "\u2630" font.pixelSize: Qt.application.font.pixelSize * 1.6 onClicked: { if (stackView.depth > 1) { stackView.pop() } else { drawer.open() } } } Label { text: stackView.currentItem.title anchors.centerIn: parent } } // ... } Теперь мы рассмотрели, как попасть на страницы About и Profile, но мы также хотим сделать так, чтобы со страницы Profile можно было попасть на страницу Edit Profile. Это делается с помощью кнопки на странице профиля. При нажатии на кнопку файл EditProfile.qml помещается в StackView .
import QtQuick import QtQuick.Controls Page { title: qsTr("Profile") Column { anchors.centerIn: parent spacing: 10 Label { anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Profile") } Button { anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Edit"); onClicked: stackView.push("EditProfile.qml") } } } Экраны "бок о бок Для данного примера мы создаем пользовательский интерфейс, состоящий из трех страниц, по которым пользователь может перемещаться. Страницы показаны на диаграмме ниже. Это может быть интерфейс приложения для отслеживания состояния здоровья, отслеживающего текущее состояние, статистику пользователя и общую статистику. На рисунке ниже показано, как выглядит страница Current в приложении. Основная часть экрана управляется с помощью SwipeView, что позволяет реализовать схему взаимодействия с экраном "бок о бок". Заголовок и текст, показанные на рисунке, взяты из страницы внутри SwipeView, а PageIndicator (три точки внизу) - из файла main.qml
и располагается под SwipeView . Индикатор страницы показывает пользователю, какая страница активна в данный момент, что помогает при навигации. Если заглянуть в файл main.qml, то он состоит из окна ApplicationWindow с файлом SwipeView . import QtQuick import QtQuick.Controls ApplicationWindow { visible: true width: 640 height: 480 title: qsTr("Side-by-side") SwipeView {
// ... } // ... } Внутри SwipeView каждая из дочерних страниц инстанцируется в том порядке, в котором они должны отображаться. Это Current, UserStats и TotalStats. ApplicationWindow { // ... SwipeView { id: swipeView anchors.fill: parent Current { } UserStats { } TotalStats { } } // ... } Наконец, свойства count и currentIndex SwipeView привязываются к элементу PageIndicator. На этом структура вокруг страниц завершена.
ApplicationWindow { // ... SwipeView { id: swipeView // ... } PageIndicator { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter currentIndex: swipeView.currentIndex count: swipeView.count } } Каждая страница состоит из Page с заголовком, состоящим из Label и некоторого содержимого. Для страниц Current и User Stats содержимое состоит из простого Label, а для страницы Community Stats предусмотрена кнопка "Назад". import QtQuick import QtQuick.Controls Page { header: Label { text: qsTr("Community Stats") font.pixelSize: Qt.application.font.pixelSize * 2 padding: 10 } // ...
Кнопка "Назад" явно вызывает функцию setCurrentIndex окна SwipeView чтобы установить индекс в ноль, возвращая пользователя непосредственно на текущую страницу. При каждом переходе между страницами SwipeView обеспечивает переход, поэтому даже при явном изменении индекса пользователь получает представление о направлении движения.
СОВЕТ При программной навигации в SwipeView важно не устанавливать currentIndex путем присваивания в JavaScript. Это связано с тем, что в этом случае будут нарушены все привязки QML, которые он переопределяет. Вместо этого используйте методы setCurrentIndex , incrementCurrentIndex и decrementCurrentIndex . Это позволит сохранить привязки QML. Page { // ... Column { anchors.centerIn: parent spacing: 10 Label { anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Community statistics") } Button { anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Back") onClicked: swipeView.setCurrentIndex(0); } } } Окна документов В данном примере показано, как реализовать ориентированный на рабочий стол пользовательский интерфейс, ориентированный на работу с документами. Идея заключается в том, чтобы иметь одно окно для каждого документа. При открытии нового документа открывается новое окно. Для пользователя каждое окно - это самодостаточный мир с единственным документом.
Код начинается с окна ApplicationWindow с меню File со стандартными операциями: New, Open, Save и Save As. Мы помещаем это в файл DocumentWindow.qml . Мы импортировали Qt.labs.platform для нативных диалогов и внесли последующие изменения в файл проекта и файл main.cpp, как описано в разделе о нативных диалогах выше. import QtQuick import QtQuick.Controls import Qt.labs.platform as NativeDialogs ApplicationWindow { id: root // ...
menuBar: MenuBar { Menu { title: qsTr("&File") MenuItem { text: qsTr("&New") icon.name: "document-new" onTriggered: root.newDocument() } MenuSeparator {} MenuItem { text: qsTr("&Open") icon.name: "document-open" onTriggered: openDocument() } MenuItem { text: qsTr("&Save") icon.name: "document-save" onTriggered: saveDocument() } MenuItem { text: qsTr("Save &As...") icon.name: "document-save-as" onTriggered: saveAsDocument() } } } ... // ... } Для загрузки программы мы создаем первый экземпляр DocumentWindow из файла main.qml , который является точкой входа в приложение. import QtQuick DocumentWindow {
visible: true } В примере, приведенном в начале этой главы, каждый MenuItem при срабатывании вызывает соответствующую функцию. Начнем с элемента New, который вызывает функцию newDocument. Эта функция, в свою очередь, опирается на функцию createNewDocument, которая динамически создает из файла DocumentWindow.qml новый экземпляр элемента, т.е. новый экземпляр DocumentWindow. Причина выделения этой части новой функции заключается в том, что мы используем ее и при открытии документов. Обратите внимание, что мы не указываем родительский элемент при создании нового экземпляра с помощью createObject . Таким образом, мы создаем новые элементы верхнего уровня. Если бы мы указали текущий документ в качестве родительского для следующего, то разрушение родительского окна привело бы к разрушению дочерних окон. ApplicationWindow { // ... function createNewDocument() { var component = Qt.createComponent("DocumentWindow.qml" var window = component.createObject(); return window; } function newDocument() { var window = createNewDocument(); window.show(); }
// ... } Если посмотреть на элемент Open, то мы увидим, что он вызывает функцию openDocument. Эта функция просто открывает диалог openDialog , который позволяет пользователю выбрать файл для открытия. Поскольку у нас нет формата документа, расширения файла или чего-либо подобного, большинство свойств диалога установлены в значения по умолчанию. В реальном приложении это было бы лучше настроить. В обработчике onAccepted с помощью метода createNewDocument инстанцируется окно нового документа, а перед его отображением задается имя файла. В этом случае реальной загрузки не происходит. СОВЕТ Мы импортировали модуль Qt.labs.platform как NativeDialogs MenuItem, . Это связано с тем, что он предоставляет который конфликтует с MenuItem, предоставляемым модулем QtQuick.Controls. ApplicationWindow { // ... function openDocument(fileName) { openDialog.open(); } NativeDialogs.FileDialog { id: openDialog title: "Open" folder: NativeDialogs.StandardPaths.writableLocation(Na onAccepted: { var window = root.createNewDocument();
window.fileName = openDialog.file; window.show(); } } // ... } Имени файла принадлежит пара свойств, описывающих документ: fileName isDirty и isDirty . В fileName х р а н и т с я имя файла документа, а устанавливается, если в документе есть несохраненные изменения. Это используется логикой сохранения и сохранения как, которая показана ниже. При попытке saveAsDocument. saveAsDialog, onAccepted сохранить документ без имени вызывается В результате происходит обход диалогового окна которое задает имя файла, а затем в обработчике снова пытается сохранить. Обратите внимание, что функции saveAsDocument и saveDocument соответствуют пунктам меню Save As и Save. После сохранения документа в функции saveDocument проверяется свойство tryToClose. Этот флаг устанавливается, если сохранение происходит в результате того, что пользователь хочет сохранить документ в момент закрытия окна. Как следствие, после выполнения операции сохранения окно будет закрыто. Опять же, в данном примере никакого сохранения не происходит. ApplicationWindow { // ... property bool isDirty: true // Has the document got property string fileName // The filename of the d property bool tryingToClose: false // Is the window trying // ...
function saveAsDocument() { saveAsDialog.open(); } function saveDocument() { if (fileName.length === 0) { root.saveAsDocument(); } else { // Save document here console.log("Saving document") root.isDirty = false; if (root.tryingToClose) root.close(); } } NativeDialogs.FileDialog { id: saveAsDialog title: "Save As" folder: NativeDialogs.StandardPaths.writableLocation(Na onAccepted: { root.fileName = saveAsDialog.file saveDocument(); } onRejected: { root.tryingToClose = false; } } // ... }
Это приводит нас к закрытию окон. При закрытии окна вызывается обработчик onClosing. Здесь код может выбрать, не принимать ли запрос на закрытие. Если в документе есть несохраненные изменения, то мы открываем диалог closeWarningDialog и отклоняем запрос на закрытие. Диалог closeWarningDialog спрашивает пользователя, следует ли сохранить изменения, но у пользователя также есть возможность отменить операцию закрытия. Отмена, обрабатываемая в onRejected, является самым простым случаем, поскольку мы отклонили закрытие при открытии диалога. Если пользователь не хочет сохранять изменения, то есть в onNoClicked, флаг isDirty устанавливается в false и окно снова закрывается. На этот раз onClosing примет закрытие, так как isDirty равен false. Наконец, когда пользователь хочет сохранить изменения, мы устанавливаем флаг tryToClose в true перед вызовом save. Это приводит нас к логике сохранения/сохранения как. ApplicationWindow { // ... onClosing: { if (root.isDirty) { closeWarningDialog.open(); close.accepted = false; } } NativeDialogs.MessageDialog { id: closeWarningDialog title: "Closing document" text: "You have unsaved changed. Do you want to save yo buttons: NativeDialogs.MessageDialog.Yes | NativeDialog onYesClicked: { // Attempt to save the document root.tryingToClose = true;
root.saveDocument(); } onNoClicked: { // Close the window root.isDirty = false; root.close() } onRejected: { // Do nothing, aborting the closing of the window } } } Ниже показан весь поток для логики закрытия и сохранения/сохранения в виде. Вход в систему осуществляется в состоянии "закрыто", а состояния "закрыто" и "не закрыто" являются исходами. Это выглядит сложным по сравнению с реализацией этого с помощью Qt Widgets и C++. Это связано с тем, что диалоги не блокируются в QML. Это означает, что мы не можем ожидать результата диалога в операторе switch. Вместо этого нам необходимо запомнить состояние и продолжить выполнение операции в соответствующих обработчиках onYesClicked, onNoClicked, onAccepted и onRejected.
Последний фрагмент головоломки - заголовок окна. Он состоит из свойства fileName и isDirty. ApplicationWindow { // ... title: (fileName.length===0?qsTr("Document"):fileName) + (i
// ... } Этот пример далеко не полный. Например, документ не загружается и не сохраняется. Еще один недостающий элемент - обработка случая закрытия всех окон одним махом, т.е. выхода из приложения. Для этой функции необходим синглтон, хранящий список всех текущих экземпляров DocumentWindow. Однако это будет лишь еще один способ инициировать закрытие окна, поэтому приведенная здесь логическая схема остается актуальной.
Стиль Imagine Одна из целей Qt Quick Controls - отделить логику элемента управления от его внешнего вида. Для большинства стилей реализация внешнего вида состоит из смеси QML-кода и графических активов. Однако, используя стиль Imagine, можно настроить внешний вид приложения на базе Qt Quick Controls, используя только графические активы. Стиль imagine основан на 9-патчевых изображениях (https://developer.android.com/guide/topics/graphics/drawables#nine-patch) . Это позволяет изображениям нести информацию о том, как они растягиваются и какие части должны рассматриваться как часть элемента, а какие - как внешние, например, тень. Для каждого элемента управления стиль поддерживает несколько элементов, и для каждого элемента доступно большое количество состояний. Предоставляя активы для определенных комбинаций этих элементов и состояний, можно детально управлять внешним видом каждого элемента управления. Подробно о 9-патчевых изображениях и о том, как можно стилизовать каждый элемент управления, рассказывается в документации по стилям Imagine (https://doc.qt.io/qt-6/qtquickcontrols2-imagine.html) . Здесь же мы создадим пользовательский стиль для интерфейса воображаемого устройства, чтобы продемонстрировать, как используется этот стиль. Стиль приложения настраивает элементы управления ApplicationWindow и Button. Для кнопок обрабатывается обычное состояние, а также состояния "нажато" и "отмечено". Демонстрационное приложение показано ниже.
В коде для этого используется столбец (Column) для кликабельных кнопок и сетка (Grid) для проверяемых. Щелкающие кнопки также растягиваются по ширине окна. import QtQuick import QtQuick.Controls ApplicationWindow { // ... visible: true width: 640 height: 480 title: qsTr("Hello World") Column { anchors.top: parent.top
anchors.left: parent.left anchors.margins: 10 width: parent.width/2 spacing: 10 // ... Repeater { model: 5 delegate: Button { width: parent.width height: 70 text: qsTr("Click me!") } } } Grid { anchors.top: parent.top anchors.right: parent.right anchors.margins: 10 columns: 2 spacing: 10 // ... Repeater { model: 10 delegate: Button { height: 70 text: qsTr("Check me!") checkable: true } }
} } Поскольку мы используем стиль Imagine, все элементы управления, которые мы хотим использовать, должны быть стилизованы с помощью графического актива. Самым простым является фон для окна . Это однопиксельная текстура, определяющая цвет ApplicationWindow фона. Если назвать файл applicationwindow-background.png, а затем указать на него стиль в файле qtquickcontrols2.conf, то файл будет подхвачен. В файле qtquickcontrols2.conf, показанном ниже, видно, как мы устанавливаем стиль Imagine , а затем задаем для стиля путь, по которому он может искать активы. Наконец, мы задаем некоторые свойства палитры. Доступные свойства палитры можно найти на странице палитры QML Basic Type (https://doc.qt.io/qt-6/qmlpalette.html#qtquickcontrols2-palette). [Controls] Style=Imagine [Imagine] Path=:images/imagine [Imagine\Palette] Text=#ffffff ButtonText=#ffffff BrightText=#ffffff Активы для элемента управления Button - button-background.9.png , button-background-pressed.9.png и button-background- checked.9.png . Они соответствуют шаблону control-element-state. Файл buttonbackground.9.png используется для всех состояний без конкретного актива. В соответствии со справочной таблицей элементов стиля Imagine (https://doc.qt.io/qt-6/qtquickcontrols2-imagine.html#elementreference) , кнопка может иметь следующие состояния:
disabled pressed checked checkable focused highlighted flat mirrored hovered Необходимые состояния зависят от пользовательского интерфейса. Например, стиль hovered никогда не используется в сенсорных интерфейсах.

Если посмотреть на увеличенную версию button-backgroundchecked.9.png выше, то можно увидеть направляющие линии 9 патчей по бокам. Пурпурный фон был добавлен для наглядности. В активе, использованном в примере, эта область на самом деле прозрачная. Пиксели по краям изображения могут быть белыми/прозрачными, черными или красными. Они имеют различные значения, которые мы рассмотрим по порядку. Черные линии вдоль левой и верхней сторон актива отмечают растягиваемые части изображения. Это означает, что закругленные углы и белый маркер в примере не пострадают при растягивании кнопки. Черные линии вдоль правой и нижней сторон актива отмечают область, используемую для содержимого элемента управления. В примере это та часть кнопки, которая используется для текста. Красные линии вдоль правой и нижней сторон актива обозначают области вставки. Эти области являются частью изображения, но не считаются частью элемента управления. Для приведенного выше изображения, отмеченного флажком, это используется для создания мягкого ореола, выходящего за пределы кнопки. Демонстрация использования области вставки показана в button- background.9.png checked.9.png (внизу) и button-background- (вверху): изображение как бы подсвечивается, но не перемещается.

Резюме В этой главе мы рассмотрели Qt Quick Controls 2. Они предлагают набор элементов, которые обеспечивают более высокоуровневые концепции, чем базовые элементы QML. В большинстве сценариев использование Qt Quick Controls 2 позволяет сэкономить память и повысить производительность, поскольку они основаны на оптимизированной логике C++, а не Javascript и QML. Мы показали, как можно использовать различные стили и как можно разработать общую кодовую базу с помощью селекторов файлов. Таким образом, единая кодовая база может быть использована для нескольких платформ с различным взаимодействием с пользователем и визуальные стили. Наконец, мы рассмотрели стиль Imagine, который позволяет полностью настроить внешний вид QML-приложения с помощью графических активов. Таким образом, приложение может быть изменено без каких-либо изменений в коде.
Модель-Вид-Делегат Как только объем данных выходит за рамки тривиального, хранить копию данных вместе с презентацией становится нецелесообразно. Это означает, что слой представления, то есть то, что видит пользователь, должен быть отделен от слоя данных, то есть реального содержимого. В Qt Quick данные отделяются от представления с помощью так называемого разделения модели и представления. Qt Quick предоставляет набор готовых представлений, в которых каждый элемент данных визуализируется делегатом. Для использования системы необходимо понимать эти классы и знать, как создать соответствующих делегатов, чтобы добиться нужного внешнего вида и настроек.
Концепция При разработке пользовательских интерфейсов распространенной схемой является разделение представления данных и их визуализации. Это позволяет отображать одни и те же данные разными способами в зависимости от того, какую задачу выполняет пользователь. Например, телефонная книга может быть представлена в виде вертикального списка текстовых записей или в виде сетки фотографий контактов. В обоих случаях данные одинаковы: телефонная книга, но визуализация разная. Такое разделение принято называть паттерном "модель-вид". В этом шаблоне данные называются моделью, а визуализация выполняется представлением. В QML модель и представление объединяются делегатом. Обязанности распределяются следующим образом: Модель предоставляет данные. Для каждого элемента данных может быть несколько значений. В приведенном примере каждая запись телефонной книги содержит имя, фотографию и номер. Данные располагаются в представлении, в котором каждый элемент визуализируется с помощью делегата. Задача представления упорядочить делегаты, при этом каждый делегат отображает значения каждого элемента. модельного элемента пользователю. Это означает, что делегат знает о содержимом модели и как его визуализировать. Представление знает о концепции делегатов и о том, как их расположить. Модель знает только о данных, которые она представляет.

Базовые модели Самым простым способом визуализации данных из модели является использование элемента Repeater. Он используется для инстанцирования массива элементов и легко сочетается с позиционером для заполнения части пользовательского интерфейса. Ретранслятор использует модель, которая может быть любой: от количества элементов для инстанцирования до полноценной модели, собирающей данные из Интернета. Использование числа В простейшем виде ретранслятор может быть использован для инстанцирования заданного количества элементов. Каждый элемент будет иметь доступ к вложенному свойству - переменной index, которая может использоваться для различения элементов. В приведенном ниже примере ретранслятор используется для создания 10 экземпляров элемента. Количество элементов контролируется с помощью свойства model и их управление визуальным представлением осуществляется с помощью свойства delegate. Для каждого элемента модели инстанцируется делегат (здесь делегат - BlueBox, который представляет собой настроенный прямоугольник, содержащий элемент Text). Как видно, свойство text установлено в значение index, поэтому элементы пронумерованы от нуля до девяти. import QtQuick import "../common" Column { spacing: 2 Repeater { model: 10

delegate: BlueBox { required property int index width: 100 height: 32 text: index } } }
Использование массива Как бы ни были хороши списки пронумерованных элементов, иногда бывает интересно отобразить более сложный набор данных. Заменив целочисленное значение модели на
JavaScript-массив, мы можем достичь этого. Содержимое массива может быть любого типа, будь то строки, целые числа или объекты. В приведенном ниже примере используется список строк. Мы попрежнему можем обращаться к переменной index и использовать ее, но у нас также есть доступ к modelData, содержащей данные для каждого элемента массива. import QtQuick import "../common" Column { spacing: 2 Repeater { model: ["Enterprise", "Columbia", "Challenger", "Discov delegate: BlueBox { required property var modelData required property int index width: 100 height: 32 radius: 3 text: modelData + ' (' + index + ')' } } }
Использование модели ListModel Имея возможность раскрывать данные массива, вы вскоре оказываетесь в ситуации, когда вам требуется несколько частей данных для каждого элемента массива. Вот тут-то и появляются модели. Одной из наиболее тривиальных моделей и одной из наиболее часто используемых является модель ListModel . Модель списка - это просто коллекция элементов ListElement. Внутри каждого элемента списка можно привязать к значениям ряд свойств. Например, в приведенном ниже примере для каждого элемента заданы имя и цвет. Свойства, связанные внутри каждого элемента, прикрепляются ретранслятором к каждому инстанцированному элементу. Это означает, что переменные name и surfaceColor доступны из области видимости каждого Rectangle и элемент Text, создаваемый ретранслятором. Это не только упрощает доступ к данным, но и облегчает чтение исходного кода. Сайт
surfaceColor - это цвет круга слева от названия, а не что-то непонятное вроде данных из столбца i строки j. import QtQuick import "../common" Column { spacing: 2 Repeater { model: ListModel { ListElement { name: "Mercury"; surfaceColor: "gray" ListElement { name: "Venus"; surfaceColor: "yellow" ListElement { name: "Earth"; surfaceColor: "blue" } ListElement { name: "Mars"; surfaceColor: "orange" ListElement { name: "Jupiter"; surfaceColor: "orang ListElement { name: "Saturn"; surfaceColor: "yellow ListElement { name: "Uranus"; surfaceColor: "lightB ListElement { name: "Neptune"; surfaceColor: "light } delegate: BlueBox { id: blueBox required property string name required property color surfaceColor width: 120 height: 32 radius: 3 text: name Box { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 4 width: 16
height: 16 radius: 8 color: blueBox.surfaceColor } } } }
Использование делегата в качестве свойства по умолчанию Свойство delegate ретранслятора является его свойством по умолчанию. Это означает, что код примера 01 можно записать и следующим образом: import QtQuick import "../common" Column { spacing: 2 Repeater { model: 10 BlueBox { required property int index width: 100 height: 32 text: index } } }
Динамические представления Повторители хорошо работают для ограниченных и статичных наборов данных, но в реальном мире модели обычно сложнее и больше. Здесь требуется более разумное решение. Для этого в Qt Quick предусмотрены элементы ListView и GridView. Оба они основаны на области Flickable, что позволяет пользователю перемещаться в большом наборе данных. В то же время они ограничивают количество одновременно инстанцируемых делегатов. Для большой модели это означает меньшее количество элементов в сцене одновременно.

Эти два элемента схожи по своему использованию. Мы начнем с ListView, а затем опишем GridView, ис п о л ь з у я первый в качестве отправной точки сравнения. Обратите внимание, что GridView размещает список элементов в двумерной сетке, слева направо или сверху вниз. Если вы хотите Для отображения таблицы с данными необходимо использовать TableView, ListView который описан в разделе Модели таблиц. аналогичен элементу Repeater. Он использует модель, инстанцирует делегат, а между делегатами могут быть промежутки. В приведенном ниже листинге показано, как может выглядеть простая настройка.
import QtQuick import "../common" Background { width: 80 height: 300 ListView { anchors.fill: parent anchors.margins: 20 clip: true model: 100 delegate: GreenBox { required property int index width: 40 height: 40 text: index } spacing: 5 } }
Если модель содержит больше данных, чем может поместиться на экране, то в ListView отображается только часть списка. Однако, как следствие поведения Qt Quick по умолчанию, представление списка не ограничивает область экрана, в которой отображаются делегаты. Это означает, что делегаты могут быть видимым за пределами представления списка, и чтобы динамическое создание и уничтожение делегатов за пределами представления списка было заметно пользователю. Для предотвращения этого необходимо активизировать обрезание на элементе ListView, установив параметр clip свойство в true. На рисунке ниже показан результат этого (вид слева) по сравнению с тем, когда свойство clip равно false (вид справа).
Для пользователя ListView представляет собой область с возможностью прокрутки. Он поддерживает кинетическую прокрутку, что означает, что его можно пролистывать для быстрого перемещения по содержимому. По умолчанию он также может растягиваться до конца содержимого, а затем отскакивать назад, сигнализируя пользователю о том, что конец прокрутки достигнут. Поведение в конце представления контролируется с помощью свойства boundsBehavior. Это перечисляемое значение, которое может изменяться от поведения по умолчанию, Flickable.DragAndOvershootBounds , когда представление можно как перетаскивать, так и пролистывать за его границы, до Flickable.StopAtBounds ,
при котором вид никогда не будет перемещаться за пределы своих границ. Средний вариант, Flickable.DragOverBounds, позволяет пользователю перетаскивать вид за его границы, но при этом щелчки будут останавливаться на границе. Можно ограничить позиции, в которых разрешается останавливать вид. Это контролируется с помощью свойства snapMode. Поведение по умолчанию, ListView.NoSnap, позволяет представлению останавливаться в любой позиции. Если задать свойству SnapMode значение ListView.SnapToItem, то вид всегда будет выравниваться по верхней части элемента. И, наконец, в качестве свойства ListView.SnapOneItem можно использовать свойство ListView.SnapOneItem вид остановится не более чем на одном элементе от первого видимого элемента, когда была отпущена кнопка мыши или касание. Последний режим очень удобен при перелистывании страниц. Ориентация По умолчанию представление списка обеспечивает вертикальную прокрутку списка, но горизонтальная прокрутка может быть не менее удобной. Направление прокрутки списка контролируется с помощью свойства orientation. Оно может быть установлено либо в значение по умолчанию ListView.Vertical, либо в ListView.Horizontal. Горизонтальное представление списка показано ниже. import QtQuick import "../common" Background { width: 480 height: 80 ListView { anchors.fill: parent anchors.margins: 20 spacing: 4 clip: true model: 100

orientation: ListView.Horizontal delegate: GreenBox { required property int index width: 40 height: 40 text: index } } } Как видно, по умолчанию направление горизонтальных потоков слева направо. Это можно контролировать с помощью свойства layoutDirection, Qt.LeftToRight которое может быть установлено в значение или Qt.RightToLeft в зависимости от направления потока. Навигация и выделение на клавиатуре При использовании ListView в сенсорном режиме достаточно самого представления. В сценарии с использованием клавиатуры или даже просто клавиш со стрелками для выбора элемента необходим механизм, указывающий на текущий элемент. В QML это называется подсветкой. Представления поддерживают делегат выделения, который отображается в представлении вместе с делегатами. Его можно рассматривать как дополнительный делегат, только он инстанцируется только один раз и перемещается в ту же позицию, что и текущий элемент. В приведенном ниже примере это продемонстрировано. Для того чтобы это сработало, необходимо задействовать два свойства. Вопервых, свойство focus устанавливается в true. Таким образом, ListView п о л у ч а е т фокус клавиатуры. Во-вторых, свойство highlight имеет значение
задается для указания делегата выделения, который необходимо использовать. Делегату подсветки передаются значения x , y и текущего высоты элемента. Если ширина не указана, то используется ширина текущего элемента. В примере для ширины используется присоединенное свойство ListView.view.width. Вложенные свойства, доступные делегатам, рассматриваются далее в разделе "Делегаты" этой главы, но полезно знать, что эти же свойства доступны и для выделенных делегатов. import QtQuick import "../common" Background { width: 240 height: 300 ListView { id: view anchors.fill: parent anchors.margins: 20 focus: true model: 100 delegate: numberDelegate highlight: highlightComponent spacing: 5 clip: true } Component { id: highlightComponent GreenBox { width: ListView.view ? ListView.view.width : 0 }
} Component { id: numberDelegate Item { id: wrapper required property int index width: ListView.view ? ListView.view.width : 0 height: 40 Text { anchors.centerIn: parent font.pixelSize: 10 text: wrapper.index } } } }
При использовании подсветки в сочетании со списком ListView можно использовать ряд свойств для управления ее поведением. Свойство highlightRangeMode управляет тем, как на подсветку влияет то, что отображается в представлении. Значение по умолчанию ListView.NoHighlightRange означает, что подсветка и видимый диапазон элементов в представлении никак не связаны. Значение ListView.StrictlyEnforceRange гарантирует, что выделение всегда будет видимым. Если действие попытается переместить выделение за пределы видимой части представления, то текущий элемент изменится соответствующим образом, чтобы выделение оставалось видимым.
Средним вариантом является значение ListView.ApplyRange. Оно пытается сохранить выделение видимым, но не изменяет текущий элемент для обеспечения этого. Вместо этого выделяемому объекту разрешается при необходимости уходить из поля зрения. В конфигурации по умолчанию представление отвечает за выбирается та, которая приводит к наиболее быстрой анимации. перемещение выделенной области в нужное положение. Скорость длительностью. Если заданы и скорость, и длительность, то перемещения и изменение размеров могут контролироваться как в 1, что указывает на то, что скорость и расстояние управляют виде скорости, так и в виде длительности. Для этого используются скорость установлена на 400 пикселей в секунду, а длительность - на следующие свойства: highlightMoveSpeed , highlightMoveDuration , highlightResizeSpeed и highlightResizeDuration . По умолчанию Для более детального управления перемещением выделенного объекта свойство highlightFollowCurrentItem может быть установлено в значение false . Это означает, что представление больше не отвечает за перемещение делегата выделения. Вместо этого движение можно контролировать с помощью Behavior или анимации. В приведенном ниже примере свойство y делегата выделения привязано к вложенному свойству ListView.view.currentItem.y . Это гарантирует, что выделение будет следовать за текущим элементом. Однако, поскольку мы не позволяем представлению перемещать выделение, мы можем управлять тем, как перемещается элемент. Это делается с помощью свойства . В приведенном з ниже примере перемещение разделено на три этапа: затухание, перемещение, а затем затухание. Обратите внимание, что элементы SequentialAnimation и Prope сочетании с NumberAnimation Component { id: ItemhighlightComponent { Item {
width: ListView.view ? ListView.view.width : 0 height: ListView.view ? ListView.view.currentItem.heigh y: ListView.view ? ListView.view.currentItem.y : 0 Behavior on y { SequentialAnimation { PropertyAnimation { target: highlightRectangle; NumberAnimation { duration: 1 } PropertyAnimation { target: highlightRectangle; } } GreenBox { id: highlightRectangle anchors.fill: parent } } } Верхнийм им нижнийм колонтитулы В каждый конец содержимого ListView можно вставить элемент заголовказ и элемент нижнегоз колонтитула. Их можно рассматривать как специальные делегаты, размещаемые в начале или конце списка. Для горизонтального списка они будут располагаться не в начале и не в конце, а в начале или в конце, в зависимости от используемого layoutDirection . В приведенном ниже примере показано, как можно использовать верхний и нижний колонтитулы для улучшения восприятия начала и конца списка. Существуют и другие варианты использования этих специальных элементов списка. Например, они могут использоваться в качестве кнопок для загрузки дополнительного содержимого. з з H import QtQuick import "../common"
Background { width: 240 height: 300 ListView { anchors.fill: parent anchors.margins: 20 clip: true model: 4 delegate: numberDelegate header: headerComponent footer: footerComponent spacing: 2 } Component { id: headerComponent YellowBox { width: ListView.view ? ListView.view.width : 0 height: 20 text: 'Header' } } Component { id: footerComponent YellowBox { width: ListView.view ? ListView.view.width : 0 height: 20 text: 'Footer' } }
Component { id: numberDelegate GreenBox { required property int index width: ListView.view.width height: 40 text: 'Item #' + index } } }
СОВЕТ Делегаты верхнего и нижнего колонтитулов не учитывают свойство spacing ListView, вместо этого они размещаются непосредственно рядом с делегатом следующего элемента в списке. Это означает, что любой интервал должен быть частью элементов верхнего и нижнего колонтитулов. GridView
Использование GridView очень похоже на использование ListView . Единственное отличие заключается в том, что в представлении сетки делегаты размещаются в двумерной сетке, а не в линейном списке. По сравнению со списочным представлением, сеточное представление не полагается на интервалы и размеры своих делегатов. Вместо этого для управления размерами делегатов содержимого используются свойства cellWidth и cellHeight. Каждый элемент делегата помещается в левый верхний угол каждой такой ячейки.
import QtQuick import "../common" Background { width: 220 height: 300 GridView { id: view anchors.fill: parent anchors.margins: 20 clip: true model: 100 cellWidth: 45 cellHeight: 45 delegate: GreenBox { required property int index width: 40 height: 40 text: index } } } GridView содержит верхние и нижние колонтитулы, может использовать делегат выделения и поддерживает режимы привязки, а также различные поведения границ. Он также может быть ориентирован в различных направлениях и ориентациях. Управление ориентацией осуществляется с помощью свойства flow. Оно может быть установлено в значение GridView.LeftToRight или GridView.TopToBottom . В первом случае сетка заполняется слева направо, а строки добавляются сверху вниз. Представление можно прокручивать в вертикальном направлении. Второе значение добавляет
элементы сверху вниз, заполняя вид слева направо. Направление прокрутки в этом случае горизонтальное. В дополнение к свойству flow свойство layoutDirection может адаптировать направление сетки к языкам слева-направо или справа-налево в зависимости от используемого значения.
Делегат Когда речь идет об использовании моделей и представлений в пользовательском интерфейсе, делегат играет огромную роль в создании внешнего вида и поведения. Поскольку каждый элемент модели визуализируется через делегат, то то, что реально видит пользователь, - это делегаты. Каждый делегат получает доступ к ряду присоединенных свойств, часть из которых поступает из модели данных, а часть - из представления. Из модели свойства передают делегату данные для каждого элемента. Из представления свойства передают информацию о состоянии делегата в рамках представления. Давайте рассмотрим свойства из представления. Наиболее часто используемыми свойствами, привязанными к представлению, являются ListView.isCurrentItem и ListView.view . Первое представляет собой булево значение, указывающее, является ли элемент текущим, а второе - ссылку на реальное представление, доступную только для чтения. Благодаря доступу к представлению можно создавать общие делегаты многократного использования, которые адаптируются к размеру и характеру представления, в котором они содержатся. В приведенном ниже примере ширина каждого делегата привязана к ширине представления, а цвет фона каждого делегата зависит от вложенного свойства ListView.isCurrentItem. import QtQuick Rectangle { width: 120 height: 300 gradient: Gradient {
GradientStop { position: 0.0; color: "#f6f6f6" } GradientStop { position: 1.0; color: "#d7d7d7" } } ListView { anchors.fill: parent anchors.margins: 20 focus: true model: 100 delegate: numberDelegate spacing: 5 clip: true } Component { id: numberDelegate Rectangle { id: wrapper required property int index width: ListView.view.width height: 40 color: ListView.isCurrentItem ? "#157efb" : "#53d76 border.color: Qt.lighter(color, 1.1) Text { anchors.centerIn: parent font.pixelSize: 10 text: wrapper.index } }
} } Если каждый элемент в модели связан с действием, например, щелчок по элементу вызывает действие над ним, то эта функциональность является частью каждого делегата. Таким образом, управление событиями разделяется между представлением, которое управляет навигацией между элементами в представлении, и делегатом, который управляет действиями над конкретным элементом.
Самый простой способ сделать это - создать MouseArea внутри каждого делегата и действовать по сигналу onClicked. Это демонстрируется в примере в следующем разделе этой главы. Анимация добавленных и удаленных элементов В некоторых случаях содержимое, отображаемое в представлении, меняется с течением времени. Элементы добавляются и удаляются по мере изменения базовой модели данных. В таких случаях часто целесообразно использовать визуальные подсказки, чтобы дать пользователю возможность сориентироваться и понять, какие данные добавляются или удаляются. Удобно, что представления QML подключают два сигнала, onAdd и onRemove , для каждого делегата элемента. Запустив анимацию на их основе, можно легко создать движение, необходимое для идентификации пользователем происходящего. Приведенный ниже пример демонстрирует это на примере динамически заполняемой модели ListModel . В нижней части экрана отображается кнопка для добавления новых элементов. При нажатии на нее в модель добавляется новый элемент с помощью метода append. Это вызывает создание нового делегата в представлении и выдачу сигнала GridView.onAdd. Запускаемая по сигналу SequentialAnimation с именем addAnimation приводит к увеличению масштаба элемента в представлении, анимируя свойство scale делегата. GridView.onAdd: addAnimation.start() SequentialAnimation { id: addAnimation NumberAnimation { target: wrapper property: "scale" from: 0
to: 1 duration: 250 easing.type: Easing.InOutQuad } } При нажатии на делегат в представлении элемент удаляется из модели через вызов метода remove. Это приводит к выдаче сигнала GridView.onRemove, removeAnimation . запускающего последовательную анимацию Однако на этот раз уничтожение элемента делегат должен быть отложен до завершения анимации. Для этого с помощью элемента PropertyAction свойству GridView.delayRemove присваивается значение true до начала анимации и false после. Это гарантирует, что анимация будет завершена до удаления элемента делегата. GridView.onRemove: removeAnimation.start() SequentialAnimation { id: removeAnimation PropertyAction { target: wrapper; property: "GridView.delay NumberAnimation { target: wrapper; property: "scale"; to: 0 PropertyAction { target: wrapper; property: "GridView.delay } Приведем полный код: import QtQuick Rectangle { width: 480 height: 300 gradient: Gradient {
GradientStop { position: 0.0; color: "#dbddde" } GradientStop { position: 1.0; color: "#5fc9f8" } } ListModel { id: theModel ListElement { number: 0 } ListElement { number: 1 } ListElement { number: 2 } ListElement { number: 3 } ListElement { number: 4 } ListElement { number: 5 } ListElement { number: 6 } ListElement { number: 7 } ListElement { number: 8 } ListElement { number: 9 } } Rectangle { property int count: 9 anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 20 height: 40 color: "#53d769" border.color: Qt.lighter(color, 1.1) Text { anchors.centerIn: parent text: "Add item!" } MouseArea { anchors.fill: parent
onClicked: { theModel.append({"number": ++parent.count}) } } } GridView { anchors.fill: parent anchors.margins: 20 anchors.bottomMargin: 80 clip: true model: theModel cellWidth: 45 cellHeight: 45 delegate: numberDelegate } Component { id: numberDelegate Rectangle { id: wrapper required property int index required property int number width: 40 height: 40 gradient: Gradient { GradientStop { position: 0.0; color: "#f8306a" GradientStop { position: 1.0; color: "#fb5b40" } Text {
anchors.centerIn: parent font.pixelSize: 10 text: wrapper.number } MouseArea { anchors.fill: parent onClicked: { if (wrapper.index == -1) { return } theModel.remove(wrapper.index) } } GridView.onRemove: removeAnimation.start() SequentialAnimation { id: removeAnimation PropertyAction { target: wrapper; property: "Gr NumberAnimation { target: wrapper; property: "s PropertyAction { target: wrapper; property: "Gr } GridView.onAdd: addAnimation.start() SequentialAnimation { id: addAnimation NumberAnimation { target: wrapper property: "scale" from: 0 to: 1 duration: 250 easing.type: Easing.InOutQuad }
} } } } Shape-Shifting Delegates В списках часто используется механизм, при котором текущий элемент расширяется при активации. Это может быть использовано для динамического расширения элемента, чтобы заполнить экран для входа в новую часть пользовательского интерфейса, или для предоставления немного больше информации для текущего элемента в данном списке. В приведенном ниже примере каждый элемент при щелчке раскрывается на весь объем содержащего его ListView. Дополнительное пространство используется для добавления дополнительной информации. Для управления этим механизмом используется состояние expanded, в которое может перейти делегат каждого элемента. В этом состоянии изменяется ряд свойств. Прежде всего, высота обертки устанавливается равной высоте ListView . Затем миниатюрное изображение увеличивается и перемещается вниз, чтобы оно переместилось из своего маленького положения в большое. Кроме того, два скрытых элемента, factsView и closeButton, показываются путем изменения непрозрачности э т и х элементов. Наконец, настраивается ListView. Настройка ListView заключается в установке содержимогоY , то есть вершины видимой части представления, на значение y делегата. Другим изменением является установка интерактивности представления в false . Это предотвращает перемещение представления. Пользователь больше не сможет прокручивать список или изменять текущий элемент. При первом щелчке на элементе он переходит в расширенное состояние, в результате чего делегат элемента заполняет ListView, а его содержимое перестраивается. Когда элемент
При нажатии на кнопку закрытия состояние очищается, что приводит к возврату делегата в предыдущее состояние и повторному включению ListView. import QtQuick Item { width: 300 height: 480 Rectangle { anchors.fill: parent gradient: Gradient { GradientStop { position: 0.0; color: "#4a4a4a" } GradientStop { position: 1.0; color: "#2b2b2b" } } } ListView { id: listView anchors.fill: parent delegate: detailsDelegate model: planets } ListModel { id: planets ListElement { name: "Mercury"; imageSource: "images/mer ListElement { name: "Venus"; imageSource: "images/venus ListElement { name: "Earth"; imageSource: "images/earth ListElement { name: "Mars"; imageSource: "images/mars.j } Component { id: detailsDelegate Item {
id: wrapper required property string name required property string imageSource required property string facts width: listView.width height: 30 Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top height: 30 color: "#333" border.color: Qt.lighter(color, 1.2) Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCent anchors.leftMargin: 4 font.pixelSize: parent.height-4 color: '#fff' text: wrapper.name } } Rectangle { id: image width: 26 height: 26 anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: 2
anchors.topMargin: 2 color: "black" Image { anchors.fill: parent fillMode: Image.PreserveAspectFit source: wrapper.imageSource } } MouseArea { anchors.fill: parent onClicked: parent.state = "expanded" } Item { id: factsView anchors.top: image.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom opacity: 0 Rectangle { anchors.fill: parent gradient: Gradient { GradientStop { position: 0.0; color: "# GradientStop { position: 1.0; color: "# } border.color: '#000000' border.width: 2 Text {
anchors.fill: parent anchors.margins: 5 clip: true wrapMode: Text.WordWrap color: '#1f1f21' font.pixelSize: 12 text: wrapper.facts } } } Rectangle { id: closeButton anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: 2 anchors.topMargin: 2 width: 26 height: 26 color: "#157efb" border.color: Qt.lighter(color, 1.1) opacity: 0 MouseArea { anchors.fill: parent onClicked: wrapper.state = "" } } states: [ State { name: "expanded"
PropertyChanges { target: wrapper; height: PropertyChanges { target: image; width: lis PropertyChanges { target: factsView; opacit PropertyChanges { target: closeButton; opac PropertyChanges { target: wrapper.ListView. } ] transitions: [ Transition { NumberAnimation { duration: 200; properties: "height,width,anchors.right } } ] } } }


Продемонстрированные здесь приемы расширения делегата на весь экран могут быть использованы для изменения формы делегата элемента в гораздо меньшую сторону. Например, при просмотре списка песен текущий элемент может быть немного увеличен в размерах, что позволит разместить больше информации об этом конкретном элементе.
Advanced Techniques PathView Элемент PathView является наиболее гибким видом представления, предоставляемым в Qt Quick, но и наиболее сложным. Он позволяет создать представление, в котором элементы располагаются по произвольному пути. Вдоль этого пути можно детально управлять такими атрибутами, как масштаб, непрозрачность и т.д. При использовании PathView необходимо определить делегата и путь. Кроме того, сам PathView может быть настроен с помощью ряда свойств. Наиболее распространенными из них являются pathItemCount, определяющее количество одновременно видимых элементов, и свойства управления диапазоном подсветки preferredHighlightBegin, preferredHighlightEnd и highlightRangeMode, определяющие, в какой части пути должен быть показан текущий элемент. Прежде чем подробно рассматривать свойства элемента управления диапазоном выделения, необходимо рассмотреть свойство path. Свойство path ожидает элемент Path, определяющий путь, по которому будут двигаться делегаты п р и прокрутке PathView. Путь задается с помощью свойств startX и startY в комбинации с такими элементами пути, как PathLine , PathQuad и PathCubic . Эти элементы соединяются вместе, образуя два элемента размерный путь. После того как путь определен, его можно дополнительно настроить с помощью элементов PathPercent и PathAttribute. Они располагаются между элементами пути и обеспечивают более тонкий контроль над путем и делегатами на нем. PathPercent определяет, насколько велика часть
путь, пройденный между каждым элементом. Это, в свою очередь, управляет распределением делегатов по пути, так как они распределяются пропорционально пройденному проценту. Именно здесь в дело в с т у п а ю т свойства preferredHighlightBegin и preferredHighlightEnd программы PathView. Оба они ожидают реальных значений в диапазоне от нуля до единицы. Также ожидается, что конец будет больше или равен началу. Если установить оба этих свойства, например, в 0.5, то текущий элемент будет отображаться в пятидесятипроцентном месте пути. В контуре Path , элементы PathAttribute располагаются между элементами, как и элементы PathPercent. Они позволяют задавать значения свойств, которые интерполируются вдоль пути. Эти свойства привязываются к делегатам и могут использоваться для управления любыми возможными свойствами.
Приведенный ниже пример демонстрирует, как элемент PathView используется для создания представления карт, которые пользователь может перелистывать. Для этого используется ряд приемов. Путь состоит из трех элементов PathLine. С помощью элементов PathPercent центральный элемент правильно центрируется и получает достаточно места, чтобы его не загромождали другие элементы. С помощью элементов PathAttribute осуществляется управление поворотом, размером и z-значением. В дополнение к пути, свойство pathItemCount в PathView была установлена. Это определяет, насколько плотно будет заселен путь. В поле
preferredHighlightBegin и preferredHighlightEnd the PathView.onPath используется для управления видимостью делегатов. PathView { anchors.fill: parent model: 100 delegate: flipCardDelegate path: Path { startX: root.width / 2 startY: 0 PathAttribute { name: "itemZ"; value: 0 } PathAttribute { name: "itemAngle"; value: -90.0; } PathAttribute { name: "itemScale"; value: 0.5; } PathLine { x: root.width / 2; y: root.height * 0.4; } PathPercent { value: 0.48; } PathLine { x: root.width / 2; y: root.height * 0.5; } PathAttribute { name: "itemAngle"; value: 0.0; } PathAttribute { name: "itemScale"; value: 1.0; } PathAttribute { name: "itemZ"; value: 100 } PathLine { x: root.width / 2; y: root.height * 0.6; } PathPercent { value: 0.52; } PathLine { x: root.width / 2; y: root.height; } PathAttribute { name: "itemAngle"; value: 90.0; } PathAttribute { name: "itemScale"; value: 0.5; } PathAttribute { name: "itemZ"; value: 0 } } pathItemCount: 16 preferredHighlightBegin: 0.5 preferredHighlightEnd: 0.5 } Делегат, показанный ниже, использует присоединенные свойства itemZ , itemAngle и itemScale из элементов PathAttribute. Это
Стоит заметить, что присоединенные свойства делегата доступны только из обертки. Так, свойство rotX определено для того, чтобы можно было получить доступ к его значению из элемента Rotation. Еще одна деталь, характерная для PathView, на которую стоит обратить внимание, - это использование присоединенного свойства PathView.onPath. Обычно принято привязывать к нему видимость, поскольку это позволяет PathView сохранять невидимые элементы для целей кэширования. Обычно это не удается сделать с помощью обрезки, поскольку делегаты элементов в PathView размещаются более свободно, чем делегаты элементов в представлениях ListView или GridView. Component { id: flipCardDelegate BlueBox { id: wrapper required property int index property real rotX: PathView.itemAngle visible: PathView.onPath width: 64 height: 64 scale: PathView.itemScale z: PathView.itemZ antialiasing: true gradient: Gradient { GradientStop { position: 0.0; color: "#2ed5fa" } GradientStop { position: 1.0; color: "#2467ec" } } transform: Rotation { axis { x: 1; y: 0; z: 0 } angle: wrapper.rotX
При преобразовании изображений или других сложных элементов в PathView часто используется прием оптимизации производительности, заключающийся в привязке свойство smooth элемента Image к присоединенному свойству PathView.view.moving . Это означает, что при движении изображения становятся менее красивыми, а при неподвижности плавно трансформируются. Нет смысла тратить вычислительную мощность на плавное масштабирование, когда изображение находится в движении, поскольку пользователь все равно не сможет этого увидеть. Совет Учитывая динамическую природу PathAttribute, инструментарий qml (в данном случае: qmllint) не знает ни itemZ, ни itemAngle, ни itemScale. При использовании PathView и программном изменении currentIndex может возникнуть необходимость контролировать направление движения пути. Это можно сделать с помощью свойства movementDirection. PathView.Shortest, Оно может быть установлено в значение которое используется по умолчанию. Это означает, что движение может осуществляться в любом направлении, в зависимости от того, какой путь является наиболее близким для перемещения к целевому значению. Вместо этого направление можно ограничить, установив для свойства MovementDirection значение PathView.Negative или PathView.Positive. Table Models
Все рассмотренные до сих пор представления так или иначе представляют массив элементов. Даже GridView ожидает, что модель предоставит одномерный список элементов. Для двумерных таблиц данных необходимо использовать элемент TableView. TableView похож на другие представления тем, что объединяет модель с делегатом для формирования сетки. Если ему дана модель, ориентированная на список, то он отображает один столбец, что делает его очень похожим на элемент ListView. Однако он может отображать и двумерные модели, в которых явно определены столбцы и строки. В приведенном ниже примере мы создали простую таблицу TableView с пользовательской моделью, открываемой из C++. В настоящее время создание таблично-ориентированных моделей непосредственно из QML невозможно, но в главе "Qt и C++" эта концепция объясняется. Работающий пример показан на рисунке ниже. В приведенном устанавливаем ниже свойства примере мы rowSpacing создаем и TableView columnSpacing и для управления горизонтальными и вертикальными промежутками между делегатами. Остальные свойства настраиваются так же, как и для любого другого типа вид.
TableView { id: view anchors.fill: parent anchors.margins: 20 rowSpacing: 5 columnSpacing: 5 clip: true model: tableModel delegate: cellDelegate } Сам делегат может нести неявный размер через implicitWidth и implicitHeight . Именно это мы и делаем в приведенном ниже примере. Сайт фактическое содержимое данных, т.е. данные, возвращаемые из роли отображения модели. Component { id: cellDelegate GreenBox { id: wrapper required property string display implicitHeight: 40 implicitWidth: 40 Text { anchors.centerIn: parent text: wrapper.display } } }
В зависимости от содержания модели можно предоставлять делегатов разных размеров, например: GreenBox { implicitHeight: (1 + row) * 10 // ... } Обратите внимание, что и ширина, и высота должны быть больше нуля. При указании неявного размера из делегата размером управляет самый высокий делегат каждой строки и самый широкий делегат каждого столбца. Это может привести к интересному поведению, если ширина элементов зависит от строки, а высота - от столбца. Это связано с тем, что не все делегаты инстанцируются постоянно, поэтому ширина столбца может меняться по мере прокрутки таблицы пользователем. Чтобы избежать проблем с указанием ширины столбцов и высоты строк с использованием неявных размеров делегата, можно предоставить функции, вычисляющие эти размеры. Для этого используются функции columnWidthProvider и rowHeightProvider . Эти функции возвращают размер ширины и строки соответственно, как показано ниже: TableView { columnWidthProvider: function (column) { return 10 * (colum // ... } Если необходимо динамически изменить ширину столбцов или высоту строк, необходимо уведомить об этом представление, вызвав метод forceLayout. Это заставит представление заново рассчитать размер и положение всех ячеек. Модель из XML
Поскольку XML является повсеместно распространенным форматом данных, в QML предусмотрен элемент XmlListModel, который представляет XML-данные в виде модели. Этот элемент может получать XML-данные локально или удаленно, а затем обрабатывать их с помощью выражений XPath. Приведенный ниже пример демонстрирует получение изображений из потока RSS. Свойство source ссылается на удаленное местоположение по протоколу HTTP, и данные загружаются автоматически. После загрузки данных они преобразуются в элементы и роли модели. Свойство query модели XmlListModel - это XPath, представляющий базовый запрос для создания элементов модели. В данном примере путь - /rss/channel/item , поэтому для каждого тега item, находящегося внутри тега channel, внутри тега RSS, создается элемент модели.
Для каждого элемента модели извлекается ряд ролей. Они представлены элементами XmlListModelRole. Каждой роли присваивается имя, к которому делегат может получить доступ через присоединенное свойство. Фактическое значение каждого такого свойства определяется через свойства elementName и (необязательно) attributeName для каждой роли. Например, свойство title соответствует XML-элементу title, возвращая содержимое между тегами <title> и </title>. Свойство imageSource извлекает значение атрибута тега вместо его содержимого. В данном случае в виде строки извлекается атрибут url тега enclosure. Свойство imageSource может быть использовано непосредственно в качестве источника для элемента Image, который загружает изображение с заданного URL. import QtQuick import QtQml.XmlListModel import "../common" Background { width: 300 height: 480 Component { id: imageDelegate Box { id: wrapper required property string title required property string imageSource width: listView.width height: 220 color: '#333' Column { Text {
text: wrapper.title color: '#e0e0e0' } Image { width: listView.width height: 200 fillMode: Image.PreserveAspectCrop source: wrapper.imageSource } } } } XmlListModel { id: imageModel source: "https://www.nasa.gov/rss/dyn/image_of_the_day. query: "/rss/channel/item" XmlListModelRole { name: "title"; elementName: "title" XmlListModelRole { name: "imageSource"; elementName: "e } ListView { id: listView anchors.fill: parent model: imageModel delegate: imageDelegate } } Списки с разделами Иногда данные в списке можно разделить на разделы. Это может быть простое разделение списка контактов на разделы под каждой буквой алфавита или музыкальных треков на альбомы. Используя ListView, можно разделить плоский список на категории, обеспечив большую глубину восприятия.
Для того чтобы использовать секции, необходимо настроить section.property определяет, и section.criteria. Свойство какое свойство использовать section.property для разделения содержимого на секции. Здесь важно знать, что для разделения содержимого на секции используется свойство модель должна быть отсортирована таким образом, чтобы каждая секция состояла из непрерывных элементов, иначе одно и то же имя свойства может появиться в нескольких местах. Критерий section.criteria ViewSection.FullString может быть установлен либо или ViewSection.FirstCharacter . Первый вариант является значением по умолчанию и может использоваться для моделей, имеющих прозрачные секции, например, треки музыкальных альбомов. Второе значение принимает первый символ свойства и означает, что для этого может быть использовано любое свойство. Наиболее распространенный пример - фамилия контактов в телефонной книге. как
После определения разделов к ним можно обращаться из каждого элемента с помощью подключаемых свойств ListView.section , ListView.previousSection и ListView.nextSection . С помощью этих свойств можно определить первый и последний элемент секции и действовать соответствующим образом. Также можно назначить компонент делегата раздела свойству section.delegate ListView . При этом создается делегат заголовка раздела, который вставляется перед любыми элементами раздела. Компонент-делегат может получить доступ к имени текущего раздела с помощью присоединенного свойства section . Приведенный ниже пример демонстрирует концепцию секций, показывая список космонавтов, разделенных по национальному признаку. Национальность используется в качестве свойства section.property . Компонент section.delegate, sectionDelegate, показывает заголовок для каждой нации, отображая ее название. В каждом разделе с помощью компонента spaceManDelegate отображаются имена космонавтов. import QtQuick import "../common" Background { width: 300 height: 290 ListView { anchors.fill: parent anchors.margins: 20 clip: true model: spaceMen delegate: spaceManDelegate
section.property: "nation" section.delegate: sectionDelegate } Component { id: spaceManDelegate Item { id: spaceManWrapper required property string name width: ListView.view.width height: 20 Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 8 font.pixelSize: 12 text: spaceManWrapper.name color: '#1f1f1f' } } } Component { id: sectionDelegate BlueBox { id: sectionWrapper required property string section width: ListView.view ? ListView.view.width : 0 height: 20 text: sectionWrapper.section fontColor: '#e0e0e0' } } ListModel { id: spaceMen
ListElement { name: "Abdul Ahad Mohmand"; nation: "Afga ListElement { name: "Marcos Pontes"; nation: "Brazil"; ListElement { name: "Alexandar Panayotov Alexandrov"; n ListElement { name: "Georgi Ivanov"; nation: "Bulgaria" ListElement { name: "Roberta Bondar"; nation: "Canada"; ListElement { name: "Marc Garneau"; nation: "Canada"; } ListElement { name: "Chris Hadfield"; nation: "Canada"; ListElement { name: "Guy Laliberte"; nation: "Canada"; ListElement { name: "Steven MacLean"; nation: "Canada"; ListElement { name: "Julie Payette"; nation: "Canada"; ListElement { name: "Robert Thirsk"; nation: "Canada"; ListElement { name: "Bjarni Tryggvason"; nation: "Canad ListElement { name: "Dafydd Williams"; nation: "Canada" } } Объектная модель В некоторых случаях требуется использовать представление списка для большого набора различных элементов. Это можно решить с помощью динамического QML и Loader, но есть и другой вариант использовать объектную модель из модуля QtQml.Models. Объектная модель отличается от других моделей тем, что позволяет поместить фактический визуальных элементов на стороне модели. Таким образом, представлению не требуется делегат
В приведенном ниже примере мы поместили в ObjectModel три элемента Rectangle . Однако у одного прямоугольника есть дочерний элемент Text, а у последнего - закругленные углы. Это привело бы к созданию модели в стиле таблицы с использованием чего-то вроде ListModel . Это также привело бы к появлению в модели пустых элементов Text. import QtQuick import QtQml.Models Rectangle { width: 320 height: 320 gradient: Gradient { GradientStop { position: 0.0; color: "#f6f6f6" } GradientStop { position: 1.0; color: "#d7d7d7" } } ObjectModel { id: itemModel Rectangle { height: 60; width: 80; color: "#157efb" } Rectangle { height: 20; width: 300; color: "#53d769"
Text { anchors.centerIn: parent; color: "black"; te } Rectangle { height: 40; width: 40; radius: 10; color: " } ListView { anchors.fill: parent anchors.margins: 10 spacing: 5 model: itemModel } } Еще одним аспектом модели ObjectModel является возможность ее динамического наполнения с помощью методов get, insert, move, remove и clear. Таким образом, содержимое модели может динамически генерироваться из различных источников и при этом легко отображаться в одном представлении. Модели с действиями Тип ListElement поддерживает привязку Javascript-функций к свойствам. Это означает, что функции можно поместить в модель. Это очень удобно при построении меню с помощью действий и других подобных конструкций. Приведенный ниже пример демонстрирует это на примере модели городов, которые приветствуют вас различными способами. Модель actionModel представляет собой модель четырех городов, но свойство hello привязано к функциям. Каждая функция принимает аргумент value , но аргументов может быть любое количество. В делегате actionDelegate MouseArea вызывает функцию hello как обычную функцию, что приводит к вызову соответствующего свойства hello в модели.
import QtQuick Rectangle { width: 120 height: 300 gradient: Gradient { GradientStop { position: 0.0; color: "#f6f6f6" } GradientStop { position: 1.0; color: "#d7d7d7" } } ListModel { id: actionModel ListElement { name: "Copenhagen" hello: function(value) { console.log(value + ": You } ListElement { name: "Helsinki" hello: function(value) { console.log(value + ": Hel } ListElement { name: "Oslo" hello: function(value) { console.log(value + ": Hei } ListElement { name: "Stockholm" hello: function(value) { console.log(value + ": Sto } } ListView { anchors.fill: parent anchors.margins: 20 focus: true model: actionModel
delegate: Rectangle { id: delegate required property int index required property string name required property var hello width: ListView.view.width height: 40 color: "#157efb" Text { anchors.centerIn: parent font.pixelSize: 10 text: delegate.name } MouseArea { anchors.fill: parent onClicked: delegate.hello(delegate.index) } } spacing: 5 clip: true } } Настройка производительности Воспринимаемая производительность представления модели очень сильно зависит от времени, необходимого для подготовки новых делегатов. Например, при прокрутке ListView вниз делегаты добавляются сразу за пределы представления снизу и удаляются сразу после того, как они выходят из поля зрения над верхней частью представления. Это становится очевидным, если свойству clip присвоено значение false . Если
делегаты требуют слишком много времени на инициализацию, и это станет очевидным для пользователя, как только представление будет прокручено слишком быстро. Чтобы обойти эту проблему, можно настроить поля в пикселях по бокам прокручиваемого представления. Для этого используется свойство cacheBuffer. В описанном выше случае вертикальной прокрутки оно будет контролировать, на сколько пикселей выше и ниже ListView будут располагаться подготовленные делегаты. Комбинируя это с асинхронной загрузкой элементов Image, можно, например, дать время изображениям загрузиться до того, как они будут выведены на экран. При большем количестве делегатов приходится жертвовать памятью ради более плавной работы и чуть большего времени на инициализацию каждого делегата. Это не решает проблему сложных делегатов. Каждый раз при инстанцировании делегата его содержимое оценивается и компилируется. На это требуется время, и если оно слишком велико, то это приведет к ухудшению качества прокрутки. Наличие большого количества элементов в делегате также снижает производительность прокрутки. Просто на перемещение большого количества элементов тратятся циклы. Для устранения двух последних проблем рекомендуется использовать элементы Loader. Они могут использоваться для инстанцирования дополнительных элементов, когда это необходимо. Например, расширяющийся делегат может использовать Loader, чтобы отложить инстанцирование своего детального представления до тех пор, пока оно не понадобится. По этой же причине полезно свести к минимуму количество JavaScript в каждом делегате. Лучше позволить им вызывать сложные фрагменты JavaScript, находящиеся вне каждого делегата. Это уменьшает время, затрачиваемое на компиляцию JavaScript при каждом создании делегата. СОВЕТ Следует помнить, что использование Loader для отсрочки инициализации как раз и приводит к отсрочке проблемы производительности. Это означает, что производительность
прокрутки будет улучшена, но для появления реального содержимого все равно потребуется время.

Резюме В этой главе мы рассмотрели модели, представления и делегаты. Для каждой записи данных в модели представление инстанцирует делегат, визуализирующий эти данные. Таким образом, данные отделяются от представления. Модель может представлять собой одно целое число, в котором делегату передается переменная index. Если в качестве модели используется массив JavaScript, то переменная modelData представляет данные текущего индекса массива, а index хранит индекс. Для более сложных случаев, когда каждому элементу данных необходимо предоставить несколько значений, лучшим решением будет использование ListModel, заполненного элементами ListElement. Для статических моделей в качестве представления может быть использован репитер. Его легко комбинировать с позиционерами типа Row , Column , Grid или Flow для построения частей пользовательского интерфейса. Для динамических или больших моделей данных более подходящими я в л я ю т с я такие представления, как ListView, GridView или TableView. Они создают экземпляры делегатов "на лету", по мере необходимости, уменьшая количество элементов, одновременно находящихся в сцене. Разница между GridView и TableView заключается в том, что табличное представление предполагает модель табличного типа с несколькими столбцами данных, а грид-представление отображает модель списочного типа в виде сетки. Делегаты, используемые в представлениях, могут быть статическими элементами со свойствами, привязанными к данным из модели, или динамическими, состояние которых зависит от того, находятся они в фокусе или нет. Используя сигналы onAdd и onRemove функции В режиме просмотра их можно даже анимировать, когда они появляются
и исчезают.

Элемент холста Одним из достоинств QML является его близость к экосистеме Javascript. Это позволяет нам повторно использовать существующие решения из веб-пространства и сочетать их с "родной" производительностью визуальных средств QML. Однако иногда хочется повторно использовать и графические решения из вебпространства. В этом случае нам поможет Здесь на помощь приходит элемент Canvas. Элемент canvas предоставляет API, очень близкий к API рисования для одноименного элемента HTML. Основная идея элемента canvas заключается в визуализации контуров с помощью контекстного 2D-объекта. Контекстный 2D-объект содержит необходимые графические функции, в то время как холст выступает в качестве канвы для рисования. Контекст 2D поддерживает обводки, градиенты заливки, текст и различный набор команд для создания контуров. Рассмотрим пример рисования простой траектории:
import QtQuick Canvas { id: root // canvas size width: 200; height: 200 // handler to override for drawing onPaint: { // get context to draw with var ctx = getContext("2d") // setup the stroke ctx.lineWidth = 4 ctx.strokeStyle = "blue" // setup the fill ctx.fillStyle = "steelblue" // begin a new path to draw ctx.beginPath() // top-left start point ctx.moveTo(50,50) // upper line ctx.lineTo(150,50) // right line ctx.lineTo(150,150) // bottom line ctx.lineTo(50,150) // left line through path closing ctx.closePath() // fill using fill style ctx.fill() // stroke using line width and stroke style ctx.stroke() } } В результате образуется заполненный прямоугольник с начальной точкой 50,50 и размером 100 и обводкой, используемой в качестве украшения границы.
Ширина обводки установлена на 4 и использует синий цвет, определяемый strokeStyle . Конечная фигура заливается через fillStyle stroke цветом "стальной синий". Только при вызове функций или fill будет нарисован реальный контур, и они могут использоваться независимо друг от друга. При вызове функции stroke или fill будет нарисован текущий контур. Сохранить контур для последующего использования невозможно, можно только сохранить и восстановить состояние рисования. В QML элемент Canvas выступает в качестве контейнера для рисунка. Объект 2D-контекста обеспечивает фактическое выполнение операции рисования. Собственно рисование должно выполняться внутри обработчика события onPaint. Canvas { width: 200; height: 200 onPaint: { var ctx = getContext("2d") // setup your path // fill or/and stroke } } Сам холст представляет собой типичную двумерную декартову систему координат, в которой левый верхний угол является точкой (0,0). Большее значение y идет вниз, а большое значение x - вправо. Типичный порядок команд для этого API, основанного на пути, следующий:
1. Настройка обводки и/или заливки 2. Создать путь 3. Штрих и/или заливка onPaint: { var ctx = getContext("2d") // setup the stroke ctx.strokeStyle = "red" // create a path ctx.beginPath() ctx.moveTo(50,50) ctx.lineTo(150,50) // stroke path ctx.stroke() } Получается горизонтальная штриховая линия от точки P1(50,50) до точки P2(150,50) . СОВЕТ Как правило, при сбросе пути всегда требуется задать начальную точку, поэтому первой операцией после beginPath часто является moveTo .
Удобный API Для операций с прямоугольниками предусмотрен удобный API, который рисует напрямую и не требует вызова обводки или заливки. import QtQuick Canvas { id: root width: 120; height: 120 onPaint: { var ctx = getContext("2d") ctx.fillStyle = 'green' ctx.strokeStyle = "blue" ctx.lineWidth = 4 // draw a filles rectangle ctx.fillRect(20, 20, 80, 80) // cut our an inner rectangle ctx.clearRect(30,30, 60, 60) // stroke a border from top-left to // inner center of the larger rectangle ctx.strokeRect(20,20, 40, 40) } }
СОВЕТ Область обводки занимает половину ширины линии по обе стороны от контура. При ширине линии 4 px будет нарисовано 2 px вне контура и 2 px внутри.
Градиенты Canvas может заливать фигуры не только цветом, но и градиентами или изображениями. onPaint: { var ctx = getContext("2d") var gradient = ctx.createLinearGradient(100,0,100,200) gradient.addColorStop(0, "blue") gradient.addColorStop(0.5, "lightsteelblue") ctx.fillStyle = gradient ctx.fillRect(50,50,100,100) } В данном примере градиент задается вдоль начальной точки (100,0) до конечной (100,200), что дает вертикальную линию в центре холста. Стопы градиента могут быть заданы в виде цвета от 0,0 (начальная точка градиента) до 1,0 (конечная точка градиента). Здесь мы используем синий цвет в точке 0.0 (100,0) и светлоголубой в точке 0.5 (100,200). Градиент определен как значительно больший, чем прямоугольник, который мы хотим нарисовать, поэтому прямоугольник облегает градиент в соответствии с его геометрией.
СОВЕТ Градиент задается в координатах холста, а не в координатах относительно закрашиваемого контура. В холсте нет понятия относительных координат, к которому мы уже привыкли из QML.
Тени С помощью объекта 2D-контекста можно визуально улучшить контур с помощью теней. Тень - это область вокруг контура со смещением, цветом и заданным размытием. Для этого можно указать shadowColor , shadowOffsetX , shadowOffsetY и shadowBlur . Все это должно быть определено с помощью 2D-контекста. 2D-контекст является единственным API для операций рисования. Тень также может быть использована для создания эффекта свечения вокруг контура. В следующем примере мы создаем текст "Canvas" с белым свечением вокруг. Все это на темном фоне для лучшей видимости. Сначала мы рисуем темный фон: // setup a dark background ctx.strokeStyle = "#333" ctx.fillRect(0,0,canvas.width,canvas.height); затем мы определяем нашу теневую конфигурацию, которая будет использоваться для следующего пути: // setup a blue shadow ctx.shadowColor = "#2ed5fa"; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; ctx.shadowBlur = 10; Наконец, мы нарисуем текст "Canvas", используя большой полужирный шрифт размером 80px из файла Семейство шрифтов Ubuntu.
// render green text ctx.font = 'bold 80px sans-serif'; ctx.fillStyle = "#24d12e"; ctx.fillText("Canvas!",30,180);
Изображения Канва QML поддерживает рисование изображений из нескольких источников. Чтобы использовать изображение внутри холста, его необходимо сначала загрузить. В приведенном ниже примере для загрузки изображения используется обработчик Component.onCompleted. onPaint: { var ctx = getContext("2d") // draw an image ctx.drawImage('assets/ball.png', 10, 10) // store current context setup ctx.save() ctx.strokeStyle = '#ff2a68' // create a triangle as clip region ctx.beginPath() ctx.moveTo(110,10) ctx.lineTo(155,10) ctx.lineTo(135,55) ctx.closePath() // translate coordinate system ctx.clip() // create clip from the path // draw image with clip applied ctx.drawImage('assets/ball.png', 100, 10) // draw stroke around path ctx.stroke() // restore previous context ctx.restore() } Component.onCompleted: {
loadImage("assets/ball.png") } Слева показано изображение шара, нарисованное в левом верхнем углу в позиции 10x10. На правом изображении шар показан с применением обтравочного контура. Изображения и любые другие контуры могут быть обрезаны с помощью другого контура. Обрезка выполняется путем определения пути и вызова функции clip(). Теперь все последующие операции рисования будут обрезаться по этому пути. Отключить обрезку можно, восстановив предыдущее состояние или установив область обрезки на весь холст.
Трансформация Холст позволяет трансформировать систему координат несколькими способами. Это очень похоже на преобразования, предлагаемые элементами QML. Вы можете масштабировать, поворачивать, переводить систему координат. В QML началом преобразования всегда является начало холста. Например, чтобы масштабировать контур вокруг его центра, необходимо перевести начало холста в центр контура. Также можно применить более сложное преобразование с помощью метода transform. import QtQuick Canvas { id: root width: 240; height: 120 onPaint: { var ctx = getContext("2d") var ctx = getContext("2d"); ctx.lineWidth = 4; ctx.strokeStyle = "blue"; // translate x/y coordinate system ctx.translate(root.width/2, root.height/2); // draw path ctx.beginPath(); ctx.rect(-20, -20, 40, 40); ctx.stroke(); // rotate coordinate system ctx.rotate(Math.PI/4); ctx.strokeStyle = "green";
// draw path ctx.beginPath(); ctx.rect(-20, -20, 40, 40); ctx.stroke(); } } Кроме перевода холст позволяет также масштабировать с помощью scale(x,y) по осям x и y, поворачивать с помощью rotate(angle) , где угол задается радиусом (360 градусов = 2*Math.PI), и использовать матричное преобразование с помощью setTransform(m11, m12, m21, m22, dx, dy) . СОВЕТ Для сброса любого преобразования можно вызвать функцию resetTransform() функция для установки матрицы преобразования обратно в матрицу тождества: js ctx.resetTransform()
Режимы композиции Композиция позволяет нарисовать фигуру и смешать ее с существующими пикселями. Холст поддерживает несколько режимов композиции с помощью операций globalCompositeOperation(mode). Например: source-over source-in source-out source-atop Начнем с небольшого примера, демонстрирующего эксклюзив или композицию: onPaint: { var ctx = getContext("2d") ctx.globalCompositeOperation = "xor" ctx.fillStyle = "#33a9ff" for(var i=0; i<40; i++) { ctx.beginPath() ctx.arc(Math.random()*400, Math.random()*200, 20, 0, 2* ctx.closePath() ctx.fill() } } Приведенный ниже пример демонстрирует все режимы композиции, итерируя их и объединяя прямоугольник и круг. Полученный результат можно найти под исходным кодом.
property var operation : [ 'source-over', 'source-in', 'source-over', 'source-atop', 'destination-over', 'destination-in', 'destination-out', 'destination-atop', 'lighter', 'copy', 'xor', 'qt-clear', 'qt-destination', 'qt-multiply', 'qt-screen', 'qt-overlay', 'qt-darken', 'qt-lighten', 'qt-color-dodge', 'qt-color-burn', 'qt-hard-light', 'qt-soft-light', 'qt-difference', 'qt-exclusion' ] onPaint: { var ctx = getContext('2d') for(var i=0; i<operation.length; i++) { var dx = Math.floor(i%6)*100 var dy = Math.floor(i/6)*100 ctx.save() ctx.fillStyle = '#33a9ff' ctx.fillRect(10+dx,10+dy,60,60) ctx.globalCompositeOperation = root.operation[i] ctx.fillStyle = '#ff33a9' ctx.globalAlpha = 0.75 ctx.beginPath() ctx.arc(60+dx, 60+dy, 30, 0, 2*Math.PI) ctx.closePath() ctx.fill() ctx.restore() } }

Пиксельные буферы При работе с холстом вы можете получать из него пиксельные данные для чтения или манипулирования пикселями холста. Для чтения данных изображения используйте createImageData(sw,sh) или getImageData(sx,sy,sw,sh) . Обе функции возвращают объект ImageData с параметрами width , height и a переменная данных. Переменная data содержит одномерный массив пиксельных данных, полученных в формате RGBA, где каждое значение изменяется в диапазоне от 0 до 255. Для установки пикселей на холсте можно использовать функцию putImageData(imagedata, dx, dy). Другим способом получения содержимого холста является сохранение данных в виде изображения. Этого можно добиться с помощью функций Canvas save(path) или toDataURL(mimeType), причем последняя функция возвращает URL-адрес изображения, который может быть использован для загрузки элементом Image. import QtQuick Rectangle { width: 240; height: 120 Canvas { id: canvas x: 10; y: 10 width: 100; height: 100 property real hue: 0.0 onPaint: { var ctx = getContext("2d") var x = 10 + Math.random(80)*80 var y = 10 + Math.random(80)*80 hue += Math.random()*0.1 if(hue > 1.0) { hue -= 1 }
ctx.globalAlpha = 0.7 ctx.fillStyle = Qt.hsla(hue, 0.5, 0.5, 1.0) ctx.beginPath() ctx.moveTo(x+5,y) ctx.arc(x,y, x/10, 0, 360) ctx.closePath() ctx.fill() } MouseArea { anchors.fill: parent onClicked: { var url = canvas.toDataURL('image/png') print('image url=', url) image.source = url } } } Image { id: image x: 130; y: 10 width: 100; height: 100 } Timer { interval: 1000 running: true triggeredOnStart: true repeat: true onTriggered: canvas.requestPaint() } } В нашем примере мы каждую секунду рисуем небольшой круг на левом холсте. Когда пользователь щелкает мышью на этой области, содержимое холста сохраняется и из него извлекается URL-адрес изображения. Затем это изображение отображается в правой части нашего примера.

Краска для холста В этом примере мы создадим небольшое приложение для рисования с использованием Canvas элемент. Для этого мы размещаем четыре цветовых квадрата в верхней части сцены с помощью позиционера строк. Цветовой квадрат - это простой прямоугольник, заполненный областью мыши для обнаружения щелчков. Row { id: colorTools property color paintColor: "#33B5E5" anchors { horizontalCenter: parent.horizontalCenter top: parent.top topMargin: 8
} spacing: 4 Repeater { model: ["#33B5E5", "#99CC00", "#FFBB33", "#FF4444"] ColorSquare { required property var modelData color: modelData active: colorTools.paintColor == color onClicked: { colorTools.paintColor = color } } } } } В массиве хранятся цвета и цвет краски. Когда пользователь щелкает в одном из квадратов, цвет квадрата присваивается свойству paintColor строки с именем colorTools . Чтобы обеспечить отслеживание событий мыши на холсте, мы разместили MouseArea, охватывающую элемент холста, и подключили обработчики нажатия и изменения положения. Canvas { id: canvas property real lastX: 0 property real lastY: 0 property color color: colorTools.paintColor anchors { left: parent.left right: parent.right top: colorTools.bottom bottom: parent.bottom margins: 8 }
onPaint: { var ctx = getContext('2d') ctx.lineWidth = 1.5 ctx.strokeStyle = canvas.color ctx.beginPath() ctx.moveTo(lastX, lastY) lastX = area.mouseX lastY = area.mouseY ctx.lineTo(lastX, lastY) ctx.stroke() } MouseArea { id: area anchors.fill: parent onPressed: { canvas.lastX = mouseX canvas.lastY = mouseY } onPositionChanged: { canvas.requestPaint() } } } При нажатии мыши начальная позиция мыши сохраняется в свойствах lastX и lastY. Каждое изменение положения мыши вызывает запрос на закрашивание холста, что приводит к вызову обработчика onPaint. Чтобы окончательно нарисовать пользовательский штрих, в обработчике onPaint мы начинаем новый путь и перемещаемся в последнюю позицию. Затем мы получаем новую позицию из области мыши и рисуем линию выбранным цветом до новой позиции. Позиция мыши сохраняется как новая последняя позиция.
Porting from HTML5 Canvas Переход от HTML5-канавы к QML-канаве достаточно прост. В этой главе мы рассмотрим приведенный ниже пример и выполним преобразование. https://developer.mozilla.org/enUS/docs/Web/API/Canvas_API/Tutorial/Transformations http://en.wikipedia.org/wiki/Spirograph Спирограф В качестве основы мы используем пример спирографа (http://en.wikipedia.org/wiki/Spirograph) из проекта Mozilla. Оригинальный HTML5 был размещен в рамках учебника по работе с холстом (https://developer.mozilla.org/en-). US/docs/Web/API/Canvas_API/Tutorial/Transformations) . Нам нужно было изменить несколько строк: Qt Quick требует объявления переменной, поэтому нам нужно было добавить несколько декларации var for (var i=0;i<3;i++) { js ... } Мы адаптировали метод draw для получения объекта Context2D function draw(ctx) { ... js
Нам пришлось адаптировать перевод для каждого спирографа из-за разных размеров ctx.translate(20+j*50,20+i*50); Наконец, мы завершили работу над обработчиком onPaint. Внутри него мы получаем контекст и вызываем нашу функцию рисования. onPaint: { var ctx = getContext("2d"); draw(ctx); } В результате получилась портированная спирографика, работающая с использованием QML canvas. Как видите, при отсутствии изменений в логике и относительно небольшом количестве изменений в самом коде перенос с HTML5 на QML вполне возможен.
Светящиеся линии Вот еще один более сложный порт от организации W3C. Сайт Оригинальные светящиеся линии (http://www.w3.org/TR/2dcontext/#examples) имеют несколько довольно приятных аспектов, что делает портирование более сложным. <!DOCTYPE HTML> <html lang="en"> <head> <title>Pretty Glowing Lines</title> </head> <body> <canvas width="800" height="450"></canvas> <script> var context = document.getElementsByTagName('canvas')[0].getCon // initial start position var lastX = context.canvas.width * Math.random(); var lastY = context.canvas.height * Math.random(); var hue = 0;
// closure function to draw // a random bezier curve with random color with a glow effect function line() { context.save(); // scale with factor 0.9 around the center of canvas context.translate(context.canvas.width/2, context.canvas.he context.scale(0.9, 0.9); context.translate(-context.canvas.width/2, -context.canvas. context.beginPath(); context.lineWidth = 5 + Math.random() * 10; // our start position context.moveTo(lastX, lastY); // our new end position lastX = context.canvas.width * Math.random(); lastY = context.canvas.height * Math.random(); // random bezier curve, which ends on lastX, lastY context.bezierCurveTo(context.canvas.width * Math.random(), context.canvas.height * Math.random(), context.canvas.width * Math.random(), context.canvas.height * Math.random(), lastX, lastY); // glow effect hue = hue + 10 * Math.random(); context.strokeStyle = 'hsl(' + hue + ', 50%, 50%)'; context.shadowColor = 'white'; context.shadowBlur = 10; // stroke the curve context.stroke(); context.restore(); } // call line function every 50msecs
setInterval(line, 50); function blank() { // makes the background 10% darker on each call context.fillStyle = 'rgba(0,0,0,0.1)'; context.fillRect(0, 0, context.canvas.width, context.canvas } // call blank function every 50msecs setInterval(blank, 40); </script> </body> </html> В HTML5 объект Context2D может рисовать на холсте в любое время. В QML он может указывать только внутри обработчика onPaint. Таймер, используемый с помощью setInterval, вызывает в HTML5 обводку линии или очистку экрана. Из-за различий в обработке в QML невозможно просто вызвать эти функции, так как необходимо пройти через обработчик onPaint. Также необходимо адаптировать цветовые представления. Давайте рассмотрим все изменения по порядку. Все начинается с элемента canvas. Для простоты мы просто используем элемент Элемент Canvas в качестве корневого элемента нашего QML-файла. import QtQuick Canvas { id: canvas width: 800; height: 450 ... }
Чтобы развязаться с прямым вызовом функций через setInterval, мы заменим вызовы setInterval двумя таймерами, которые будут запрашивать перерисовку. Таймер срабатывает через небольшой промежуток времени и позволяет нам выполнить некоторый код. Поскольку мы не можем указать функции paint, какую операцию мы хотим запустить, мы определяем для каждой операции флаг bool, запрашивающий операцию и вызывающий затем запрос перекраски. Здесь приведен код для операции со строкой. Аналогично выполняется операция blank. ... property bool requestLine: false Timer { id: lineTimer interval: 40 repeat: true triggeredOnStart: true onTriggered: { canvas.requestLine = true canvas.requestPaint() } } Component.onCompleted: { lineTimer.start() } ... Теперь мы имеем представление о том, какую операцию (строку или пробел или даже обе) нам нужно выполнить в операции onPaint. При входе в обработчик onPaint для каждого запроса на закрашивание нам необходимо извлечь инициализацию переменной в элемент canvas. Canvas { ... property real hue: 0
property real lastX: width * Math.random(); property real lastY: height * Math.random(); ... } Теперь наша функция paint должна выглядеть следующим образом: onPaint: { var context = getContext('2d') if(requestLine) { line(context) requestLine = false } if(requestBlank) { blank(context) requestBlank = false } } Для холста в качестве аргумента была извлечена линейная функция. function line(context) { context.save(); context.translate(canvas.width/2, canvas.height/2); context.scale(0.9, 0.9); context.translate(-canvas.width/2, -canvas.height/2); context.beginPath(); context.lineWidth = 5 + Math.random() * 10; context.moveTo(lastX, lastY); lastX = canvas.width * Math.random(); lastY = canvas.height * Math.random(); context.bezierCurveTo(canvas.width * Math.random(), canvas.height * Math.random(), canvas.width * Math.random(), canvas.height * Math.random(), lastX, lastY);
hue += Math.random()*0.1 if(hue > 1.0) { hue -= 1 } context.strokeStyle = Qt.hsla(hue, 0.5, 0.5, 1.0); // context.shadowColor = 'white'; // context.shadowBlur = 10; context.stroke(); context.restore(); } Самым большим изменением стало использование функций QML Qt.rgba() и Qt.hsla(), что потребовало приведения значений к используемому в QML диапазону 0,0 ... 1,0. То же самое относится и к функции blank. function blank(context) { context.fillStyle = Qt.rgba(0,0,0,0.1) context.fillRect(0, 0, canvas.width, canvas.height); } Конечный результат будет выглядеть примерно так.

Формы До сих пор мы использовали элемент Rectangle и элементы управления, но для создания фигур свободной формы нам приходится полагаться на изображения. С помощью модуля Qt Quick Shapes можно создавать фигуры действительно свободной формы. Это позволяет гибко создавать визуализации непосредственно из QML. В этой главе мы рассмотрим, как использовать фигуры, различные доступные элементы контура, как фигуры могут быть заполнены различными способами, а также как сочетать фигуры с возможностями QML для плавной анимации фигур.
Базовая форма Модуль shape позволяет создавать произвольные контуры с последующим обводкой контура и заливкой внутренней части. Определение пути может быть повторно использовано в других местах, где используются пути, например, для элемента PathView, используемого с моделями. Но для рисования контура используется элемент Shape, а различные элементы контура помещаются в ShapePath . В приведенном ниже примере создается контур, показанный на скриншоте. Вся фигура, все пять залитых областей, создаются из одного контура, который затем обводится штрихом и заливается.
import QtQuick import QtQuick.Shapes Rectangle { id: root width: 600 height: 600 Shape { anchors.centerIn: parent ShapePath { strokeWidth: 3
strokeColor: "darkGray" fillColor: "lightGray" startX: -40; startY: 200 // The circle PathArc { x: 40; y: 200; radiusX: 200; radiusY: 200 PathLine { x: 40; y: 120 } PathArc { x: -40; y: 120; radiusX: 120; radiusY: 12 PathLine { x: -40; y: 200 } // The dots PathMove { x: -20; y: 80 } PathArc { x: 20; y: 80; radiusX: 20; radiusY: 20; u PathArc { x: -20; y: 80; radiusX: 20; radiusY: 20; PathMove { x: -20; y: 130 } PathArc { x: 20; y: 130; radiusX: 20; radiusY: 20; PathArc { x: -20; y: 130; radiusX: 20; radiusY: 20; PathMove { x: -20; y: 180 } PathArc { x: 20; y: 180; radiusX: 20; radiusY: 20; PathArc { x: -20; y: 180; radiusX: 20; radiusY: 20; PathMove { x: -20; y: 230 } PathArc { x: 20; y: 230; radiusX: 20; radiusY: 20; PathArc { x: -20; y: 230; radiusX: 20; radiusY: 20; } } } Путь состоит из дочерних элементов ShapePath, т.е. элементов PathArc, PathLine и PathMove в приведенном примере. В следующем разделе мы подробно рассмотрим составные элементы контуров.

Пути строительства Как мы видели в предыдущем разделе, фигуры строятся из путей, которые строятся из элементов пути. Наиболее распространенным способом построения контура является его замыкание, т.е. чтобы он начинался и заканчивался в одной и той же точке. Однако можно создавать и открытые контуры, например, только для обводки. При заполнении открытого контура о н закрывается прямой линией, по сути, добавляется линия PathLine, которая используется при заполнении контура, но не при его обводке. Как показано на рисунке ниже, существует несколько основных фигур, которые можно использовать для построения траектории. К ним относятся: линии, дуги и различные кривые. Также возможно перемещение без рисования с помощью элемента PathMove. Помимо этих элементов, элемент ShapePath также позволяет указать начальную точку с помощью свойств startX и startY.
Линии рисуются с помощью элемента PathLine, как показано ниже. Для создания нескольких независимых линий можно использовать элемент PathMultiline. Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" startX: 20; startY: 70 PathLine { x: 180 y: 130 } } } При создании полилинии, т.е. линии, состоящей из нескольких отрезков, можно использовать элемент PathPolyline. Это позволяет сэкономить на вводе текста, так как конечная точка последней линии принимается за начальную точку следующей линии. Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" PathPolyline { path: [ Qt.point(220, 100), Qt.point(260, 20), Qt.point(300, 170), Qt.point(340, 60), Qt.point(380, 100) ] } } }
Для создания дуг, т.е. сегментов окружностей или эллипсов, используются элементы PathArc и PathAngleArc. Они предоставляют инструменты для создания дуг, причем PathArc используется, когда известны координаты начальной и конечной точек, а PathAngleArc когда нужно контролировать, на сколько градусов развернется дуга. Оба элемента дают одинаковый результат, поэтому выбор элемента зависит от того, какие аспекты дуги наиболее важны в вашем приложении. Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" startX: 420; startY: 100 PathArc { x: 580; y: 180 radiusX: 120; radiusY: 120 } } } После линий и дуг следуют различные кривые. Здесь Qt Quick Shapes предлагает три варианта. Сначала мы рассмотрим PathQuad, который позволяет создать квадратичную кривую Безье на основе начальной и конечной точек (начальная точка является неявной) и одной контрольной точки. Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" startX: 20; startY: 300
PathQuad { x: 180; y: 300 controlX: 60; controlY: 250 } } } Элемент PathCubic создает кубическую кривую Безье из начальной и конечной точек (начальная точка является неявной) и двух контрольных точек. Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" startX: 220; startY: 300 PathCubic { x: 380; y: 300 control1X: 260; control1Y: 250 control2X: 360; control2Y: 350 } } } Наконец, PathCurve создает кривую, проходящую через список заданных контрольных точек. Кривая создается путем предоставления нескольких элементов PathCurve, каждый из которых содержит одну контрольную точку. Для создания кривой, проходящей через контрольные точки, используется сплайн Catmull-Rom. Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" startX: 420; startY: 300
PathCurve { x: 460; y: 220 } PathCurve { x: 500; y: 370 } PathCurve { x: 540; y: 270 } PathCurve { x: 580; y: 300 } } } Существует еще один полезный элемент пути - PathSvg . Этот элемент позволяет обводить и заливать контур SVG. СОВЕТ Элемент PathSvg не всегда можно комбинировать с другими элементами пути. Это зависит от используемого бэкенда рисования, поэтому убедитесь, что для одного пути используется элемент PathSvg или другие элементы. Если вы смешиваете PathSvg с другими элементами контура, то это зависит от ваших возможностей.
Заполнение форм Фигура может быть заполнена различными способами. В этом разделе мы рассмотрим общее правило заполнения, а также различные способы заполнения контура. Qt Quick Shapes предоставляет два правила заливки, управляемые с помощью свойства fillRule элемента ShapePath. Различные результаты показаны на скриншоте ниже. Свойство может быть установлено в значение ShapePath.OddEvenFill, которое и с п о л ь з у е т с я п о умолчанию. При этом каждая часть контура заполняется отдельно, что означает возможность создания фигуры с отверстиями. Альтернативным правилом является ShapePath.WindingFill , которое заполняет все между крайними точками на каждой горизонтальной линии, пересекающей фигуру. Независимо от правила заливки контур фигуры рисуется пером, поэтому даже при использовании правила извилистой заливки контур рисуется внутри фигуры. Приведенные ниже примеры демонстрируют использование двух правил заполнения, как показано на скриншоте выше. Shape { ShapePath {
strokeWidth: 3 strokeColor: "darkgray" fillColor: "orange" fillRule: ShapePath.OddEvenFill PathPolyline { path: [ Qt.point(100, 20), Qt.point(150, 180), Qt.point( 20, 75), Qt.point(180, 75), Qt.point( 50, 180), Qt.point(100, 20), ] } } } Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" fillColor: "orange" fillRule: ShapePath.WindingFill PathPolyline { path: [ Qt.point(300, 20), Qt.point(350, 180), Qt.point(220, 75), Qt.point(380, 75), Qt.point(250, 180), Qt.point(300, ] } } } 20),
Послез тогоз какз правилоз заливкиз выбрано существуетз несколькоз Дляз заливкиз фигурыз сплошнымз цветомз и с п о л ь з у е т с я свойствоз з ез Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" fillColor: "lightgreen"
startX: 20; startY: 140 PathLine { x: 180 y: 140 } PathArc { x: 20 y: 140 radiusX: 80 radiusY: 80 direction: PathArc.Counterclockwise useLargeArc: true } } } Если вы не хотите использовать сплошной цвет, можно применить градиент. Градиент применяется с помощью свойства fillGradient элемента ShapePath. Первый градиент, который мы рассмотрим, - LinearGradient . Он создает линейный градиент между начальной и конечной точками. Конечные точки могут быть расположены как угодно, например, для создания градиента под углом. Между конечными точками можно вставить ряд элементов GradientStop. Они располагаются в диапазоне от 0.0, y2. что является позицией x1, y1, до 1.0, что является позицией x2, Для каждой такой остановки задается цвет. Затем градиент создает мягкие переходы между цветами. СОВЕТ Если форма выходит за пределы конечных точек, то первый или последний цвет либо продолжается, либо градиент повторяется или зеркально отражается. Это поведение задается с помощью свойства spread элемента LinearGradient.
Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" fillGradient: LinearGradient { x1: 50; y1: 300 x2: 150; y2: 280 GradientStop { position: 0.0; color: "lightgreen" } GradientStop { position: 0.7; color: "yellow" } GradientStop { position: 1.0; color: "darkgreen" } } startX: 20; startY: 340 PathLine { x: 180 y: 340 } PathArc { x: 20 y: 340 radiusX: 80 radiusY: 80 direction: PathArc.Counterclockwise useLargeArc: true } } } Для создания градиента, распространяющегося вокруг начала координат, напоминающего часы, используется градиент ConicalGradient. Здесь центральная точка задается с помощью свойств centerX и centerY, а начальный угол - с помощью свойства angle. Затем остановки градиента распространяются от заданного угла по часовой стрелке на 360 градусов.
Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" fillGradient: ConicalGradient { centerX: 300; centerY: 100 angle: 45 GradientStop { position: 0.0; color: "lightgreen" } GradientStop { position: 0.7; color: "yellow" } GradientStop { position: 1.0; color: "darkgreen" } } startX: 220; startY: 140 PathLine { x: 380 y: 140 } PathArc { x: 220 y: 140 radiusX: 80 radiusY: 80 direction: PathArc.Counterclockwise useLargeArc: true } } } Для создания градиента, образующего круги, напоминающие кольца на воде, используется RadialGradient. Для него задаются две окружности - фокальная и центральная. Стопы градиента идут от фокальной окружности к центральной, а за пределами этих окружностей последний цвет продолжается, зеркально отражается или повторяется, в зависимости от свойства spread.
Shape { ShapePath { strokeWidth: 3 strokeColor: "darkgray" fillGradient: RadialGradient { centerX: 300; centerY: 250; centerRadius: 120 focalX: 300; focalY: 220; focalRadius: 10 GradientStop { position: 0.0; color: "lightgreen" } GradientStop { position: 0.7; color: "yellow" } GradientStop { position: 1.0; color: "darkgreen" } } startX: 220; startY: 340 PathLine { x: 380 y: 340 } PathArc { x: 220 y: 340 radiusX: 80 radiusY: 80 direction: PathArc.Counterclockwise useLargeArc: true } } } СОВЕТ Опытный пользователь может использовать фрагментный шейдер для заливки фигуры. При этом предоставляется полная свобода выбора способа заливки фигуры. Более подробную информацию о шейдерах см. в главе "Эффекты".

Анимация фигур Одним из приятных аспектов использования Qt Quick Shapes является то, что рисуемые контуры определяются непосредственно в QML. Это означает, что их свойства можно связывать, переходить и анимировать, как и любые другие свойства в QML. В приведенном ниже примере мы используем базовую форму из самого первого раздела этой главы, но вводим переменную t, которая изменяется от 0,0
в 1 .0 в цикле. Затем мы используем эту переменную для смещения позиции малых кругов, а также размер верхнего и нижнего круга. Это создает анимацию, при которой кажется, что круги появляются вверху и исчезают к низу. import QtQuick import QtQuick.Shapes Rectangle { id: root width: 600 height: 600 Shape { anchors.centerIn: parent ShapePath { id: shapepath property real t: 0.0 NumberAnimation on t { from: 0.0; to: 1.0; duration strokeWidth: 3 strokeColor: "darkGray" fillColor: "lightGray" startX: -40; startY: 200 // The circle PathArc { x: 40; y: 200; radiusX: 200; radiusY: 200 PathLine { x: 40; y: 120 } PathArc { x: -40; y: 120; radiusX: 120; radiusY: 12 PathLine { x: -40; y: 200 } // The dots
PathMove { x: -20+(1.0-shapepath.t)*20; y: 80 + sha PathArc { x: 20-(1.0-shapepath.t)*20; y: 80 + shape PathArc { x: -20+(1.0-shapepath.t)*20; y: 80 + shap PathMove { x: -20; y: 130 + shapepath.t*50 } PathArc { x: 20; y: 130 + shapepath.t*50; radiusX: PathArc { x: -20; y: 130 + shapepath.t*50; radiusX: PathMove { x: -20; y: 180 + shapepath.t*50 } PathArc { x: 20; y: 180 + shapepath.t*50; radiusX: PathArc { x: -20; y: 180 + shapepath.t*50; radiusX: PathMove { x: -20+shapepath.t*20; y: 230 + shapepat PathArc { x: 20-shapepath.t*20; y: 230 + shapepath. PathArc { x: -20+shapepath.t*20; y: 230 + shapepath } } } Обратите внимание, что вместо NumberAnimation использовать любую другую привязку, для t например, к внешнему состоянию и т.д. Воображение не ограничивается. можно ползунку,
Резюме В этой главе мы рассмотрим возможности модуля Qt Quick Shapes. С его помощью мы можем создавать произвольные фигуры непосредственно в QML, а также использовать систему привязки свойств QML для создания динамических фигур. Мы также рассмотрели различные сегменты пути, которые можно использовать для построения фигур из таких элементов, как линии, дуги и различные кривые. Наконец, мы изучили возможности заливки, где градиенты могут быть использованы для создания интересных визуальных эффектов на основе контура.
Эффекты в QML В этой главе мы рассмотрим инструменты для создания различных эффектов в QML. Основное внимание будет уделено: Particle Effects Shader Effects Эффекты частиц Эффекты частиц позволяют создавать группы частиц, т.е. экземпляры данного элемента. Они генерируются стохастическим образом и позволяют работать не с отдельными элементами, а с их группами. Это позволяет создавать такие эффекты, как падающие листья, взрывы, огонь, облака и звездные поля. Эффекты шейдеров Шейдерные эффекты применяются в конвейере рендеринга графики и позволяют изменять как размер, так и цвет любого видимого элемента QML. Это может быть использовано для создания переходов, таких как эффект джинна, волны и завесы, или фильтров, таких как размытие, градации серого и смешивание. Шейдеры пишутся на языке шейдеров, который затем запекается и импортируется в сцену QML, как и другие ресурсы. Эти шейдеры могут быть применены к изображениям или другим элементам для создания сложных визуальных эффектов.
СОВЕТ Работа с шейдерными эффектами - это более сложная тема.
Концепция частиц В основе моделирования частиц лежит система частиц ParticleSystem, которая управляет общей временной шкалой. Сцена может иметь несколько систем частиц, каждая из которых имеет независимую временную линию. Частица испускается с помощью элемента Emitter и визуализируется с помощью ParticlePainter, который может быть изображением, элементом QML или шейдером. Эмиттер также задает направление частицы с помощью векторного пространства. Испущенные частицы уже не могут управляться эмиттером. Модуль частиц предоставляет Affector , который позволяет манипулировать параметрами частицы после ее испускания. Частицы в системе могут совместно использовать временные переходы с помощью элемента ParticleGroup. По умолчанию каждая частица находится в пустой ('') группе. ParticleSystem - управляет общей временной линией между эмиттерами Emiter - излучает логические частицы в систему ParticlePainter - частицы визуализируются художником чстиц Direction - векторное пространство для излучаемых частиц ParticleGroup - каждая частица является членом группы
Affector - манипулирует частицами после их испускания
Простое моделирование Для начала рассмотрим очень простую симуляцию. Qt Quick позволяет очень просто начать работу с рендерингом частиц. Для этого нам понадобятся: ParticleSystem, Emitter, которая связывает все элементы в симуляцию который излучает частицы в систему Производный элемент ParticlePainter, который визуализирует частицы import QtQuick import QtQuick.Particles Rectangle { id: root width: 480; height: 160 color: "#1f1f1f" ParticleSystem { id: particleSystem } Emitter { id: emitter anchors.centerIn: parent width: 160; height: 80 system: particleSystem emitRate: 10 lifeSpan: 1000 lifeSpanVariation: 500 size: 16 endSize: 32 Tracer { color: 'green' }
} ImageParticle { source: "assets/particle.png" system: particleSystem } } Результат примера будет выглядеть следующим образом: В качестве корневого элемента и фона мы начинаем с темного прямоугольника размером 80x80 пикселей. В нем мы объявляем систему частиц ParticleSystem . Это всегда первый шаг, поскольку система связывает все остальные элементы воедино. Как правило, следующим элементом является эмиттер, который определяет область излучения на основе ее ограничительного поля и основных параметров излучаемых частиц. Эмиттер привязывается к системе с помощью свойства system. В данном примере излучатель испускает 10 частиц в секунду ( emitRate: 10 ) по всей площади излучателя с периодом жизни каждой из них 1000 мс ( lifeSpan: 1000 ) и разбросом времени жизни между испускаемыми частицами 500 мс. ( lifeSpanVariation: 500 ). Размер частицы в начале ее существования составляет 16px ( size: 16 ), а в конце ее жизни - 32px ( endSize: 32 ). Зеленый прямоугольник с рамкой - это элемент трассировки, показывающий геометрию эмиттера. Это наглядно показывает, что, хотя частицы испускаются внутри граничного поля излучателя, рендеринг не ограничивается граничным полем излучателя. Позиция рендеринга зависит от времени жизни и направления частицы. Это станет более понятным, когда мы рассмотрим, как изменять направление частиц.
Эмиттер излучает логические частицы. Логическая частица визуализируется с помощью ParticlePainter, в данном примере мы используем ImageParticle, который в качестве свойства источника принимает URL изображения. Частица изображения имеет также несколько других свойств, которые управляют внешним видом средней частицы. : количество частиц, испускаемых в секунду (по умолчанию 10 в секунду) emitRate lifeSpan : миллисекунды, в течение которых должна существовать частица (по умолчанию 1000 мс) size , endSize : размер частиц в начале и в конце их жизни (по умолчанию 16 px) Изменение этих свойств может кардинально повлиять на результат Emitter { id: emitter anchors.centerIn: parent width: 20; height: 20 system: particleSystem emitRate: 40 lifeSpan: 2000 lifeSpanVariation: 500 size: 64 sizeVariation: 32 Tracer { color: 'green' } } Помимо увеличения скорости излучения до 40 и времени жизни до 2 секунд, размер теперь начинается с 64 пикселей и уменьшается на 32 пикселя в конце времени жизни частицы.
Увеличение endSize еще больше приведет к получению более или менее белого фона. Обратите внимание, что когда частицы излучаются только в области, заданной эмиттером, рендеринг не ограничивается ею.
Параметры частиц Мы уже видели, как можно изменить поведение эмиттера, чтобы изменить нашу симуляцию. Используемый художник частиц позволяет визуализировать изображение частицы для каждой частицы. Возвращаясь к нашему примеру, обновим ImageParticle . Сначала мы изменим изображение частицы на изображение маленькой искрящейся звезды: ImageParticle { ... source: 'assets/star.png' } Частицы должны быть окрашены в золотистый цвет, который варьируется от частицы к частице на +/- 20%: color: '#FFD700' colorVariation: 0.2 Чтобы сделать сцену более живой, мы хотели бы вращать частицы. Каждая частица должна стартовать на 15 градусов по часовой стрелке и изменяться между частицами на +/-5 градусов. Дополнительно частица должна непрерывно вращаться со скоростью 45 градусов в секунду. Скорость также должна меняться от частицы к частице на +/15 градусов в секунду: rotation: 15 rotationVariation: 5
rotationVelocity: 45 rotationVelocityVariation: 15 В последнюю очередь мы изменим эффект входа для частицы. Это эффект, который используется, когда частица оживает. В данном случае мы хотим использовать эффект масштабирования: entryEffect: ImageParticle.Scale Теперь повсюду появляются вращающиеся золотые звезды. Вот код, который мы изменили для изображения-частицы в одном блоке. ImageParticle { source: "assets/star.png" system: particleSystem color: '#FFD700' colorVariation: 0.2 rotation: 0 rotationVariation: 45 rotationVelocity: 15 rotationVelocityVariation: 15 entryEffect: ImageParticle.Scale }

Направленные частицы Мы видели, что частицы могут вращаться. Но частицы могут также иметь траекторию. Траектория задается как скорость или ускорение частиц, определяемое стохастическим направлением, которое также называется векторным пространством. Для определения скорости или ускорения частицы существуют различные векторные пространства: AngleDirection - направление, изменяющееся по углу PointDirection - направление, изменяющееся в компонентах x и y TargetDirection - направление на целевую точку Попробуем переместить частицы из левой части сцены в правую, используя направления скоростей. Сначала попробуем использовать AngleDirection . Для этого нам необходимо указать AngleDirection как элемент свойства velocity нашего эмиттера: velocity: AngleDirection { } Угол, под которым излучаются частицы, задается с помощью свойства angle. Угол задается в виде значения в диапазоне 0...360 градусов, причем 0 указывает вправо. В нашем примере мы хотим, чтобы частицы двигались в направлении
вправо, так что 0 - это уже правильное направление. Частицы должны разлетаться на +/- 5 градусов: velocity: AngleDirection { angle: 0 angleVariation: 15 } Теперь, когда мы задали направление, необходимо указать скорость частицы. Она задается величиной. Величина определяется в пикселях в секунду. Так как у нас есть ок. 640px для перемещения, 100 кажется хорошим числом. Это означает, что в среднем за 6,4 секунды частица пересечет открытое пространство. Чтобы сделать путешествие частиц более интересным, мы изменяем величину с помощью параметра magnitudeVariation и устанавливаем ее на половину величины: velocity: AngleDirection { ... magnitude: 100 magnitudeVariation: 50 }
Здесь приведен полный исходный код, среднее время жизни установлено на 6,4 секунды. Мы установили ширину и высоту эмиттера равными 1px. Это означает, что все частицы испускаются в одном и том же месте и далее движутся по заданной нами траектории. Emitter { id: emitter anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter width: 1; height: 1 system: particleSystem lifeSpan: 6400 lifeSpanVariation: 400 size: 32 velocity: AngleDirection { angle: 0 angleVariation: 15 magnitude: 100 magnitudeVariation: 50 } } Что же тогда делает ускорение? Ускорение добавляет к каждой частице вектор ускорения, который изменяет вектор скорости с течением времени. Например, сделаем траекторию в виде звездной дуги. Для этого изменим направление скорости на -45 градусов и уберем вариации, чтобы лучше представить последовательную дугу: velocity: AngleDirection { angle: -45 magnitude: 100 } Направление ускорения должно составлять 90 градусов (направление вниз), для чего выбираем одну четвертую часть величины скорости:
ускорение: AngleDirection { угол: 90 величина: 25 } В результате получается дуга, идущая от левого центра к правому низу. Значения определяются методом проб и ошибок. Вот полный код нашего эмиттера. Emitter { id: emitter anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter width: 1; height: 1 system: particleSystem emitRate: 10 lifeSpan: 6400 lifeSpanVariation: 400 size: 32 velocity: AngleDirection { angle: -45 angleVariation: 0
magnitude: 100 } acceleration: AngleDirection { angle: 90 magnitude: 25 } } В следующем примере мы хотим, чтобы частицы снова перемещались слева направо, но на этот раз мы используем векторное пространство PointDirection. Векторное пространство PointDirection формируется из компонент x и y. Например, если вы хотите, чтобы частицы двигались по вектору с углом 45 градусов, необходимо задать одинаковые значения для x и y. В нашем случае мы хотим, чтобы частицы двигались слева направо, образуя конус с углом 15 градусов. Для этого в качестве пространства вектора скорости мы задаем PointDirection: velocity: PointDirection { } Чтобы получить скорость перемещения 100 px в секунду, зададим компоненту x равной 100. Для 15 градусов (что составляет 1/6 часть от 90 градусов) мы задаем любое изменение 100/6: velocity: PointDirection { x: 100 y: 0 xVariation: 0 yVariation: 100/6 } В результате частицы должны двигаться в 15-градусном конусе справа налево.
Переходим к нашему последнему претенденту - TargetDirection . Направление цели позволяет указать целевую точку в виде координат x и y относительно эмиттера или элемента. При указании элемента его центр становится целевой точкой. Можно получить 15-градусный конус, задав изменение цели на 1/6 часть от x: velocity: TargetDirection { targetX: 100 targetY: 0 targetVariation: 100/6 magnitude: 100 } СОВЕТ Направление цели удобно использовать, когда у вас есть определенная координата x/y, к которой вы хотите направить поток частиц. Я избавляю вас от изображения, поскольку оно выглядит так же, как и предыдущее, вместо этого у меня есть для вас квест.
На следующем рисунке красный и зеленый кружки задают каждый целевой элемент для целевого направления свойства velocity, соответствующего свойству acceleration. Каждое целевое направление имеет одинаковые параметры. Здесь возникает вопрос: Кто отвечает за скорость, а кто за ускорение?
Воздействующие частицы Частицы испускаются эмиттером. После того как частица была испущена, она уже не может быть изменена эмиттером. Аффекторы позволяют воздействовать на частицы после их испускания. Каждый тип аффектора воздействует на частицы по-своему: Возраст - изменяет место частицы в ее жизненном цикле Аттрактор Трение - притягивает частицы к определенной точке - замедляет движение пропорционально текущей скорости частицы - задает ускорение по углу Гравитация Турбулентность - гидродинамические силы, основанные на шумовом изображении Блуждать - произвольно изменять траекторию движения - изменение состояния группы частиц GroupGoal SpriteGoal - изменение состояния частицы спрайта Возраст Позволяет частице стареть быстрее. Свойство lifeLeft указывает, сколько жизни должно остаться у частицы. Age { anchors.horizontalCenter: parent.horizontalCenter width: 240; height: 120 system: particleSystem advancePosition: true lifeLeft: 1200 once: true
Tracer {} } В примере мы сокращаем жизнь верхних частиц один раз, когда они достигают возраста аффектора в 1200 мсек. Поскольку мы установили опцию advancePosition в true, мы видим, что частица снова появляется на позиции, когда ей осталось жить 1200 мс. Аттрактор Аттрактор притягивает частицы к определенной точке. Точка задается с помощью параметров pointX и pointY , которые относятся к геометрии аттрактора. Сила задает силу притяжения. В нашем примере частицы движутся слева направо. Аттрактор располагается сверху, и половина частиц проходит через аттрактор. Аттрактор воздействует на частицы только тогда, когда они находятся в своей граничной области. Такое разделение позволяет нам одновременно видеть обычный поток и поток, подверженный влиянию. Attractor { anchors.horizontalCenter: parent.horizontalCenter width: 160; height: 120 system: particleSystem
pointX: 0 pointY: 0 strength: 1.0 Tracer {} } Легко заметить, что верхняя половина частиц подвержена влиянию притяжения сверху. Точка притяжения установлена в левом верхнем углу (точка 0/0) аттрактора с силой 1,0. Фрикцион Фрикционный аффектор замедляет частицы в определенный раз, пока не будет достигнут определенный порог. Friction { anchors.horizontalCenter: parent.horizontalCenter width: 240; height: 120 system: particleSystem factor : 0.8 threshold: 25 Tracer {} }
В верхней зоне трения частицы замедляются в 0,8 раза пока частица не достигнет скорости 25 пикселей в секунду. Порог действует как фильтр. Частицы, движущиеся со скоростью выше пороговой, замедляются на заданный коэффициент. Гравитация Гравитационный аффектор придает ускорение В примере мы направляем частицы снизу вверх, используя угловое направление. Правая сторона не затрагивается, а на левую накладывается эффект гравитации. Гравитация направлена под углом 90 градусов (направление снизу) с величиной 50. Gravity { width: 240; height: 240 system: particleSystem magnitude: 50 angle: 90 Tracer {} }
Частицы, находящиеся слева, пытаются подняться вверх, но постоянное приложенное ускорение к низу увлекает их в направлении действия силы тяжести. Турбулентность Аффектор турбулентности накладывает на частицы карту хаоса из векторов сил. Карта хаоса определяется шумовым образом, который можно задать с помощью свойства noiseSource. Сила определяет, насколько сильный вектор будет применяться к движениям частиц. Turbulence { anchors.horizontalCenter: parent.horizontalCenter width: 240; height: 120 system: particleSystem strength: 100 Tracer {} } В верхней области примера частицы находятся под воздействием турбулентности. Их движение более неустойчиво. Величина нестабильного отклонения от первоначальной траектории определяется силой.
Wander Манипуляция управляет траекторией. С помощью свойства affectedParameter можно указать, на какой параметр (скорость, положение или ускорение) влияет движение. Свойство pace задает максимальное количество изменений атрибута в секунду. Свойства yVariance и yVariance задают влияние на x- и y-компоненту траектории частицы. Wander { anchors.horizontalCenter: parent.horizontalCenter width: 240; height: 120 system: particleSystem affectedParameter: Wander.Position pace: 200 yVariance: 240 Tracer {} } В верхнем блуждании частицы аффектора перемещаются по случайным траекториям. В данном случае положение меняется 200 раз в секунду в направлении y.

Группы частиц В начале этой главы мы указали, что частицы находятся в группах, которая по умолчанию является пустой группой (''). Используя аффектор GroupGoal, можно позволить частице менять группы. Для наглядности мы хотели бы создать небольшой фейерверк, в котором ракеты стартуют в космос и взрываются в воздухе, образуя эффектный фейерверк. Пример разделен на 2 части. Первая часть под названием "Время запуска" предназначена для создания сцены и введения групп частиц, а вторая часть под названием "Пусть будет фейерверк" посвящена смене групп. Давайте начнем! Время запуска Для начала создадим типичную темную сцену:
import QtQuick 2.5 import QtQuick.Particles 2.0 Rectangle { id: root width: 480; height: 240 color: "#1F1F1F" property bool tracer: false } Свойство tracer будет использоваться для включения и выключения широкой сцены трассировки. Следующим шагом будет объявление нашей системы частиц: ParticleSystem { id: particleSystem } И две наши частицы-изображения (одна для ракеты, другая для дыма от выхлопных газов): ImageParticle { id: smokePainter system: particleSystem groups: ['smoke'] source: "assets/particle.png" alpha: 0.3 entryEffect: ImageParticle.None } ImageParticle { id: rocketPainter system: particleSystem groups: ['rocket'] source: "assets/rocket.png"
entryEffect: ImageParticle.None } На изображениях видно, что для объявления того, к какой группе принадлежит частица, используется свойство groups. Достаточно просто объявить имя, и Qt Quick создаст неявную группу. Теперь пришло время выпустить в воздух несколько ракет. Для этого мы создадим эмиттер в нижней части нашей сцены и зададим скорость в направлении вверх. Для имитации гравитации зададим ускорение вниз: Emitter { id: rocketEmitter anchors.bottom: parent.bottom width: parent.width; height: 40 system: particleSystem group: 'rocket' emitRate: 2 maximumEmitted: 4 lifeSpan: 4800 lifeSpanVariation: 400 size: 32 velocity: AngleDirection { angle: 270; magnitude: 150; magn acceleration: AngleDirection { angle: 90; magnitude: 50 } Tracer { color: 'red'; visible: root.tracer } } Излучатель находится в группе 'rocket', такой же, как и наш художник частиц ракеты. Благодаря имени группы они связаны друг с другом. Эмиттер излучает частицы в группу 'rocket', а художник частиц ракеты закрашивает их. Для выхлопа мы используем эмиттер следа, который следует за нашей ракетой. Он объявляет собственную группу 'smoke' и следует за частицами из группы 'rocket':
TrailEmitter { id: smokeEmitter system: particleSystem emitHeight: 1 emitWidth: 4 group: 'smoke' follow: 'rocket' emitRatePerParticle: 96 velocity: AngleDirection { angle: 90; magnitude: 100; angle lifeSpan: 200 size: 16 sizeVariation: 4 endSize: 0 } Дым направлен вниз, чтобы имитировать силу, с которой дым выходит из ракеты. Параметры emitHeight и emitWidth задают область вокруг частицы, из которой будут вылетать частицы дыма. Если они не указаны, то берется высота частицы, за которой она следует, но в данном примере мы хотим усилить эффект того, что частицы исходят из центральная точка вблизи конца ракеты. Если запустить пример сейчас, то можно увидеть, как ракеты взлетают вверх, а некоторые даже вылетают за пределы сцены. Так как это не очень желательно, нам нужно замедлить их до того, как они покинут экран. Здесь можно использовать аффектор трения, чтобы замедлить частицы до минимального порога: Friction { groups: ['rocket'] anchors.top: parent.top width: parent.width; height: 80 system: particleSystem threshold: 5 factor: 0.9 }
В аффекторе трения также необходимо указать, на какие группы частиц он будет воздействовать. Трение замедлит все ракеты, находящиеся на расстоянии 80 пикселей от верхней границы экрана, в 0,9 раза (попробуйте 100, и вы увидите, что они почти сразу остановятся), пока они не достигнут скорости 5 пикселей в секунду. Поскольку частицы все еще имеют ускорение, направленное вниз, ракеты начнут опускаться к земле после того, как достигнут конца своего времени жизни. Поскольку подъем в воздух - тяжелая работа и очень нестабильная ситуация, мы хотим смоделировать некоторые турбулентности во время подъема корабля: Turbulence { groups: ['rocket'] anchors.bottom: parent.bottom width: parent.width; height: 160 system: particleSystem strength: 25 Tracer { color: 'green'; visible: root.tracer } } Кроме того, турбулентность должна объявить, на какие группы она должна воздействовать. Сама турбулентность простирается снизу на 160 пикселей вверх (пока не достигнет границы трения). Они также могут накладываться друг на друга. Запустив пример, вы увидите, что ракеты поднимаются вверх, затем замедляются за счет трения и падают на землю под действием все еще приложенного ускорения вниз. Следующим шагом будет запуск фейерверка.
СОВЕТ На изображении показана сцена с включенными трассерами для отображения различных областей. Ракетные частицы выбрасываются в красной области и затем подвергаются воздействию турбулентности в синей области. Наконец, они замедляются под действием трения в зеленой области и снова начинают падать, поскольку к ним постоянно прикладывается ускорение вниз. Пусть будет фейерверк Чтобы превратить ракету в красивый фейерверк, необходимо добавить ParticleGroup для инкапсуляции изменений: ParticleGroup { name: 'explosion' system: particleSystem } Мы переходим к группе частиц с помощью аффектора GroupGoal. Группа аффектор цели располагается вблизи вертикального центра экрана и воздействует на
группа 'ракета'. С помощью свойства groupGoal мы устанавливаем целевую группу для изменения на 'explosion', нашу ранее определенную группу частиц: GroupGoal { id: rocketChanger anchors.top: parent.top width: parent.width; height: 80 system: particleSystem groups: ['rocket'] goalState: 'explosion' jump: true Tracer { color: 'blue'; visible: root.tracer } } Свойство jump определяет, что изменение групп должно происходить немедленно, а не через некоторое время. СОВЕТ В альфа-версии Qt 5 мы смогли добиться того, что длительность изменения группы не работает. Есть идеи? Поскольку группа ракеты теперь меняется на нашу группу частиц "взрыв", когда частица ракеты попадает в область цели группы, нам нужно добавить фейерверк внутри группы частиц: // inside particle group TrailEmitter { id: explosionEmitter anchors.fill: parent group: 'sparkle' follow: 'rocket' lifeSpan: 750 emitRatePerParticle: 200 size: 32
velocity: AngleDirection { angle: -90; angleVariation: 180; } Взрыв выбрасывает частицы в группу 'sparkle'. В ближайшее время мы определим художник частиц для этой группы. Используемый излучатель следа следует за частицей ракеты и излучает на каждую ракету 200 частиц. Частицы направлены вверх и изменяются на 180 градусов. Поскольку частицы излучаются в группу 'sparkle', нам также необходимо определить художник частиц для этих частиц: ImageParticle { id: sparklePainter system: particleSystem groups: ['sparkle'] color: 'red' colorVariation: 0.6 source: "assets/star.png" alpha: 0.3 } Блестки нашего фейерверка должны представлять собой маленькие красные звездочки почти прозрачного цвета, позволяющие получить эффект сияния. Чтобы сделать фейерверк более эффектным, мы также добавим в группу частиц второй излучатель trail, который будет испускать частицы узким конусом вниз: // inside particle group TrailEmitter { id: explosion2Emitter anchors.fill: parent group: 'sparkle' follow: 'rocket' lifeSpan: 250 emitRatePerParticle: 100
size: 32 velocity: AngleDirection { angle: 90; angleVariation: 15; m } В остальном установка аналогична установке другого излучателя взрывного следа. Вот и все. Вот окончательный результат. Здесь приведен полный исходный код ракетного фейерверка. import QtQuick import QtQuick.Particles Rectangle { id: root width: 480; height: 240 color: "#1F1F1F" property bool tracer: false ParticleSystem { id: particleSystem } ImageParticle { id: smokePainter
system: particleSystem groups: ['smoke'] source: "assets/particle.png" alpha: 0.3 } ImageParticle { id: rocketPainter system: particleSystem groups: ['rocket'] source: "assets/rocket.png" entryEffect: ImageParticle.Fade } Emitter { id: rocketEmitter anchors.bottom: parent.bottom width: parent.width; height: 40 system: particleSystem group: 'rocket' emitRate: 2 maximumEmitted: 8 lifeSpan: 4800 lifeSpanVariation: 400 size: 128 velocity: AngleDirection { angle: 270; magnitude: 150; acceleration: AngleDirection { angle: 90; magnitude: 50 Tracer { color: 'red'; visible: root.tracer } } TrailEmitter { id: smokeEmitter system: particleSystem group: 'smoke' follow: 'rocket' size: 16 sizeVariation: 8 emitRatePerParticle: 16 velocity: AngleDirection { angle: 90; magnitude: 100; a lifeSpan: 200
Tracer { color: 'blue'; visible: root.tracer } } Friction { groups: ['rocket'] anchors.top: parent.top width: parent.width; height: 80 system: particleSystem threshold: 5 factor: 0.9 } Turbulence { groups: ['rocket'] anchors.bottom: parent.bottom width: parent.width; height: 160 system: particleSystem strength:25 Tracer { color: 'green'; visible: root.tracer } } ImageParticle { id: sparklePainter system: particleSystem groups: ['sparkle'] color: 'red' colorVariation: 0.6 source: "assets/star.png" alpha: 0.3 } GroupGoal { id: rocketChanger anchors.top: parent.top width: parent.width; height: 80 system: particleSystem groups: ['rocket'] goalState: 'explosion'
jump: true Tracer { color: 'blue'; visible: root.tracer } } ParticleGroup { name: 'explosion' system: particleSystem TrailEmitter { id: explosionEmitter anchors.fill: parent group: 'sparkle' follow: 'rocket' lifeSpan: 750 emitRatePerParticle: 200 size: 32 velocity: AngleDirection { angle: -90; angleVariati } TrailEmitter { id: explosion2Emitter anchors.fill: parent group: 'sparkle' follow: 'rocket' lifeSpan: 250 emitRatePerParticle: 100 size: 32 velocity: AngleDirection { angle: 90; angleVariatio } } } } }
Particle Painters До сих пор для визуализации частиц мы использовали только художник частиц, основанный на изображениях. В состав Qt входят и другие программы для рисования частиц: ItemParticle : художник частиц на основе делегата CustomParticle : художник частиц на основе шейдеров Элемент ItemParticle может быть использован для эмиссии элементов QML в виде частиц. Для этого необходимо указать собственный делегат для частицы. ItemParticle { id: particle system: particleSystem delegate: itemDelegate } В данном случае наш делегат - это случайное изображение (с помощью Math.random()), визуализированное с белой рамкой и случайным размером. Component { id: itemDelegate Item { id: container width: 32 * Math.ceil(Math.random() * 3) height: width Image { anchors.fill: parent anchors.margins: 4 source: 'assets/' + root.images[Math.floor(Math.ran
} } } Мы излучаем 4 изображения в секунду с периодом жизни каждого из них 4 секунды. Частицы автоматически затухают и исчезают. Для более динамичных случаев можно также создать элемент самостоятельно и позволить частице взять его под контроль с помощью команды take(item, priority) . При этом симуляция частицы берет под контроль вашу частицу и работает с ней как с обычной частицей. Вернуть контроль над элементом можно с помощью команды give(item) . Вы можете еще больше повлиять на частицы элемента, остановив их жизнь с помощью freeze(item) и возобновив ее с помощью unfreeze(item).
Graphics Shaders Для отрисовки графики используется конвейер рендеринга, разбитый на этапы. Существует несколько API для управления рендерингом графики. Qt поддерживает OpenGL, Metal, Vulcan и Direct3D. Рассматривая упрощенный конвейер OpenGL, мы можем заметить вершинный и фрагментный шейдеры. Эти понятия существуют и для всех остальных конвейеров рендеринга. В конвейере вершинный шейдер получает вершинные данные, т.е. расположение углов каждого элемента, составляющего сцену, и вычисляет gl_Position . Это означает, что вершинный шейдер может перемещать графические элементы. На следующем этапе вершины обрезаются, трансформируются и растеризуются для вывода пикселей. Затем пиксели, также известные как фрагменты, пропускаются через фрагментный шейдер, который вычисляет цвет каждого пикселя. Полученный цвет возвращается через переменную gl_FragColor. Вкратце: вершинный шейдер вызывается для каждой угловой точки многоугольника (vertex = point in 3D) и отвечает за любые 3Dманипуляции с этими точками. Фрагментный шейдер (fragment = pixel) вызывается для каждого пикселя и определяет цвет этого пикселя. Поскольку Qt не зависит от базового API рендеринга, то для написания шейдеров Qt полагается на стандартный язык. Инструменты Qt Shader Tools опираются на Vulcan-совместимый GLSL. Более подробно мы рассмотрим это в примерах данной главы.

Shader Elements Для программирования шейдеров Qt Quick предоставляет два элемента. ShaderEffectSource и ShaderEffect . Эффект шейдера применяет пользовательские шейдеры, а источник эффекта шейдера преобразует QML-элемент в текстуру и отображает ее. Эффект шейдера может применять пользовательские шейдеры к прямоугольной форме и использовать источники для работы шейдера. Источником может быть изображение, которое используется в качестве текстуры или источника шейдерного эффекта. Шейдер по умолчанию использует исходный текст и отображает его без изменений. Ниже мы впервые видим QML-файл с двумя элементами ShaderEffect. Один из них не содержит никаких шейдеров, а другой явно указывает на вершинные и фрагментные шейдеры по умолчанию. Шейдеры мы рассмотрим в ближайшее время. import QtQuick Rectangle { width: 480; height: 240 color: '#1e1e1e' Row { anchors.centerIn: parent spacing: 20 Image { id: sourceImage width: 80; height: width source: '../assets/tulips.jpg' } ShaderEffect { id: effect width: 80; height: width property variant source: sourceImage
} ShaderEffect { id: effect2 width: 80; height: width property variant source: sourceImage vertexShader: "default.vert.qsb" fragmentShader: "default.frag.qsb" } } } В приведенном примере мы имеем ряд из 3 изображений. Первое реальное изображение. Второе отрисовано с использованием шейдера по умолчанию, а третье - с использованием кода шейдера для фрагмента и вершины, как показано ниже. Давайте посмотрим на шейдеры. Вершинный шейдер принимает текстурную координату, qt_MultiTexCoord0 , и передает ее в переменную qt_TexCoord0. Он также принимает позицию qt_Vertex, умножает ее на матрицу трансформации Qt, u b u f .qt_Matrix, и возвращает через переменную gl_Position. При этом положение текстуры и вершины на экране остается неизменным.
layout(location=0) in vec4 qt_Vertex; layout(location=1) in vec2 qt_MultiTexCoord0; layout(location=0) out vec2 qt_TexCoord0; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; } ubuf; out gl_PerVertex { vec4 gl_Position; }; void main() { qt_TexCoord0 = qt_MultiTexCoord0; gl_Position = ubuf.qt_Matrix * qt_Vertex; } Фрагментный шейдер берет текстуру из исходного 2D-сэмплера, т.е. текстуру, по координате qt_TexCoord0 и умножает ее на непрозрачность Qt, u b u f .qt_Opacity, чтобы вычислить fragColor - цвет, который будет использоваться для пикселя. #version 440 layout(location=0) in vec2 qt_TexCoord0; layout(location=0) out vec4 fragColor; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; } ubuf; layout(binding=1) uniform sampler2D source; void main() {
fragColor = texture(source, qt_TexCoord0) * ubuf.qt_Opacity } Обратите внимание, что эти два шейдера могут послужить шаблоном для ваших собственных шейдеров. Переменные, расположение и привязка - это то, что ожидает Qt. Более подробно об этом можно прочитать в документации по шейдерным эффектам (https://doc-snapshots.qt.io/qt6-6.2/qml-qtquickshadereffect.html#details) . Прежде чем мы сможем использовать шейдеры, их необходимо запечь. Если шейдеры являются частью большого проекта Qt и включены в качестве ресурсов, это можно автоматизировать. Однако при работе с шейдерами и qml-файлом необходимо явно запекать их вручную. Для этого используются следующие две команды: qsb --glsl 100es,120,150 --hlsl 50 --msl 12 -o default.frag. qsb --glsl 100es,120,150 --hlsl 50 --msl 12 -b -o default.vert. Инструмент qsb находится в каталоге bin вашей инсталляции Qt 6. TIP Если вы не хотите видеть исходное изображение, а только то, на которое накладывается эффект, можно установить для элемента Image значение invisible (`` visible: false``). При этом шейдерные эффекты будут по-прежнему использовать данные изображения, просто элемент Image не будет отображаться. В следующих примерах мы будем играть с некоторыми простыми шейдерными механиками. Сначала мы остановимся на фрагментном шейдере, а затем вернемся к вершинному шейдеру.
Fragment Shaders Фрагментный шейдер вызывается для каждого пикселя, подлежащего рендерингу. В этой главе мы разработаем небольшую красную линзу, которая будет увеличивать значение красного цветового канала источника. Setting up the scene Сначала мы устанавливаем сцену, в поле которой центрируется сетка и выводится исходное изображение. import QtQuick Rectangle { width: 480; height: 240 color: '#1e1e1e' Grid { anchors.centerIn: parent spacing: 20 rows: 2; columns: 4 Image { id: sourceImage width: 80; height: width source: '../../assets/tulips.jpg' } } }
A red shader Далее добавим шейдер, который отображает красный прямоугольник, задавая для каждого фрагмента значение красного цвета. #version 440 layout(location=0) in vec2 qt_TexCoord0; layout(location=0) out vec4 fragColor; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; } ubuf; layout(binding=1) uniform sampler2D source; void main() { fragColor = vec4(1.0, 0.0, 0.0, 1.0) * ubuf.qt_Opacity; }
В шейдере фрагмента мы просто присваиваем vec4(1.0, 0.0, 0.0, 1.0), представляющий собой красный непрозрачностью (alpha=1.0), фрагменту цвет fragColor с полной для каждого фрагмента, превращая каждый пиксель в сплошной красный. A red shader with texture Теперь мы хотим применить красный цвет к каждому пикселю текстуры. Для этого нам нужно вернуть текстуру в вершинный шейдер. Поскольку в вершинном шейдере мы больше ничего не делаем, нам достаточно стандартного вершинного шейдера. Нам нужно только предоставить совместимый фрагментный шейдер. #version 440 layout(location=0) in vec2 qt_TexCoord0; layout(location=0) out vec4 fragColor; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; } ubuf; layout(binding=1) uniform sampler2D source;
void main() { fragColor = texture(source, qt_TexCoord0) * vec4(1.0, 0.0, } Теперь полный шейдер содержит источник изображения в качестве свойства variant, а вершинный шейдер, который, если не указан, является вершинным шейдером по умолчанию, мы оставили без внимания. В фрагментном шейдере мы выбираем текстуру fragment texture(source, qt_TexCoord0) и применяем к ней красный цвет. The red channel property Не очень удобно жестко кодировать значение красного канала, поэтому мы хотели бы управлять им со стороны QML. Для этого мы добавим свойство redChannel в наш шейдерный эффект, а также объявим float redChannel внутри равномерного буфера фрагментного шейдера. Это все, что нам нужно сделать, чтобы значение со стороны QML стало доступно коду шейдера.
TIP Обратите внимание, что redChannel должен идти после неявных qt_Matrix и qt_Opacity в однородном буфере u b u f . Порядок следования параметров после параметров qt_ зависит от вас, но qt_Matrix и qt_Opacity должны идти первыми и именно в таком порядке. #version 440 layout(location=0) in vec2 qt_TexCoord0; layout(location=0) out vec4 fragColor; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float redChannel; } ubuf; layout(binding=1) uniform sampler2D source; void main() { fragColor = texture(source, qt_TexCoord0) * vec4(ubuf.redCh } Чтобы линза действительно стала линзой, мы изменим vec4 color на vec4(redChannel, 1.0, 1.0, 1.0), чтобы остальные цвета умножались на 1.0, а только красная часть умножалась на нашу переменную redChannel.
The red channel animated Поскольку свойство redChannel является обычным свойством, оно также может быть анимировано, как и все свойства в QML. Таким образом, мы можем использовать свойства QML для анимирования значений на GPU для воздействия на шейдеры. Как это здорово! ShaderEffect { id: effect4 width: 80; height: width property variant source: sourceImage property real redChannel: 0.3 visible: root.step>3 NumberAnimation on redChannel { from: 0.0; to: 1.0; loops: Animation.Infinite; duration } fragmentShader: "red3.frag.qsb" } Вот окончательный результат.
Эффект шейдера во втором ряду анимируется от 0,0 до 1,0 с длительностью 4 секунды. Таким образом, изображение переходит от отсутствия красной информации (0,0 красного) к нормальному изображению (1,0 красного). Выпечка И снова нам необходимо запечь шейдеры. Это можно сделать следующими командами из командной строки: qsb --glsl 100es,120,150 --hlsl 50 --msl 12 -o red1.frag.qsb re qsb --glsl 100es,120,150 --hlsl 50 --msl 12 -o red2.frag.qsb re qsb --glsl 100es,120,150 --hlsl 50 --msl 12 -o red3.frag.qsb re
Волновой эффект В этом более сложном примере мы создадим эффект волны с помощью фрагментного шейдера. Форма волны основана на синусоидальной кривой и влияет на текстурные координаты, используемые для цвета. В файле qml определяются свойства и анимация. import QtQuick 2.5 Rectangle { width: 480; height: 240 color: '#1e1e1e' Row { anchors.centerIn: parent spacing: 20 Image { id: sourceImage width: 160; height: width source: "../assets/coastline.jpg" } ShaderEffect { width: 160; height: width property variant source: sourceImage property real frequency: 8 property real amplitude: 0.1 property real time: 0.0 NumberAnimation on time { from: 0; to: Math.PI*2; duration: 1000; loops: } fragmentShader: "wave.frag.qsb"
Фрагментный шейдер принимает свойства и вычисляет цвет каждого пикселя на основе этих свойств. #version 440 layout(location=0) in vec2 qt_TexCoord0; layout(location=0) out vec4 fragColor; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float frequency; float amplitude; float time; } ubuf; layout(binding=1) uniform sampler2D source; void main() { vec2 pulse = sin(ubuf.time - ubuf.frequency * qt_TexCoord0) vec2 coord = qt_TexCoord0 + ubuf.amplitude * vec2(pulse.x, fragColor = texture(source, coord) * ubuf.qt_Opacity; } Расчет волны основан на импульсе и манипуляциях с координатами текстуры. Уравнение импульса дает нам синусоиду в зависимости от текущего времени и используемой координаты текстуры: vec2 pulse = sin(ubuf.time - ubuf.frequency * qt_TexCoord0);
Без фактора времени мы имели бы просто искажение, но не перемещающееся искажение, каким являются волны. Для цвета мы используем цвет в другой текстурной координате: vec2 coord = qt_TexCoord0 + ubuf.amplitude * vec2(pulse.x, -pul На текстурную координату влияет значение x нашего импульса. В результате получается движущаяся волна. В данном примере мы используем фрагментный шейдер, то есть перемещаем пиксели внутри текстуры прямоугольного элемента. Если бы мы хотели, чтобы весь предмет перемещался в виде волны, нам пришлось бы использовать вершинный шейдер.
Вершинный шейдер Вершинный шейдер может использоваться для манипулирования вершинами, предоставляемыми шейдерным эффектом. В обычных случаях шейдерный эффект имеет 4 вершины (верхняя-левая, верхняя-правая, нижняя-левая и нижняя-правая). Каждая вершина имеет тип vec4 . Для визуализации вершинного шейдера мы запрограммируем эффект джинна. Этот эффект используется для того, чтобы прямоугольная область окна исчезала в одной точке, подобно джинну, исчезающему в лампе. Настройка сцены Сначала мы зададим сцену с изображением и шейдерным эффектом. import QtQuick Rectangle { width: 480; height: 240 color: '#1e1e1e'
Image { id: sourceImage width: 160; height: width source: "../../assets/lighthouse.jpg" visible: false } Rectangle { width: 160; height: width anchors.centerIn: parent color: '#333333' } ShaderEffect { id: genieEffect width: 160; height: width anchors.centerIn: parent property variant source: sourceImage property bool minimized: false MouseArea { anchors.fill: parent onClicked: genieEffect.minimized = !genieEffect.min } } } Здесь представлена сцена с темным фоном и шейдерным эффектом, использующим изображение в качестве исходной текстуры. Исходное изображение не видно на картинке, созданной нашим эффектом джинна. Дополнительно мы добавили темный прямоугольник на ту же геометрию, что и шейдерный эффект, чтобы можно было лучше определить, где нужно щелкнуть, чтобы вернуть эффект.
Эффект срабатывает при нажатии на изображение, которое определяется областью мыши, охватывающей эффект. В обработчике onClicked мы переключаем пользовательское булево свойство minimized. В дальнейшем мы будем использовать это свойство для переключения эффекта. Минимизация и нормализация После настройки сцены мы определяем свойство типа real под названием minimize, которое будет содержать текущее значение нашей минимизации. Значение будет изменяться от 0,0 до 1,0 и управляться последовательной анимацией. property real minimize: 0.0 SequentialAnimation on minimize { id: animMinimize running: genieEffect.minimized PauseAnimation { duration: 300 } NumberAnimation { to: 1; duration: 700; easing.type: Easing PauseAnimation { duration: 1000 } } SequentialAnimation on minimize {
id: animNormalize running: !genieEffect.minimized NumberAnimation { to: 0; duration: 700; easing.type: Easing PauseAnimation { duration: 1300 } } Анимация запускается при переключении свойства minimized. Теперь, когда мы настроили все окружение, мы можем посмотреть на наш вершинный шейдер. #version 440 layout(location=0) in vec4 qt_Vertex; layout(location=1) in vec2 qt_MultiTexCoord0; layout(location=0) out vec2 qt_TexCoord0; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float minimize; float width; float height; } ubuf; out gl_PerVertex { vec4 gl_Position; }; void main() { qt_TexCoord0 = qt_MultiTexCoord0; vec4 pos = qt_Vertex; pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize); pos.x = mix(qt_Vertex.x, ubuf.width, ubuf.minimize); gl_Position = ubuf.qt_Matrix * pos; }
Вершинный шейдер вызывается для каждой вершины, таким образом, в нашем случае четыре раза. По умолчанию в qt задаются такие параметры, как qt_Matrix, qt_Vertex, qt_MultiTexCoord0, qt_TexCoord0. Переменные мы уже обсуждали ранее. Дополнительно мы связываем переменные minimize, width и height из нашего шейдерного эффекта с кодом вершинного шейдера. В функции main мы сохраняем текущую текстурную координату в qt_TexCoord0, чтобы сделать ее доступной для фрагментного шейдера. Теперь мы копируем текущую позицию и изменяем положение x и y вершины: vec4 pos = qt_Vertex; pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize); pos.x = mix(qt_Vertex.x, ubuf.width, ubuf.minimize); Функция mix(...) обеспечивает линейную интерполяцию между первыми двумя параметрами в точке (0,0-1,0), заданной третьим параметром. Таким образом, в нашем случае мы интерполируем для y между текущим положением y и высотой на основе текущего минимизированного значения, аналогично для x. Следует помнить, что минимизированное значение анимируется нашей последовательной анимацией и перемещается от 0,0 к 1,0 (или наоборот). Полученный эффект не совсем является эффектом джинна, но уже является большим шагом к нему.
Примитивная гибка Таким образом, мы минимизировали компоненты x и y наших вершин. Теперь мы хотели бы немного изменить манипуляцию с x и сделать ее зависимой от текущего значения y. Необходимые изменения довольно незначительны. Позиция y вычисляется, как и раньше. Интерполяция x-позиции теперь зависит от y-позиции вершины: float t = pos.y / ubuf.height; pos.x = mix(qt_Vertex.x, ubuf.width, t * minimize); Это приводит к тому, что положение x стремится к ширине, когда положение y больше. Другими словами, две верхние вершины вообще не затрагиваются, так как их y-позиция равна 0, а x-позиции двух нижних вершин отклоняются в сторону ширины, поэтому они отклоняются в одну и ту же x-позицию. Улучшенный изгиб
Поскольку на данный момент изгиб не очень удовлетворяет, мы добавим несколько деталей для улучшения ситуации. Во-первых, мы улучшим нашу анимацию, чтобы она поддерживала собственное свойство сгибания. Это необходимо, поскольку изгиб должен происходить немедленно, а минимизация по y должна быть отложена на некоторое время. Обе анимации имеют в сумме одинаковую длительность (300+700+1000 и 700+1300). Сначала мы добавим и анимируем изгиб из QML. property real bend: 0.0 property bool minimized: false // change to parallel animation ParallelAnimation { id: animMinimize running: genieEffect.minimized SequentialAnimation { PauseAnimation { duration: 300 } NumberAnimation { target: genieEffect; property: 'minimize'; to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1000 } } // adding bend animation SequentialAnimation { NumberAnimation { target: genieEffect; property: 'bend' to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1300 } } }
Затем мы добавляем изгиб в равномерный буфер, ubuf, и используем его в шейдере для достижения более плавного изгиба. #version 440 layout(location=0) in vec4 qt_Vertex; layout(location=1) in vec2 qt_MultiTexCoord0; layout(location=0) out vec2 qt_TexCoord0; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float minimize; float width; float height; float bend; } ubuf; out gl_PerVertex { vec4 gl_Position; }; void main() { qt_TexCoord0 = qt_MultiTexCoord0; vec4 pos = qt_Vertex; pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize); float t = pos.y / ubuf.height; t = (3.0 - 2.0 * t) * t * t; pos.x = mix(qt_Vertex.x, ubuf.width, t * ubuf.bend); gl_Position = ubuf.qt_Matrix * pos; } Кривая начинается плавно при значении 0,0, затем растет и плавно останавливается к значению 1,0. Здесь представлен график функции в указанном диапазоне. Для нас интерес представляет только диапазон от 0...1.
Также необходимо увеличить количество вершинных точек. Количество используемых вершинных точек может быть увеличено за счет использования сетки. mesh: GridMesh { resolution: Qt.size(16, 16) } Теперь шейдерный эффект имеет равномерно распределенную сетку из 16x16 вершин вместо прежних 2x2 вершин. Благодаря этому интерполяция между вершинами выглядит гораздо более плавной. Также видно влияние используемой кривой, так как изгиб хорошо сглаживается в конце. Именно здесь изгиб оказывает наиболее сильное влияние. Выбор сторон
В качестве последнего усовершенствования мы хотим иметь возможность переключения сторон. Сторона - это то, в какую сторону исчезает эффект джинна. До сих пор он исчезал всегда в направлении ширины. Добавив свойство side, мы сможем изменять точку между 0 и шириной. ShaderEffect { ... property real side: 0.5 ... } #version 440 layout(location=0) in vec4 qt_Vertex; layout(location=1) in vec2 qt_MultiTexCoord0; layout(location=0) out vec2 qt_TexCoord0; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float minimize; float width; float height; float bend; float side; } ubuf; out gl_PerVertex { vec4 gl_Position; }; void main() { qt_TexCoord0 = qt_MultiTexCoord0; vec4 pos = qt_Vertex; pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
float t = pos.y / ubuf.height; t = (3.0 - 2.0 * t) * t * t; pos.x = mix(qt_Vertex.x, ubuf.side * ubuf.width, t * ubuf.b gl_Position = ubuf.qt_Matrix * pos; } Упаковка Последнее, что необходимо сделать, - это красиво упаковать наш эффект. Для этого мы извлекаем код эффекта джинна в собственный компонент под названием GenieEffect . В качестве корневого элемента он содержит шейдерный эффект. Мы удалили область мыши, поскольку она не должна находиться внутри компонента, так как срабатывание эффекта можно переключить с помощью свойства minimized. // GenieEffect.qml import QtQuick ShaderEffect { id: genieEffect width: 160; height: width anchors.centerIn: parent property variant source mesh: GridMesh { resolution: Qt.size(10, 10) }
property real minimize: 0.0 property real bend: 0.0 property bool minimized: false property real side: 1.0 ParallelAnimation { id: animMinimize running: genieEffect.minimized SequentialAnimation { PauseAnimation { duration: 300 } NumberAnimation { target: genieEffect; property: 'minimize'; to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1000 } } SequentialAnimation { NumberAnimation { target: genieEffect; property: 'bend' to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1300 } } } ParallelAnimation { id: animNormalize running: !genieEffect.minimized SequentialAnimation { NumberAnimation { target: genieEffect; property: 'minimize'; to: 0; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1300 } } SequentialAnimation { PauseAnimation { duration: 300 }
NumberAnimation { target: genieEffect; property: 'bend' to: 0; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1000 } } } vertexShader: "genieeffect.vert.qsb" } // genieeffect.vert #version 440 layout(location=0) in vec4 qt_Vertex; layout(location=1) in vec2 qt_MultiTexCoord0; layout(location=0) out vec2 qt_TexCoord0; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float minimize; float width; float height; float bend; float side; } ubuf; out gl_PerVertex { vec4 gl_Position; }; void main() { qt_TexCoord0 = qt_MultiTexCoord0; vec4 pos = qt_Vertex; pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
float t = pos.y / ubuf.height; t = (3.0 - 2.0 * t) * t * t; pos.x = mix(qt_Vertex.x, ubuf.side * ubuf.width, t * ubuf.b gl_Position = ubuf.qt_Matrix * pos; } Теперь эффект можно использовать просто так: import QtQuick Rectangle { width: 480; height: 240 color: '#1e1e1e' GenieEffect { source: Image { source: '../../assets/lighthouse.jpg' } MouseArea { anchors.fill: parent onClicked: parent.minimized = !parent.minimized } } } Мы упростили код, удалив фоновый прямоугольник, и присвоили изображение непосредственно эффекту, вместо того чтобы загружать его в отдельный элемент изображения.
Эффект занавеса В последнем примере для пользовательских шейдерных эффектов я хотел бы представить вам эффект занавеса. Впервые этот эффект был опубликован в мае 2011 года в рамках Qt labs for shader effects (http://labs.qt.nokia.com/2011/05/03/qml- shadereffectitem-onqgraphicsview/) . В то время мне очень нравились эти эффекты, и эффект занавеса был моим любимым из них. Мне просто нравится, как занавеска открывается и скрывает фоновый объект. В этой главе эффект был адаптирован для Qt 6. Кроме того, он был несколько упрощен, чтобы сделать его более наглядным. Изображение занавеса называется fabric.png . Затем эффект использует вершинный шейдер для раскачивания занавески вперед и назад и фрагментный шейдер для наложения теней, чтобы показать, как ткань складывается. На приведенной ниже схеме показана работа шейдера. Волны вычисляются через кривую sin с 7 периодами (7*PI=21.99...). Другой частью является
качание. Верхняя ширина занавеса анимируется при открытии или закрытии занавеса. Нижняя ширина повторяет верхнюю ширину с помощью SpringAnimation. Это создает эффект свободного колебания нижней части занавеса. Вычисляемый компонент качания - это сила качания, основанная на y-компоненте вершин. Эффект занавеса реализован в файле CurtainEffect.qml, где в качестве источника текстуры выступает изображение ткани. В коде QML свойство mesh настраивается таким образом, чтобы увеличить количество вершин для получения более гладкого результата. import QtQuick ShaderEffect { anchors.fill: parent mesh: GridMesh { resolution: Qt.size(50, 50) } property real topWidth: open?width:20 property real bottomWidth: topWidth property real amplitude: 0.1 property bool open: false property variant source: effectSource
Behavior on bottomWidth { SpringAnimation { easing.type: Easing.OutElastic; velocity: 250; mass: 1.5; spring: 0.5; damping: 0.05 } } Behavior on topWidth { NumberAnimation { duration: 1000 } } ShaderEffectSource { id: effectSource sourceItem: effectImage; hideSource: true } Image { id: effectImage anchors.fill: parent source: "../assets/fabric.png" fillMode: Image.Tile } vertexShader: "curtain.vert.qsb" fragmentShader: "curtain.frag.qsb" } Вершинный шейдер, показанный ниже, изменяет форму занавеса на основе свойств topWidth и bottomWidth, экстраполируя смещение на основе координаты y. Он также вычисляет значение тени, которое используется во фрагментном шейдере. Свойство shade передается через дополнительный вывод в месте 1.
#version 440 layout(location=0) in vec4 qt_Vertex; layout(location=1) in vec2 qt_MultiTexCoord0; layout(location=0) out vec2 qt_TexCoord0; layout(location=1) out float shade; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float topWidth; float bottomWidth; float width; float height; float amplitude; } ubuf; out gl_PerVertex { vec4 gl_Position; }; void main() { qt_TexCoord0 = qt_MultiTexCoord0; vec4 shift = vec4(0.0, 0.0, 0.0, 0.0); float swing = (ubuf.topWidth - ubuf.bottomWidth) * (qt_Vert shift.x = qt_Vertex.x * (ubuf.width - ubuf.topWidth + swing shade = sin(21.9911486 * qt_Vertex.x / ubuf.width); shift.y = ubuf.amplitude * (ubuf.width - ubuf.topWidth + sw gl_Position = ubuf.qt_Matrix * (qt_Vertex - shift); shade = 0.2 * (2.0 - shade) * ((ubuf.width - ubuf.topWidth }
В приведенном ниже фрагментном шейдере тень б е р е т с я в качестве входного сигнала в месте 1 и используется для вычисления fragColor , который применяется для отрисовки рассматриваемого пикселя. #version 440 layout(location=0) in vec2 qt_TexCoord0; layout(location=1) in float shade; layout(location=0) out vec4 fragColor; layout(std140, binding=0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float topWidth; float bottomWidth; float width; float height; float amplitude; } ubuf; layout(binding=1) uniform sampler2D source; void main() { highp vec4 color = texture(source, qt_TexCoord0); color.rgb *= 1.0 - shade; fragColor = color; } Сочетание QML-анимации и передачи переменных из вершинного шейдера во фрагментный шейдер демонстрирует, как QML и шейдеры могут использоваться вместе для создания сложных анимированных эффектов. Сам эффект используется из файла curtaindemo.qml, показанного ниже.
import QtQuick Item { id: root width: background.width; height: background.height Image { id: background anchors.centerIn: parent source: '../assets/background.png' } Text { anchors.centerIn: parent font.pixelSize: 48 color: '#efefef' text: 'Qt 6 Book' } CurtainEffect { id: curtain anchors.fill: parent } MouseArea { anchors.fill: parent onClicked: curtain.open = !curtain.open } } Шторка открывается с помощью пользовательского свойства open для эффекта шторки. Мы используем область MouseArea для запуска открытия и закрытия завесы, когда пользователь щелкает или касается этой области.
Резюме При создании новых пользовательских интерфейсов эффекты могут сделать разницу между скучным и ярким интерфейсом. В этой главе мы рассмотрели эффекты частиц и шейдеры. Частицы представляют собой мощный и интересный способ выражения графических явлений, таких как дым, фейерверк, случайные визуальные элементы. Частицы выглядят игриво и имеют большой потенциал при разумном использовании для создания привлекающих внимание элементов в любом пользовательском интерфейсе. Использование слишком большого количества эффектов частиц в пользовательском интерфейсе, безусловно, приведет к тому, что у пользователя сложится впечатление об игре. Создание игр также является реальной сильной стороной частиц. С помощью шейдеров можно вывести сцену QML на новый уровень. С помощью вершинных шейдеров можно изменять форму элементов, а фрагментные шейдеры используются для изменения текстуры элемента, например, для изменения цвета или преобразования поверхности для создания эффектов типа волн. В этой главе мы лишь поверхностно коснулись этих двух тем. Для заинтересованного читателя существует множество других возможностей.
Мультимедиа Мультимедийные элементы Qt Multimedia позволяют воспроизводить и записывать мультимедиа, такие как звук, видео или изображения. Декодирование и кодирование осуществляется с помощью бэкендов, специфичных для конкретной платформы. Например, в Linux используется популярный фреймворк GStreamer, в Windows - WMF, в OS X и iOS - AVFramework, а в Android - мультимедийные API. Мультимедийные элементы не являются частью API ядра Qt Quick. Вместо этого они предоставляются через отдельный API, доступный при импорте Qt Multimedia, как показано ниже: import QtMultimedia
Воспроизведение мультимедиа Самый простой случай интеграции мультимедиа в QML-приложение это воспроизведение поддерживает MediaPlayer это, мультимедиа. предоставляя Модуль специальный QtMultimedia QML-компонент: . Компонент MediaPlayer представляет собой невизуальный элемент, соединяющий источник мультимедиа с одним или несколькими выходными каналами. В зависимости от характера медиаданных (аудио, изображение или видео) могут быть сконфигурированы различные выходные каналы. Воспроизведение звука В следующем примере MediaPlayer воспроизводит в пустом окне аудиофайл образца mp3 с удаленного URL: import QtQuick import QtMultimedia Window { width: 1024 height: 768 visible: true MediaPlayer { id: player source: "https://file-examples-com.github.io/uploads/20 audioOutput: AudioOutput {} } Component.onCompleted: {
player.play() } } В данном примере MediaPlayer определяет два атрибута: источник : содержит URL-адрес воспроизводимого мультимедиа. Он может быть либо встроенным ( qrc:// ), либо локальным ( file:// ), либо удаленным ( https:// ). audioOutput : содержит канал вывода звука AudioOutput, подключенный к физическому устройству вывода. По умолчанию используется устройство вывода звука, установленное в системе по умолчанию. Как только основной компонент будет полностью инициализирован, игрок вызывается функция воспроизведения: Component.onCompleted: { player.play() } Воспроизведение видео Если вы хотите воспроизводить визуальные медиаданные, такие как изображения или видео, то необходимо также определить элемент VideoOutput для размещения полученного изображения или видео в пользовательском интерфейсе. В следующем примере MediaPlayer воспроизводит видеофайл с образцом mp4 с удаленного URL-адреса и центрирует видеоданные в окне: import QtQuick import QtMultimedia Window { width: 1920 height: 1080
visible: true MediaPlayer { id: player source: "https://file-examples-com.github.io/uploads/20 audioOutput: AudioOutput {} videoOutput: videoOutput } VideoOutput { id: videoOutput anchors.fill: parent anchors.margins: 20 } Component.onCompleted: { player.play() } } В данном примере MediaPlayer определяет третий атрибут: videoOutput : содержит канал вывода видео, VideoOutput, представляющий собой визуальное пространство, зарезервированное для отображения видео в пользовательском интерфейсе. СОВЕТ Обратите внимание, что компонент VideoOutput является визуальным элементом. Поэтому важно, чтобы он был создан в иерархии визуальных компонентов, а не в самом MediaPlayer. Управление воспроизведением
Компонент MediaPlayer обладает рядом полезных свойств. Например, свойства duration и position могут быть использованы для построения индикатора выполнения. Если свойство seekable равно true, то можно даже обновлять позицию при нажатии на индикатор. Также можно использовать свойства AudioOutput и VideoOutput, чтобы настроить работу и, например, обеспечить регулировку громкости. Следующий пример добавляет пользовательские элементы управления для воспроизведения: регулятор громкости кнопка воспроизведения/ паузы ползунок прогресса import QtQuick import QtQuick.Controls import QtMultimedia Window { id: root width: 1920 height: 1080 visible: true MediaPlayer { id: player source: Qt.resolvedUrl("sample-5s.mp4") audioOutput: audioOutput videoOutput: videoOutput } AudioOutput { id: audioOutput volume: volumeSlider.value } VideoOutput { id: videoOutput

width: videoOutput.sourceRect.width height: videoOutput.sourceRect.height anchors.horizontalCenter: parent.horizontalCenter } Slider { id: volumeSlider anchors.top: parent.top anchors.right: parent.right anchors.margins: 20 orientation: Qt.Vertical value: 0.5 } Item { height: 50 anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 20 Button { anchors.horizontalCenter: parent.horizontalCenter text: player.playbackState === MediaPlayer.Playing onClicked: { switch(player.playbackState) { case MediaPlayer.PlayingState: player.pause case MediaPlayer.PausedState: player.play() case MediaPlayer.StoppedState: player.play( } } } Slider { id: progressSlider width: parent.width anchors.bottom: parent.bottom enabled: player.seekable value: player.duration > 0 ? player.position / play background: Rectangle {
implicitHeight: 8 color: "white" radius: 3 Rectangle { width: progressSlider.visualPosition * pare height: parent.height color: "#1D8BF8" radius: 3 } } handle: Item {} onMoved: function () { player.position = player.duration * progressSli } } } Component.onCompleted: { player.play() } } Ползунок громкости В правом верхнем углу окна добавляется вертикальный компонент Slider, позволяющий пользователю управлять громкостью медиафайлов: Slider { id: volumeSlider anchors.top: parent.top anchors.right: parent.right anchors.margins: 20 orientation: Qt.Vertical value: 0.5 }
Затем атрибут громкости AudioOutput сопоставляется со значением ползунка: AudioOutput { id: audioOutput volume: volumeSlider.value } Воспроизведение / Пауза Компонент Button отражает состояние воспроизведения медиафайла и позволяет пользователю управлять этим состоянием: Button { anchors.horizontalCenter: parent.horizontalCenter text: player.playbackState === MediaPlayer.PlayingState ? onClicked: { switch(player.playbackState) { case MediaPlayer.PlayingState: player.pause(); brea case MediaPlayer.PausedState: player.play(); break; case MediaPlayer.StoppedState: player.play(); break } } } В зависимости от состояния воспроизведения в кнопке будет отображаться различный текст. При нажатии на кнопку запускается соответствующее действие, которое либо воспроизводит, либо приостанавливает медиафайл.
СОВЕТ Возможные состояния воспроизведения перечислены ниже: : Медиафайл воспроизводится в данный MediaPlayer.PlayingState момент. : Воспроизведение MediaPlayer.PausedState медиафайла приостановлено. MediaPlayer.StoppedState : Воспроизведение медиафайла еще не началось. Интерактивный слайдер прогресса Для отражения текущего хода воспроизведения добавляется компонент Slider. Он также позволяет пользователю управлять текущей позицией воспроизведения. Slider { id: progressSlider width: parent.width anchors.bottom: parent.bottom enabled: player.seekable value: player.duration > 0 ? player.position / player.durat background: Rectangle { implicitHeight: 8 color: "white" radius: 3 Rectangle { width: progressSlider.visualPosition * parent.width height: parent.height color: "#1D8BF8" radius: 3 } } handle: Item {} onMoved: function () {
player.position = player.duration * progressSlider.posi } } Следует отметить несколько моментов, связанных с этим образцом: Этот ползунок будет включен только в том случае, если носитель доступен (строка 5) для поиска Его значение будет установлено на текущий прогресс медиафайла, т.е. player.position / player.duration (строка 6) Позиция носителя будет (также) обновляться при перемещении слайдера пользователем (строки 19-21) Состояние средств массовой информации При использовании MediaPlayer для создания медиаплеера полезно отслеживать свойство status плеера. Здесь приводится перечисление возможных статусов, начиная от MediaPlayer.Buffered и заканчивая . Возможные значения перечислены ниже: MediaPlayer.InvalidMedia . Не был установлен носитель. Воспроизведение MediaPlayer.NoMedia остановлено. . В настоящее время идет загрузка MediaPlayer.Loading медиафайла. MediaPlayer.Loaded . Медиафайл загружен. Воспроизведение остановлено. . Медиаплеер буферизует данные. MediaPlayer.Buffering MediaPlayer.Stalled . Воспроизведение было прервано, пока носитель буферизует данные. MediaPlayer.Buffered . Медиафайл был буферизован, это означает, что плеер может начать воспроизведение медиафайла. MediaPlayer.EndOfMedia . Достигнут конец медиафайла. Воспроизведение остановлено. MediaPlayer.InvalidMedia . Медиафайл не может быть воспроизведен. Воспроизведение остановлено.
MediaPlayer.UnknownStatus . Статус носителя неизвестен. Как уже говорилось выше, состояние воспроизведения может меняться с течением времени. Вызов воспроизведения, паузы или остановки изменяет это состояние, но на это может влиять и сам носитель. Например, конец может быть достигнут, или он может быть недействительным, что приведет к остановке воспроизведения. СОВЕТ Можно также разрешить MediaPlayer зацикливать мультимедийный элемент. Свойство loops управляет тем, сколько раз б у д е т в о с п р о и з в о д и т ь с я источник. Установка свойства в значение MediaPlayer.Infinite приводит к бесконечному циклическому воспроизведению. Это удобно для непрерывной анимации или зацикленной фоновой композиции.
Звуковые эффекты При воспроизведении звуковых эффектов важным становится время отклика от запроса на воспроизведение до самого воспроизведения. В этой ситуации на помощь приходит элемент SoundEffect. Если задать свойство source, то простой вызов функции play немедленно запустит воспроизведение. Это может быть использовано для звуковой обратной связи при нажатии на экран, как показано ниже. import QtQuick import QtMultimedia Window { width: 300 height: 200 visible: true SoundEffect { id: beep source: Qt.resolvedUrl("beep.wav") } Rectangle { id: button anchors.centerIn: parent width: 200 height: 100 color: "red"
MouseArea { anchors.fill: parent onClicked: beep.play() } } } Этот элемент также может использоваться для сопровождения перехода звуком. Для запуска воспроизведения из перехода используется элемент ScriptAction. В следующем примере показано, как элементы звуковых эффектов могут использоваться для сопровождения перехода между визуальными состояниями с помощью анимации: import QtQuick import QtQuick.Controls import QtMultimedia Window { width: 500 height: 500 visible: true SoundEffect { id: beep; source: "file:beep.wav"} SoundEffect { id: swosh; source: "file:swosh.wav" } Rectangle { id: rectangle anchors.centerIn: parent width: 300 height: width color: "red" state: "DEFAULT" states: [ State {
name: "DEFAULT" PropertyChanges { target: rectangle; rotation: }, State { name: "REVERSE" PropertyChanges { target: rectangle; rotation: } ] transitions: [ Transition { to: "DEFAULT" ParallelAnimation { ScriptAction { script: swosh.play(); } PropertyAnimation { properties: "rotation"; } }, Transition { to: "REVERSE" ParallelAnimation { ScriptAction { script: beep.play(); } PropertyAnimation { properties: "rotation"; } } ] } Button { anchors.centerIn: parent text: "Flip!" onClicked: rectangle.state = rectangle.state === "DEFAU } } В данном примере мы хотим применить анимацию поворота на 180 к нашему прямоугольнику при нажатии кнопки "Flip!". Мы также хотим воспроизводить различные звуки, когда прямоугольник поворачивается в ту или иную сторону.
Для этого мы сначала загрузим наши эффекты: SoundEffect { id: beep; source: "file:beep.wav"} SoundEffect { id: swosh; source: "file:swosh.wav" } Затем мы определяем два состояния нашего прямоугольника - DEFAULT и REVERSE, указывая для каждого из них ожидаемый угол поворота: states: [ State { name: "DEFAULT" PropertyChanges { target: rectangle; rotation: 0; } }, State { name: "REVERSE" PropertyChanges { target: rectangle; rotation: 180; } } ] Для обеспечения анимации между состояниями мы определяем два перехода: transitions: [ Transition { to: "DEFAULT" ParallelAnimation { ScriptAction { script: swosh.play(); } PropertyAnimation { properties: "rotation"; duratio } }, Transition { to: "REVERSE" ParallelAnimation { ScriptAction { script: beep.play(); } PropertyAnimation { properties: "rotation"; duratio }
} ] Обратите внимание на строку ScriptAction { script: swosh.play(); }. Используя компонент ScriptAction, мы можем запустить произвольный скрипт в рамках анимации, что позволяет воспроизвести нужный звуковой эффект в рамках анимации. СОВЕТ В дополнение к функции воспроизведения доступен ряд свойств, аналогичных тем, которые предлагает MediaPlayer. Примерами являются громкость и циклы. Последний может быть установлен в значение SoundEffect.Infinite для бесконечного воспроизведения. Чтобы остановить воспроизведение, вызов функции останова. ВНИМАНИЕ При использовании бэкенда PulseAudio остановка не будет происходить мгновенно, а только предотвратит дальнейшее зацикливание. Это связано с ограничениями базового API.
Видеопотоки Элемент VideoOutput не ограничивается использованием в сочетании с элементом MediaPlayer. Он также может использоваться с различными источниками видеосигнала для отображения видеопотоков. Например, мы можем использовать VideoOutput для отображения живого видеопотока с камеры пользователя. Для этого мы объединим его с двумя компонентами: Camera и CaptureSession . import QtQuick import QtMultimedia Window { width: 1024 height: 768 visible: true CaptureSession { id: captureSession camera: Camera {} videoOutput: output } VideoOutput { id: output anchors.fill: parent } Component.onCompleted: captureSession.camera.start() }
Компонент CaptureSession предоставляет простой способ чтения потока с камеры, захвата неподвижных изображений или записи видео. Как и компонент MediaPlayer, элемент CaptureSession предоставляет атрибут videoOuput. Таким образом, мы можем использовать этот атрибут для настройки собственного визуального компонента. Наконец, когда приложение загружено, мы можем начать запись с камеры: Component.onCompleted: captureSession.camera.start() СОВЕТ В зависимости от операционной системы для работы данного приложения могут потребоваться разрешения на доступ к конфиденциальным данным. При запуске данного примера приложения с использованием бинарного файла qml эти разрешения будут запрошены автоматически. Однако если вы запускаете его как самостоятельную программу, то, возможно, придется сначала запросить эти разрешения (например, в MacOS для этого потребуется специальный .plist-файл, поставляемый вместе с приложением).
Захват изображений Одна из ключевых особенностей элемента Camera заключается в том, что с его помощью можно делать снимки. Мы используем эту возможность в простом приложении "стоп-моушен". Создавая приложение, вы научитесь показывать видоискатель, переключаться между камерами, делать снимки и вести учет сделанных снимков. Интерфейс пользователя показан ниже. Он состоит из трех основных частей. На заднем плане находится видоискатель, справа - колонка кнопок, а внизу - список сделанных снимков. Идея заключается в том, чтобы сделать серию снимков, а затем нажать кнопку Play Sequence (Воспроизвести последовательность). При этом изображения будут воспроизводиться, создавая простой фильм в режиме "стоп-кадр". Видоискатель
Видоискатель VideoOutput камеры выполнен с использованием элемента в качестве канала видеовыхода сессии CaptureSession . в свою очередь, использует компонент Сессия CaptureSession, Camera для настройки устройства. В результате на экран будет выводиться живой видеопоток с камеры. CaptureSession { id: captureSession videoOutput: output camera: Camera {} imageCapture: ImageCapture { onImageSaved: function (id, path) { imagePaths.append({"path": path}) listView.positionViewAtEnd() } } } VideoOutput { VideoOutput { id: output id: output anchors.fill: parent anchors.fill: parent } } СОВЕТ Вы можете получить больше контроля над поведением камеры, используя специальные свойства Camera, такие как exposureMode , whiteBalanceMode или zoomFactor . Список захваченных изображений Список фотографий представляет собой ListView, ориентированный горизонтально, в котором отображаются изображения из ListModel с именем imagePaths . В к а ч е с т в е фона используется полупрозрачный черный Rectangle.
ListModel { id: imagePaths } ListView { id: listView anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.bottomMargin: 10 height: 100 orientation: ListView.Horizontal spacing: 10 model: imagePaths delegate: Image { required property string path height: 100 source: path fillMode: Image.PreserveAspectFit } Rectangle { anchors.fill: parent anchors.topMargin: -10 color: "black" opacity: 0.5 } } Для съемки изображений элемент CaptureSession содержит набор подэлементов для различных задач. Для съемки неподвижных изображений используется элемент
Используется элемент CaptureSession.imageCapture. При вызове метода captureToFile происходит получение изображения и сохранение его в локальном каталоге изображений пользователя. В результате элемент CaptureSession.imageCapture выдает сигнал imageSaved. Button { id: shotButton width: parent.buttonWidth height: parent.buttonHeight text: qsTr("Take Photo") onClicked: { captureSession.imageCapture.captureToFile() } } В этом случае нам не нужно показывать изображение предварительного просмотра, а достаточно добавить полученное изображение в ListView в нижней части экрана. Как показано в примере ниже, путь к сохраненному изображению указывается в качестве аргумента path в сигнале. CaptureSession { id: captureSession videoOutput: output camera: Camera {} imageCapture: ImageCapture { onImageSaved: function (id, path) { imagePaths.append({"path": path}) listView.positionViewAtEnd() } } }
СОВЕТ Для показа предварительного просмотра подключитесь к сигналу imageCaptured и используйте аргумент сигнала preview в качестве источника элемента Image. Сигнальный аргумент id передается как по сигналу imageCaptured, так и по сигналу imageSaved. Это значение возвращается из метода захвата. С его помощью можно п р о с л е д и т ь весь цикл захвата изображения. Таким образом, сначала может быть использован предварительный просмотр , а затем он будет заменен правильно сохраненным изображением. Однако в примере мы этого не делаем. Переключение между камерами Если у пользователя есть несколько камер, то может оказаться удобным обеспечить возможность переключения между ними. Для этого можно использовать элемент MediaDevices в сочетании со списком ListView . В нашем случае мы будем использовать компонент ComboBox: MediaDevices { id: mediaDevices } ComboBox { id: cameraComboBox width: parent.buttonWidth height: parent.buttonHeight model: mediaDevices.videoInputs textRole: "description" displayText: captureSession.camera.cameraDevice.description onActivated: function (index) {
captureSession.camera.cameraDevice = cameraComboBox.cur } } Свойство модели ComboBox устанавливается в свойство videoInputs нашего MediaDevices . Это последнее свойство содержит список используемых видеовходов. Затем мы устанавливаем displayText элемента управления в описание устройства камеры ( captureSession.camera.cameraDevice.description ). Наконец, когда пользователь переключает видеовход, устройство камеры обновляется с учетом этого изменения: captureSession.camera.cameraDevice = cameraComboBox.currentValue . Воспроизведение Последняя часть приложения - собственно воспроизведение. Оно осуществляется с помощью элемента Timer и некоторых элементов JavaScript. Переменная _imageIndex используется для отслеживания текущего показанного изображения. Когда последнее изображение будет показано, воспроизведение будет остановлено. В примере переменная root.state используется для скрытия частей пользовательского интерфейса при воспроизведении последовательности. property int _imageIndex: -1 function startPlayback() { root.state = "playing" root.setImageIndex(0) playTimer.start() } function setImageIndex(i) { root._imageIndex = i if (root._imageIndex >= 0 && root._imageIndex < imagePaths.
image.source = imagePaths.get(root._imageIndex).path } else { image.source = "" } } Timer { id: playTimer interval: 200 repeat: false onTriggered: { if (root._imageIndex + 1 < imagePaths.count) { root.setImageIndex(root._imageIndex + 1) playTimer.start() } else { root.setImageIndex(-1) root.state = "" } } }
Резюме Медиа API, предоставляемый Qt, обеспечивает механизмы воспроизведения и захвата видео и аудио. С помощью элемента VideoOutput видеопотоки могут быть отображены в пользовательском интерфейсе. С помощью элемента MediaPlayer можно обрабатывать большинство воспроизведений, хотя для воспроизведения звуков с низкой задержкой можно использовать SoundEffect. Для захвата или записи потоков с камеры можно использовать комбинацию элементов CaptureSession и Camera.
Qt Quick 3D Модуль Qt Quick 3D позволяет использовать возможности QML в третьем измерении. С помощью Qt Quick 3D можно создавать трехмерные сцены и использовать привязки свойств, управление состояниями, анимацию и многое другое из QML для придания сцене интерактивности. Можно даже смешивать 2D- и 3D-содержимое для создания смешанной среды. Подобно тому, как Qt предоставляет абстракцию для 2D-графики, Qt Quick 3D опирается на слой абстракции для различных поддерживаемых API рендеринга. Для использования Qt Quick 3D рекомендуется применять платформу, поддерживающую хотя бы один из следующих API: OpenGL 3.3+ (поддержка с версии 3.0) OpenGL ES 3.0+ (ограниченная поддержка OpenGL ES 2) Direct3D 11.1 Вулкан 1.0+ Metal 1.2+ Qt Quick Software Adaption, т.е. стек рендеринга только для программного обеспечения, не поддерживает трехмерное содержимое. В этой главе мы рассмотрим основы Qt Quick 3D, позволяющие создавать интерактивные 3D-сцены на основе встроенных сеток, а также активов, созданных с помощью внешних инструментов. Мы также рассмотрим анимацию и смешивание 2D- и 3D-содержимого.
Основы В этом разделе мы рассмотрим основы работы с Qt Quick 3D. Сюда входит работа со встроенными формами (сетками), использование освещения и трансформации в 3D. Базовая сцена 3D-сцена состоит из нескольких стандартных элементов: View3D , который является QML-элементом верхнего уровня, представляющим всю 3D-сцену. , управляет отрисовкой сцены, в том числе SceneEnvironment отрисовкой фона, или небесного поля. PerspectiveCamera OrthographicCamera камера в сцене. Также может быть или даже пользовательская камера с пользовательской матрицей проекции. Кроме того, сцена обычно содержит экземпляры Model, представляющие объекты в трехмерном пространстве, и освещение. Мы рассмотрим взаимодействие этих элементов, создав сцену, показанную ниже.
Прежде всего, в QML-коде устанавливается View3D в качестве основного элемента, заполняющего окно. Мы также импортируем модуль QtQuick3D. Элемент View3D можно рассматривать как любой другой элемент Qt Quick, просто внутри него будет визуализироваться 3D-содержимое. import QtQuick import QtQuick3D Window { width: 640 height: 480 visible: true title: qsTr("Basic Scene") View3D { anchors.fill: parent
// ... } } Затем мы устанавливаем в SceneEnvironment сплошной цвет фона. Это делается внутри элемента View3D. environment: SceneEnvironment { clearColor: "#222222" backgroundMode: SceneEnvironment.Color } SceneEnvironment может использоваться для управления множеством других параметров рендеринга, но сейчас мы используем его только для установки цвета сплошного фона. Следующим шагом является добавление сетки в сцену. Сетка представляет собой объект в трехмерном пространстве. Каждая сетка создается с помощью QML-элемента Model. Модель может быть использована для загрузки 3D-активов, но есть несколько встроенных сеток, позволяющих нам начать работу без привлечения сложностей управления 3D-активами. В приведенном ниже коде мы создаем #Cone и #Sphere . Помимо формы сетки, мы позиционируем их в 3D-пространстве и снабжаем материалом с простым, диффузным базовым цветом. Подробнее о материалах мы поговорим в разделе [Materials and Light]("Материалы и освещение") При позиционировании элементов в трехмерном пространстве координаты выражаются в виде Qt.vector3d(x, y, z), где ось x управляет горизонтальным перемещением, y - вертикальным, а z тем, насколько близко или далеко находится объект. По умолчанию положительное направление оси x - вправо, положительное y - влево. направлена вверх, а положительная z - за пределы экрана. Я говорю "по умолчанию", потому что
это зависит от проекционной матрицы камеры. Model { position: Qt.vector3d(0, 0, 0) scale: Qt.vector3d(1, 1.25, 1) source: "#Cone" materials: [ PrincipledMaterial { baseColor: "yellow"; } ] } Model { position: Qt.vector3d(80, 0, 50) source: "#Sphere" materials: [ PrincipledMaterial { baseColor: "green"; } ] } После того как в сцене появился свет, мы добавляем направленный свет (DirectionalLight), который работает подобно солнцу. Он добавляет равномерный свет в заранее заданном направлении. Управление направлением осуществляется с помощью свойства eulerRotation, с помощью которого можно вращать направление света вокруг различных осей. Установив свойство castsShadow в true, мы добиваемся того, чтобы свет создавал тени, как это видно на конусе, где видна тень от сферы. DirectionalLight { eulerRotation.x: -20 eulerRotation.y: 110 castsShadow: true } Последний фрагмент головоломки - добавление камеры в сцену. Существуют различные камеры для различных перспектив, но для реалистичной проекции лучше всего использовать ProjectionCamera.
PerspectiveCamera { position: Qt.vector3d(0, 200, 300) Component.onCompleted: lookAt(Qt.vector3d(0, 0, 0)) }
источник света, например, DirectionalLight, и что-то, с помощью чего можно смотреть, например PerspectiveCamera . Встроенные сетки В предыдущем примере мы использовали встроенные конус и сферу. Qt Quick 3D поставляется со следующими встроенными сетками: #Cube #Cone #Sphere #Cylinder #Rectangle Все они показаны на рисунке ниже. (Слева вверху: Куб, вверху справа: Конус, центр: Сфера, слева внизу: Цилиндр, внизу справа: Прямоугольник)
Наконечник Одна оговорка - прямоугольник #Rectangle является односторонним. Это означает, что он виден только с одного направления. Это означает, что свойство eulerRotation является важным. При работе с реальными сценами сетки экспортируются из проекта и затем импортируется в сцену Qt Quick 3D. Более подробно мы рассмотрим это в разделе Работа с активами (/ch12qtquick3d/assets.html). Светильники Как и в случае с сетками, Qt Quick 3D поставляется с несколькими предопределенными источниками света. Они используются для освещения сцены различными способами. Первый из них, DirectionalLight, должен быть знаком нам по предыдущему примеру. Он работает подобно солнцу и равномерно освещает сцену в заданном направлении. Если свойство castsShadow установлено в true, то свет будет отбрасывать тени, как показано на рисунке ниже. Это свойство доступно для всех источников света.
DirectionalLight { eulerRotation.x: 210 eulerRotation.y: 20 castsShadow: true } Следующим источником света является PointLight . Это свет, который излучается из заданной точки пространства и затем спадает в сторону темноты на основе значений свойств constantFade , linearFade и quadraticFace , где свет рассчитывается как constantFade + distance * (linearFade * 0.01) + distance^2 * (quadraticFade * 0.0001). для По умолчанию используются значения 1.0 постоянного и квадратичного затухания и 0.0 для линейного затухания, что означает, что свет спадает по закону обратного квадрата.
PointLight { позиция: Qt.vector3d(100, 100, 150) castsShadow: true } Последним из источников света является SpotLight, излучающий конус света в заданном направлении, подобно реальному прожектору. Конус состоит из внутреннего и внешнего конусов. Их ширина определяется параметрами innerConeAngle и coneAngle, задаваемыми в градусах от нуля до 180 градусов. Свет во внутреннем конусе ведет себя подобно PointLight и может управляться с помощью свойств constantFade , linearFade и quadraticFace . Кроме того, по мере приближения к внешнему конусу свет уменьшается в сторону темноты, что контролируется свойством coneAngle .
SpotLight { position: Qt.vector3d(50, 200, 50) eulerRotation.x: -90 brightness: 5 ambientColor: Qt.rgba(0.1, 0.1, 0.1, 1.0) castsShadow: true } В дополнение к свойству castsShadow все источники света имеют также широко используемые свойства color и brightness, которые управляют цветом и интенсивностью излучаемого света. У светильников также есть свойство ambientColor, определяющее базовый цвет, который будет применяться к материалам до того, как они будут освещены источником света. По умолчанию это свойство имеет значение black, но может быть использовано для создания базового освещения в сцене.
В приведенных примерах мы использовали только один источник света, но, конечно, можно объединить несколько источников света в одной сцене.
Работа с активами При работе с 3D-сценами встроенные сетки быстро устаревают. Вместо этого необходим хороший поток из инструмента моделирования в QML. Qt Quick 3D поставляется с инструментом импорта активов Balsam, который используется для преобразования распространенных форматов активов в формат, удобный для Qt Quick 3D. Цель Balsam - упростить работу с активами, созданными в таких распространенных инструментах, как Blender (https://www.blender.org/) , Maya или 3ds Max, и использовать их из Qt Quick 3D. Balsam поддерживает следующие типы активов: COLLADA ( *.dae ) FBX ( *.fbx ) GLTF2 ( *.gltf , *.glb ) STL ( *.stl ) Wavefront ( *.obj ) Для некоторых форматов текстурные активы также могут быть экспортированы в формат, дружественный Qt Quick 3D, если Qt Quick 3D поддерживает данный актив. Блендер Для создания актива, который мы можем импортировать, мы используем Blender для создания сцены с головой обезьяны. Затем мы экспортируем ее в файл COLLADA (https://en.wikipedia.org/wiki/COLLADA), чтобы затем преобразовать его в формат, удобный для Qt Quick 3D, с помощью Balsam. Blender доступен по адресу https://www.blender.org/. (https://www.blender.org/) , а освоение Blender - это тема для другого
книгу, поэтому мы сделаем самое простое, что только возможно. Удалите исходный куб (выделите куб левой кнопкой мыши, нажмите shift + x , выберите Delete), добавьте сетку (с клавиатуры shift + a , выберите Mesh) и выберите для добавления обезьяну (выберите Monkey из списка доступных сеток). Существует ряд видеоуроков, демонстрирующих, как это сделать. Полученный пользовательский интерфейс Blender со сценой головы обезьяны показан ниже. После того как голова установлена, перейдите в меню Файл -> Экспорт -> КОЛЛАДА.
В результате откроется диалог Export COLLADA, в котором можно экспортировать полученную сцену.
Наконечник Как сцена из блендера, так и экспортированный файл COLLADA ( *.dae ) можно найти среди файлов примеров к этой главе. Бальзам После сохранения файла COLLADA на диск мы можем подготовить его для использования в Qt Quick 3D с помощью инструмента Balsam. Balsam доступен как в виде инструмента командной строки, так и в виде графического интерфейса пользователя с помощью инструмента balsamui. Сайт Графический инструмент является лишь тонким слоем поверх инструмента командной строки, поэтому нет никакой разницы в том, что можно делать с помощью того или иного инструмента. Начнем с того, что добавим файл monkey.dae в раздел входных файлов и установим выходную папку по приемлемому пути. Скорее всего, ваши пути будут отличаться от тех, что показаны на скриншоте.
Вкладка Настройки в balsamui включает в себя все опции. Все они соответствуют опциям командной строки инструмента balsam. На данный момент мы оставим все эти параметры со значениями по умолчанию.
Теперь вернитесь на вкладку Input и нажмите кнопку Convert. В результате в разделе состояния пользовательского интерфейса появится следующий результат: Converting 1 files... [1/1] Successfully converted '/home/.../src/basicasset/monkey.d
Successfully converted all files! Если вы запустили balsamui из командной строки, то там же вы увидите следующий вывод: generated file: "/home/.../src/basicasset/Monkey.qml" generated file: "/home/.../src/basicasset/meshes/suzanne.mesh" Это означает, что Balsam сгенерировал файл * .qml и файл * .mesh. Трехмерные активы Qt Quick Для использования файлов из проекта Qt Quick необходимо добавить их в проект. Это делается в файле CMakeLists.txt, в макросе qt_add_qml_module. Добавьте файлы в секции QML_FILES и RESOURCES, как показано ниже. qt_add_qml_module(appbasicasset URI basicasset VERSION 1.0 QML_FILES main.qml Monkey.qml RESOURCES meshes/suzanne.mesh ) Сделав это, мы можем заполнить View3D файлом Monkey.qml, как показано ниже. View3D { anchors.fill: parent environment: SceneEnvironment { clearColor: "#222222"
backgroundMode: SceneEnvironment.Color } Monkey {} } Monkey.qml содержит всю сцену Blender, включая камеру и свет, поэтому в результате получается полная сцена, как показано ниже. Заинтересованному читателю предлагается изучить файл Monkey.qml. Как вы увидите, он содержит совершенно обычную 3D-сцену Qt Quick, построенную из элементов, которые мы уже использовали в этой главе.
Наконечник Поскольку файл Monkey.qml генерируется программой, не следует изменять его вручную. В противном случае изменения будут перезаписаны при повторной генерации файла с помощью Balsam. Альтернативой использованию всей сцены из Blender является использование файла * .mesh файл в трехмерной сцене Qt Quick. Это демонстрируется в приведенном ниже коде. Здесь мы помещаем DirectionalLight и PerspectiveCamera в View3D и объединяем их с сеткой с помощью элемента Model. Таким образом, мы можем управлять позиционированием, масштабом и освещением из QML. Мы также задаем другой, желтый, материал для головы обезьяны. View3D { anchors.fill: parent environment: SceneEnvironment { clearColor: "#222222" backgroundMode: SceneEnvironment.Color } Model { source: "meshes/suzanne.mesh" scale: Qt.vector3d(2, 2, 2) eulerRotation.y: 30 eulerRotation.x: -80 materials: [ DefaultMaterial { diffuseColor: "yellow"; } PerspectiveCamera { position: Qt.vector3d(0, 0, 15) Component.onCompleted: lookAt(Qt.vector3d(0, 0, 0))
} DirectionalLight { eulerRotation.x: -20 eulerRotation.y: 110 castsShadow: true } } Полученный вид показан ниже. Здесь показано, как простая сетка может быть экспортирована из 3Dпроекта инструмента, такого как blender, конвертируется в формат Qt Quick 3D и затем используется из QML. Следует помнить, что мы можем импортировать всю сцену как есть, т.е. используя Monkey.qml , или использовать только активы, например, suzanne.mesh . Таким образом, вы сами определяете компромисс между простым импортом сцены,
и дополнительной сложности при одновременном повышении гибкости за счет настройки сцены в QML.
Материалы и свет До сих пор мы работали только с базовыми материалами. Для создания убедительной 3D-сцены необходимы соответствующие материалы и более совершенное освещение. Qt Quick 3D поддерживает ряд приемов для достижения этой цели, и в этом разделе мы рассмотрим некоторые из них. Встроенные материалы Прежде всего, мы рассмотрим встроенные материалы. Qt Quick 3D поставляется с тремя типами материалов: DefaultMaterial, PrincipledMaterial и CustomMaterial. В этой главе мы рассмотрим два первых типа, в то время как последний позволяет создавать действительно пользовательские материалы, предоставляя собственные вершинные и фрагментные шейдеры. DefaultMaterial позволяет управлять внешним видом материала с помощью свойств specular, roughness и diffuseColor. PrincipledMaterial позволяет управлять внешним видом материала с помощью свойств metalness, roughness и baseColor. Примеры двух типов материалов приведены ниже, при этом Слева - PrincipledMaterial, справа - DefaultMaterial.
Сравнивая две "Сюзанны", мы видим, как устроены оба материала. Для DefaultMaterial мы используем свойства diffuseColor, specularTint и specularAmount. Как вариации этих свойств влияют на внешний вид объектов, мы рассмотрим далее в этом разделе. Model { source: "meshes/suzanne.mesh" position: Qt.vector3d(5, 4, 0) scale: Qt.vector3d(2, 2, 2) rotation: Quaternion.fromEulerAngles(Qt.vector3d(-80, 30, 0 materials: [ DefaultMaterial { diffuseColor: "yellow"; specularTint: "red"; specularAmount: 0.7
} ] } Для PrincipledMaterial мы настраиваем свойства baseColor, metalness и roughness. Как влияют изменения этих свойств на внешний вид, мы рассмотрим далее в этом разделе. Model { source: "meshes/suzanne.mesh" position: Qt.vector3d(-5, 4, 0) scale: Qt.vector3d(2, 2, 2) rotation: Quaternion.fromEulerAngles(Qt.vector3d(-80, 30, 0 materials: [ PrincipledMaterial { baseColor: "yellow"; metalness: 0.8 roughness: 0.3 } ] } Свойства материала по умолчанию На рисунке ниже показан материал по умолчанию с различными значениями для specularAmount и свойства specularRoughness.
Значение specularAmount варьируется от 0 ,8 (крайний слева) до 0 ,5 (в центре), до 0 ,2 (крайний справа). Значение specularRoughness изменяется от 0,0 (вверху), через 0,4 (в середине) до 0,8 (внизу). Ниже приведен код средней модели. Model { source: "meshes/suzanne.mesh" position: Qt.vector3d(0, 0, 0) scale: Qt.vector3d(2, 2, 2) rotation: Quaternion.fromEulerAngles(Qt.vector3d(-80, 30, 0 materials: [ DefaultMaterial { diffuseColor: "yellow"; specularTint: "red";
specularAmount: 0.5 specularRoughness: 0.4 } ] } Принципиальные свойства материалов На рисунке ниже показан принципиальный материал с различными значениями для свойства металличности и шероховатости. Металличность 0,2 варьируется от 0 ,8 (крайний левый), через 0 ,5 (центр) до (крайний справа). Шероховатость (внизу). изменяется от 0 ,9 (вверху), через 0 ,6 (в середине) до 0 ,3
Model { source: "meshes/suzanne.mesh" position: Qt.vector3d(0, 0, 0) scale: Qt.vector3d(2, 2, 2) rotation: Quaternion.fromEulerAngles(Qt.vector3d(-80, 30, 0 materials: [ PrincipledMaterial { baseColor: "yellow"; metalness: 0.5 roughness: 0.6 } ] } Освещение на основе изображений Последняя деталь основного примера в этом разделе - скайбокс. В данном примере вместо одноцветного фона мы используем изображение в качестве skybox.
Чтобы создать скайбокс, присвойте свойству LightProbe среды SceneEnvironment текстуру, как показано в приведенном ниже коде. Это означает, что сцена получает свет на основе изображения, т.е. для освещения сцены используется скайбокс. Мы также настраиваем свойство probeExposure, которое используется для управления количеством света, проходящего через зонд, т.е. тем, насколько ярко будет освещена сцена. В этой сцене для окончательного освещения мы объединили зонд со светильником DirectionalLight. environment: SceneEnvironment { clearColor: "#222222" backgroundMode: SceneEnvironment.SkyBox lightProbe: Texture { source: "maps/skybox.jpg" } probeExposure: 0.75 }
Кроме того, ориентация светового зонда может быть изменена с помощью вектора probeOrientation, а свойство probeHorizon может быть использовано для затемнения нижней половины окружения, имитируя, что свет идет сверху, т.е. с неба, а не со всех сторон.
Анимация Существует несколько способов добавления анимации в 3D-сцены Qt Quick. Самый простой из них - перемещение, вращение и масштабирование элементов модели в сцене. Однако во многих случаях мы хотим изменять реальные сетки. Для этого существует два основных подхода: морфинг-анимация и скелетная анимация. Морфинг-анимация позволяет создать несколько целевых фигур, которым могут быть присвоены различные веса. Комбинируя целевые формы в соответствии с весами, можно получить деформированную, т.е. морфированную, форму. Это широко используется для моделирования деформаций мягких материалов. Скелетная анимация используется для позиционирования объекта, например, тела, на основе положения скелета, состоящего из костей. Эти кости воздействуют на тело, тем самым деформируя его в требуемую позу. Для обоих типов анимации наиболее распространенным подходом является определение морфинга целевых фигур и костей в инструменте моделирования, а затем экспорт в QML с помощью инструмента Balsam. В этой главе мы сделаем именно это для скелетной анимации, но для морфинг-анимации подход аналогичен. Скелетная анимация Цель этого примера - заставить Сюзанну, голову обезьяны из Blender, махать одним из своих ушей.
Скелетную анимацию иногда называют вершинной анимацией. По сути, скелет помещается внутрь сетки, а вершины сетки привязываются к скелету. Таким образом, при перемещении скелета сетка деформируется, принимая различные позы. Поскольку обучение работе с Blender выходит за рамки данной книги, ключевые слова, которые вы ищете, - это позирование и арматура. Арматуры - это то, что в Blender называют костями. Существует множество видеоуроков, объясняющих, как это делается. На скриншоте ниже показана сцена с Сюзанной и арматурами в Blender. Обратите внимание, что арматурам ушей были присвоены имена, чтобы мы могли идентифицировать их в QML.
После того как сцена в Blender готова, мы экспортируем ее в файл COLLADA и конвертируем в QML и сетку, как это было сделано в разделе "Работа с активами". Полученный QML-файл называется Monkey_with_bones.qml . Затем мы должны сослаться на эти файлы в нашем утверждении qt_add_qml_module в файле CMakeLists.txt: qt_add_qml_module(appanimations URI animations VERSION 1.0 QML_FILES main.qml Monkey_with_bones.qml RESOURCES meshes/suzanne.mesh ) Исследуя сгенерированный QML, можно заметить, что скелет построен из QML-элементов типов Skeleton и Joint . С этими элементами можно работать как с кодом в QML, но гораздо чаще их создают в инструментах проектирования. Node { id: armature z: -0.874189
Skeleton { id: qmlskeleton Joint { id: armature_Bone rotation: Qt.quaternion(0.707107, 0.707107, 0, 0) index: 0 skeletonRoot: qmlskeleton Joint { id: armature_Bone_001 y: 1 Затем на элемент Skeleton ссылается свойство skeleton элемента Model, перед свойством inverseBindPoses, связывающим суставы с моделью. Model { id: suzanne skeleton: qmlskeleton inverseBindPoses: [ Qt.matrix4x4(1, 0, 0, 0, 0, 0, 1, 0.748378, 0, -1, 0, 0 Qt.matrix4x4(), Qt.matrix4x4(0.283576, -0.11343, 0.952218, 1.00072, -0. Qt.matrix4x4(), Qt.matrix4x4(0.311833, 0.101945, -0.944652, -1.01739, 0 Qt.matrix4x4() ] source: "meshes/suzanne.mesh" Следующим шагом будет включение только что созданного элемента Monkey_with_bones в нашу основную сцену View3D: View3D { anchors.fill: parent Monkey_with_bones {
id: monkey } А затем мы создаем SequentialAnimation, построенный из двух NumberAnimations, чтобы заставить ухо болтаться вперед и назад. SequentialAnimation { NumberAnimation { target: monkey property: "left_ear_euler" duration: 1000 from: -30 to: 60 easing: InOutQuad } NumberAnimation { target: monkey property: "left_ear_euler" duration: 1000 from: 60 to: -30 easing: InOutQuad } loops: Animation.Infinite running: true }
Совет Для того чтобы получить доступ к свойству eulerRotation.y сустава извне файла Monkey_with_bones, нам необходимо раскрыть его как псевдоним свойства верхнего уровня. Это означает модификацию сгенерированного файла, что не очень приятно, но решает проблему. Node { id: scene property alias left_ear_euler: armature_left_ear.eul property alias right_ear_euler: armature_right_ear.e Получившееся "висячее ухо" можно увидеть ниже: Как видите, удобно импортировать и использовать скелеты, созданные в инструменте проектирования. Это делает удобным анимирование сложных 3D-моделей из QML.

Смешивание 2D и 3D контента Qt Quick 3D был создан для интеграции с традиционным Qt Quick, используемым для создания динамического двумерного содержимого. 3D-содержимое в 2D-сцене Подмешать 3D-содержимое в 2D-сцену несложно, поскольку View3D элемент представляет собой двумерную поверхность в сцене Qt Quick. Есть несколько свойств, которые могут представлять интерес при объединении 3D-содержимого в 2D-сцену таким образом. Во-первых, режим рендеринга View3D, который позволяет контролировать, будет ли 3D-содержимое отображаться позади, перед или в линию с 2D-содержимым. Оно также может быть выведено во внеэкранный буфер, который затем объединяется с 2Dсценой. Другое свойство - backgroundMode среды SceneEnvironment, привязанной к свойству окружения View3D. Мы видели, что он имеет значение Color или SkyBox, но можно установить и значение Transparent, View3D что позволяет видеть любое 2D-содержимое за через 3D-сцену. При построении комбинированной 2D- и 3D-сцены полезно также знать, что в одной сцене Qt Quick можно объединить несколько элементов View3D. Например, если необходимо иметь несколько 3Dмоделей в одной сцене, но они являются отдельными частями 2Dинтерфейса, то можно поместить их в отдельные элементы View3D и управлять компоновкой со стороны 2D Qt Quick.
2D-содержимое в 3D-сцене Чтобы поместить 2D-содержимое в 3D-сцену, его необходимо разместить на 3D-поверхности. Элемент Qt Quick 3D Texture имеет свойство sourceItem, которое позволяет интегрировать 2D-сцену Qt Quick в качестве текстуры для произвольной 3D-поверхности. Другой подход заключается в том, чтобы поместить двумерные элементы Qt Quick непосредственно в сцену. Именно такой подход использован в приведенном ниже примере, где мы предоставляем бейдж с именем для Сюзанны. Здесь мы создаем узел Node, который служит точкой привязки в 3Dсцене. Затем мы помещаем внутрь узла элементы Rectangle и Text . Эти два элемента являются 2D-элементами Qt Quick. Затем мы можем
управление трехмерным положением, поворотом и масштабом через соответствующие свойства элемента Node. Node { y: -30 eulerRotation.y: -10 Rectangle { anchors.horizontalCenter: parent.horizontalCenter color: "orange" width: text.width+10 height: text.height+10 Text { anchors.centerIn: parent id: text text: "I'm Suzanne" font.pointSize: 14 color: "black" } } }
Резюме Qt Quick 3D предлагает богатый способ интеграции трехмерного содержимого в сцену Qt Quick, обеспечивая тесную интеграцию через QML. При работе с трехмерным содержимым чаще всего используется работа с активами, созданными в других инструментах, таких как Blender, Maya или 3ds Max. С помощью инструмента Balsam можно импортировать в QML сетки, материалы, а также анимационные скелеты из этих моделей. Затем они могут быть использованы для рендеринга, а т а к ж е д л я взаимодействия с моделями. QML по-прежнему используется для настройки сцены, а также для инстанцирования моделей. Это означает, что сцена может быть построена во внешнем инструменте или динамически инстанцирована из QML с использованием элементов, созданных с помощью внешнего инструмента. В самых простых случаях сцены могут быть созданы из встроенных сеток, поставляемых с Qt Quick 3D. Благодаря тесной интеграции двухмерного содержимого Qt Quick и трехмерного Qt Quick можно создавать современные и интуитивно понятные пользовательские интерфейсы. Благодаря возможности QML привязывать свойства C++ к свойствам QML, это позволяет легко связать состояние 3D-модели с базовым состоянием C++. В этой главе мы лишь поверхностно рассмотрели возможности использования Qt Quick 3D. Существуют и более продвинутые концепции, начиная от пользовательских фильтров и шейдеров и заканчивая динамической генерацией сетки из C++. Существует также большой набор методов оптимизации, которые могут быть использованы для обеспечения хорошей производительности рендеринга сложного 3D-содержимого. Подробнее об этом можно прочитать в справочной документации по Qt Quick 3D (https://doc.qt.io/qt- 6/qtquick3d-index.html) .

Работа в сети Qt 6 поставляется с богатым набором сетевых классов на стороне C++. Например, имеются высокоуровневые классы для работы с протокольным уровнем HTTP по схеме "запрос-ответ", такие как QNetworkRequest , QNetworkReply и QNetworkAccessManager . А также классы более низкого уровня на уровне протокола TCP/IP или UDP, такие как QTcpSocket , QTcpServer и QUdpSocket . Существуют дополнительные классы для управления прокси-серверами, сетевым кэшем, а также сетевой конфигурацией системы. В этой главе речь пойдет не о сетевом взаимодействии на C++, а о Qt Quick и сетевом взаимодействии. Итак, как я могу соединить свой пользовательский интерфейс QML/JS непосредственно с сетевой службой или как я могу обслуживать свой пользовательский интерфейс через сетевую службу? Существуют хорошие книги и справочники по сетевому программированию на Qt/C++. Тогда достаточно прочитать главу об интеграции на C++, чтобы придумать интеграционный слой для передачи данных в мир Qt Quick.
Обслуживание пользовательского интерфейса через HTTP Для загрузки простого пользовательского интерфейса по протоколу HTTP нам необходим веб-сервер, который обслуживает документы пользовательского интерфейса. Мы начнем с создания собственного простого веб-сервера с помощью однострочника на языке python. Но сначала нам нужно создать демонстрационный пользовательский интерфейс. Для этого мы создадим небольшой файл RemoteComponent.qml в папке проекта и создадим внутри него красный прямоугольник. // RemoteComponent.qml import QtQuick Rectangle { width: 320 height: 320 color: '#ff0000' } Для обслуживания этого файла мы можем запустить небольшой pythonскрипт: sh cd <PROJECT> python -m http.server 8080 Теперь наш файл должен быть доступен через http://localhost:8080/RemoteComponent.qml . Вы можете протестировать его с помощью: curl http://localhost:8080/RemoteComponent.qml sh
Или просто укажите браузеру на это место. К сожалению, ваш браузер не понимает QML и не сможет отобразить документ. К счастью, такой QML-браузер существует. Он называется Canonic (https://www.canonic.com) . Canonic сам построен на QML и запускается в браузере через WebAssembly. Однако если вы используете WebAssembly-версию Canonic, вы не сможете просматривать файлы, обслуживаемые с localhost; немного позже вы увидите, как сделать ваши QML-файлы доступными для использования с WebAssemblyверсией Canonic. При желании можно загрузить код для запуска Canonic в качестве приложения на рабочем столе, однако это связано с проблемами безопасности (подробнее см. здесь (https://docs.page/canonic/canonic) и здесь (https://doc.qt.io/qt-6/qtqmldocuments-networktransparency.html#implications-for-application-security)). Кроме того, Qt 6 предоставляет двоичный файл qml, который можно использовать как веб-браузер. Вы можете напрямую загрузить удаленный QML-документ с помощью следующей команды: sh qml http://localhost:8080/RemoteComponent.qml Сладкий и простой. СОВЕТ Если программа qml отсутствует в вашем пути, ее можно найти в исполняемых файлах Qt: <qt-install-path>/<qtversion>/<your-os>/bin/qml . Другим способом импорта удаленного QML-документа является его динамическая загрузка с помощью QML! Для этого мы используем элемент Loader для получения удаленного документа.
// LocalHostExample.qml import QtQuick Loader { id: root source: 'http://localhost:8080/RemoteComponent.qml' onLoaded: { root.width = root.item.width // qmllint disable root.height = root.item.height // qmllint disable } } Теперь мы можем попросить исполняемый файл qml загрузить локальный Документ-загрузчик LocalHostExample.qml. sh qml LocalHostExample.qml
СОВЕТ Если вы не хотите запускать локальный сервер, вы также можете воспользоваться сервисом gist с GitHub. gist представляет собой буфер обмена, подобный таким онлайнсервисам, как Pastebin и другие. Он доступен по адресу https://gist.github.com (https://gist.github.com) . Для данного примера я создал небольшой gist по URL https://gist.github.com/jryannel/7983492 (https://gist.github.com/jryannel/7983492) . В результате появится зеленый прямоугольник. Поскольку URL gist будет предоставлять сайт в виде HTML-кода, к URL нужно прикрепить /raw, чтобы получить исходный файл, а не HTMLкод. Поскольку это содержимое размещено на веб-сервере с публичным веб-адресом, для его просмотра теперь можно использовать веб-версию Canonic. Для этого достаточно указать в браузере адрес https://app.canonic.com/#http://gist.github.com/jryannel/7983492(htt ps://app.canonic.com/#http://gist.github.com/jryannel/7983492) . Разумеется, д л я просмотра собственных файлов необходимо изменить часть после #. // GistExample.qml import QtQuick Loader { id: root source: 'https://gist.github.com/jryannel/7983492/raw' onLoaded: { root.width = root.item.width // qmllint disable root.height = root.item.height } } // qmllint disable
Для загрузки другого файла по сети из RemoteComponent.qml необходимо создать специальный файл qmldir в том же каталоге на сервере. После этого вы сможете ссылаться на компонент по его имени. Сетевые компоненты Давайте проведем небольшой эксперимент. Добавим на сторону пульта небольшую кнопку в качестве компонента многократного использования. Вот структура каталогов, которую мы будем использовать: sh ./src/SimpleExample.qml ./src/remote/qmldir ./src/remote/Button.qml ./src/remote/RemoteComponent.qml Наш файл SimpleExample.qml аналогичен предыдущему файлу main.qml пример: import QtQuick Loader { id: root anchors.fill: parent source: 'http://localhost:8080/RemoteComponent.qml' onLoaded: { root.width = root.item.width // qmllint disable root.height = root.item.height // qmllint disable } } В удаленном каталоге мы обновим файл RemoteComponent.qml, чтобы он использовал пользовательский компонент Button:
// remote/RemoteComponent.qml import QtQuick Rectangle { width: 320 height: 320 color: '#ff0000' Button { anchors.centerIn: parent text: qsTr('Click Me') onClicked: Qt.quit() } } Поскольку наши компоненты размещаются удаленно, движку QML необходимо знать, какие еще компоненты доступны удаленно. Для этого мы определяем содержимое нашего удаленного каталога в файле qmldir: # qmldir Button 1.0 Button.qml И, наконец, создадим наш фиктивный файл Button.qml: // remote/Button.qml import QtQuick.Controls Button { } Теперь мы можем запустить наш веб-сервер (не забывайте, что теперь у нас есть удаленный подкаталог):
sh cd src/serve-qml-networked-components/ python -m http.server --directory ./remote 8080 И удаленный загрузчик QML: sh qml SimpleExample.qml Импорт каталога компонентов QML Определив файл qmldir, можно также напрямую импортировать библиотеку компонентов из удаленного хранилища. Для этого работает классический импорт: import QtQuick import "http://localhost:8080" as Remote Rectangle { width: 320 height: 320 color: 'blue' Remote.Button { anchors.centerIn: parent text: qsTr('Quit') onClicked: Qt.quit() } }
СОВЕТ При использовании компонентов из локальной файловой системы они создаются немедленно, без задержки. При загрузке компонентов по сети они создаются асинхронно. Это приводит к тому, что время создания компонента неизвестно, и он может быть еще не полностью загружен, когда другие уже завершены. Учитывайте это при работе с компонентами, загруженными по сети. ВНИМАНИЕ Будьте очень осторожны при загрузке компонентов QML из Интернета. При этом возникает риск случайной загрузки вредоносных компонентов, которые могут нанести вред вашему компьютеру. Эти риски безопасности были задокументированы (https://doc.qt.io/qt- 6/qtqml-documentsnetworktransparency.html#implications-for- application-security) компанией Qt. Ссылка на страницу Qt уже приводилась на этой странице, но предупреждение стоит повторить.
Шаблоны При работе с HTML-проектами часто используется разработка на основе шаблонов. Небольшой HTML-заглушка разворачивается на стороне сервера с помощью кода, сгенерированного сервером с использованием механизма шаблонов. Например, для списка фотографий заголовок списка будет закодирован в HTML, а динамический список изображений будет динамически генерироваться с помощью механизма шаблонов. В принципе, это можно сделать и с помощью QML, но есть некоторые проблемы. Во-первых, в этом нет необходимости. Разработчики HTML делают это для того, чтобы преодолеть ограничения бэкенда HTML. В HTML пока нет компонентной модели, поэтому динамические аспекты приходится решать с помощью этих механизмов или программно используя javascript на стороне клиента. Существует множество JS-фреймворков (jQuery, dojo, backbone, angular, ...), позволяющих решить эту проблему и заложить больше логики в браузер на стороне клиента для соединения с сетевым сервисом. Тогда клиент будет просто использовать API веб-сервиса (например, обслуживающего данные в формате JSON или XML) для связи с сервером. Такой подход представляется более удачным и для QML. Второй проблемой является кэш компонентов из QML. Когда QML обращается к компоненту, он кэширует дерево рендеринга и для рендеринга загружает только кэшированную версию. Измененная версия на диске или на удаленном компьютере не будет обнаружена без перезапуска клиента. Чтобы решить эту проблему, можно воспользоваться одним приемом. Мы можем использовать фрагменты URL для загрузки URL (например, http://localhost:8080/main.qml#1234 (http://localhost:8080/main.qml#1234) ), где '#1234' - это фрагмент. HTTP-сервер обслуживает всегда один и тот же документ, но QML будет хранить этот документ, используя полный URL, включая фрагмент. Каждый раз, когда мы будем
обращаться к этому URL, фрагмент должен будет меняться, а кэш QML
не даст положительного результата. В качестве фрагмента может выступать, например, текущее время в миллисекундах или случайное число. Loader { source: 'http://localhost:8080/main.qml#' + new Date().getT } Таким образом, использование шаблонов возможно, но не рекомендуется и не соответствует сильным сторонам QML. Более эффективным подходом является использование веб-сервисов, которые предоставляют данные в формате JSON или XML.
HTTP-запросы HTTP-запрос в Qt обычно выполняется с помощью QNetworkRequest и QNetworkReply с сайта c++, а затем ответ передается с помощью интеграции Qt/C++ в пространство QML. Поэтому мы попытаемся немного расширить границы и использовать текущие средства, которые предоставляет нам Qt Quick для взаимодействия с конечной точкой сети. Для этого мы используем объект-помощник для выполнения цикла HTTP-запрос-ответ. Он представлен в виде объекта javascript XMLHttpRequest. Объект XMLHttpRequest позволяет пользователю зарегистрировать функцию-обработчик ответа и URL. Запрос может быть отправлен с использованием одного из глаголов HTTP (get, post, put, delete, ...). После получения ответа вызывается функция-обработчик. Функцияобработчик вызывается несколько раз. Каждый раз, когда состояние запроса изменилось (например, пришли заголовки или запрос выполнен). Приведем небольшой пример: function request() { js var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) print('HEADERS_RECEIVED'); } else if(xhr.readyState === XMLHttpRequest.DONE) { print('DONE'); } } xhr.open("GET", "http://example.com"); xhr.send(); }
Для ответа можно получить XML-формат или просто необработанный текст. Можно выполнить итерацию по полученному XML, но чаще всего сейчас используется необработанный текст для ответа в формате JSON. Документ JSON будет использоваться для преобразования текста в JS-объект с помощью JSON.parse(text) . /* ... */ js } else if(xhr.readyState === XMLHttpRequest.DONE) { var object = JSON.parse(xhr.responseText.toString()); print(JSON.stringify(object, null, 2)); } В обработчике ответа мы получаем необработанный текст ответа и преобразуем его в объект javascript. Теперь этот JSON-объект является корректным JS-объектом (в javascript объектом может быть как объект, так и массив). СОВЕТ Похоже, что преобразование toString() сначала делает код более стабильным. Без явного преобразования у меня несколько раз возникали ошибки парсера. Не знаю точно, в чем причина. Вызовы Flickr Рассмотрим более реальный пример. Типичным примером является использование сервиса Flickr для получения публичной ленты новых загруженных фотографий. Для этого мы можем использовать URL http://api.flickr.com/services/feeds/photos_public.gne. К сожалению, по умолчанию он возвращает XML-поток, который может быть легко разобран XmlListModel в qml. В рамках данного примера мы хотели бы остановиться на данных в формате JSON. Чтобы получить чистый JSON-ответ, необходимо добавить к запросу некоторые параметры:
http://api.flickr.com/services/feeds/photos_public.gne? format=json&nojsoncallback=1 . В результате будет возвращен ответ в формате JSON без обратного вызова JSON. СОВЕТ Обратный вызов JSON превращает ответ JSON в вызов функции. Это сокращение, используемое в HTMLпрограммировании, когда для выполнения JSON-запроса используется тег script. Ответ вызывает локальную функцию, определенную обратным вызовом. В QML не существует механизма, работающего с обратными вызовами JSON. Сначала рассмотрим ответ с помощью curl: curl "http://api.flickr.com/services/feeds/photos_public.gne?fo Ответ будет примерно таким: { "title": "Recent Uploads tagged munich", ... "items": [ { "title": "Candle lit dinner in Munich", "media": {"m":"http://farm8.staticflickr.com/7313/11444 ... },{ "title": "Munich after sunset: a train full of \"must h "media": {"m":"http://farm8.staticflickr.com/7394/11443 ... } ] ... }
Возвращаемый JSON-документ имеет определенную структуру. Объект, который имеет свойство title и свойство items. Где заголовок это строка, а items - массив объектов. При преобразовании этого текста в JSON-документ можно получить доступ к отдельным записям, поскольку это корректная структура JS-объект/массив. // JS code obj = JSON.parse(response); js print(obj.title) // => "Recent Uploads tagged munich" for(var i=0; i<obj.items.length; i++) { // iterate of the items array entries print(obj.items[i].title) // title of picture print(obj.items[i].media.m) // url of thumbnail } Будучи допустимым массивом JS, мы можем использовать массив obj.items и в качестве модели для представления списка. Сейчас мы попробуем это сделать. Сначала нам нужно получить ответ и преобразовать его в корректный JS-объект. А затем мы можем просто установить свойство response.items в качестве модели для представления списка. function request() { const xhr = new XMLHttpRequest() xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) print('HEADERS_RECEIVED') } else if(xhr.readyState === XMLHttpRequest.DONE) { print('DONE') const response = JSON.parse(xhr.responseText.toStri // Set JS object as model for listview view.model = response.items } } xhr.open("GET", "http://api.flickr.com/services/feeds/photo xhr.send() }
Здесь приведен полный исходный код, в котором мы создаем запрос при загрузке компонента. Ответ на запрос затем используется в качестве модели для нашего простого представления списка. import QtQuick Rectangle { id: root width: 320 height: 480 ListView { id: view anchors.fill: parent delegate: Thumbnail { required property var modelData width: view.width text: modelData.title iconSource: modelData.media.m } } function request() { const xhr = new XMLHttpRequest() xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.HEADERS_RECEI print('HEADERS_RECEIVED') } else if(xhr.readyState === XMLHttpRequest.DONE) { print('DONE') const response = JSON.parse(xhr.responseText.to // Set JS object as model for listview view.model = response.items } } xhr.open("GET", "http://api.flickr.com/services/feeds/p xhr.send() }
Component.onCompleted: { root.request() } } Когда документ полностью загружен ( Component.onCompleted ), мы запрашиваем у Flickr последнее содержимое ленты. По прибытии мы разбираем ответ в формате JSON и устанавливаем массив items в качестве модели для нашего представления. У представления списка есть делегат, который отображает в строке значок миниатюры и текст заголовка. Другой вариант - иметь заполнитель ListModel и добавлять каждый элемент в модель списка. Для поддержки больших моделей необходимо поддерживать пагинацию (например, страница 1 из 10) и ленивый поиск содержимого.
Локальные файлы Возможно ли также загружать локальные (XML/JSON) файлы с помощью XMLHttpRequest. Например, локальный файл с именем "colors.json" может быть загружен с помощью: js xhr.open("GET", "colors.json") Мы используем его для чтения таблицы цветов и отображения ее в виде сетки. Модификация файла со стороны Qt Quick невозможна. Для сохранения данных обратно в источник нам потребуется небольшой HTTP-сервер на основе REST или собственное расширение Qt Quick для доступа к файлам. import QtQuick Rectangle { width: 360 height: 360 color: '#000' GridView { id: view anchors.fill: parent cellWidth: width / 4 cellHeight: cellWidth delegate: Rectangle { required property var modelData width: view.cellWidth height: view.cellHeight color: modelData.value }
} function request() { const xhr = new XMLHttpRequest() xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.HEADERS_RECEI print('HEADERS_RECEIVED') } else if(xhr.readyState === XMLHttpRequest.DONE) { print('DONE') const response = JSON.parse(xhr.responseText.to view.model = response.colors } } xhr.open("GET", "colors.json") xhr.send() } Component.onCompleted: { request() } } СОВЕТ По умолчанию использование GET для локального файла запрещено движком QML. Чтобы преодолеть это ограничение, можно установить переменную окружения QML_XHR_ALLOW_FILE_READ в значение 1: sh QML_XHR_ALLOW_FILE_READ=1 qml localfiles.qml Проблема заключается в том, что если разрешить QMLприложению читать локальные файлы через XMLHttpRequest, т.е. XHR, то это открывает всю файловую систему для чтения, что является потенциальной проблемой безопасности. Qt позволяет читать локальные файлы только в том случае, если установлена переменная окружения, так что это решение является осознанным.
Вместо XMLHttpRequest д л я доступа к локальным файлам можно также использовать XmlListModel. import QtQuick import QtQml.XmlListModel Rectangle { width: 360 height: 360 color: '#000' GridView { id: view anchors.fill: parent cellWidth: width / 4 cellHeight: cellWidth model: xmlModel delegate: Rectangle { id: delegate required property var model width: view.cellWidth height: view.cellHeight color: model.value Text { anchors.centerIn: parent text: delegate.model.name } } } XmlListModel { id: xmlModel source: "colors.xml" query: "/colors/color" XmlListModelRole { name: 'name'; elementName: 'name' } XmlListModelRole { name: 'value'; elementName: 'value' } }
С помощью XmlListModel можно читать только XML-файлы, но не JSON-файлы.
REST API Чтобы использовать веб-сервис, его нужно сначала создать. Для создания простого цветового веб-сервиса мы будем использовать Flask (https://flask.palletsprojects.com (https://flask.palletsprojects.com) ) - простой HTTP-сервер приложений на основе python. Можно также использовать любой другой веб-сервер, принимающий и возвращающий данные в формате JSON. Идея заключается в том, чтобы иметь список именованных цветов, которыми можно управлять с помощью веб-сервиса. Под управлением в данном случае понимается CRUD (создание-чтение-обновление-удаление). Простой веб-сервис на Flask может быть написан в одном файле. Мы начинаем с пустого файла server.py. Внутри этого файла мы создадим некоторый кодовый код и загрузим наши начальные цвета из внешнего JSON-файла. См. также документацию Flask quickstart (https://flask.palletsprojects.com/en/2.0.x/quickstart/). py from flask import Flask, jsonify, request import json with open('colors.json', 'r') as file: colors = json.load(file) app = Flask(__name__) py # Регистрация и внедрение услуг... if __name__ == '__main__': app.run(debug = True) py
При запуске этого скрипта будет предоставлен web-сервер по адресу http://localhost:5000 (http://localhost:5000) , который пока не обслуживает ничего полезного. Теперь мы начнем добавлять конечные точки CRUD (Create, Read, Update, Delete) в наш маленький веб-сервис. Запрос на чтение Для чтения данных с нашего веб-сервера мы предоставим метод GET для всех цветов. @app.route('/colors', methods = ['GET']) py def get_colors(): return jsonify({ "data" : colors }) В результате будут возвращены цвета по конечной точке '/colors'. Чтобы проверить это, мы можем использовать curl для создания HTTP-запроса. curl -i -GET http://localhost:5000/colors sh Что вернет нам список цветов в виде JSON-данных. Читать запись Для чтения отдельного цвета по имени мы предоставляем конечную точку details, которая находится по адресу /colors/<name> . Имя - это параметр конечной точки, который идентифицирует отдельный цвет. py @app.route('/colors/<name>', methods = ['GET']) def get_color(name): for color in colors: if color["name"] == name:
return jsonify(color) return jsonify({ 'error' : True }) И мы можем проверить это, снова используя curl. Например, чтобы получить запись красного цвета. sh curl -i -GET http://localhost:5000/colors/red Он вернет одну запись о цвете в виде данных в формате JSON. Создать запись До сих пор мы использовали только метод HTTP GET. Для создания записи на стороне сервера мы будем использовать метод POST и передадим информацию о новом цвете вместе с данными POST. Местонахождение конечной точки такое же, как и для получения всех цветов. Но на этот раз мы ожидаем POST-запрос. py @app.route('/colors', methods= ['POST']) def create_color(): print('create color') color = { 'name': request.json['name'], 'value': request.json['value'] } colors.append(color) return jsonify(color), 201 Curl достаточно гибок, чтобы позволить нам предоставлять JSONданные в качестве новой записи внутри POST-запроса. curl -i -H "Content-Type: application/json" -X POST -d '{"name"
Обновить запись Для обновления отдельной записи мы используем HTTP-метод PUT. Конечная точка такая же, как и для получения отдельной записи о цвете. При успешном обновлении цвета мы возвращаем обновленный цвет в виде JSON-данных. @app.route('/colors/<name>', methods= ['PUT']) py def update_color(name): for color in colors: if color["name"] == name: color['value'] = request.json.get('value', color['v return jsonify(color) return jsonify({ 'error' : True }) В запросе curl мы указываем только обновляемые значения в виде JSON-данных, а затем именованную конечную точку для идентификации обновляемого цвета. curl -i -H "Content-Type: application/json" -X PUT -d '{"value" Удалить запись Для удаления записи используется HTTP-глагол DELETE. Для отдельного цвета используется та же конечная точка, но на этот раз HTTP-глагол DELETE. @app.route('/colors/<name>', methods=['DELETE']) def delete_color(name): for color in colors: if color["name"] == name: colors.remove(color) py
return jsonify(color) return jsonify({ 'error' : True }) Этот запрос выглядит аналогично GET-запросу для отдельного цвета. sh curl -i -X DELETE http://localhost:5000/colors/red Глаголы HTTP Теперь мы можем читать все цвета, читать определенный цвет, создавать новый цвет, обновлять цвет и удалять цвет. Кроме того, мы знаем конечные точки HTTP для нашего API. sh # Read All GET http://localhost:5000/colors # Create Entry POST http://localhost:5000/colors # Read Entry GET http://localhost:5000/colors/${name} # Update Entry PUT http://localhost:5000/colors/${name} # Delete Entry DELETE http://localhost:5000/colors/${name} Теперь наш маленький REST-сервер готов, и мы можем сосредоточиться на QML и клиентской части. Чтобы создать простой в использовании API, нам нужно сопоставить каждое действие с отдельным HTTP-запросом и предоставить простой API нашим пользователям. Клиентский REST Для демонстрации REST-клиента мы написали небольшую цветовую сетку. Цветовая сетка отображает цвета, полученные от веб-сервиса через HTTP-запросы. Наш
Пользовательский интерфейс предоставляет следующие команды: Get a color list Create color Read the last color Update last color Delete the last color Мы упаковываем наш API в собственный JS-файл colorservice.js и импортируем его в наш пользовательский интерфейс как Service . Внутри модуля сервиса ( colorservice.js ), мы создаем вспомогательную функцию, которая будет выполнять за нас HTTP-запросы: function request(verb, endpoint, obj, cb) { js print('request: ' + verb + ' ' + BASE + (endpoint ? '/' + e var xhr = new XMLHttpRequest() xhr.onreadystatechange = function() { print('xhr: on ready state change: ' + xhr.readyState) if(xhr.readyState === XMLHttpRequest.DONE) { if(cb) { var res = JSON.parse(xhr.responseText.toString( cb(res) } } } xhr.open(verb, BASE + (endpoint ? '/' + endpoint : '')) xhr.setRequestHeader('Content-Type', 'application/json') xhr.setRequestHeader('Accept', 'application/json') var data = obj ? JSON.stringify(obj) : '' xhr.send(data) } Он принимает четыре аргумента. Параметр verb , который определяет используемый HTTP-глагол (GET, POST, PUT, DELETE). Второй параметр - конечная точка, которая будет использоваться в качестве постфикса к адресу BASE (например, 'http://localhost:5000/colors (http://localhost:5000/colors) '). Третий
параметр - необязательный obj, который будет отправлен сервису в виде JSON-данных. Последний параметр определяет обратный вызов, который будет вызван при возврате ответа. Обратный вызов получает объект response с данными ответа. Перед отправкой запроса мы указываем, что отправляем и принимаем JSON-данные, изменяя заголовок запроса. С помощью этой вспомогательной функции запроса мы можем реализовать простые команды, определенные нами ранее (create, read, update, delete). Этот код находится в реализации сервиса: function getColors(cb) { // GET http://localhost:5000/colors request('GET', null, null, cb) } function createColor(entry, cb) { // POST http://localhost:5000/colors request('POST', null, entry, cb) } function getColor(name, cb) { // GET http://localhost:5000/colors/${name} request('GET', name, null, cb) } function updateColor(name, entry, cb) { // PUT http://localhost:5000/colors/${name} request('PUT', name, entry, cb) } function deleteColor(name, cb) { // DELETE http://localhost:5000/colors/${name} request('DELETE', name, null, cb) } js
В пользовательском интерфейсе мы используем сервис для реализации наших команд. В качестве поставщика данных для GridView у нас есть ListModel с идентификатором gridModel. Команды отображаются с помощью UI-элемента Button. Импорт нашей библиотеки сервисов довольно прост: import "colorservice.js" as Service Чтение списка цветов с сервера: Button { text: 'Read Colors' onClicked: { Service.getColors(function(response) { print('handle get colors response: ' + JSON.stringi gridModel.clear() const entries = response.data for(let i=0; i<entries.length; i++) { gridModel.append(entries[i]) } }) } } Создайте на сервере новую запись о цвете: Button { text: 'Create New' onClicked: { const index = gridModel.count - 1 const entry = { name: 'color-' + index, value: Qt.hsla(Math.random(), 0.5, 0.5, 1.0).toStri } Service.createColor(entry, function(response) {
print('handle create color response: ' + JSON.strin gridModel.append(response) }) } } Чтение цвета по его названию: Button { text: 'Read Last Color' onClicked: { const index = gridModel.count - 1 const name = gridModel.get(index).name Service.getColor(name, function(response) { print('handle get color response:' + JSON.stringify message.text = response.value }) } } Обновление записи о цвете на сервере на основе имени цвета: Button { text: 'Update Last Color' onClicked: { const index = gridModel.count - 1 const name = gridModel.get(index).name const entry = { value: Qt.hsla(Math.random(), 0.5, 0.5, 1.0).toStri } Service.updateColor(name, entry, function(response) { print('handle update color response: ' + JSON.strin gridModel.setProperty(gridModel.count - 1, 'value', }) } }
Удаление цвета по имени цвета: Button { text: 'Delete Last Color' onClicked: { const index = gridModel.count - 1 const name = gridModel.get(index).name Service.deleteColor(name) gridModel.remove(index, 1) } } На этом CRUD-операции (создание, чтение, обновление, удаление) с использованием REST API завершены. Существуют и другие возможности создания Web-Service API. Например, на основе модулей, и каждый модуль будет иметь одну конечную точку. При этом API может быть определен с помощью JSON RPC (http://www.jsonrpc.org/ (http://www.jsonrpc.org/) ). Конечно, можно использовать и API на основе XML, но подход на основе JSON имеет большие преимущества, поскольку парсинг встроен в QML/JS как часть JavaScript.
Аутентификация с использованием OAuth OAuth - это открытый протокол, позволяющий осуществлять безопасную авторизацию простым и стандартным способом из вебприложений, мобильных и настольных систем. OAuth используется для аутентификации клиента в таких распространенных вебсервисах, как Google, Facebook и Twitter. СОВЕТ Для пользовательского веб-сервиса можно также использовать стандартную HTTP-аутентификацию, например, с помощью имени пользователя и пароля XMLHttpRequest в методе get (например, xhr.open(verb, url, true, username, password) ). В настоящее время OAuth не является частью QML/JS API. Поэтому необходимо написать код на языке C++ и экспортировать аутентификацию в QML/JS. Другой проблемой является безопасное хранение маркера доступа. Вот некоторые ссылки, которые мы считаем полезными: http://oauth.net/ http://hueniverse.com/oauth/ https://github.com/pipacs/o2 http://www.johanpaul.com/blog/2011/05/oauth2-explained-with-qt-quick/ Пример интеграции
В этом разделе мы рассмотрим пример интеграции OAuth с использованием Spotify API (https://developer.spotify.com/documentation/web-api/) . В данном примере используется комбинация классов C++ и QML/JS. Подробнее об этой интеграции см. главу 16. Задача этого приложения - получить десять самых любимых исполнителей аутентифицированного пользователя. Создание приложения Сначала необходимо создать специальное приложение на портале разработчиков Spotify (https://developer.spotify.com/dashboard/applications) . После создания приложения вы получите два ключа: client id и ключ client secret .
Файл QML Процесс разделен на две фазы: 1. Приложение подключается к API Spotify, который, в свою очередь, запрашивает у пользователя авторизацию; 2. В случае авторизации приложение отображает список десяти любимых исполнителей пользователя. Авторизация приложения Начнем с первого шага: import QtQuick import QtQuick.Window import QtQuick.Controls import Spotify
При запуске приложения мы сначала импортируем пользовательскую библиотеку Spotify, которая определяет компонент SpotifyAPI (об этом мы поговорим позже). Затем этот компонент будет инстанцирован: ApplicationWindow { width: 320 height: 568 visible: true title: qsTr("Spotify OAuth2") BusyIndicator { visible: !spotifyApi.isAuthenticated anchors.centerIn: parent } SpotifyAPI { id: spotifyApi onIsAuthenticatedChanged: if(isAuthenticated) spotifyMo } После загрузки приложения компонент SpotifyAPI запросит авторизацию в Spotify: Component.onCompleted: { spotifyApi.setCredentials("CLIENT_ID", "CLIENT_SECRET") spotifyApi.authorize() } Пока авторизация не будет предоставлена, в центре приложения будет отображаться индикатор занятости.
СОВЕТ Обратите внимание, что по соображениям безопасности учетные данные API никогда не должны помещаться непосредственно в QML-файл! Вывод списка любимых исполнителей пользователя Следующий шаг происходит после того, как авторизация была получена. Для отображения списка исполнителей мы будем использовать паттерн Model/View/Delegate: SpotifyModel { id: spotifyModel spotifyApi: spotifyApi } ListView { visible: spotifyApi.isAuthenticated width: parent.width height: parent.height model: spotifyModel delegate: Pane { id: delegate required property var model topPadding: 0 Column { width: 300 spacing: 10 Rectangle { height: 1 width: parent.width color: delegate.model.index > 0 ? "#3d3d3d" : " } Row { spacing: 10
Item { width: 20 height: width Rectangle { width: 20 height: 20 anchors.top: parent.top anchors.right: parent.right color: "black" Label { anchors.centerIn: parent font.pointSize: 16 text: delegate.model.index + 1 color: "white" } } } Image { width: 80 height: width source: delegate.model.imageURL fillMode: Image.PreserveAspectFit } Column { Label { text: delegate.model.name font.pointSize: 16 font.bold: true } Label { text: "Followers: " + delegate.mode } } } } }
Модель SpotifyModel определена в библиотеке Spotify. Для правильной работы ей необходим SpotifyAPI . ListView отображает вертикальный список исполнителей. Исполнитель представлен именем, изображением и общим количеством подписчиков. SpotifyAPI Теперь немного углубимся в процесс аутентификации. Мы сосредоточимся на Класс SpotifyAPI, QML_ELEMENT, определенный на стороне C++. #ifndef SPOTIFYAPI_H #define SPOTIFYAPI_H #include <QtCore> #include <QtNetwork> #include <QtQml/qqml.h> #include <QOAuth2AuthorizationCodeFlow> class SpotifyAPI: public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(bool isAuthenticated READ isAuthenticated WRITE public: SpotifyAPI(QObject *parent = nullptr); void setAuthenticated(bool isAuthenticated) { if (m_isAuthenticated != isAuthenticated) { m_isAuthenticated = isAuthenticated; emit isAuthenticatedChanged(); } }
bool isAuthenticated() const { return m_isAuthenticated; } QNetworkReply* getTopArtists(); public slots: void setCredentials(const QString& clientId, const QString& void authorize(); signals: void isAuthenticatedChanged(); private: QOAuth2AuthorizationCodeFlow m_oauth2; bool m_isAuthenticated; }; #endif // SPOTIFYAPI_H Сначала мы импортируем класс <QOAuth2AuthorizationCodeFlow>. Этот класс является частью модуля QtNetworkAuth, который содержит различные реализации OAuth. #include <QOAuth2AuthorizationCodeFlow> Наш класс, SpotifyAPI, определит свойство isAuthenticated: Q_PROPERTY(bool isAuthenticated READ isAuthenticated WRITE setA Два общедоступных слота, которые мы использовали в файлах QML: void setCredentials(const QString& clientId, const QString& cli void authorize();
И частный член, представляющий поток аутентификации: QOAuth2AuthorizationCodeFlow m_oauth2; На стороне реализации мы имеем следующий код: #include "spotifyapi.h" #include <QtGui> #include <QtCore> #include <QtNetworkAuth> SpotifyAPI::SpotifyAPI(QObject *parent): QObject(parent), m_isA m_oauth2.setAuthorizationUrl(QUrl("https://accounts.spotify m_oauth2.setAccessTokenUrl(QUrl("https://accounts.spotify.c m_oauth2.setScope("user-top-read"); m_oauth2.setReplyHandler(new QOAuthHttpServerReplyHandler(8 m_oauth2.setModifyParametersFunction([&](QAbstractOAuth::St if(stage == QAbstractOAuth::Stage::RequestingAuthorizat parameters->insert("duration", "permanent"); } }); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorize connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::statusCha if (status == QAbstractOAuth::Status::Granted) { setAuthenticated(true); } else { setAuthenticated(false); } }); } void SpotifyAPI::setCredentials(const QString& clientId, const m_oauth2.setClientIdentifier(clientId);
m_oauth2.setClientIdentifierSharedKey(clientSecret); } void SpotifyAPI::authorize() { m_oauth2.grant(); } QNetworkReply* SpotifyAPI::getTopArtists() { return m_oauth2.get(QUrl("https://api.spotify.com/v1/me/top } Задача конструктора состоит в основном в настройке потока аутентификации. Сначала мы определяем маршруты Spotify API, которые будут служить в качестве аутентификаторов. m_oauth2.setAuthorizationUrl(QUrl("https://accounts.spotify.com m_oauth2.setAccessTokenUrl(QUrl("https://accounts.spotify.com/a Затем мы выбираем область действия (= авторизации Spotify), которую мы хотим использовать: m_oauth2.setScope("user-top-read"); Поскольку OAuth - это двусторонний процесс взаимодействия, мы создаем специальный локальный сервер для обработки ответов: m_oauth2.setReplyHandler(new QOAuthHttpServerReplyHandler(8000, Наконец, мы настраиваем два сигнала и слоты. connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWith connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::statusChanged
Первая настраивает авторизацию в веб-браузере (через &QDesktopServices::openUrl ), а вторая обеспечивает уведомление о завершении процесса авторизации. Метод authorize() - это только место для вызова метода grant(), лежащего в основе потока аутентификации. Именно этот метод запускает процесс. void SpotifyAPI::authorize() { m_oauth2.grant(); } Наконец, функция getTopArtists() вызывает web api, используя контекст авторизации, предоставленный менеджером сетевого доступа m_oauth2. QNetworkReply* SpotifyAPI::getTopArtists() { return m_oauth2.get(QUrl("https://api.spotify.com/v1/me/top } Модель Spotify Этот класс представляет собой QML_ELEMENT, который является подклассом QAbstractListModel для представления нашего списка исполнителей. Он использует SpotifyAPI исполнителей с удаленной конечной точки. #ifndef SPOTIFYMODEL_H #define SPOTIFYMODEL_H #include <QtCore> #include "spotifyapi.h" QT_FORWARD_DECLARE_CLASS(QNetworkReply) для получения
class SpotifyModel : public QAbstractListModel { Q_OBJECT QML_ELEMENT Q_PROPERTY(SpotifyAPI* spotifyApi READ spotifyApi WRITE set public: SpotifyModel(QObject *parent = nullptr); void setSpotifyApi(SpotifyAPI* spotifyApi) { if (m_spotifyApi != spotifyApi) { m_spotifyApi = spotifyApi; emit spotifyApiChanged(); } } SpotifyAPI* spotifyApi() const { return m_spotifyApi; } enum { NameRole = Qt::UserRole + 1, ImageURLRole, FollowersCountRole, HrefRole, }; QHash<int, QByteArray> roleNames() const override; int rowCount(const QModelIndex &parent) const override; int columnCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const ove signals: void spotifyApiChanged(); void error(const QString &errorString); public slots:
void update(); private: QPointer<SpotifyAPI> m_spotifyApi; QList<QJsonObject> m_artists; }; #endif // SPOTIFYMODEL_H Этот класс определяет свойство spotifyApi: Q_PROPERTY(SpotifyAPI* spotifyApi READ spotifyApi WRITE setSpot Перечисление ролей (в соответствии с QAbstractListModel ): enum { NameRole = Qt::UserRole + 1, // The artist's name ImageURLRole, // The artist's image FollowersCountRole, // The artist's followers c HrefRole, // The link to the artist's }; Слот для запуска обновления списка исполнителей: public slots: void update(); И, конечно же, список исполнителей, представленный в виде JSONобъектов: public slots: QList<QJsonObject> m_artists; Со стороны реализации мы имеем:
#include "spotifymodel.h" #include <QtCore> #include <QtNetwork> SpotifyModel::SpotifyModel(QObject *parent): QAbstractListModel QHash<int, QByteArray> SpotifyModel::roleNames() const { static const QHash<int, QByteArray> names { { NameRole, "name" }, { ImageURLRole, "imageURL" }, { FollowersCountRole, "followersCount" }, { HrefRole, "href" }, }; return names; } int SpotifyModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_artists.size(); } int SpotifyModel::columnCount(const QModelIndex &parent) const Q_UNUSED(parent); return m_artists.size() ? 1 : 0; } QVariant SpotifyModel::data(const QModelIndex &index, int role) Q_UNUSED(role); if (!index.isValid()) return QVariant(); if (role == Qt::DisplayRole || role == NameRole) { return m_artists.at(index.row()).value("name").toString } if (role == ImageURLRole) { const auto artistObject = m_artists.at(index.row()); const auto imagesValue = artistObject.value("images");
Q_ASSERT(imagesValue.isArray()); const auto imagesArray = imagesValue.toArray(); if (imagesArray.isEmpty()) return ""; const auto imageValue = imagesArray.at(0).toObject(); return imageValue.value("url").toString(); } if (role == FollowersCountRole) { const auto artistObject = m_artists.at(index.row()); const auto followersValue = artistObject.value("followe return followersValue.value("total").toInt(); } if (role == HrefRole) { return m_artists.at(index.row()).value("href").toString } return QVariant(); } void SpotifyModel::update() { if (m_spotifyApi == nullptr) { emit error("SpotifyModel::error: SpotifyApi is not set. return; } auto reply = m_spotifyApi->getTopArtists(); connect(reply, &QNetworkReply::finished, [=]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit error(reply->errorString()); return; } const auto json = reply->readAll(); const auto document = QJsonDocument::fromJson(json);
Q_ASSERT(document.isObject()); const auto rootObject = document.object(); const auto artistsValue = rootObject.value("items"); Q_ASSERT(artistsValue.isArray()); const auto artistsArray = artistsValue.toArray(); if (artistsArray.isEmpty()) return; beginResetModel(); m_artists.clear(); for (const auto artistValue : qAsConst(artistsArray)) { Q_ASSERT(artistValue.isObject()); m_artists.append(artistValue.toObject()); } endResetModel(); }); } Метод update() вызывает метод getTopArtists() и обрабатывает его ответ, извлекая отдельные элементы из JSON-документа и обновляя список исполнителей в модели. auto reply = m_spotifyApi->getTopArtists(); connect(reply, &QNetworkReply::finished, [=]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit error(reply->errorString()); return; } const auto json = reply->readAll(); const auto document = QJsonDocument::fromJson(json); Q_ASSERT(document.isObject()); const auto rootObject = document.object();
const auto artistsValue = rootObject.value("items"); Q_ASSERT(artistsValue.isArray()); const auto artistsArray = artistsValue.toArray(); if (artistsArray.isEmpty()) return; beginResetModel(); m_artists.clear(); for (const auto artistValue : qAsConst(artistsArray)) { Q_ASSERT(artistValue.isObject()); m_artists.append(artistValue.toObject()); } endResetModel(); }); Метод data() извлекает, в зависимости от запрашиваемой роли модели, соответствующие атрибуты Артиста и возвращает в виде QVariant : if (role == Qt::DisplayRole || role == NameRole) { return m_artists.at(index.row()).value("name").toString } if (role == ImageURLRole) { const auto artistObject = m_artists.at(index.row()); const auto imagesValue = artistObject.value("images"); Q_ASSERT(imagesValue.isArray()); const auto imagesArray = imagesValue.toArray(); if (imagesArray.isEmpty()) return ""; const auto imageValue = imagesArray.at(0).toObject(); return imageValue.value("url").toString(); } if (role == FollowersCountRole) { const auto artistObject = m_artists.at(index.row());
const auto followersValue = artistObject.value("followe return followersValue.value("total").toInt(); } if (role == HrefRole) { return m_artists.at(index.row()).value("href").toString }


Веб-сокеты Модуль WebSockets предоставляет реализацию протокола WebSockets протокол для клиентов и серверов WebSockets. Он является зеркальным отражением модуля Qt CPP. Он позволяет отправлять строковые и двоичные сообщения по полнодуплексному каналу связи. Обычно WebSocket устанавливается путем создания HTTPсоединения с сервером, после чего сервер "модернизирует" это соединение до WebSocket-соединения. В Qt/QML можно также просто использовать объекты WebSocket и WebSocketServer для создания прямого WebSocket-соединения. Протокол WebSocket использует схему URL "ws" или "wss" для безопасного соединения. Вы можете использовать модуль web socket qml, предварительно импортировав его. import QtWebSockets WebSocket { id: socket } WS Server Вы можете легко создать свой собственный WS-сервер, используя C++ часть Qt WebSocket, или использовать другую реализацию WS, что мне кажется очень интересным. Она интересна тем, что позволяет соединить удивительное качество рендеринга QML с большим расширением серверов веб-приложений. В данном примере мы будем использовать сервер веб-сокетов на базе Node JS с помощью модуля ws (https://npmjs.org/package/ws). Для этого сначала необходимо
установить node js (http://nodejs.org/) . Затем создайте папку ws_server и установите пакет ws с помощью менеджера пакетов node (npm). Код должен создать простой эхо-сервер в NodeJS, который будет передавать наши сообщения обратно QML-клиенту. sh cd ws_server npm install ws Инструмент npm загружает и устанавливает пакет ws и зависимости в локальную папку. Файл server.js будет представлять собой реализацию нашего сервера. Код сервера создаст сервер веб-сокетов на порту 3000 и будет слушать входящее соединение. При входящем соединении он будет посылать приветствие и
ожидает сообщений от клиента. Каждое сообщение, отправленное клиентом по сокету, будет отправлено обратно клиенту. const WebSocketServer = require('ws').Server const server = new WebSocketServer({ port : 3000 }) server.on('connection', function(socket) { console.log('client connected') socket.on('message', function(msg) { console.log('Message: %s', msg) socket.send(msg.toString()) }); socket.send('Welcome to Awesome Chat') }); console.log('listening on port ' + server.options.port) Необходимо привыкнуть к нотации JavaScript и обратным вызовам функций. Клиент WS На стороне клиента нам необходимо представление списка для отображения сообщений и TextInput для ввода пользователем нового сообщения в чате. В примере мы будем использовать метку белого цвета. // Label.qml import QtQuick Text { color: '#fff' horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter } Наше представление чата представляет собой представление списка, в котором текст добавляется к модели списка. Каждая запись отображается с помощью строки с префиксом и меткой сообщения. Мы используем коэффициент ширины ячейки cw для разбиения на 24 столбца. // ChatView.qml import QtQuick ListView { id: root width: 100 height: 62 model: ListModel {} function append(prefix, message) { model.append({prefix: prefix, message: message}) } delegate: Row { id: delegate required property var model property real cw: width / 24 width: root.width height: 18 Label { width: delegate.cw * 1 height: parent.height text: delegate.model.prefix } Label { width: delegate.cw * 23
height: parent.height text: delegate.model.message } } } Ввод чата представляет собой обычный текстовый ввод, обведенный цветной рамкой. // ChatInput.qml import QtQuick FocusScope { id: root property alias text: input.text signal accepted(string text) width: 240 height: 32 Rectangle { anchors.fill: parent color: '#000' border.color: '#fff' border.width: 2 } TextInput { id: input anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 4 anchors.rightMargin: 4 color: '#fff' focus: true onAccepted: function () { root.accepted(text)
} } } Когда веб-сокет получает сообщение, он добавляет его в представление чата. То же самое относится и к изменению статуса. Также, когда пользователь вводит сообщение в чат, его копия добавляется в представление чата на стороне клиента, а само сообщение отправляется на сервер. // ws_client.qml import QtQuick import QtWebSockets Rectangle { width: 360 height: 360 color: '#000' ChatView { id: box anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: input.top } ChatInput { id: input anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom focus: true onAccepted: function(text) { print('send message: ' + text) socket.sendTextMessage(text) box.append('>', text) text = ''
} } WebSocket { id: socket url: "ws://localhost:3000" active: true onTextMessageReceived: function (message) { box.append('<', message) } onStatusChanged: { if (socket.status == WebSocket.Error) { box.append('#', 'socket error ' + socket.errorS } else if (socket.status == WebSocket.Open) { box.append('#', 'socket open') } else if (socket.status == WebSocket.Closed) { box.append('#', 'socket closed') } } } } Сначала необходимо запустить сервер, а затем клиент. В нашем простом клиенте нет механизма повторного соединения. Запуск сервера cd ws_server node server.js sh Запуск клиента cd ws_client qml ws_client.qml sh
При вводе текста и нажатии клавиши Enter вы должны увидеть примерно следующее.
Резюме На этом мы завершаем главу о работе с сетями QML. Следует помнить, что в Qt нативный сетевой API гораздо богаче, чем в QML. Но идея главы заключается в том, чтобы расширить границы сетевых возможностей QML и способов интеграции с облачными сервисами.
Хранение В этой главе мы рассмотрим, как хранить и извлекать данные из Qt Quick. Qt Quick предлагает лишь ограниченные возможности для непосредственного хранения локальных данных. В этом смысле он больше похож на браузер. Во многих проектах хранение данных осуществляется в бэкенде на языке C++, а необходимая функциональность экспортируется во фронтенд Qt Quick. Qt Quick не предоставляет доступа к файловой системе хоста для чтения и записи файлов, как это принято на стороне Qt C++. Поэтому задача бэкендинженера - написать такой плагин или, возможно, использовать сетевой канал для связи с локальным сервером, предоставляющим такие возможности. Каждое приложение нуждается в постоянном хранении все более мелкой и крупной информации. Это может быть сделано локально в файловой системе или удаленно на сервере. Часть информации будет структурированной и простой (например, настройки), часть большой и сложной, например, файлы документации, а часть большой и структурированной и потребует подключения к базе данных. Здесь мы рассмотрим в основном встроенные возможности Qt Quick по хранению данных, а также сетевые способы.
Настройки Qt поставляется с элементом Settings для загрузки и хранения настроек. Он все еще находится в модуле лаборатории, что означает, что API может сломаться в будущем. Поэтому будьте внимательны. Приведем небольшой пример, который применяет значение цвета к базовому прямоугольнику. Каждый раз, когда пользователь щелкает по окну, генерируется новый случайный цвет. При закрытии и повторном запуске приложения должен отображаться последний цвет. По умолчанию должен использоваться цвет, первоначально установленный для корневого прямоугольника. import QtQuick import Qt.labs.settings 1.0 Rectangle { id: root width: 320 height: 240 color: '#fff' // default color Settings { property alias color: root.color } MouseArea { anchors.fill: parent // random color onClicked: root.color = Qt.hsla(Math.random(), 0.5, 0.5 } }
Значение настроек сохраняется при каждом изменении значения. Это может быть не всегда нужно. Чтобы сохранять настройки только при необходимости, можно использовать стандартные свойства в сочетании с функцией, которая изменяет настройки при вызове. Rectangle { id: root color: settings.color Settings { id: settings property color color: '#000000' } function storeSettings() { // executed maybe on destruction settings.color = root.color } } Также можно сгруппировать настройки по различным категориям с помощью кнопки свойство категории. Settings { category: 'window' property alias x: window.x property alias y: window.x property alias width: window.width property alias height: window.height } Настройки хранятся в соответствии с именем вашего приложения, организации и домена. Обычно эта информация задается в главной функции вашего кода на языке C++. int main(int argc, char** argv) { ...
QCoreApplication::setApplicationName("Awesome Application") QCoreApplication::setOrganizationName("Awesome Company"); QCoreApplication::setOrganizationDomain("org.awesome"); ... } Если вы пишете чистое QML-приложение, то те же самые атрибуты можно задать с помощью глобальных свойств Qt.application.name , Qt.application.organization и Qt.application.domain .
Локальное хранилище - SQL Qt Quick поддерживает API локального хранилища, известный из веббраузеров как API локального хранилища. API доступен в разделе "import QtQuick.LocalStorage 2.0". В общем случае она сохраняет содержимое в базе данных SQLite в специфическом для системы месте в уникальном файле с идентификатором, основанным на заданном имени и версии базы данных. Перечисление или удаление существующих баз данных невозможно. Место хранения можно узнать с помощью функции QQmlEngine::offlineStoragePath() . При использовании API сначала создается объект базы данных, а затем создаются транзакции с этой базой. Каждая транзакция может содержать один или несколько SQL-запросов. Транзакция откатывается назад, если внутри транзакции произошел сбой SQLзапроса. Например, для чтения из простой таблицы заметок с текстовым столбцом можно использовать локальное хранилище следующим образом: import QtQuick import QtQuick.LocalStorage 2.0 Item { Component.onCompleted: { const db = LocalStorage.openDatabaseSync("MyExample", " db.transaction( function(tx) { const result = tx.executeSql('select * from notes') for(let i = 0; i < result.rows.length; i++) { print(result.rows[i].text) } })
Сумасшедший прямоугольник В качестве примера предположим, что мы хотим хранить положение прямоугольника на нашей сцене. Вот основа примера. Она содержит прямоугольник crazy, который можно перетаскивать и отображать его текущее положение по x и y в виде текста. Item { width: 400 height: 400 Rectangle {
id: crazy objectName: 'crazy' width: 100 height: 100 x: 50 y: 50 color: "#53d769" border.color: Qt.lighter(color, 1.1) Text { anchors.centerIn: parent text: Math.round(parent.x) + '/' + Math.round(paren } MouseArea { anchors.fill: parent drag.target: parent } } // ... Прямоугольник можно свободно перетаскивать. При закрытии приложения и его повторном запуске прямоугольник будет находиться в том же положении. Теперь мы хотим добавить, что положение x/y прямоугольника хранится в SQL-базе. Для этого нам необходимо добавить функции init , read и функция хранения базы данных. Эти функции вызываются при завершении работы компонента и при его уничтожении. import QtQuick import QtQuick.LocalStorage 2.0 Item { // reference to the database object property var db function initDatabase() { // initialize the database object }
function storeData() { // stores data to DB } function readData() { // reads and applies data from DB } Component.onCompleted: { initDatabase() readData() } Component.onDestruction: { storeData() } } Можно также вынести код БД в собственную JS-библиотеку, которая будет выполнять всю логику. Этот способ будет предпочтительным, если логика будет более сложной. В функции инициализации базы данных мы создаем объект DB и обеспечиваем создание таблицы SQL. Обратите внимание, что функции базы данных достаточно разговорчивы, чтобы можно было следить за ними на консоли. function initDatabase() { // initialize the database object print('initDatabase()') db = LocalStorage.openDatabaseSync("CrazyBox", "1.0", "A bo db.transaction( function(tx) { print('... create table') tx.executeSql('CREATE TABLE IF NOT EXISTS data(name TEX }) }
Далее приложение вызывает функцию read для чтения существующих данных из базы данных. Здесь нам необходимо определить, есть ли уже данные в таблице. Для проверки мы смотрим, сколько строк вернул оператор select. function readData() { // reads and applies data from DB print('readData()') if(!db) { return } db.transaction(function(tx) { print('... read crazy object') const result = tx.executeSql('select * from data where if(result.rows.length === 1) { print('... update crazy geometry') // get the value column const value = result.rows[0].value // convert to JS object const obj = JSON.parse(value) // apply to object crazy.x = obj.x crazy.y = obj.y } }) } Мы ожидаем, что данные будут храниться в строке JSON внутри столбца value. Это не похоже на типичный SQL, но хорошо сочетается с JS-кодом. Поэтому вместо того, чтобы хранить x,y как свойства в таблице, мы сохраняем их как полный JS-объект, используя методы JSON stringify/parse. В итоге мы получаем корректный JS-объект со свойствами x и y, который мы можем применить к нашему безумному прямоугольнику. Для хранения данных нам необходимо различать случаи update и insert. Мы используем update, если запись уже существует, и insert, если записи с именем "crazy" не существует. function storeData() { // stores data to DB
print('storeData()') if(!db) { return } db.transaction(function(tx) { print('... check if a crazy object exists') var result = tx.executeSql('SELECT * from data where na // prepare object to be stored as JSON var obj = { x: crazy.x, y: crazy.y } if(result.rows.length === 1) { // use update print('... crazy exists, update it') result = tx.executeSql('UPDATE data set value=? whe } else { // use insert print('... crazy does not exists, create it') result = tx.executeSql('INSERT INTO data VALUES (?, } } } Вместо выборки всего набора записей мы могли бы также использовать функцию SQLite count следующим образом: SELECT COUNT(*) from data where name = "crazy", которая вернула бы одну строку с количеством строк, затронутых запросом select. В остальном это обычный SQL-код. В качестве дополнительной возможности мы используем привязку значений SQL с помощью символа ? в запросе. Теперь можно перетаскивать прямоугольник, а при выходе из приложения база данных сохраняет положение x/y и применяет его при следующем запуске приложения.
Динамический QML До сих пор мы рассматривали QML как инструмент для построения статического набора сцен и навигации между ними. В зависимости от различных состояний и логических правил создается живой и динамичный пользовательский интерфейс. При более динамичной работе с QML и JavaScript гибкость и возможности расширяются еще больше. Компоненты могут загружаться и инстанцироваться во время выполнения программы, элементы могут уничтожаться. Динамически созданные пользовательские интерфейсы могут быть сохранены на диске и впоследствии восстановлены.
Динамическая загрузка компонентов Самый простой способ динамической загрузки различных частей QML - это использование элемента Loader. Он служит в качестве заполнителя загружаемого элемента. Управление загружаемым элементом осуществляется либо через свойство source, либо через свойство sourceComponent. В первом случае элемент загружается с заданного URL, а во втором - инстанцируется компонент . Так как загрузчик служит заполнителем для загружаемого элемента, его размер зависит от размера элемента, и наоборот. Если элемент Loader имеет размер, либо заданный шириной и высотой, либо привязанный, то загружаемому элементу будет присвоен размер загрузчика. Если загрузчик не имеет размера, то его размер изменяется в соответствии с размером загружаемого элемента. Описанный ниже пример демонстрирует, как две отдельные части пользовательского интерфейса могут быть загружены в одно пространство с помощью элемента Loader. Идея состоит в том, чтобы иметь быстрый набор номера, который может быть как цифровым, так и аналоговым, как показано на рисунке ниже. Код, окружающий циферблат, не зависит от того, какой элемент загружен в данный момент.
Первым шагом в приложении является объявление элемента Loader. Обратите внимание, что свойство source оставлено без внимания. Это связано с тем, что источник зависит от того, в каком состоянии находится пользовательский интерфейс.
Loader { id: dialLoader anchors.fill: parent } В свойстве states родителя dialLoader набор элементов PropertyChanges управляет загрузкой различных QML-файлов в зависимости от состояния. Свойство source в данном примере представляет собой относительный путь к файлу, но с тем же успехом это может быть и полный URL, получающий элемент через Интернет. states: [ State { name: "analog" PropertyChanges { target: analogButton; color: "green"; PropertyChanges { target: dialLoader; source: "Analog.q }, State { name: "digital" PropertyChanges { target: digitalButton; color: "green" PropertyChanges { target: dialLoader; source: "Digital. } ] Для того чтобы загруженный элемент ожил, его свойство скорости должно быть привязано к свойству скорости корня. Это невозможно сделать прямым связыванием, поскольку элемент не всегда загружен и изменяется со временем. Вместо этого необходимо использовать элемент Binding. Свойство target этой привязки изменяется каждый раз, когда Loader срабатывает по сигналу onLoaded. Loader { id: dialLoader
anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: analogButton.top onLoaded: { binder.target = dialLoader.item; } } Binding { id: binder property: "speed" value: root.speed } Сигнал onLoaded позволяет загружаемому QML действовать, когда элемент загружен. Аналогичным образом загружаемый QML может полагаться на сигнал Component.onCompleted. Этот сигнал фактически доступен для всех компонентов, независимо от способа их загрузки. Например, корневой компонент всего приложения может использовать его для самозапуска после загрузки всего пользовательского интерфейса. Косвенное подключение При динамическом создании QML-элементов нельзя подключаться к сигналам, используя подход onSignalName, применяемый при статической настройке. Вместо этого необходимо использовать элемент Connections. Он подключается к любому количеству сигналов целевого элемента. Установив целевое свойство элемента Connections, можно подключать сигналы, как обычно, то есть используя подход onSignalName. Однако, изменяя свойство target, можно контролировать разные элементы в разное время.
В приведенном примере пользователю представлен интерфейс, состоящий из двух областей, на которые можно нажать. При нажатии на одну из областей она мигает с помощью анимации. Левая область показана в приведенном ниже фрагменте кода. В области MouseArea срабатывает анимация leftClickedAnimation, в результате чего область начинает мигать. Rectangle { id: leftRectangle width: 290 height: 200 color: "green" MouseArea { id: leftMouseArea anchors.fill: parent onClicked: leftClickedAnimation.start() } Text {
anchors.centerIn: parent font.pixelSize: 30 color: "white" text: "Click me!" } } В дополнение к двум щелкаемым областям используется элемент Connections. Он запускает третью анимацию при нажатии на активный, т.е. целевой, элемент. Connections { id: connections function onClicked() { activeClickedAnimation.start() } } Для определения целевой области MouseArea определяются два состояния. Обратите внимание, что мы не можем установить свойство target с помощью элемента PropertyChanges, поскольку он уже содержит свойство target. Вместо этого и с п о л ь з у е т с я сценарий StateChangeScript. states: [ State { name: "left" StateChangeScript { script: connections.target = leftMouseArea } }, State { name: "right" StateChangeScript { script: connections.target = rightMouseArea } } ]
При отработке примера следует обратить внимание на то, что при использовании нескольких обработчиков сигналов вызываются все. Однако порядок их выполнения не определен. При создании элемента Connections без установки свойства target это свойство по умолчанию принимает значение parent . Это означает, что оно должно быть явно установлено в null, чтобы не перехватывать сигналы от родителя до тех пор, пока не будет установлена цель. Такое поведение делает возможным создание пользовательских компоненты обработчика сигналов на основе элемента Connections. Таким образом, код, реагирующий на сигналы, может быть инкапсулирован и использован повторно. В приведенном ниже примере компонент Flasher может быть помещен внутрь любой MouseArea. При нажатии он запускает анимацию, заставляя родительскую область мигать. В той же MouseArea может выполняться и собственно запускаемая задача. Таким образом, стандартная обратная связь с пользователем, т.е. мигание, отделяется от фактического действия. import QtQuick Connections { function onClicked() { // Automatically targets the parent } } Чтобы использовать Flasher, достаточно инстанцировать Flasher в каждой MouseArea, и все заработает. import QtQuick Item { // A background flasher that flashes the background of any }
При использовании элемента Connections для мониторинга сигналов нескольких типов целевых элементов иногда возникает ситуация, когда доступные сигналы различаются между целевыми элементами. Это приводит к тому, что элемент Connections выдает ошибки времени выполнения, поскольку сигналы пропущены. Чтобы избежать этого, свойству ignoreUnknownSignal можно присвоить значение true . При этом все подобные ошибки будут игнорироваться. СОВЕТ Подавлять сообщения об ошибках, как правило, не рекомендуется, и если вы это делаете, не забудьте указать причину в комментарии. Косвенное связывание Как нельзя напрямую подключиться к сигналам динамически создаваемых элементов, так и нельзя связать свойства динамически создаваемого элемента без работы с элементом bridge. Для связывания свойства любого элемента, в том числе и динамически создаваемого, используется элемент Binding. Элемент Binding позволяет указать целевой элемент, свойство для связывания и значение для привязки. С помощью элемента Binding можно, например, связать свойства динамически загружаемого элемента. Это было продемонстрировано во вводном примере данной главы, как показано ниже. Loader { id: dialLoader anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: analogButton.top
onLoaded: { binder.target = dialLoader.item; } } Binding { id: binder property: "speed" value: root.speed } Поскольку целевой элемент привязки не всегда задан и, возможно, не всегда имеет заданное свойство, свойство when элемента привязки может быть использовано для ограничения времени, в течение которого привязка активна. Например, она может быть ограничена определенными режимами в пользовательском интерфейсе. Элемент Binding также имеет свойство delayed. Когда это свойство установлено в true, привязка не будет передана цели до тех пор, пока очередь событий не будет опустошена. В ситуациях с высокой нагрузкой это может служить оптимизацией, поскольку промежуточные значения не передаются целевому объекту.
Создание и уничтожение объектов Элемент Loader позволяет динамически заполнять часть пользовательского интерфейса. Однако общая структура интерфейса при этом остается статичной. С помощью JavaScript можно сделать еще один шаг и полностью динамически инстанцировать элементы QML. Прежде чем мы погрузимся в детали динамического создания элементов, необходимо понять суть рабочего процесса. При загрузке фрагмента QML из файла или даже через Интернет создается компонент. Компонент заключает в себе интерпретируемый QML-код и может быть использован для создания элементов. Это означает, что загрузка фрагмента QML-кода и создание на его основе элементов двухэтапный процесс. Сначала QML-код разбирается на компоненты. Затем компонент используется для инстанцирования реальных объектов элементов. Помимо создания элементов из QML-кода, хранящегося в файлах или на серверах, можно также создавать QML-объекты непосредственно из текстовых строк, содержащих QML-код. Динамически созданные элементы после инстанцирования обрабатываются по аналогии с . Динамическая загрузка и инстанцирование элементов При загрузке фрагмента QML он сначала интерпретируется как компонент. Это включает в себя загрузку зависимостей и проверку кода. Местом расположения загружаемого QML может быть как локальный файл, так и ресурс Qt или даже удаленное сетевое местоположение, заданное URL. Это означает, что время загрузки может быть от мгновенного, например, Qt-ресурса, расположенного в оперативной памяти без каких-либо незагруженных зависимостей, до
очень длительного, то есть кусок
кода, расположенного на медленном сервере и имеющего множество зависимостей, которые необходимо загрузить. Состояние создаваемого компонента можно отследить по его свойству status. Доступны следующие значения: Component.Null , Component.Loading , Component.Ready и Component.Error . Обычным является поток Null - Loading - Ready. На любом этапе статус может измениться на Error . В этом случае компонент не может быть использован для создания новых экземпляров объектов. Для получения читаемого пользователем описания ошибки можно использовать функцию Component.errorString(). При загрузке компонентов через медленные соединения может быть полезно свойство progress. Оно варьируется от 0.0, что означает, что ничего не загружено, до 1.0, указывая на то, что все они были загружены. Когда статус компонента меняется на Ready , компонент можно использовать для инстанцирования объектов. Приведенный ниже код демонстрирует, как это можно сделать, учитывая событие готовности компонента или невозможность его непосредственного создания, а также случай, когда компонент готов несколько позже. var component; js function createImageObject() { component = Qt.createComponent("dynamic-image.qml"); if (component.status === Component.Ready || component.statu finishCreation(); } else { component.statusChanged.connect(finishCreation); } } function finishCreation() { if (component.status === Component.Ready) { var image = component.createObject(root, {"x": 100, "y" if (image === null) { console.log("Error creating image");
} } else if (component.status === Component.Error) { console.log("Error loading component:", component.error } } Приведенный выше код хранится в отдельном исходном файле JavaScript, на который есть ссылка из основного файла QML. import QtQuick import "create-component.js" as ImageCreator Item { id: root width: 1024 height: 600 Component.onCompleted: ImageCreator.createImageObject(); } Функция createObject компонента используется для создания экземпляров объектов, как показано выше. Это относится не только к динамически загружаемым компонентам, но и к элементам Component, встроенным в QML-код. Полученный объект может быть использован в QML-сцене как любой другой объект. Единственное отличие заключается в том, что он не имеет идентификатора . Функция createObject принимает два аргумента. Первый родительский объект типа Item . Второй - список свойств и значений в формате {"name": value, "name": value}. Это продемонстрировано в примере ниже. Обратите внимание, что аргумент properties является необязательным. var image = component.createObject(root, {"x": 100, "y": 100});
СОВЕТ Динамически созданный экземпляр компонента не отличается от встроенного элемента Component. Элемент in-line Component также предоставляет функции для динамического инстанцирования объектов. Инкубационные компоненты При создании компонентов с помощью createObject создание компонента-объекта является блокирующим. Это означает, что инстанцирование сложного элемента может заблокировать основной поток, что приведет к заметному сбою. В качестве альтернативы сложные компоненты могут быть разбиты на части и загружены поэтапно с помощью элементов Loader. Для решения этой проблемы компонент может быть инстанцирован с помощью метода incubateObject. Он может работать так же, как и метод createObject, и возвращать экземпляр сразу, либо вызываться, когда компонент будет готов. В зависимости от конкретной настройки, этот способ может быть как хорошим, так и не очень способом решения проблем с анимацией, связанных с инстанцированием. Чтобы использовать инкубатор, достаточно воспользоваться им как createComponent . Однако возвращаемый объект - это инкубатор, а не сам экземпляр объекта. Когда статус инкубатора равен Component.Ready , объект доступен через свойство object инкубатора. Все это показано в примере ниже: function finishCreate() { js if (component.status === Component.Ready) { var incubator = component.incubateObject(root, {"x": 10 if (incubator.status === Component.Ready) { var image = incubator.object; // Created at once } else { incubator.onStatusChanged = function(status) { if (status === Component.Ready) { var image = incubator.object; // Created as
} }; } } } Динамическое создание элементов из текста Иногда удобно иметь возможность инстанцировать объект из текстовой строки QML. Это быстрее, чем размещать код в отдельном исходном файле. Для этого используется функция Qt.createQmlObject. Функция принимает три аргумента: qml, parent и filepath. Аргумент Аргумент qml содержит строку QML-кода для инстанцирования. Аргумент parent содержит родительский объект для вновь создаваемого объекта. Аргумент filepath используется при сообщении об ошибках, возникших при создании объекта. Результатом, возвращаемым функцией, является либо новый объект, либо null. ВНИМАНИЕ Функция createQmlObject всегда возвращается немедленно. Для успешной работы функции необходимо, чтобы все зависимости вызова были загружены. Это означает, что если код, переданный функции, ссылается на незагруженный компонент, то вызов будет неудачным и вернет null. Чтобы лучше справиться с этой проблемой, необходимо использовать подход createComponent / createObject. Объекты, созданные с помощью функции Qt.createQmlObject, похожи на любой другой динамически создаваемый объект. Это означает, что он идентичен любому другому QML-объекту, за исключением того, что не имеет идентификатора . В примере ниже, новый элемент Rectangle инстанцируется из встроенного QMLкода после создания корневого элемента.
import QtQuick Item { id: root width: 1024 height: 600 function createItem() { Qt.createQmlObject("import QtQuick 2.5; Rectangle { x: } Component.onCompleted: root.createItem() } Управление динамически создаваемыми элементами С динамически создаваемыми объектами можно обращаться так же, как и с любыми другими объектами в сцене QML. Однако здесь есть несколько подводных камней, о которых необходимо знать. Наиболее важным из них является понятие контекста создания. Контекст создания динамически создаваемого объекта - это контекст, в котором он создается. Это не обязательно тот же контекст, в котором существует родитель. Когда контекст создания разрушается, разрушаются и привязки, касающиеся объекта. Это означает, что важно реализовать создание динамических объектов в том месте кода, которое будет инстанцироваться в течение всего времени жизни объектов. Динамически созданные объекты могут быть также динамически уничтожены. При этом существует правило: никогда не пытайтесь уничтожить объект, который вы не создавали. Сюда также относятся элементы, которые были созданы, но не с помощью динамического механизма, такого как Component.createObject или createQmlObject .
Уничтожение объекта осуществляется вызовом его функции destroy. Функция принимает необязательный аргумент, представляющий собой целое число, указывающее, сколько миллисекунд должен существовать объект до его уничтожения. Это удобно, например, для того, чтобы дать объекту завершить последний переход. item = Qt.createQmlObject(...) ... item.destroy() СОВЕТ Можно уничтожить объект изнутри, что позволяет, например, создавать саморазрушающиеся всплывающие окна. js
Отслеживание динамических объектов При работе с динамическими объектами часто возникает необходимость отслеживать созданные объекты. Другой распространенной особенностью является возможность сохранения и восстановления состояния динамических объектов. Обе эти задачи легко решаются с помощью динамически заполняемой модели XmlListModel. В приведенном ниже примере два типа элементов, ракеты и НЛО, могут быть созданы и перемещены пользователем. Для того чтобы иметь возможность манипулировать всей сценой с динамически создаваемыми элементами, мы используем модель для отслеживания элементов. Модель, XmlListModel, заполняется по мере создания элементов. Ссылка на объект отслеживается вместе с URL-адресом источника, использованным при его создании. Последний не является обязательным для отслеживания объектов, но пригодится в дальнейшем. import QtQuick import "create-object.js" as CreateObject Item { id: root ListModel { id: objectsModel } function addUfo() { CreateObject.create("ufo.qml", root, itemAdded) } function addRocket() {
CreateObject.create("rocket.qml", root, itemAdded) } function itemAdded(obj, source) { objectsModel.append({"obj": obj, "source": source}) } Как видно из приведенного примера, create-object.js п р е д с т а в л я е т собой более обобщенную форму представленного ранее JavaScript. Метод create использует три аргумента: исходный URL-адрес, корневой элемент и обратный вызов, который должен быть вызван после завершения работы. Обратный вызов вызывается с двумя аргументами: ссылкой на только что созданный объект и используемым исходным URL. Это означает, что при каждом вызове функций addUfo или addRocket после создания нового объекта будет вызываться функция itemAdded. Последняя добавит ссылку на объект и URL источника в модель objectsModel. Модель objectsModel может быть использована различными способами. В рассматриваемом примере на нее опирается функция clearItems. Эта функция демонстрирует две вещи. Во-первых, как выполнить итерацию по модели и выполнить задачу, т.е. вызвать функцию destroy для каждого элемента, чтобы удалить его. Вовторых, она демонстрирует тот факт, что модель не обновляется при уничтожении объектов. Вместо удаления элемента модели, связанного с рассматриваемым объектом, свойство obj этого элемента модели устанавливается в null. Чтобы исправить это, код должен явно очищать элемент модели по мере удаления объектов. function clearItems() { while(objectsModel.count > 0) { objectsModel.get(0).obj.destroy() objectsModel.remove(0) } }
Имея модель, представляющую все динамически создаваемые объекты, легко создать функцию, которая сериализует эти объекты. В коде примера сериализованная информация состоит из исходного URL каждого объекта и его свойства x и y. Именно эти свойства могут быть изменены пользователем. Полученная информация используется для построения строки XML-документа. function serialize() { var res = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<sce for(var ii=0; ii < objectsModel.count; ++ii) { var i = objectsModel.get(ii) res += " <item>\n <source>" + i.source + "</source> } res += "</scene>" return res } СОВЕТ В настоящее время в модели XmlListModel из Qt 6 отсутствуют свойство xml и функция get(), необходимые для работы сериализации и десериализации. Строка XML-документа может быть использована с моделью XmlListModel путем установки свойства xml модели. В приведенном ниже коде модель показана вместе с функцией десериализации. Функция deserialize запускает десериализацию, устанавливая dsIndex для ссылки на первый элемент модели, а затем вызывая создание этого элемента. Обратный вызов, dsItemAdded, устанавливает свойства x и y вновь созданного объекта. Затем обновляется индекс и создается следующий объект, если таковой имеется.
XmlListModel { id: xmlModel query: "/scene/item" XmlListModelRole { name: "source"; elementName: "source" } XmlListModelRole { name: "x"; elementName: "x" } XmlListModelRole { name: "y"; elementName: "y" } } function deserialize() { dsIndex = 0 CreateObject.create(xmlModel.get(dsIndex).source, root, dsI } function dsItemAdded(obj, source) { itemAdded(obj, source) obj.x = xmlModel.get(dsIndex).x obj.y = xmlModel.get(dsIndex).y dsIndex++ if (dsIndex < xmlModel.count) { CreateObject.create(xmlModel.get(dsIndex).source, root, } } property int dsIndex Пример демонстрирует, как можно использовать модель для отслеживания созданных элементов и как легко сериализовать и десериализовать такую информацию. Это может быть использовано для хранения динамически заполняемой сцены, например набора виджетов. В примере для отслеживания каждого элемента использовалась модель. Альтернативным решением может быть использование свойства children корня сцены для отслеживания элементов. Однако для этого необходимо, чтобы сами элементы знать исходный URL для их повторного создания. Это также требует от нас реализации способа, позволяющего отличать динамически создаваемые элементы от
элементы, являющиеся частью исходной сцены, что позволяет избежать попыток сериализации и последующей десериализации исходных элементов.
Резюме В этой главе мы рассмотрели динамическое создание элементов QML. Это позволяет нам свободно создавать QML-сцены, открывая возможности для создания конфигурируемых пользователем архитектур и архитектур на основе подключаемых модулей. Самый простой способ динамической загрузки QML-элемента - это использование Loader элемент. Он служит в качестве заполнителя для загружаемого содержимого. Для более динамичного подхода можно использовать функцию Qt.createQmlObject для инстанцирования строки QML. Однако такой подход имеет свои ограничения. Полноценным решением является динамическое создание Компонента с помощью функции Qt.createComponent. createObject Затем объекты создаются путем вызова функции компонента. Поскольку привязки и сигнальные соединения зависят от существования идентификатора объекта или доступа к инстанциям объекта, для динамически создаваемых объектов необходимо использовать альтернативный подход. Для создания привязки используется элемент Binding. Элемент Connections позволяет подключаться к сигналам динамически создаваемого объекта. Одна из трудностей работы с динамически создаваемыми элементами заключается в том, чтобы отслеживать их. Это можно сделать с помощью модели. Имея модель, отслеживающую динамически создаваемые элементы, можно реализовать функции для сериализации и десериализации, что позволит хранить и восстанавливать динамически создаваемые сцены.
JavaScript JavaScript - это лингва-франка для разработки веб-клиентов. Он также начинает набирать обороты в разработке веб-серверов, в основном на базе node js. Поэтому он является подходящим дополнением к декларативному языку QML в качестве императивного языка. Сам QML как декларативный язык используется для выражения иерархии пользовательского интерфейса, но ограничен для выражения операционного кода. Иногда требуется способ выражения операций, и здесь на помощь приходит JavaScript. СОВЕТ В сообществе Qt существует открытый вопрос о правильном сочетании QML/JS/Qt C++ в современном Qt-приложении. По общему мнению, рекомендуется ограничить JS-часть приложения до минимума и выполнять бизнес-логику в Qt C++, а логику пользовательского интерфейса - в QML/JS. Эта книга раздвигает границы, что не всегда и не для всех является правильным сочетанием для разработки продукта. Важно следовать навыкам своей команды и своему личному вкусу. В случае сомнений следуйте рекомендациям. Приведем небольшой пример того, как выглядит JS, используемый в QML: Button { width: 200 height: 300 property bool checked: false text: "Click to toggle"
// JS function function doToggle() { checked = !checked } onClicked: { // this is also JavaScript doToggle(); console.log('checked: ' + checked) } } Таким образом, JavaScript может присутствовать в QML во многих местах: как отдельная JS-функция, как JS-модуль и как правая сторона привязки свойства. import "util.js" as Util // import a pure JS module Button { width: 200 height: width*2 // JS on the right side of property binding // standalone function (not really useful) function log(msg) { console.log("Button> " + msg); } onClicked: { // this is JavaScript log(); Qt.quit(); } } В QML вы декларируете пользовательский интерфейс, а с помощью JavaScript делаете его функциональным. Так сколько же JavaScript следует писать? Это зависит от вашего
стиль и насколько хорошо вы знакомы с разработкой на JS. JS - слабо типизированный язык, что затрудняет выявление дефектов типов. Кроме того, функции ожидают всех вариаций аргументов, что может быть очень неприятной ошибкой. Способом обнаружения дефектов является строгое модульное тестирование или приемочное тестирование. Поэтому, если вы разрабатываете реальную логику (а не несколько строк кода) на JS, вам следует начать использовать подход "тест-первый". Вообще, смешанные команды (Qt/C++ и QML/JS) очень успешны, когда они минимизируют количество JS во фронтенде в качестве доменной логики и делают всю тяжелую работу на Qt C++ в бэкенде. Бэкенд должен пройти строгое модульное тестирование, чтобы разработчики фронтенда могли доверять коду и сосредоточиться на всех этих мелких требованиях к пользовательскому интерфейсу. СОВЕТ В целом: backend-разработчики ориентированы на функциональность, а frontend-разработчики - на историю пользователя.
Браузер/HTML против Qt Quick/QML Браузер - это среда выполнения, в которой происходит рендеринг HTML и выполнение Javascript, связанного с HTML. В настоящее время современные веб-приложения содержат гораздо больше JavaScript, чем HTML. Javascript внутри браузера представляет собой стандартную среду ECMAScript с некоторыми дополнительными API браузера. Типичная среда JS в браузере имеет глобальный объект window, который используется для взаимодействия с окном браузера (заголовок, расположение URL, дерево DOM и т.д.) Браузеры предоставляют функции для доступа к узлам DOM по их id, классу и т.д. (которые были использованы jQuery для предоставления селекторов CSS), а в последнее время и к селекторам CSS ( querySelector , querySelectorAll ). Кроме того, существует возможность вызова функции через определенное время ( setTimeout ) и многократного вызова ( setInterval ). Кроме этих (и других браузерных API), среда аналогична QML/JS. Еще одно различие заключается в том, как JS может проявляться в HTML и QML. В HTML JS можно выполнять только при начальной загрузке страницы или в обработчиках событий (например, загрузка страницы, нажатие кнопки мыши). Например, ваш JS инициализируется обычно при загрузке страницы, что сравнимо с Component.onCompleted в QML. По умолчанию в браузере нельзя использовать JS для привязки свойств (AngularJS расширяет дерево DOM, позволяя использовать их, но это далеко от стандартного HTML). В QML язык JS является в большей степени гражданином первого класса и гораздо глубже интегрирован в дерево рендеринга QML. Это делает синтаксис гораздо более читаемым. Помимо этих различий,
люди, разрабатывавшие HTML/JS-приложения, должны чувствовать себя в QML/JS как дома.

Язык JS В этой главе не будет дано общее представление о JavaScript. Для общего ознакомления с JavaScript существуют и другие книги, пожалуйста, посетите этот замечательный раздел на сайте Mozilla Developer Network (https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_reintroduction_to_JavaScript) . На первый взгляд JavaScript - очень обычный язык и мало чем отличается от других языков: function countDown() { js for(var i=0; i<10; i++) { console.log('index: ' + i) } } function countDown2() { var i=10; while( i>0 ) { i--; } } Но учтите, что в JS есть область видимости функций, а не блоков, как в C++ (см. раздел Функции и область видимости функций (https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Functions_and_function_scope) ). Операторы if ... else, break, continue также работают как положено. В случае switch можно сравнивать и другие типы, а не только целочисленные значения: function getAge(name) { // switch over a string switch(name) { case "father": return 58; case "mother": return 56; js
} return unknown; } JS знает несколько значений, которые могут быть ложными, например, false , 0 , "" , undefined , null ). Например, функция по умолчанию возвращает значение undefined . Для проверки на false используется оператор тождества ===. Оператор равенства == выполняет преобразование типов для проверки равенства. По возможности используйте более быстрый и качественный оператор === strict equality, который проверяет тождество (см. Операторы сравнения (https://developer.mozilla.org/en-). US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) ). Под капотом у javascript есть свои собственные способы выполнения задач. Например, массивы: js function doIt() { var a = [] // empty arrays a.push(10) // addend number on arrays a.push("Monkey") // append string on arrays console.log(a.length) // prints 2 a[0] // returns 10 a[1] // returns Monkey a[2] // returns undefined a[99] = "String" // a valid assignment console.log(a.length) // prints 100 a[98] // contains the value undefined } Кроме того, для людей, пришедших из C++ или Java, которые привыкли к ОО-языкам, JS просто работает по-другому. JS не является в чистом виде ОО-языком, это так называемый язык, основанный на прототипах. Каждый объект имеет объект-прототип. Объект создается на основе своего объектапрототипа. Подробнее об этом можно прочитать в книге Javascript the Good Parts Дугласа Крокфорда (http://javascript.crockford.com) . Для тестирования небольших JS-фрагментов можно воспользоваться онлайнконсолью JS Console (http://jsconsole.com) или просто собрать небольшой кусочек QML-кода: import QtQuick 2.5 Item { function runJS() { console.log("Your JS code goes here"); } Component.onCompleted: {
runJS(); } }

JS-объекты При работе с JS есть некоторые объекты и методы, которые используются чаще всего. Здесь собрана небольшая их коллекция. Math.floor(v) , Math.ceil(v) , Math.round(v) - наибольшее, наименьшее, округленное целое число из float Math.random() - создание случайного числа в диапазоне от 0 до 1 Object.keys(o) - получение ключей из объекта (включая QObject) JSON.parse(s) , JSON.stringify(o) - преобразование между JS- объектом и JSON-строкой Number.toFixed(p) Дата - плавающая цифра фиксированной точности - манипуляция с датой Их также можно найти на сайте: Справочник по JavaScript (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference) Здесь приведены небольшие и ограниченные примеры использования JS в QML. Они должны дать вам представление о том, как можно использовать JS внутри QML Вывести все ключи из элемента QML Item { id: root Component.onCompleted: { var keys = Object.keys(root); for(var i=0; i<keys.length; i++) {
var key = keys[i]; // prints all properties, signals, functions from o console.log(key + ' : ' + root[key]); } } } Разбор объекта до JSON-строки и обратно Item { property var obj: { key: 'value' } Component.onCompleted: { var data = JSON.stringify(obj); console.log(data); var obj = JSON.parse(data); console.log(obj.key); // > 'value' } } Текущая дата Item { Timer { id: timeUpdater interval: 100 running: true repeat: true onTriggered: { var d = new Date(); console.log(d.getSeconds()); }
} } Вызов функции по имени Item { id: root function doIt() { console.log("doIt()") } Component.onCompleted: { // Call using function execution root["doIt"](); var fn = root["doIt"]; // Call using JS call method (could pass in a custom th fn.call() } }
Создание JS-консоли В качестве небольшого примера мы создадим JS-консоль. Нам необходимо поле ввода, в которое пользователь сможет вводить свои JS-выражения, а в идеале должен быть список выводимых результатов. Поскольку консоль должна быть больше похожа на настольное приложение, мы используем модуль Qt Quick Controls. СОВЕТ JS-консоль в вашем следующем проекте может быть очень полезна для тестирования. Дополненная эффектом QuakeTerminal, она также хороша для того, чтобы произвести впечатление на клиентов. Чтобы использовать ее с умом, необходимо контролировать область видимости, в которой оценивается JS-консоль, например, текущий экран, основная модель данных, синглтонный объект ядра или все вместе.
Мы используем Qt Creator для создания проекта Qt Quick UI с использованием элементов управления Qt Quick. Назовем проект JSConsole. После завершения работы мастера у нас уже есть базовая структура приложения с окном приложения и меню для выхода из него. Для ввода данных мы используем текстовое поле TextField и кнопку Button для отправки ввода на оценку. Результат оценки выражения отображается с помощью ListView с моделью ListModel в качестве модели и двумя метками для отображения выражения и результата оценки. Наше приложение будет разбито на два файла: : основной вид приложения JSConsole.qml jsconsole.js : библиотека javascript, отвечающая за оценку высказываний пользователя
JSConsole.qml Окно приложения // JSConsole.qml import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import "jsconsole.js" as Util ApplicationWindow { id: root title: qsTr("JSConsole") width: 640 height: 480 visible: true menuBar: MenuBar { Menu { title: qsTr("File") MenuItem { text: qsTr("Exit") onTriggered: Qt.quit() } } } Форма
ColumnLayout { anchors.fill: parent anchors.margins: 9 RowLayout { Layout.fillWidth: true TextField { id: input Layout.fillWidth: true focus: true onAccepted: { // call our evaluation function on root root.jsCall(input.text) } } Button { text: qsTr("Send") onClicked: { // call our evaluation function on root root.jsCall(input.text) } } } Item { Layout.fillWidth: true Layout.fillHeight: true Rectangle { anchors.fill: parent color: '#333' border.color: Qt.darker(color) opacity: 0.2 radius: 2 } ScrollView { id: scrollView anchors.fill: parent anchors.margins: 9 ListView { id: resultView
model: ListModel { id: outputModel } delegate: ColumnLayout { id: delegate required property var model width: ListView.view.width Label { Layout.fillWidth: true color: 'green' text: "> " + delegate.model.expression } Label { Layout.fillWidth: true color: delegate.model.error === "" ? 'b text: delegate.model.error === "" ? "" } Rectangle { height: 1 Layout.fillWidth: true color: '#333' opacity: 0.2 } } } } } } Вызов библиотеки Функция оценки jsCall выполняет оценку не сама по себе, для более четкого разделения она была перенесена в JS-модуль (jsconsole.js). import "jsconsole.js" as Util
function jsCall(exp) { const data = Util.call(exp) // insert the result at the beginning of the list outputModel.insert(0, data) } СОВЕТ Для безопасности мы не используем функцию eval из JS, поскольку это позволило бы пользователю изменять локальную область видимости. Мы используем конструктор Function для создания JS-функции во время выполнения и передаем нашу область видимости в качестве этой переменной. Поскольку функция создается каждый раз, она не действует как закрытие и хранит свою собственную область видимости, нам нужно использовать this.a = 10, чтобы сохранить значение внутри этой области видимости функции. Эта область видимости устанавливается скриптом в переменную scope. jsconsole.js // jsconsole.js js .pragma library const scope = { // our custom scope injected into our function evaluation } function call(msg) { const exp = msg.toString() console.log(exp) const data = { expression : msg, result: "", error: "" }
try { const fun = new Function('return (' + exp + ')') data.result = JSON.stringify(fun.call(scope), null, 2) console.log('scope: ' + JSON.stringify(scope, null, 2), } catch(e) { console.log(e.toString()) data.error = e.toString() } return data } Данные, возвращаемые из функции вызова, представляют собой JSобъект со свойствами result, expression и error: data: { expression: "", result: "", error: "" } . Мы можем использовать этот JS- объект непосредственно внутри ListModel и обращаться к нему затем из делегата, например, delegate.model.expression дает нам входное выражение.
Qt и C++ Qt - это инструментарий на языке C++ с расширением для QML и Javascript. Существует множество языковых связок для Qt, но поскольку сам Qt разработан на C++. Дух C++ можно обнаружить во всех классах. В этом разделе мы рассмотрим Qt с точки зрения C++, чтобы лучше понять, как расширить QML с помощью собственных подключаемых модулей, разработанных на C++. С помощью C++ можно расширять и контролировать среду выполнения, предоставляемую QML. Эта глава, как и Qt, потребует от читателя базовых знаний языка C++. Qt не опирается на продвинутые возможности C++, и я вообще считаю стиль C++ в Qt очень удобным для чтения, поэтому не беспокойтесь, если вы чувствуете, что ваши знания C++ шаткие.
Qt C++ Если подойти к Qt с точки зрения языка C++, то можно обнаружить, что Qt обогащает C++ рядом современных языковых возможностей, которые обеспечиваются за счет доступности данных интроспекции. Это стало возможным благодаря использованию базового класса QObject. Данные интроспекции, или метаданные, сохраняют информацию о классах во время выполнения, чего не делает обычный C++. Это позволяет динамически запрашивать у объектов информацию о таких деталях, как их свойства и доступные методы. Qt использует эту метаинформацию для реализации очень слабо связанной концепции обратного вызова с использованием сигналов и слотов. Каждый сигнал может быть связан с любым количеством слотов или даже других сигналов. Когда от экземпляра объекта исходит сигнал, вызываются связанные с ним слоты. Поскольку объекту, излучающему сигнал , не нужно ничего знать об объекте, владеющем слотом, и наоборот, этот механизм используется для создания очень многократно используемых компонентов с очень малым количеством межкомпонентных зависимостей. Qt для Python Возможности интроспекции также используются для создания динамических привязок к языку , позволяя отобразить экземпляр объекта C++ в QML и сделать функции C++ вызываемыми из Javascript. Существуют и другие привязки для Qt C++. Кроме стандартной привязки для Javascript, официальной является привязка для Python, называемая PySide6 (https://www.qt.io/qt-for-python) . Кросс-платформа
В дополнение к этой центральной концепции Qt делает возможной разработку кроссплатформенных приложений с использованием языка C++. Qt C++ обеспечивает абстракцию платформы на различных операционных системах, что позволяет разработчику сконцентрироваться на поставленной задаче, а не на деталях того, как открыть файл в разных операционных системах. Это означает, что вы можете перекомпилировать один и тот же исходный код для Windows, OS X и Linux, а Qt позаботится о различных способах работы ОС с определенными вещами. В итоге получаются встроенные приложения с внешним видом, соответствующим целевой платформе. Поскольку мобильный компьютер - это новый рабочий стол, новые версии Qt также могут работать с несколькими мобильными платформами, используя один и тот же исходный код, например, iOS, Android, Jolla, BlackBerry, Ubuntu Phone, Tizen. Что касается повторного использования, то не только исходный код может быть использован повторно, но и навыки разработчиков также могут быть использованы повторно. Команда, знающая Qt, может охватить гораздо больше платформ, чем команда, ориентированная только на одну платформу, а поскольку Qt очень гибок, команда может создавать различные компоненты системы, используя одну и ту же технологию. Для всех платформ Qt предлагает набор базовых типов, например, строки с полной поддержкой Unicode, списки, векторы, буферы. Кроме того, он предоставляет общую абстракцию для главного цикла целевой платформы, а также кроссплатформенную поддержку потоков и сетей. Общая философия заключается в том, что для разработчика приложений Qt поставляется со всей необходимой функциональностью. Для решения специфических задач, таких как взаимодействие с родными библиотеками, Qt поставляется с несколькими вспомогательными классами, облегчающими эту задачу.
Шаблонное приложение Лучший способ понять Qt - начать с небольшого примера. Это приложение создает простую строку "Hello World!" и записывает ее в файл с использованием символов Unicode. #include <QCoreApplication> #include <QString> #include <QFile> #include <QDir> #include <QTextStream> #include <QDebug> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); // prepare the message QString message("Hello World!"); // prepare a file in the users home directory named out.txt QFile file(QDir::home().absoluteFilePath("out.txt")); // try to open the file in write mode if(!file.open(QIODevice::WriteOnly)) { qWarning() << "Can not open file with write access"; return -1; } // as we handle text we need to use proper text codecs QTextStream stream(&file); // write message to file via the text stream stream << message;
// do not start the eventloop as this would wait for extern // app.exec(); // no need to close file, closes automatically when scope e return 0; } Пример демонстрирует использование доступа к файлам и запись текста в файл с помощью текстовых кодеков, используя текстовый поток. Для двоичных данных существует кроссплатформенный двоичный поток QDataStream, который заботится о концевых значениях и других деталях. Различные классы, которые мы используем, включаются в файл с помощью имени класса в верхней части файла. Вы также можете включить классы, используя имя модуля и класса, например, #include <QtCore/QFile> . Для ленивых есть возможность включить все классы из модуля с помощью команды #include <QtCore> . Например, в QtCore н а х о д я т с я наиболее распространенные классы, используемые в приложении и не связанные с пользовательским интерфейсом. Посмотрите список классов QtCore (http://doc.qt.io/qt-5/qtcore-module.html) или обзор QtCore (http://doc.qt.io/qt-5/qtcore-index.html) . Сборка приложения выполняется с помощью CMake и make. CMake считывает файл проекта, CMakeLists.txt, и генерирует Makefile, который используется для сборки приложения. CMake поддерживает и другие системы сборки, например, ninja. Файл проекта не зависит от платформы, и CMake имеет некоторые правила для применения специфических для платформы настроек к генерируемому makefile. Проект также может содержать платформенные диапазоны для правил, специфичных для конкретной платформы, которые требуются в некоторых специфических случаях. Ниже приведен пример простого файла проекта, сгенерированного Qt Creator. Обратите внимание, что Qt пытается создать файл, совместимый как с Qt 5, так и с Qt 6, а также с различными платформами, такими как Android, OS X и т.д. sh
cmake_minimum_required(VERSION 3.14) project(projectname VERSION 0.1 LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # QtCreator supports the following variables for Android, which # Check https://doc.qt.io/qt/deployment-android.html for more i # They need to be set before the find_package(...) calls below. #if(ANDROID) # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR # if (ANDROID_ABI STREQUAL "armeabi-v7a") # set(ANDROID_EXTRA_LIBS # ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libcrypto.so # ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libssl.so) # endif() #endif() find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Quick REQUIRED) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Quick REQUIR set(PROJECT_SOURCES main.cpp qml.qrc ) if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) qt_add_executable(projectname MANUAL_FINALIZATION ${PROJECT_SOURCES} ) else() if(ANDROID) add_library(projectname SHARED ${PROJECT_SOURCES}
) else() add_executable(projectname ${PROJECT_SOURCES} ) endif() endif() target_compile_definitions(projectname PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_Q target_link_libraries(projectname PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Qu set_target_properties(projectname PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR} ) if(QT_VERSION_MAJOR EQUAL 6) qt_import_qml_plugins(projectname) qt_finalize_executable(projectname) endif() Мы не будем углубляться в глубины этого файла. Просто запомните, что Qt использует файлы CMakeLists.txt для генерации специфических для платформы make-файлов, которые затем используются для сборки проекта. В разделе "Система сборки" мы рассмотрим более простые, рукописные файлы CMake. Приведенный выше пример просто записывает текст и выходит из приложения. Для инструмента командной строки этого достаточно. Для пользовательского интерфейса необходим цикл событий, который ожидает ввода пользователя и каким-то образом планирует операции рисования. Ниже приведен тот же пример, но теперь для запуска записи используется кнопка. Наш main.cpp удивительным образом стал меньше. Мы перенесли код в отдельный класс, чтобы иметь возможность использовать сигналы и слоты Qt для пользовательского ввода, т.е. для обработки
щелчок по кнопке. Механизм сигналов и слотов обычно требует наличия экземпляра объекта, как вы увидите в ближайшее время, но он также может быть использован с ламбдами языка Си++. #include <QtCore> #include <QtGui> #include <QtWidgets> #include "mainwindow.h" int main(int argc, char** argv) { QApplication app(argc, argv); MainWindow win; win.resize(320, 240); win.setVisible(true); return app.exec(); } В функции main мы создаем объект приложения - окно, а затем запускаем цикл событий с помощью функции exec() . Пока что приложение находится в цикле событий и ожидает ввода данных пользователем. int main(int argc, char** argv) { QApplication app(argc, argv); // init application // create the ui return app.exec(); // execute event loop } Используя Qt, можно создавать пользовательские интерфейсы как на QML, так и на Widgets. В этой книге мы сосредоточимся на QML, но в этой главе мы рассмотрим виджеты. Это позволяет
Мы создаем программу только на языке C++. Само главное окно является виджетом. Оно становится окном верхнего уровня, поскольку не имеет родителя. Это происходит из-за того, что Qt рассматривает пользовательский интерфейс как дерево элементов пользовательского интерфейса. В данном случае главное окно является корневым элементом, поэтому становится окном, а кнопка, являющаяся дочерним элементом главного окна, становится виджетом внутри окна. #ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QtWidgets> class MainWindow : public QMainWindow { public: MainWindow(QWidget* parent=0); ~MainWindow(); public slots: void storeContent(); private: QPushButton *m_button; }; #endif // MAINWINDOW_H
Кроме того, мы определяем публичный слот storeContent() в пользовательской секции заголовочного файла. Слоты могут быть public, protected или private и могут вызываться так же, как и любой другой метод класса. Вы также можете встретить секцию signals с набором сигнатур сигналов. Эти методы должны не вызываются и не должны быть реализованы. Как сигналы, так и слоты обрабатываются метаинформационной системой Qt и могут быть интроспективно обнаружены и динамически вызваны во время выполнения программы. Назначение слота storeContent() состоит в том, чтобы он вызывался при нажатии на кнопку. Давайте сделаем так, чтобы это произошло! #include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_button = new QPushButton("Store Content", this); setCentralWidget(m_button); connect(m_button, &QPushButton::clicked, this, &MainWindow: } MainWindow::~MainWindow() { } void MainWindow::storeContent() { qDebug() << "... store content"; QString message("Hello World!"); QFile file(QDir::home().absoluteFilePath("out.txt")); if(!file.open(QIODevice::WriteOnly)) { qWarning() << "Can not open file with write access"; return; }
QTextStream stream(&file); stream << message; } В главном окне мы сначала создаем кнопку, а затем регистрируем сигнал clicked() в слоте storeContent() с помощью метода connect. Каждый раз, когда раздается сигнал clicked, в ы з ы в а е т с я слот storeContent(). И теперь связь между двумя объектами осуществляется через сигналы и слоты, несмотря на то, что они не знают друг о друге. Это называется свободной связью и становится возможным благодаря использованию базового класса QObject, от которого происходит большинство классов Qt.
Объект QObject Как было описано во введении, QObject позволяет реализовать многие основные функции Qt, такие как сигналы и слоты. Это реализуется с помощью интроспекции, которую и о б е с п е ч и в а е т QObject. QObject является базовым классом почти всех классов в Qt. Исключение составляют такие типы значений, как QColor , QString и QList . Объект Qt - это стандартный объект C++, но с дополнительными возможностями. Их можно разделить на две группы: интроспекция и управление памятью. Первое означает, что объект Qt знает имя своего класса, его отношение к другим классам, а также свои методы и свойства. Концепция управления памятью означает, что каждый объект Qt может быть родителем дочерних объектов. Родитель владеет дочерними объектами, и когда родитель уничтожается, он отвечает за уничтожение своих дочерних объектов. Лучший способ понять, как способности QObject влияют на класс, это взять стандартный класс C++ и включить в него Qt. Приведенный ниже класс представляет собой обычный такой класс. Класс person представляет собой класс данных со свойствами name и gender. Класс person использует объектную систему Qt для добавления метаинформации к классу c++. Это позволяет пользователям объекта person подключаться к слотам и получать уведомления об изменении свойств. class Person : public QObject { Q_OBJECT // enabled meta object abilities // property declarations required for QML
Q_PROPERTY(QString name READ name WRITE setName NOTIFY name Q_PROPERTY(Gender gender READ gender WRITE setGender NOTIFY // enables enum introspections Q_ENUM(Gender) // makes the type creatable in QML QML_ELEMENT public: // standard Qt constructor with parent for memory managemen Person(QObject *parent = 0); enum Gender { Unknown, Male, Female, Other }; QString name() const; Gender gender() const; public slots: // slots can be connected to signals, or called void setName(const QString &); void setGender(Gender); signals: // signals can be emitted void nameChanged(const QString &name); void genderChanged(Gender gender); private: // data members QString m_name; Gender m_gender; }; Конструктор передает родителя суперклассу и инициализирует его члены. Классы значений Qt инициализируются автоматически. В данном случае QString будет инициализироваться нулевой строкой ( QString::isNull() ), а член gender будет явно инициализирован неизвестным полом.
Person::Person(QObject *parent) : QObject(parent) , m_gender(Person::Unknown) { } Функция getter называется по имени свойства и обычно является базовой функцией const. При изменении свойства сеттер выдает сигнал changed. Чтобы убедиться, что значение действительно изменилось, мы вставляем защитную функцию, которая сравнивает текущее значение с новым. Только когда значение отличается, мы присваиваем его переменной-члену и подаем сигнал change. QString Person::name() const { return m_name; } void Person::setName(const QString &name) { if (m_name != name) // guard { m_name = name; emit nameChanged(m_name); } } Имея класс, производный от QObject, мы получили дополнительные возможности метаобъекта, которые можно исследовать с помощью метода metaObject(). Например, получение имени класса из объекта. Person* person = new Person(); person->metaObject()->className(); // "Person" Person::staticMetaObject.className(); // "Person"
Существует множество других возможностей, доступ к которым можно получить с помощью базового класса QObject и метаобъекта. Пожалуйста, ознакомьтесь с документацией по QMetaObject. СОВЕТ QObject , а макрос Q_OBJECT имеет облегченного собрата: Q_GADGET . Макрос Q_GADGET может быть вставлен в секцию private классов, не являющихся производными от QObject, для раскрытия свойств и вызываемых методов. Следует иметь в виду, что объект Q_GADGET не может иметь сигналов, поэтому свойства не могут предоставлять сигнал уведомления об изменении. Тем не менее, это может быть полезно для обеспечения QML-подобного интерфейса к структурам данных, передаваемым из C++ в QML, без затрат на полноценный QObject.
Построение систем Создание надежного программного обеспечения на различных платформах может оказаться сложной задачей. Вы столкнетесь с различными средами с разными компиляторами, путями и вариантами библиотек. Задача Qt - оградить разработчика приложений от этих кроссплатформенных проблем. Qt опирается на CMake (https://cmake.org/) для преобразования файлов проекта CMakeLists.txt в файлы make для конкретной платформы, которые затем могут быть собраны с помощью инструментария для конкретной платформы. СОВЕТ Qt поставляется с тремя различными системами сборки. Оригинальная система сборки Qt называлась qmake . Другой системой сборки, специфичной для Qt, является QBS, которая использует декларативный подход к описанию последовательности сборки. Начиная с версии 6, Qt перешел от qmake к CMake в качестве официальной системы сборки. Типичный поток сборки в Qt под Unix выглядит следующим образом: vim CMakeLists.txt sh cmake . // generates Makefile make В Qt рекомендуется использовать теневые сборки. Теневая сборка это сборка вне места расположения исходного кода. Предположим, что у нас есть папка myproject с файлом CMakeLists.txt. Процесс будет выглядеть следующим образом:
sh mkdir build cd build cmake .. Мы создаем папку build и затем вызываем cmake из папки build с указанием местоположения папки нашего проекта. При этом makefile будет настроен таким образом, что все артефакты сборки будут храниться в папке сборки, а не в папке исходного кода. Это позволит нам одновременно создавать сборки для различных версий qt и конфигураций сборки, а также не загромождать папку с исходным кодом, что всегда хорошо. Когда вы используете Qt Creator, он выполняет все эти действия за вас, и в большинстве случаев вам не нужно беспокоиться об этих шагах. Для больших проектов и для более глубокого понимания процесса рекомендуется научиться собирать свой Qt-проект из командной строки , чтобы обеспечить полный контроль над происходящим. CMake CMake - это инструмент, созданный компанией Kitware. Компания Kitware известна своим программным обеспечением для 3D-визуализации VTK, а также CMake, кроссплатформенным генератором makefile. Он использует серию файлов CMakeLists.txt для генерации make-файлов, специфичных для конкретной платформы. CMake используется в проекте KDE и, соответственно, имеет особые отношения с сообществом Qt и, начиная с версии 6, является предпочтительным способом сборки проектов Qt. CMakeLists.txt - э т о файл, используемый для хранения конфигурации проекта. Для простого hello world, использующего Qt Core, файл проекта будет выглядеть следующим образом: // ensure cmake version is at least 3.16.0 cmake_minimum_required(VERSION 3.16.0)
// определяет проект с версией project(foundation_tests VERSION 1.0.0 LANGUAGES CXX) // выбираем используемый стандарт C++, в данном случае C++17 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) // указать CMake на автоматический запуск Qt-инструментов moc, rcc и uic set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) // настраиваем модули Qt 6 core и test find_package(Qt6 COMPONENTS Core REQUIRED) find_package(Qt6 COMPONENTS Test REQUIRED) // определить исполняемый файл, собранный из исходного файла add_executable(foundation_tests tst_foundation.cpp ) // указываем cmake, что нужно связать исполняемый файл с ядром Qt 6 и тестом target_link_libraries(foundation_tests PRIVATE Qt6::Core Qt6::T В результате будет создан исполняемый файл foundations_tests, использующий tst_foundation.cpp и установить связь с библиотеками Core и Test из Qt 6. В этой книге вы найдете больше примеров файлов CMake, поскольку мы используем CMake для всех примеров, основанных на C++. CMake - мощный, но сложный инструмент, и требуется некоторое время, чтобы привыкнуть к его синтаксису. CMake очень гибкий инструмент, и в больших и сложных проектах он проявляет себя с лучшей стороны. Ссылки
CMake Help (http://www.cmake.org/documentation/) - доступен в Интернете, а также в формате Qt Help Запуск CMake (http://www.cmake.org/runningcmake/) KDE CMake Tutorial (https://techbase.kde.org/Development/Tutorials/CMake) Книга CMake (http://www.kitware.com/products/books/CMakeBook.html) CMake и Qt (http://www.cmake.org/cmake/help/v3.0/manual/cmakeqt.7.html) QMake QMake - это инструмент, который считывает файл проекта и генерирует файл сборки. Файл проекта - это упрощенная запись конфигурации вашего проекта, внешние зависимости, а также ваши исходные файлы. Самый простой файл проекта, вероятно, выглядит так: js // myproject.pro SOURCES += main.cpp Здесь мы собираем исполняемое приложение, которое будет иметь имя myproject, основанное на имени файла проекта. Сборка будет содержать только исходный файл main.cpp. И по умолчанию для этого проекта мы будем использовать модуль QtCore и QtGui. Если бы наш проект был QML-приложением, то мы необходимо добавить в список модули QtQuick и QtQml: js // myproject.pro QT += qml quick
SOURCES += main.cpp Теперь файл сборки знает, что нужно связать с модулями QtQml и QtQuick Qt. QMake использует понятия = , += и -= для назначения, добавления, удаления элементов из списка опций соответственно. Для чисто консольной сборки без зависимостей от пользовательского интерфейса необходимо удалить модуль QtGui: js // myproject.pro QT -= gui SOURCES += main.cpp Если требуется собрать не приложение, а библиотеку, необходимо изменить шаблон сборки: js // myproject.pro TEMPLATE = lib QT -= gui HEADERS += utils.h SOURCES += utils.cpp Теперь проект будет собираться как библиотека без зависимостей от пользовательского интерфейса с использованием заголовка utils.h и исходного файла utils.cpp. Формат библиотеки будет зависеть от ОС, на которой вы собираете проект. Часто возникают более сложные ситуации, когда необходимо собрать набор проектов. Для этого qmake предлагает шаблон subdirs. Предположим, что у нас есть проект mylib и проект myapp. Тогда наша установка может выглядеть следующим образом:
js my.pro mylib/mylib.pro mylib/utils.h mylib/utils.cpp myapp/myapp.pro myapp/main.cpp Мы уже знаем, как будут выглядеть mylib.pro и myapp.pro. Файл my.pro как общий файл проекта будет выглядеть следующим образом: js // my.pro TEMPLATE = subdirs subdirs = mylib \ myapp myapp.depends = mylib Здесь объявляется проект с двумя подпроектами: mylib и myapp, где myapp зависит от mylib. При запуске qmake на этом файле проекта будет сгенерирован файл сборки для каждого проекта в соответствующей папке. При запуске makefile для my.pro все подпроекты также будут собраны. Иногда требуется сделать одно на одной платформе и другое на других платформах в зависимости от конфигурации. Для этого в qmake введено понятие диапазонов. Область применяется, когда опция конфигурации установлена в true. Например, для использования специфической для Unix реализации utils можно использовать: js unix { SOURCES += utils_unix.cpp } else {
SOURCES += utils.cpp } В нем говорится, что если переменная CONFIG содержит опцию Unix, то следует применить эту область, в противном случае используется другой путь. Типичный пример - удаление обвязки приложений под mac: js macx { CONFIG -= app_bundle } В результате приложение будет создано как обычный исполняемый файл под mac, а не как папка .app, которая используется при установке приложений. Проекты на основе QMake обычно являются выбором номер один, когда вы начинаете программировать Qt-приложения. Существуют и другие варианты. Все они имеют свои преимущества и недостатки. Эти варианты мы рассмотрим далее. Ссылки Руководство по QMake (http://doc.qt.io/qt-5//qmake-manual.html) Оглавление руководства по qmake Язык QMake (http://doc.qt.io/qt-5//qmake-language.html) - присвоение значений, области видимости и т.п. Переменные QMake (http://doc.qt.io/qt-5//qmake-variable-reference.html) - Переменные типа TEMPLATE, CONFIG, QT описаны здесь
Общие классы Qt Большинство классов Qt являются производными от класса QObject. В нем заключены центральные концепции Qt. Однако в фреймворке существует гораздо больше классов. Прежде чем продолжить рассмотрение QML и способов его расширения, мы рассмотрим некоторые основные классы Qt, о которых полезно знать. Примеры кода, приведенные в этом разделе, написаны с использованием библиотеки Qt Test. Таким образом, мы можем убедиться в работоспособности кода, не строя вокруг него целые программы. Функции QVERIFY и QCOMPARE из библиотеки Test служат для утверждения определенного условия. Мы будем использовать диапазоны {}, чтобы избежать коллизий имен. Не позволяйте этому сбить вас с толку. QString В целом, работа с текстом в Qt основана на Unicode. Для этого используется класс QString. Он поставляется с множеством замечательных функций, которые можно ожидать от современного фреймворка. Для 8-битных данных обычно используется класс QByteArray, а для ASCII-идентификаторов - QLatin1String для экономии памяти. Для списка строк можно использовать QList<QString> или просто класс QStringList (который является производным от QList<QString> ). Ниже приведены примеры использования класса QString. QString может быть создан в стеке, но хранит свои данные в куче. Кроме того, при присваивании одной строки другой данные не копируются только ссылка на них. Таким образом, это очень дешево и позволяет разработчику сосредоточиться на коде, а не на работе с памятью. QString использует счетчики ссылок, чтобы знать, когда данные можно безопасно удалить. Это
Функция называется Implicit Sharing (http://doc.qt.io/qt-6/implicitsharing.html) и используется во многих классах Qt. QString data("A,B,C,D"); // создание простой строки // разбить на части QStringList list = data.split(","); // создаем новую строку из частей QString out = list.join(","); // проверить, что оба значения одинаковы QVERIFY(data == out); // изменить первый символ на верхний регистр QVERIFY(QString("A") == out[0].toUpper()); Ниже показано, как преобразовать число в строку и обратно. Существуют также функции преобразования для float, double и других типов. Просто найдите в документации Qt использованную здесь функцию, и вы найдете другие. // создаем некоторые переменные int v = 10; int base = 10; // преобразование int в строку QString a = QString::number(v, base); // и обратно, используя и устанавливая значение ok в true при успехе bool ok(false); int v2 = a.toInt(&ok, base); // проверяем результаты QVERIFY(ok == true); QVERIFY(v = v2); Часто в тексте необходимо иметь параметризованный текст. Одним из вариантов может быть использование QString("Hello" + name), но более гибким методом является подход с использованием маркеров arg. Он сохраняет порядок и при переводе, когда порядок может измениться.
// создаем имя QString name("Joe"); // получить день недели в виде строки QString weekday = QDate::currentDate().toString("dddd"); // форматирование текста с использованием параметров (%1, %2) QString hello = QString("Здравствуйте %1. Сегодня %2.").arg(name).arg // Это сработало в понедельник. Обещаю! if(Qt::Monday == QDate::currentDate().dayOfWeek()) { QCOMPARE(QString("Hello Joe. Today is Monday."), hello); } else { QVERIFY(QString("Привет, Джо. Сегодня понедельник.") != hello); } Иногда требуется использовать символы Unicode непосредственно в коде. Для этого необходимо вспомнить, как их обозначать для классов QChar и QString. // Создаем юникодный символ, используя юникод для smile :-) QChar smile(0x263A); // вы должны увидеть на консоли символ :-) qDebug() << smile; // Использование юникода в строке QChar smile2 = QString("\u263A").at(0); QVERIFY(smile == smile2); // Создаем 12 смайлов в векторе QVector<QChar> smilies(12); smilies.fill(smile); // Видите ли вы смайлы qDebug() << smilies; Это дает вам несколько примеров того, как можно легко обрабатывать текст в Qt с поддержкой Unicode. Для неюникодных текстов класс QByteArray также имеет множество вспомогательных функций для преобразования. Пожалуйста, прочитайте документацию Qt по QString, так как она содержит множество хороших примеров.
Последовательные контейнеры Список, очередь, вектор или связный список - это последовательный контейнер. Наиболее часто используемым последовательным контейнером является класс QList. Он является классом, основанным на шаблоне, и должен быть инициализирован типом. Он также является неявно разделяемым и хранит данные внутри кучи. Все контейнерные классы должны создаваться на стеке. Обычно вы никогда не хотите использовать new QList<T>(), что означает, что вы никогда не используете new с контейнером. QList так же универсален, как и класс QString, и предлагает отличный API для работы с данными. Ниже приведен небольшой пример использования списка и его итерации с использованием некоторых новых возможностей C++ 11. // Создание простого списка интов с использованием нового инициализатора C++11 // для этого в файл pro необходимо добавить "CONFIG += c++11". QList<int> list{1,2}; // добавляем еще один список int list << 3; // Мы используем диапазоны, чтобы избежать столкновения имен переменных { // итерация по списку с помощью Qt for each int sum(0); foreach (int v, list) { sum += v; } QVERIFY(sum == 6); } { // итерация по списку с помощью цикла на основе диапазона в C++ 11 int sum = 0; for(int v : list) { sum+= v;
} QVERIFY(sum == 6); } { // итерация по списку с использованием итераторов в стиле JAVA int sum = 0; QListIterator<int> i(list); while (i.hasNext()) { sum += i.next(); } QVERIFY(sum == 6); } { // итерация по списку с использованием итератора в стиле STL int sum = 0; QList<int>::iterator i; for (i = list.begin(); i != list.end(); ++i) { sum += *i; } QVERIFY(sum == 6); } // использование std::sort с мутабельным итератором на C++11 // список будет отсортирован в порядке убывания std::sort(list.begin(), list.end(), [](int a, int b) { return a QVERIFY(list == QList<int>({3,2,1})); int value = 3; { // использование std::find с const итератором QList<int>::const_iterator result = std::find(list.constBeg QVERIFY(*result == value); } { // использование std::find с использованием лямбд C++ и автопеременной C++ 11 auto result = std::find_if(list.constBegin(), list.constBeg QVERIFY(*result == value); }
Ассоциативные контейнеры Примерами ассоциативных контейнеров являются карта, словарь или набор. Они хранят значение с помощью ключа. Они известны своей скоростью поиска. Мы продемонстрируем использование самого распространенного ассоциативного контейнера QHash, а также покажем некоторые новые возможности C++ 11. QHash<QString, int> hash({{"b",2},{"c",3},{"a",1}}); qDebug() << hash.keys(); // a,b,c - неупорядоченные qDebug() << hash.values(); // 1,2,3 - неупорядоченные, но такие же, как ord QVERIFY(hash["a"] == 1); QVERIFY(hash.value("a") == 1); QVERIFY(hash.contains("c") == true); { // JAVA итератор int sum =0; QHashIterator<QString, int> i(hash); while (i.hasNext()) { i.next(); sum+= i.value(); qDebug() << i.key() << " = " << i.value(); } QVERIFY(sum == 6); } { // STL итератор int sum = 0; QHash<QString, int>::const_iterator i = hash.constBegin(); while (i != hash.constEnd()) { sum += i.value(); qDebug() << i.key() << " = " << i.value(); i++; }
QVERIFY(sum == 6); } hash.insert("d", 4); QVERIFY(hash.contains("d") == true); hash.remove("d"); QVERIFY(hash.contains("d") == false); { // поиск хэша не удался QHash<QString, int>::const_iterator i = hash.find("e"); QVERIFY(i == hash.end()); } { // успешный поиск хэша QHash<QString, int>::const_iterator i = hash.find("c"); while (i != hash.end()) { qDebug() << i.value() << " = " << i.key(); i++; } } // QMap QMap<QString, int> map({{"b",2},{"c",2},{"a",1}}); qDebug() << map.keys(); // a,b,c - в порядке возрастания QVERIFY(map["a"] == 1); QVERIFY(map.value("a") == 1); QVERIFY(map.contains("c") == true); // Итераторы JAVA и STL работают так же, как и QHash Файловый ввод-вывод Он часто требуется для чтения и записи из файлов. QFile фактически является QObject, но в большинстве случаев он создается на стеке. QFile содержит сигналы, информирующие пользователя о том, когда данные могут быть прочитаны. Это позволяет читать фрагменты данных асинхронно, пока не будет прочитан весь файл. Для удобства,
также позволяет читать данные в блокирующем режиме. Это следует использовать только для небольших объемов данных, а не для больших файлов. К счастью, в этих примерах мы используем только небольшие объемы данных. Помимо чтения исходных данных из файла в массив QByteArray можно также читать типы данных с помощью QDataStream и строки Unicode с помощью QTextStream . Мы покажем вам, как это сделать. QStringList data({"a", "b", "c"}); { // запись бинарных файлов QFile file("out.bin"); if(file.open(QIODevice::WriteOnly)) { QDataStream stream(&file); stream << data; } } { // чтение бинарного файла QFile file("out.bin"); if(file.open(QIODevice::ReadOnly)) { QDataStream stream(&file); QStringList data2; stream >> data2; QCOMPARE(data, data2); } } { // запись текстового файла QFile file("out.txt"); if(file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); QString sdata = data.join(","); stream << sdata; } } { // чтение текстового файла QFile file("out.txt"); if(file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); QStringList data2;
QString sdata; stream >> sdata; data2 = sdata.split(","); QCOMPARE(data, data2); } } Другие классы Qt - это богатый фреймворк приложений. Как таковой, он содержит тысячи классов. Требуется некоторое время, чтобы освоиться со всеми этими классами и понять, как их использовать. К счастью, Qt имеет очень хорошую документацию с множеством полезных примеров. В большинстве случаев при поиске класса наиболее часто встречающиеся примеры использования уже представлены в виде фрагментов. Это означает, что вы просто копируете и адаптируете эти фрагменты. Кроме того, большую помощь оказывают примеры Qt в исходном коде Qt. Убедитесь, что они доступны и доступны для поиска, чтобы сделать свою жизнь более продуктивной. Не теряйте времени. Сообщество Qt всегда готово помочь. Когда вы спрашиваете, очень полезно задавать точные вопросы и приводить простой пример, отражающий ваши потребности. Это значительно увеличит время ответа других пользователей. Так что потратьте немного времени, чтобы облегчить жизнь тем, кто хочет вам помочь �. Здесь представлены некоторые классы, документацию по которым автор считает обязательной для прочтения: QObject , QString , QByteArray QFile , QDir , QFileInfo , QIODevice QTextStream , QDataStream QDebug , QLoggingCategory QTcpServer , QTcpSocket , QNetworkRequest , QNetworkReply QAbstractItemModel , QRegularExpression QList , QHash QThread , QProcess
QJsonDocument , QJSValue Для начала этого должно быть достаточно.
Модели в C++ Одним из наиболее распространенных способов интеграции C++ и QML являются модели. Модель предоставляет данные представлению, такому как ListViews, GridView, PathViews и другие представления, которые берут модель и создают экземпляр делегата для каждой записи в модели. Представление достаточно интеллектуально, чтобы создавать только те экземпляры, которые видны или находятся в диапазоне кэша. Это позволяет иметь большие модели с десятками тысяч записей, но при этом иметь очень удобный пользовательский интерфейс. Делегат действует как шаблон, на который выводятся данные записей модели. Итак, вкратце: представление отображает записи из модели, используя делегат в качестве шаблона. Модель является поставщиком данных для представлений. Если вы не хотите использовать язык C++, вы также можете определять модели на чистом QML. У вас есть несколько способов предоставить модель для представления. Для работы с данными, поступающими из C++, или с большим объемом данных модель C++ подходит больше, чем этот чистый QML-подход. Но часто вам нужно всего несколько записей, тогда эти QML-модели вполне подходят. ListView { // using a integer as model model: 5 delegate: Text { text: 'index: ' + index } } ListView { // using a JS array as model model: ['A', 'B', 'C', 'D', 'E'] delegate: Text { 'Char['+ index +']: ' + modelData } }
ListView { // using a dynamic QML ListModel as model model: ListModel { ListElement { char: 'A' } ListElement { char: 'B' } ListElement { char: 'C' } ListElement { char: 'D' } ListElement { char: 'E' } } delegate: Text { 'Char['+ index +']: ' + model.char } } Представления QML знают, как работать с этими различными типами моделей. Для моделей, пришедших из мира C++, представление ожидает, что будет использоваться определенный протокол. Этот протокол определяется в API, определенном в QAbstractItemModel, вместе с документацией, описывающей динамическое поведение. API был разработан для управления представлениями в мире виджетов для настольных компьютеров и является достаточно гибким, чтобы служить основой для деревьев или многоколоночных таблиц, а также списков. В QML мы обычно используем либо списочный вариант API, QAbstractListModel, либо, для элемента TableView, табличный вариант API, QAbstractTableModel. API содержит некоторые функции, которые должны быть реализованы, и некоторые опциональные, расширяющие возможности модели. Опциональные части в основном предназначены для динамических случаев использования для изменения, добавления или удаления данных. Простая модель Типичная модель QML C++ является производной от QAbstractListModel rowCount. и реализует, как минимум, функции data и В приведенном ниже примере мы будем использовать серию имен цветов SVG, предоставляемых классом QColor, и отображать их с помощью нашей модели. Данные хранятся в контейнере данных QList<QString>.
Наша модель DataEntryModel является производной от QAbstractListModel и реализует обязательные функции. Мы можем игнорировать родителя в rowCount, поскольку он используется только в древовидной модели. Класс QModelIndex предоставляет информацию о строке и столбце ячейки, для которой представление хочет получить данные. Представление извлекает информацию из модели на основе строк/столбцов и ролей. Модель QAbstractListModel определена в QtCore, а QColor - в QtGui. Поэтому мы имеем дополнительную зависимость от QtGui. Для QMLприложений вполне допустимо зависеть от QtGui, но обычно не следует зависеть от QtWidgets. #ifndef DATAENTRYMODEL_H #define DATAENTRYMODEL_H #include <QtCore> #include <QtGui> class DataEntryModel : public QAbstractListModel { Q_OBJECT public: explicit DataEntryModel(QObject *parent = 0); ~DataEntryModel(); public: // QAbstractItemModel interface virtual int rowCount(const QModelIndex &parent) const; virtual QVariant data(const QModelIndex &index, int role) c private: QList<QString> m_data; }; #endif // DATAENTRYMODEL_H Со стороны реализации наиболее сложной частью является функция данных. Сначала нам необходимо выполнить проверку диапазона, чтобы убедиться, что нам предоставлен корректный индекс. Затем мы проверяем, поддерживается ли роль отображения. Каждый элемент в
модель может иметь несколько ролей отображения, определяющих различные аспекты содержащихся в ней данных. Qt::DisplayRole это текстовая роль по умолчанию, которую будет запрашивать представление. В Qt существует небольшой набор ролей по умолчанию, которые можно использовать, но обычно модель определяет свои собственные роли для наглядности. В примере все вызовы, не содержащие роли display, в данный момент игнорируются, и возвращается значение по умолчанию QVariant(). #include "dataentrymodel.h" DataEntryModel::DataEntryModel(QObject *parent) : QAbstractListModel(parent) { // initialize our data (QList<QString>) with a list of colo m_data = QColor::colorNames(); } DataEntryModel::~DataEntryModel() { } int DataEntryModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); // return our data count return m_data.count(); } QVariant DataEntryModel::data(const QModelIndex &index, int rol { // the index returns the requested row and column informati // we ignore the column and only use the row information int row = index.row(); // boundary check for the row if(row < 0 || row >= m_data.count()) { return QVariant(); }
// A model can return data for different roles. // The default role is the display role. // it can be accesses in QML with "model.display" switch(role) { case Qt::DisplayRole: // Return the color name for the particular row // Qt automatically converts it to the QVariant typ return m_data.value(row); } // The view asked for other data, just return an empty QVar return QVariant(); } Следующим шагом будет регистрация модели в QML с помощью вызова qmlRegisterType. Это делается внутри файла main.cpp до загрузки QML-файла. #include <QtGui> #include <QtQml> #include "dataentrymodel.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); // register the type DataEntryModel // under the url "org.example" in version 1.0 // under the name "DataEntryModel" qmlRegisterType<DataEntryModel>("org.example", 1, 0, "DataE QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec(); }
Теперь вы можете получить доступ к DataEntryModel с помощью оператора импорта QML import import org.example 1.0 и использовать ее так же, как и другие элементы QML DataEntryModel {} . В данном примере мы используем его для отображения простого списка цветовых записей. import org.example 1.0 ListView { id: view anchors.fill: parent model: DataEntryModel {} delegate: ListDelegate { // use the defined model role "display" text: model.display } highlight: ListHighlight { } } ListDelegate - это пользовательский тип для отображения некоторого текста. ListHighlight - э т о просто прямоугольник. Код был сокращен для компактности примера. Теперь представление может отображать список строк, используя модель C++ и свойство display модели. Это еще очень просто, но уже пригодно для использования в QML. Обычно данные предоставляются извне модели, а модель выступает в качестве интерфейса представления. СОВЕТ Для представления таблицы данных вместо списка используется модель QAbstractTableModel. Единственное отличие от реализации QAbstractListModel заключается в том, что необходимо также предоставить метод columnCount.
Более сложные данные В реальности данные модели часто оказываются гораздо сложнее. Поэтому возникает необходимость в определении пользовательских ролей, чтобы представление могло запрашивать другие данные через свойства. Например, модель может предоставлять не только цвет в виде шестнадцатеричной строки, но и оттенок, насыщенность и яркость из цветовой модели HSV в виде "model.hue", "model.saturation" и "model.brightness" в QML. #ifndef ROLEENTRYMODEL_H #define ROLEENTRYMODEL_H #include <QtCore> #include <QtGui> class RoleEntryModel : public QAbstractListModel { Q_OBJECT public: // Define the role names to be used enum RoleNames { NameRole = Qt::UserRole, HueRole = Qt::UserRole+2, SaturationRole = Qt::UserRole+3, BrightnessRole = Qt::UserRole+4 }; explicit RoleEntryModel(QObject *parent = 0); ~RoleEntryModel(); // QAbstractItemModel interface public: virtual int rowCount(const QModelIndex &parent) const overr virtual QVariant data(const QModelIndex &index, int role) c protected:
// return the roles mapping to be used by QML virtual QHash<int, QByteArray> roleNames() const override; private: QList<QColor> m_data; QHash<int, QByteArray> m_roleNames; }; #endif // ROLEENTRYMODEL_H В заголовке мы добавили ролевое отображение, которое будет использоваться для QML. Теперь, когда QML пытается получить доступ к свойству из модели (например, "model.name"), в заголовке будет указано listview будет искать связку для "name" и запрашивать данные у модели, используя роль NameRole . Определяемые пользователем роли должны начинаться с Qt::UserRole и должны быть уникальными для каждой модели. #include "roleentrymodel.h" RoleEntryModel::RoleEntryModel(QObject *parent) : QAbstractListModel(parent) { // Set names to the role name hash container (QHash<int, QB // model.name, model.hue, model.saturation, model.brightnes m_roleNames[NameRole] = "name"; m_roleNames[HueRole] = "hue"; m_roleNames[SaturationRole] = "saturation"; m_roleNames[BrightnessRole] = "brightness"; // Append the color names as QColor to the data list (QList for(const QString& name : QColor::colorNames()) { m_data.append(QColor(name)); } } RoleEntryModel::~RoleEntryModel() { }
int RoleEntryModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_data.count(); } QVariant RoleEntryModel::data(const QModelIndex &index, int rol { int row = index.row(); if(row < 0 || row >= m_data.count()) { return QVariant(); } const QColor& color = m_data.at(row); qDebug() << row << role << color; switch(role) { case NameRole: // return the color name as hex string (model.name) return color.name(); case HueRole: // return the hue of the color (model.hue) return color.hueF(); case SaturationRole: // return the saturation of the color (model.saturation return color.saturationF(); case BrightnessRole: // return the brightness of the color (model.brightness return color.lightnessF(); } return QVariant(); } QHash<int, QByteArray> RoleEntryModel::roleNames() const { return m_roleNames; } Теперь реализация изменилась только в двух местах. Во-первых, в инициализации. Теперь мы инициализируем список данных типами данных QColor.
Кроме того, мы определяем карту ролевых имен, которая должна быть доступна для QML. Эта карта будет возвращена позже в функции ::roleNames. Второе изменение - в функции ::data. Мы расширяем переключатель, чтобы охватить другие роли (например, оттенок, насыщенность, яркость). Не существует способа вернуть SVG-имя из цвета, поскольку цвет может принимать любой цвет, а SVG-имена ограничены. Поэтому мы пропускаем эту возможность. Для хранения имен потребуется создать структуру struct { QColor, QString }, чтобы иметь возможность идентифицировать именованный цвет. После регистрации типа мы можем использовать модель и ее элементы в нашем пользовательском интерфейсе. ListView { id: view anchors.fill: parent model: RoleEntryModel {} focus: true delegate: ListDelegate { text: 'hsv(' + Number(model.hue).toFixed(2) + ',' + Number(model.saturation).toFixed() + ',' + Number(model.brightness).toFixed() + ')' color: model.name } highlight: ListHighlight { } } Мы преобразуем возвращаемый тип в тип числа JS, чтобы иметь возможность отформатировать число с помощью нотации с фиксированной точкой. Код будет работать и без вызова Number (например, просто model.saturation.toFixed(2) ). Какой формат выбрать, зависит от того, насколько вы доверяете входящим данным. Динамические данные
Динамические данные охватывают аспекты вставки, удаления и очистки данных из модели. Модель QAbstractListModel ожидает определенного поведения при удалении или вставке записей. Это поведение выражается в сигналах, которые необходимо вызывать до и после манипуляции. Например, чтобы вставить строку в модель, необходимо сначала выдать сигнал beginInsertRows, затем выполняет манипуляции с данными и, наконец, выдает endInsertRows . Мы добавим в наши заголовки следующие функции. Эти функции объявлены с использованием Q_INVOKABLE, чтобы иметь возможность вызывать их из QML. Другим способом было бы объявить их как общедоступные слоты. // inserts a color at the index (0 at begining, count-1 at end) Q_INVOKABLE void insert(int index, const QString& colorValue); // uses insert to insert a color at the end Q_INVOKABLE void append(const QString& colorValue); // removes a color from the index Q_INVOKABLE void remove(int index); // clear the whole model (e.g. reset) Q_INVOKABLE void clear(); Кроме того, мы определяем свойство count для получения размера модели и метод get для получения цвета по заданному индексу. Это удобно, когда т р е б у е т с я перебирать содержимое модели из QML. // задает размер модели Q_PROPERTY(int count READ count NOTIFY countChanged) // получает цвет по индексу Q_INVOKABLE QColor get(int index); Реализация для вставки сначала проверяет границы, и если заданное значение является действительным. Только после этого начинается вставка данных.
void DynamicEntryModel::insert(int index, const QString &colorV { if(index < 0 || index > m_data.count()) { return; } QColor color(colorValue); if(!color.isValid()) { return; } // view protocol (begin => manipulate => end] emit beginInsertRows(QModelIndex(), index, index); m_data.insert(index, color); emit endInsertRows(); // update our count property emit countChanged(m_data.count()); } Функция Append очень проста. Мы повторно используем функцию insert с размером модели. void DynamicEntryModel::append(const QString &colorValue) { insert(count(), colorValue); } Remove аналогичен insert, но вызывает его в соответствии с протоколом операции remove. void DynamicEntryModel::remove(int index) { if(index < 0 || index >= m_data.count()) { return; } emit beginRemoveRows(QModelIndex(), index, index); m_data.removeAt(index);
emit endRemoveRows(); // do not forget to update our count property emit countChanged(m_data.count()); } } Вспомогательная функция count тривиальна. Она просто возвращает количество данных. Сайт Функция get также достаточно проста. QColor DynamicEntryModel::get(int index) { if(index < 0 || index >= m_data.count()) { return QColor(); } return m_data.at(index); } Необходимо следить за тем, чтобы возвращать только те значения, которые понимает QML. Если это не один из базовых типов QML или типов, известных QML, необходимо сначала зарегистрировать тип с помощью qmlRegisterType или qmlRegisterUncreatableType . Вы используете qmlRegisterUncreatableType если пользователь не должен иметь возможности инстанцировать свой собственный объект в QML. Теперь вы можете использовать модель в QML и вставлять, добавлять, удалять записи из модели. Приведем небольшой пример, позволяющий пользователю ввести название цвета или его шестнадцатеричное значение, после чего цвет будет добавлен к модели модели и отображается в представлении списка. Красный кружок на делегате позволяет пользователю удалить эту запись из модели. После удаления записи представление списка получает уведомление от модели и обновляет свое содержимое.
А вот код QML. Полный исходный код вы также найдете в активах к этой главе. Чтобы сделать код более компактным, в примере используются м о д у л и QtQuick.Controls и QtQuick.Layout. Модуль controls предоставляет набор элементов пользовательского интерфейса, связанных с рабочим столом, в Qt Quick, а модуль layouts - несколько очень полезных менеджеров компоновки. import QtQuick import QtQuick.Window import QtQuick.Controls import QtQuick.Layouts // our module import org.example 1.0
Window { visible: true width: 480 height: 480 Background { // a dark background id: background } // our dyanmic model DynamicEntryModel { id: dynamic onCountChanged: { // we print out count and the last entry when count print('new count: ' + dynamic.count) print('last entry: ' + dynamic.get(dynamic.count } } ColumnLayout { anchors.fill: parent anchors.margins: 8 ScrollView { Layout.fillHeight: true Layout.fillWidth: true ListView { id: view // set our dynamic model to the views model pro model: dynamic delegate: ListDelegate { required property var model width: ListView.view.width // construct a string based on the models p text: 'hsv(' + Number(model.hue).toFixed(2) + ',' + Number(model.saturation).toFixed() + Number(model.brightness).toFixed() + // sets the font color of our custom delega color: model.name
onClicked: { // make this delegate the current item view.currentIndex = model.index view.focus = true } onRemove: { // remove the current entry from the mo dynamic.remove(model.index) } } highlight: ListHighlight { } // some fun with transitions :-) add: Transition { // applied when entry is added NumberAnimation { properties: "x"; from: -view.width; duration: 250; easing.type: Easing.InCi } NumberAnimation { properties: "y"; from: vi duration: 250; easing.type: Easing.InCi } } remove: Transition { // applied when entry is removed NumberAnimation { properties: "x"; to: view.width; duration: 250; easing.type: Easing.InBo } } displaced: Transition { // applied when entry is moved // (e.g because another element was removed SequentialAnimation { // wait until remove has finished PauseAnimation { duration: 250 } NumberAnimation { properties: "y"; dura } } } }
} TextEntry { id: textEntry onAppend: function (color) { // called when the user presses return on the t // or clicks the add button dynamic.append(color) } onUp: { // called when the user presses up while the te view.decrementCurrentIndex() } onDown: { // same for down view.incrementCurrentIndex() } } } } Программирование модельных представлений - одна из наиболее сложных задач разработки в Qt. Это один из немногих классов, в котором необходимо реализовывать интерфейс, как обычный разработчик приложений. Все остальные классы вы просто используете. Эскизирование моделей всегда должно начинаться на стороне QML. Вы должны представить себе, как пользователи будут использовать вашу модель в QML. Для этого часто бывает полезно сначала создать прототип с использованием ListModel, чтобы увидеть, как это лучше всего работает в QML. Это справедливо и при определении QML API. Переход от C++ к QML - это не только технологическая граница, но и смена парадигмы программирования с императивного на декларативный стиль программирования. Так что будьте готовы к некоторым неудачам и моментам "ага":-).
Расширение QML с помощью C++ Создание приложений с использованием только QML иногда может быть ограничено. Время выполнения QML разработано с использованием языка C++, и среда выполнения может быть расширена, что позволяет использовать всю производительность и свободу окружающей системы.
Понимание времени выполнения QML При работе QML выполняется в среде времени выполнения. Время выполнения реализовано на языке C++ в модуле QtQml. Она состоит из движка, отвечающего за выполнение QML, контекстов, хранящих глобальные свойства, доступные для каждого компонента, и компонентов - элементов QML, которые могут быть инстанцированы из QML. #include <QtGui> #include <QtQml> int main(int argc, char **argv) { QGuiApplication app(argc, argv); QUrl source(QStringLiteral("qrc:/main.qml")); QQmlApplicationEngine engine; engine.load(source); return app.exec(); } В примере QGuiApplication инкапсулирует все, что связано с экземпляром приложения (например, имя приложения, аргументы командной строки и управление циклом событий). QQmlApplicationEngine управляет иерархическим порядком контекстов и компонентов. Он требует загрузки типичного QML-файла в качестве начальной точки приложения. В данном случае это файл main.qml, содержащий окно и текстовый тип.
СОВЕТ Загрузка файла main.qml с простым Item в качестве корневого типа через QmlApplicationEngine ничего не покажет на экране, так как для этого требуется окно, управляющее поверхностью для рендеринга. Движок способен загружать QML-код, не содержащий пользовательского интерфейса (например, простые объекты). Поэтому по умолчанию окно не создается. Внутренняя среда выполнения qml сначала проверит, содержит ли основной QML-файл окно в качестве корневого элемента, и если нет, то создаст его и установит корневой элемент в качестве дочернего для вновь созданного окна. import QtQuick 2.5 import QtQuick.Window 2.2 Window { visible: true width: 512 height: 300 Text { anchors.centerIn: parent text: "Hello World!" } } В QML-файле мы объявляем наши зависимости, здесь это QtQuick и QtQuick.Window . Эти объявления вызовут поиск этих модулей в путях импорта и в случае успеха загрузят необходимые подключаемые модули в движок. Вновь загруженные типы будут доступны среде QML через объявление в файле qmldir, представляющем отчет.
Можно также сократить время создания плагина, добавив наши типы непосредственно в движок в файле main.cpp . Здесь мы предполагаем, что у нас есть CurrentTime , который классе QObject. является классом, основанным на базовом QQmlApplicationEngine engine(); qmlRegisterType<CurrentTime>("org.example", 1, 0, "CurrentTime" engine.load(source); Теперь мы можем использовать тип CurrentTime и в нашем QML-файле. import org.example 1.0 CurrentTime { // access properties, functions, signals } Если нам не нужно иметь возможность инстанцировать новый класс из QML, мы можем использовать свойства контекста для отображения объектов C++ в QML, например QScopedPointer<CurrentTime> current(new CurrentTime()); QQmlApplicationEngine engine(); engine.rootContext().setContextProperty("current", current.valu engine.load(source);
СОВЕТ Не путайте setContextProperty() и setProperty() . Первая устанавливает свойство контекста на qml-контекст, а setProperty() устанавливает динамическое значение свойства на QObject и не поможет вам. Теперь вы можете использовать свойство current везде в своем приложении. Оно доступно везде в коде QML благодаря наследованию контекста. Текущий объект регистрируется в самом внешнем корневом контексте, который наследуется повсеместно. import QtQuick import QtQuick.Window Window { visible: true width: 512 height: 300 Component.onCompleted: { console.log('current: ' + current) } } Вот различные способы расширения QML в целом: Свойства контекста - setContextProperty() Регистрация типа в движке - вызов qmlRegisterType в вашем main.cpp Подключаемые модули расширения QML - максимальная гибкость, будет обсуждаться далее Контекстные свойства удобны для использования в небольших приложениях. Они не требуют никаких усилий, вы просто представляете API своей системы в виде глобальных объектов. Полезно убедиться в отсутствии конфликтов именования (например, с помощью
используя для этого специальный символ ( $ ), например $ .currentTime ). $ является допустимым символом для переменных JS. Регистрация QML-типов позволяет пользователю управлять жизненным циклом объекта C++ из QML. Это невозможно при использовании свойств контекста. Кроме того, при этом не загрязняется глобальное пространство имен. Тем не менее все типы должны быть сначала зарегистрированы, а значит, все библиотеки должны быть скомпонованы при запуске приложения, что в большинстве случаев не представляет особой проблемы. Наиболее гибкую систему обеспечивают подключаемые модули расширения QML. Они позволяют регистрировать типы в подключаемом модуле, который загружается при вызове идентификатора импорта в первом QML-файле. Кроме того, при использовании синглтона QML отпадает необходимость в засорении глобального пространства имен. Подключаемые модули позволяют повторно использовать модули в разных проектах, что очень удобно, когда вы делаете несколько проектов с Qt. Вернемся к нашему простому примеру файла main.qml: import QtQuick 2.5 import QtQuick.Window 2.2 Window { visible: true width: 512 height: 300 Text { anchors.centerIn: parent text: "Hello World!" } } Когда мы импортируем QtQuick и QtQuick.Window, мы указываем времени выполнения QML найти соответствующие плагины
расширения QML и загрузить их. Это делается движком QML путем поиска этих
модулей в путях импорта QML. После этого вновь загруженные типы станут доступны среде QML. В оставшейся части этой главы основное внимание будет уделено подключаемым модулям расширения QML. Поскольку они обеспечивают наибольшую гибкость и возможность повторного использования.
Содержание плагина Плагин - это библиотека с определенным интерфейсом, которая загружается по требованию. Это отличается от библиотеки тем, что библиотека связывается и загружается при запуске приложения. В случае QML интерфейс называется QQmlExtensionPlugin initializeEngine() . В нем есть два интересных для нас метода и registerTypes() . При загрузке плагина сначала вызывается метод initializeEngine(), который позволяет нам получить доступ к движку для отображения объектов плагина в корневой контекст. В большинстве случаев вы будете использовать только метод registerTypes(). Он позволяет зарегистрировать пользовательские QML-типы в движке по указанному URL. Для начала создадим небольшой класс утилиты FileIO. Он позволит читать и записывать текстовые файлы из QML. Первая итерация может выглядеть следующим образом в имитированной реализации QML. // FileIO.qml (good) QtObject { function write(path, text) {}; function read(path) { return "TEXT" } } Это чистая QML-реализация возможного QML API на базе C++. Мы используем ее для изучения API. Мы видим, что нам нужны функции чтения и записи. Мы также видим, что функция записи принимает путь и text , а функция read принимает путь и возвращает текст. Как видно, path и text являются общими параметрами, и, возможно, мы можем извлечь их как свойства, чтобы упростить использование API в декларативном контексте.
// FileIO.qml (better) QtObject { property url source property string text function write() {} // open file and write text function read() {} // read file and assign to text } Да, это больше похоже на QML API. Мы используем свойства для того, чтобы среда могла привязываться к нашим свойствам и реагировать на изменения. Для создания такого API на языке C++ нам потребуется создать интерфейс Qt C++, который будет выглядеть следующим образом. class FileIO : public QObject { ... Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY s Q_PROPERTY(QString text READ text WRITE setText NOTIFY text ... public: Q_INVOKABLE void read(); Q_INVOKABLE void write(); ... } Тип FileIO должен быть зарегистрирован в движке QML. Мы хотим использовать его в модуле "org.example.io" import org.example.io 1.0 FileIO { }
Плагин может отображать несколько типов с помощью одного модуля. Но он не может отображать несколько модулей от одного плагина. Поэтому между модулями и плагинами существует связь "один к одному". Эта связь выражается идентификатором модуля.
Создание плагина Qt Creator содержит мастер создания подключаемого модуля QtQuick 2 QML Extension Plugin, который находится в разделе Library при создании нового проекта. Мы используем его для создания подключаемого модуля fileio с объектом FileIO для запуска внутри модуля org.example.io . СОВЕТ Текущий мастер генерирует проект на основе QMake. Для создания проекта на основе CMake используйте пример из этой главы. Проект должен состоять из файлов fileio.h и fileio.cpp , которые объявляют и реализуют тип FileIO, и файла fileio_plugin.cpp, содержащего собственно класс подключаемого модуля, который позволяет движку QML обнаруживать расширение. Класс подключаемого модуля является производным от класса QQmlEngineExtensionPlugin Q_PLUGIN_METADATA. и содержит макросы Q_OBJECT и Весь файл можно увидеть ниже. #include <QQmlEngineExtensionPlugin> class FileioPlugin : public QQmlEngineExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid) };
#include "fileio_plugin.moc" Расширение автоматически обнаружит и зарегистрирует все типы, помеченные QML_ELEMENT и QML_NAMED_ELEMENT . Как это делается, мы увидим ниже в разделе "Реализация FileIO". Для того чтобы импорт модуля работал, пользователю также необходимо указать URI, например, import org.example.io . Интересно, что мы нигде не видим URI модуля. Он задается извне с помощью файла qmldir, либо в файле CMakeLists.txt вашего проекта. Файл qmldir определяет содержимое вашего QML-плагина или, лучше сказать, QML-сторону вашего плагина. Собственноручно написанный файл qmldir для нашего плагина должен выглядеть примерно так: module org.example.io plugin fileio Модуль - это URI, который импортирует пользователь, а после него вы указываете, какой плагин нужно загрузить для данного URI. Строка plugin должна совпадать с именем файла plugin (под mac это будет libfileio_debug.dylib в файловой системе и fileio в qmldir, для Linuxсистемы такая же строка будет выглядеть как libfileio.so). Эти файлы создаются программой Qt Creator на основе заданной информации. Более простой способ создания корректного файла qmldir - в CMakeLists.txt для вашего проекта, в макросе qt_add_qml_module. Здесь параметр URI используется для указания URI плагина, например, org.example.io . Таким образом, файл qmldir генерируется при сборке проекта. ::: TODO Как установить модуль? :::
При импорте модуля с именем "org.example.io" движок QML будет искать в одном из путей импорта и попытается найти путь "org/example/io" с qmldir. Затем qmldir укажет движку, какую библиотеку следует загрузить в качестве подключаемого модуля расширения QML, используя URI модуля. Два модуля с одинаковыми URI будут переопределять друг друга.
Реализация FileIO Помните, что FileIO API, который мы хотим создать, должен выглядеть следующим образом. class FileIO : public QObject { ... Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY s Q_PROPERTY(QString text READ text WRITE setText NOTIFY text ... public: Q_INVOKABLE void read(); Q_INVOKABLE void write(); ... } Свойства мы опустим, так как они представляют собой простые сеттеры и геттеры. Метод read открывает файл в режиме чтения и считывает данные с помощью текстового потока. void FileIO::read() { if(m_source.isEmpty()) { return; } QFile file(m_source.toLocalFile()); if(!file.exists()) { qWarning() << "Does not exist: " << m_source.toLocalFil return; } if(file.open(QIODevice::ReadOnly)) { QTextStream stream(&file);
m_text = stream.readAll(); emit textChanged(m_text); } } При изменении текста необходимо сообщить об этом другим пользователям с помощью команды emit textChanged(m_text) . В противном случае привязка свойств не будет работать. Метод write делает то же самое, но открывает файл в режиме записи и использует поток для записи содержимого свойства text. void FileIO::read() { if(m_source.isEmpty()) { return; } QFile file(m_source.toLocalFile()); if(!file.exists()) { qWarning() << "Does not exist: " << m_source.toLocalFil return; } if(file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); m_text = stream.readAll(); emit textChanged(m_text); } } Чтобы сделать тип видимым для QML, мы добавляем макрос QML_ELEMENT сразу после строк Q_PROPERTY. Это сообщает Qt, что тип должен быть доступен для QML. Если необходимо задать имя, отличное от имени класса C++, можно использовать макрос QML_NAMED_ELEMENT. TODO TODO TODO
Не забудьте в конце вызвать make install. В противном случае файлы вашего плагина не будут скопированы в папку qml, и движок qml не сможет найти модуль. TODO TODO TODO СОВЕТ Поскольку чтение и запись являются блокирующими вызовами функций, использовать этот FileIO следует только для небольших текстов, иначе вы заблокируете UI-поток Qt. Будьте внимательны!
Использование FileIO Теперь мы можем использовать наш созданный файл для доступа к некоторым данным. В этом примере мы получим некоторые данные о городе в формате JSON и отобразим их в таблице. Мы создаем два проекта: один для плагина расширения (называется fileio), который предоставляет нам возможность чтения и записи текста из файла, и второй, который отображает данные в таблице, (CityUI). CityUI использует расширение fileio для чтения и записи файлов. Данные JSON - это просто текст, отформатированный таким образом, что его можно преобразовать в корректный JSобъект/массив и обратно в текст. Мы используем наш FileIO для чтения данных в формате JSON и преобразования их в JS-объект с помощью встроенной в Javascript функции JSON.parse() . В дальнейшем эти данные используются в качестве модели для табличного представления. Это реализовано в функциях read document и write document, показанных ниже.
FileIO { id: io } function readDocument() { io.source = openDialog.fileUrl io.read() view.model = JSON.parse(io.text) } function saveDocument() { var data = view.model io.text = JSON.stringify(data, null, 4) io.write() } Данные JSON, используемые в этом примере, находятся в файле cities.json. Он содержит список записей данных о городах, где каждая запись содержит интересные данные о городе, как показано ниже. [ { "area": "1928", "city": "Shanghai", "country": "China", "flag": "22px-Flag_of_the_People's_Republic_of_China.sv "population": "13831900" }, ... ] Окно приложения
Мы используем мастер Qt Creator QtQuick Application для создания приложения на основе Qt Quick Controls 2. Мы не будем использовать новые формы QML, поскольку это трудно объяснить в книге, хотя новый подход к формам с файлом ui.qml гораздо удобнее предыдущего. Поэтому пока можно удалить файл форм. В базовой комплектации это окно ApplicationWindow, которое может содержать панель инструментов, меню и строку состояния. Менюбар мы будем использовать только для создания некоторых стандартных пунктов меню для открытия и сохранения документа. В базовой конфигурации будет просто отображаться пустое окно. import QtQuick 2.5 import QtQuick.Controls 1.3 import QtQuick.Window 2.2 import QtQuick.Dialogs 1.2 ApplicationWindow { id: root title: qsTr("City UI") width: 640 height: 480 visible: true } Использование действий Для более эффективного использования команд мы используем тип QML Action. Это позволит нам в дальнейшем использовать то же действие и для потенциальной панели инструментов. Действия открытия, сохранения и выхода вполне стандартны. Действия открытия и сохранения пока не содержат никакой логики, к этому мы придем позже. Менюбар создается с помощью меню файла и этих трех действий. Дополнительно мы подготовим уже файловый диалог, который позволит нам выбрать наш городской документ
позже. Диалог не виден при объявлении, необходимо использовать функцию open() метод для его отображения. Action { id: save text: qsTr("&Save") shortcut: StandardKey.Save onTriggered: { saveDocument() } } Action { id: open text: qsTr("&Open") shortcut: StandardKey.Open onTriggered: openDialog.open() } Action { id: exit text: qsTr("E&xit") onTriggered: Qt.quit(); } menuBar: MenuBar { Menu { title: qsTr("&File") MenuItem { action: open } MenuItem { action: save } MenuSeparator {} MenuItem { action: exit } } } FileDialog { id: openDialog onAccepted: { root.readDocument()
} } Форматирование таблицы Содержимое данных о городе должно быть представлено в виде таблицы. Для этого мы используем элемент управления TableView и объявляем 4 колонки: город, страна, район, население. Каждый столбец представляет собой стандартный TableViewColumn . Позже мы добавим столбцы для операций установки флага и удаления, что потребует создания собственного делегата столбца. TableView { id: view anchors.fill: parent TableViewColumn { role: 'city' title: "City" width: 120 } TableViewColumn { role: 'country' title: "Country" width: 120 } TableViewColumn { role: 'area' title: "Area" width: 80 } TableViewColumn { role: 'population' title: "Population" width: 80 } }
Теперь в приложении должна появиться менубара с файловым меню и пустая таблица с 4 заголовками. Следующим шагом будет заполнение таблицы полезными данными с помощью нашего расширения FileIO. Документ cities.json представляет собой массив записей городов. Приведем пример. [ { "area": "1928", "city": "Shanghai", "country": "China", "flag": "22px-Flag_of_the_People's_Republic_of_China.sv "population": "13831900" }, ... ] Наша задача - дать пользователю возможность выбрать файл, прочитать его, преобразовать и установить в табличное представление. Чтение данных Для этого мы разрешаем действию open открывать диалог файла. Когда пользователь выбрал файл, в файловом диалоге вызывается метод onAccepted. В нем мы вызываем функцию readDocument(). Функция readDocument() устанавливает URL из файлового диалога в наш объект FileIO и вызывает функцию read()
метод. Загруженный текст из FileIO затем разбирается с помощью метода JSON.parse(), и полученный объект непосредственно устанавливается на табличное представление в качестве модели. Насколько это удобно? Action { id: open ... onTriggered: { openDialog.open() } } ... FileDialog { id: openDialog onAccepted: { root.readDocument() } } function readDocument() { io.source = openDialog.fileUrl io.read() view.model = JSON.parse(io.text) } FileIO { id: io } Данные для записи Для сохранения документа мы подключаем действие "сохранить" к файлу функция saveDocument(). Функция сохранения документа принимает модель
из представления, которое является JS-объектом, и преобразуем его в строку с помощью функции JSON.stringify(). Полученная строка устанавливается в свойство text нашего объекта FileIO, и мы вызываем функцию write() для сохранения данных на диск. Параметры "null" и "4" функции stringify отформатируют полученные JSON-данные, используя отступ в 4 пробела. Это необходимо для лучшего чтения сохраненного документа. Action { id: save ... onTriggered: { saveDocument() } } function saveDocument() { var data = view.model io.text = JSON.stringify(data, null, 4) io.write() } FileIO { id: io } По сути, это приложение для чтения, записи и отображения JSONдокумента. Подумайте о том, сколько времени тратится на написание программ чтения и записи XML. В случае JSON все, что вам нужно, это способ чтения и записи текстового файла или передачи и приема текстового буфера.
Завершающий штрих Приложение еще не полностью готово. Мы все еще хотим показать флаги и позволить пользователю модифицировать документ, удаляя города из модели. В данном примере файлы флагов хранятся относительно документа main.qml в папке flags. Чтобы иметь возможность отображать их в колонке таблицы, необходимо определить пользовательский делегат для отрисовки изображения флага. TableViewColumn { delegate: Item { Image { anchors.centerIn: parent source: 'flags/' + styleData.value } } role: 'flag'
title: "Flag" width: 40 } Это все, что требуется для отображения флага. Он передает делегату свойство flag из JS-модели в виде styleData.value. Затем делегат настраивает путь к изображению так, чтобы предварительно добавить 'flags/', и отображает его как Элемент изображения. Для удаления мы используем аналогичную технику отображения кнопки удаления. TableViewColumn { delegate: Button { iconSource: "remove.png" onClicked: { var data = view.model data.splice(styleData.row, 1) view.model = data } } width: 40 } Для операции удаления данных мы завладеваем моделью представления и удаляем одну запись с помощью функции JS splice. Этот метод доступен нам, поскольку модель относится к типу JS array. Метод splice изменяет содержимое массива, удаляя существующие элементы и/или добавляя новые. JS-массив, к сожалению, не так умен, как Qt-модель, например QAbstractItemModel, которая будет уведомлять представление об изменении строки или данных. Представление не будет показывать обновленные данные, поскольку оно никогда не уведомляется о каких-либо изменениях. Только когда мы устанавливаем данные обратно в представление, представление распознает, что появились новые данные, и обновляет содержимое представления.
Повторная установка модели с помощью view.model = data - э т о способ сообщить представлению о том, что данные были изменены.
Резюме Плагин, созданный в этой главе, является очень простым плагином. но он может быть повторно использован и расширен другими типами для различных приложений. Использование плагинов создает очень гибкое решение. Например, теперь вы можете запускать пользовательский интерфейс, просто используя qml . Откройте папку, в которой находится ваш проект CityUI, и запустите пользовательский интерфейс с помощью qml main.qml . Расширение легко доступно движку QML из любого проекта и может быть импортировано куда угодно. Вам рекомендуется писать свои приложения таким образом, чтобы они работали с qml. Это значительно увеличивает время выполнения заказа для разработчика пользовательского интерфейса, а также является хорошей привычкой для четкого разделения логики и представления приложения. Единственный недостаток использования подключаемых модулей заключается в том, что развертывание становится все сложнее. Это становится тем очевиднее, чем проще приложение (поскольку накладные расходы на создание и развертывание подключаемого модуля остаются неизменными). Теперь вам необходимо развернуть подключаемый модуль вместе с вашим приложением. Если для вас это проблема, вы можете использовать все тот же класс FileIO и зарегистрировать его непосредственно в своем приложении main.cpp прежним. с использованием qmlRegisterType . Код QML останется В больших проектах приложение как таковое не используется. Вы используете простую среду выполнения qml, аналогичную команде qml, поставляемой с Qt, и требуете, чтобы вся нативная функциональность поставлялась в виде плагинов. И ваши проекты являются простыми чистыми qml-проектов с использованием этих плагинов расширения qml. Это
обеспечивает большую гибкость и устраняет этап компиляции при изменении пользовательского интерфейса. После редактирования QML-файла нужно просто запустить пользовательский интерфейс. Это позволяет авторам пользовательского интерфейса сохранять гибкость и гибкость при внесении всех этих мелких изменений в пиксели.
Плагины обеспечивают хорошее и чистое разделение между разработкой бэкенда на C++ и фронтенда на QML. При разработке QML-плагинов всегда помните о стороне QML и не стесняйтесь начинать с макета только на QML, чтобы проверить свой API, прежде чем реализовывать его на C++. Если API написан на C++, люди часто не решаются его изменить или, тем более, переписать. Макетирование API на QML обеспечивает гораздо большую гибкость и меньшие первоначальные инвестиции. При использовании подключаемых модулей переход от имитируемого API к реальному API заключается в изменении пути импорта для среды выполнения qml.
Qt для Python В этой главе описывается модуль PySide6 из проекта Qt for Python. Вы узнаете, как его установить и как использовать QML совместно с Python.
Введение Проект Qt for Python предоставляет инструментарий для связывания C++ и Qt с Python, а также полный Python API для Qt. Это означает, что все, что можно сделать с помощью Qt и C++, можно сделать и с помощью Qt и Python. Это включает в себя все: от безголовых сервисов до пользовательских интерфейсов на основе виджетов. В этой главе мы рассмотрим, как интегрировать QML и Python. В настоящее время Qt для Python доступен для всех настольных платформ, но не для мобильных. В зависимости от используемой платформы настройка Python несколько отличается, но как только у вас появляется Python (https://www.python.org/) и PyPA (https://www.pypa.io/en/latest/), можно установить Qt для Python с помощью команды pip . Более подробно это рассматривается далее. Поскольку проект Qt for Python предоставляет совершенно новую языковую привязку для Qt, он также поставляется с новым набором документации. При изучении этого модуля полезно ознакомиться со следующими ресурсами. Справочная документация: https://doc.qt.io/qtforpython/ Qt for Python wiki: https://wiki.qt.io/Qt_for_Python Оговорки: https://wiki.qt.io/Qt_for_Python/Considerations Связка Qt для Python создается с помощью инструмента Shiboken. Иногда бывает интересно почитать и о нем, чтобы понять, что происходит. Предпочтительным местом для поиска информации о Shiboken является справочная документация (https://doc.qt.io/qtforpython/shiboken6/index.html) . Если вы хотите смешать собственный код на Си++ с Python и QML, то Shiboken - это тот инструмент, который вам нужен.
СОВЕТ В этой главе мы будем использовать Python 3.7.
Установка Qt для Python доступен через PyPA с помощью pip под именем pyside6 . В приведенном ниже примере мы создаем среду venv, в которую будем устанавливать последнюю версию Qt для Python: sh mkdir qt-for-python cd qt-for-python python3 -m venv . source bin/activate (qt-for-python) $ python --version Python 3.9.6 Когда среда настроена, продолжаем установку pyside6, используя pip : sh (qt-for-python) $ pip install pyside6 Collecting pyside6 Downloading [ ... ] (60.7 MB) Collecting shiboken6==6.1.2 Downloading [ ... ] (1.0 MB) Installing collected packages: shiboken6, pyside6 Successfully installed pyside6-6.1.2 shiboken6-6.1.2 После установки мы можем протестировать его, запустив пример Hello World из интерактивного приглашения Python: (qt-for-python) $ python Python 3.9.6 (default, Jun 28 2021, 06:20:32) [Clang 12.0.0 (clang-1200.0.32.29)] on darwin sh
Type "help", "copyright", "credits" or "license" for more infor >>> from PySide6 import QtWidgets >>> import sys >>> app = QtWidgets.QApplication(sys.argv) >>> widget = QtWidgets.QLabel("Hello World!") >>> widget.show() >>> app.exec() 0 >>> В результате выполнения примера появляется окно, подобное показанному ниже. Для завершения работы программы закройте окно.
Построение приложения В этой главе мы рассмотрим, как можно объединить Python и QML. Наиболее естественный способ совместить эти два мира - сделать это так же, как в случае с C++ и QML, т.е. реализовать логику на Python, а представление на QML. Для этого необходимо понять, как объединить QML и Python в одну программу, а затем реализовать интерфейсы между двумя мирами . В следующих подразделах мы рассмотрим, как это делается. Мы начнем с простого и перейдем к примеру, раскрывающему возможности модуля Python в QML через модель элементов Qt. Запуск QML из Python Самый первый шаг - это создание программы на языке Python, в которой может быть размещена QML-программа Hello World, показанная ниже. import QtQuick import QtQuick.Window Window { width: 640 height: 480 visible: true title: qsTr("Hello Python World!") } Для этого нам нужен основной цикл Qt, предоставляемый QGuiApplication из Модуль QtGui. Нам также необходим QQmlApplicationEngine из модуля
Модуль QtQml. Для того чтобы передать ссылку на исходный файл механизму приложения QML, нам также необходим класс QUrl из модуля QtCore. В приведенном ниже коде мы эмулируем функциональность шаблонного C++ кода, генерируемого Qt Creator для QML-проектов. Он инстанцирует объект приложения и создает движок приложения QML. Затем загружается QML и проверяется, был ли создан корневой объект. Наконец, он выходит из программы и возвращает значение, возвращенное методом exec объекта приложения. import sys from PySide6.QtGui import QGuiApplication from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtCore import QUrl if __name__ == '__main__': app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() engine.load(QUrl("main.qml")) if not engine.rootObjects(): sys.exit(-1) sys.exit(app.exec()) В результате выполнения примера появляется окно с заголовком Hello Python World.
СОВЕТ В примере предполагается, что он выполняется из каталога, содержащего исходный файл main.qml. Определить местоположение выполняемого Python-файла можно с помощью переменной file переменной. Это может быть использовано для определения местоположения QML-файлов относительно Python-файла, как показано в этой записи блога (http://blog.qt.io/blog/2018/05/14/qml-qt-python/) . Экспонирование объектов Python в QML Наиболее простым способом обмена информацией между Python и QML является представление объекта Python в QML. Это делается путем регистрации контекста
свойство через QQmlApplicationEngine . Прежде чем это сделать, необходимо определить класс, чтобы иметь объект, который мы будем экспонировать. Классы Qt поставляются с рядом функций, которые мы хотим иметь возможность использовать. К ним относятся: сигналы, слоты и свойства. В этом первом примере мы ограничимся базовой парой сигнал-слот. Остальное будет рассмотрено в последующих примерах. Сигналы и слоты Начнем с класса NumberGenerator . Он имеет конструктор, метод updateNumber и сигнал nextNumber. Идея заключается в том, что при вызове метода updateNumber выдается сигнал nextNumber со значением новое случайное число. Код класса приведен ниже, но сначала мы рассмотрим детали. Прежде всего, мы убеждаемся в том, что вызов QObject. init из нашего конструктора. Это очень важно, так как без этого пример работать не будет. Затем мы объявляем сигнал, создавая экземпляр класса Signal из модуля PySide6.QtCore. В данном случае сигнал несет целочисленное значение, поэтому в качестве параметра используется int . Имя параметра сигнала, number , задается в параметре arguments. Наконец, мы украшаем метод updateNumber декоратором @Slot(), тем самым превращая его в слот. В Qt для Python нет понятия invokables, поэтому все вызываемые методы должны быть слотами. В методе updateNumber мы выдаем сигнал nextNumber с помощью метода emit. Это несколько отличается от синтаксиса QML или C++, так как сигнал представлен объектом, а не вызываемой функцией.
py import random from PySide6.QtCore import QObject, Signal, Slot class NumberGenerator(QObject): def __init__(self): QObject.__init__(self) nextNumber = Signal(int, arguments=['number']) @Slot() def giveNumber(self): self.nextNumber.emit(random.randint(0, 99)) Далее необходимо объединить только что созданный класс с шаблонным кодом для объединения QML и Python, описанным ранее. В результате мы получаем следующий код начальной точки. Интересны строки, в которых мы сначала инстанцируем NumberGenerator . Затем этот объект передается в QML с помощью метода setContextProperty корневого контекста движка QML. При этом объект отображается в QML как глобальная переменная с именем numberGenerator . import sys py from PySide6.QtGui import QGuiApplication from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtCore import QUrl if __name__ == '__main__': app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() number_generator = NumberGenerator() engine.rootContext().setContextProperty("numberGenerator",
engine.load(QUrl("main.qml")) if not engine.rootObjects(): sys.exit(-1) sys.exit(app.exec()) Переходя к коду QML, можно увидеть, что мы создали пользовательский интерфейс Qt Quick Controls 2, состоящий из кнопки и метки . В обработчике onClicked кнопки вызывается функция numberGenerator.updateNumber(). Это слот объекта, инстанцированного на стороне Python. Для получения сигнала от объекта, который был инстанцирован вне QML, необходимо использовать элемент Connections. Это позволяет прикрепить обработчик сигнала к существующей цели. import QtQuick import QtQuick.Window import QtQuick.Controls Window { id: root width: 640 height: 480 visible: true title: qsTr("Hello Python World!") Flow { Button { text: qsTr("Give me a number!") onClicked: numberGenerator.giveNumber() } Label { id: numberLabel
text: qsTr("no number") } } Connections { target: numberGenerator function onNextNumber(number) { numberLabel.text = number } } } Свойства Вместо того чтобы полагаться исключительно на сигналы и слоты, общепринятым способом представления состояния в QML являются свойства. Свойство - это комбинация сеттера, геттера и сигнала уведомления. Сеттер является необязательным, так как мы можем иметь свойства, доступные только для чтения. Чтобы попробовать это сделать, мы обновим NumberGenerator из предыдущего примера до версии, основанной на свойствах. У него будет два свойства: number - свойство только для чтения, содержащее последнее случайное число, и maxNumber - свойство для чтения и записи, содержащее максимальное значение, которое может быть возвращено. Также у него будет слот updateNumber, который обновляет случайное число. Прежде чем погрузиться в детали свойств, мы создадим для этого базовый класс Python. Он содержит соответствующие геттеры и сеттеры, но не Qt-сигнализацию. Собственно говоря, единственная Qtчасть здесь - это наследование от QObject . Даже имена методов написаны в стиле Python, т.е. с использованием подчеркивания вместо camelCase. Обратите внимание на знаки подчеркивания (" ") в начале метод set_number. Это означает, что он является приватным методом. Таким образом, даже если свойство number доступно только для чтения, мы предоставляем сеттер. Мы просто не
сделать его общедоступным. Это позволит нам выполнять действия при изменении значения (например, выдавать сигнал уведомления). py class NumberGenerator(QObject): def __init__(self): QObject.__init__(self) self.__number = 42 self.__max_number = 99 def set_max_number(self, val): if val < 0: val = 0 if self.__max_number != val: self.__max_number = val if self.__number > self.__max_number: self.__set_number(self.__max_number) def get_max_number(self): return self.__max_number def __set_number(self, val): if self.__number != val: self.__number = val def get_number(self): return self.__number Для определения свойств нам необходимо импортировать понятия Signal , Slot и Property из PySide2.QtCore . В полном примере импортов больше, но это те, которые имеют отношение к свойствам. from PySide6.QtCore import QObject, Signal, Slot, Property py
Теперь мы готовы определить первое свойство - number . Начнем с объявления сигнала numberChanged , который затем вызовем в функции метод set_number, чтобы при изменении значения выдавался сигнал. После этого остается только инстанцировать объект Property. Сайт В данном случае конструктор свойств принимает три аргумента: тип ( int ), геттер ( get_number ) и сигнал уведомления, который имеет вид передается в качестве именованного аргумента ( notify=numberChanged ). Обратите внимание, что геттер имеет Python-имя, т.е. использует подчеркивание, а не camelCase, поскольку используется для чтения значения из Python. Для QML используется имя свойства number. class NumberGenerator(QObject): py # ... # number numberChanged = Signal(int) def __set_number(self, val): if self.__number != val: self.__number = val self.numberChanged.emit(self.__number) def get_number(self): return self.__number number = Property(int, get_number, notify=numberChanged) Это приводит нас к следующему свойству, maxNumber . Это свойство предназначено для чтения и записи, поэтому нам необходимо предоставить сеттер, а также все то, что мы делали для свойства number.
Сначала мы объявим сигнал maxNumberChanged. На этот раз мы используем декоратор @Signal вместо инстанцирования объекта Signal. Мы также предоставляем слот сеттера setMaxNumber с Qt- именем (camelCase), который просто вызывает Python-метод set_max_number, а также геттер с Python-именем. Опять же, при обновлении значения сеттер издает сигнал change. Наконец, мы объединяем все эти части в свойство чтения и записи, инстанцируя объект Property, принимая в качестве аргументов тип, геттер, сеттер и сигнал уведомления. py class NumberGenerator(QObject): # ... # maxNumber @Signal def maxNumberChanged(self): pass @Slot(int) def setMaxNumber(self, val): self.set_max_number(val) def set_max_number(self, val): if val < 0: val = 0 if self.__max_number != val: self.__max_number = val self.maxNumberChanged.emit() if self.__number > self.__max_number: self.__set_number(self.__max_number) def get_max_number(self):
return self.__max_number maxNumber = Property(int, get_max_number, set_max_number, n Теперь у нас есть свойства для текущего случайного числа, number , и максимального случайного числа, maxNumber . Остался только слот для создания нового случайного числа. Он называется updateNumber и просто устанавливает новое случайное число. py class NumberGenerator(QObject): # ... @Slot() def updateNumber(self): self.__set_number(random.randint(0, self.__max_number)) Наконец, генератор чисел отображается в QML через свойство корневого контекста. if __name__ == '__main__': app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() number_generator = NumberGenerator() engine.rootContext().setContextProperty("numberGenerator", engine.load(QUrl("main.qml")) if not engine.rootObjects(): sys.exit(-1) sys.exit(app.exec())
В QML мы можем привязываться к свойствам number и maxNumber объекта NumberGenerator. В обработчике onClicked объекта В кнопке мы вызываем метод updateNumber для генерации нового случайного числа, а в обработчике onValueChanged слайдера устанавливаем свойство maxNumber с помощью метода setMaxNumber. Это связано с тем, что изменение свойства напрямую через Javascript приведет к разрушению привязки к свойству. Явное использование метода setter позволяет избежать этого. import QtQuick import QtQuick.Window import QtQuick.Controls Window { id: root width: 640 height: 480 visible: true title: qsTr("Hello Python World!") Column { Flow { Button { text: qsTr("Give me a number!") onClicked: numberGenerator.updateNumber() } Label { id: numberLabel text: numberGenerator.number } } Flow { Slider { from: 0 to: 99 value: numberGenerator.maxNumber onValueChanged: numberGenerator.setMaxNumber(va
} } } } Экспонирование класса Python в QML До сих пор мы инстанцировали объект Python и использовали метод setContextProperty корневого контекста, чтобы сделать его доступным для QML. Возможность инстанцировать объект из QML позволяет лучше контролировать жизненный цикл объекта из QML. Для этого нам необходимо предоставить QML не объект, а класс. На класс, который подвергается воздействию QML, не влияет место его инстанцирования. Никаких изменений в определении класса не требуется. Однако вместо вызова setContextProperty используется функция qmlRegisterType. Эта функция берется из модуля PySide2.QtQml и принимает пять аргументов: Ссылка на класс, NumberGenerator в примере ниже. Имя модуля, 'Generators'. Версия модуля, состоящая из мажорного и минорного номера, 1 и 0 Значение 1 .0 . QML-имя класса, ' NumberGenerator' py import random import sys from PySide6.QtGui import QGuiApplication from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterTyp from PySide6.QtCore import QUrl, QObject, Signal, Slot class NumberGenerator(QObject): def __init__(self):
QObject.__init__(self) nextNumber = Signal(int, arguments=['number']) @Slot() def giveNumber(self): self.nextNumber.emit(random.randint(0, 99)) if __name__ == '__main__': app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() qmlRegisterType(NumberGenerator, 'Generators', 1, 0, 'Numbe engine.load(QUrl("main.qml")) if not engine.rootObjects(): sys.exit(-1) sys.exit(app.exec()) В QML нам необходимо импортировать модуль, например Generators 1.0, а затем инстанцировать класс в виде NumberGenerator { ... } . Теперь экземпляр работает как любой другой элемент QML. import QtQuick import QtQuick.Window import QtQuick.Controls import Generators Window { id: root width: 640 height: 480 visible: true
title: qsTr("Hello Python World!") Flow { Button { text: qsTr("Give me a number!") onClicked: numberGenerator.giveNumber() } Label { id: numberLabel text: qsTr("no number") } } NumberGenerator { id: numberGenerator } Connections { target: numberGenerator function onNextNumber(number) { numberLabel.text = number } } } Модель на основе языка Python Одним из наиболее интересных типов объектов или классов, которые можно передать из Python в QML, являются модели элементов. Они используются с различными представлениями или элементом Repeater для динамического построения пользовательского интерфейса на основе содержимого модели. В этом разделе мы возьмем существующую python-утилиту для мониторинга загрузки процессора (и не только), psutil, и подключим ее к QML через пользовательскую модель элементов под названием CpuLoadModel. Ниже показана программа в действии:
СОВЕТ Библиотека psutil находится по адресу https://pypi.org/project/psutil/ (https://pypi.org/project/psutil/) . "psutil (process and system utilities) - кроссплатформенная библиотека для получения информации о запущенных процессах и использовании системы (процессор, память, диски, сеть, датчики) на языке Python." Установить psutil можно с помощью pip install psutil . Мы будем использовать функцию psutil.cpu_percent (документация (https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent) ) для ежесекундной выборки загрузки процессора на ядро. Для управления выборкой мы используем QTimer . Все это реализуется через модель CpuLoadModel, которая представляет собой QAbstractListModel .
Модели элементов очень интересны. Они позволяют представить двумерный набор данных или даже вложенные наборы данных, если использовать QAbstractItemModel QAbstractListModel позволяет . Используемая представить список нами модель элементов, поэтому одномерный набор данных. Можно реализовать вложенный набор списков, создав дерево, но мы будем создавать только один уровень. Для реализации QAbstractListModel необходимо реализовать методы rowCount и data. Метод rowCount возвращает количество ядер процессора, которое мы получаем с помощью метода psutil.cpu_count. Метод data возвращает данные для различных ролей. Мы поддерживаем только Qt.DisplayRole, что соответствует тому, что получается при обращении к display внутри элемента делегата из QML. Если посмотреть на код модели, то можно увидеть, что фактические данные хранятся в списке cpu_load. Если к данным сделан корректный запрос, т.е. правильно указаны строка, столбец и роль, то мы возвращаем нужный элемент из списка список cpu_load. В противном случае мы возвращаем None, что соответствует неинициализированному QVariant на стороне Qt. Каждый раз, когда истекает таймер обновления ( update_timer ), срабатывает метод update. Здесь обновляется список cpu_load, но при этом выдается сигнал dataChanged, указывающий на то, что все данные были изменены. Мы не выполняем сброс модели (modelReset), поскольку это также подразумевает, что количество элементов могло измениться. Наконец, модель CpuLoadModel отображается в QML как зарегистрированный тип в Модуль PsUtils. import psutil import sys from PySide6.QtGui import QGuiApplication from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterTyp from PySide6.QtCore import Qt, QUrl, QTimer, QAbstractListModel

class CpuLoadModel(QAbstractListModel): def __init__(self): QAbstractListModel.__init__(self) self.__cpu_count = psutil.cpu_count() self.__cpu_load = [0] * self.__cpu_count self.__update_timer = QTimer(self) self.__update_timer.setInterval(1000) self.__update_timer.timeout.connect(self.__update) self.__update_timer.start() # The first call returns invalid data psutil.cpu_percent(percpu=True) def __update(self): self.__cpu_load = psutil.cpu_percent(percpu=True) self.dataChanged.emit(self.index(0,0), self.index(self. def rowCount(self, parent): return self.__cpu_count def data(self, index, role): if (role == Qt.DisplayRole and index.row() >= 0 and index.row() < len(self.__cpu_load) and index.column() == 0): return self.__cpu_load[index.row()] else: return None if __name__ == '__main__': app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() qmlRegisterType(CpuLoadModel, 'PsUtils', 1, 0, 'CpuLoadMode engine.load(QUrl("main.qml")) if not engine.rootObjects():
sys.exit(-1) sys.exit(app.exec()) На стороне QML мы используем ListView для отображения загрузки процессора. Модель привязывается к свойству model. Для каждого элемента модели будет инстанцирован элемент-делегат. В данном случае это прямоугольник с зеленой полосой (еще один прямоугольник) и элемент Text, отображающий текущую загрузку. import QtQuick import QtQuick.Window import PsUtils Window { id: root width: 640 height: 480 visible: true title: qsTr("CPU Load") ListView { anchors.fill: parent model: CpuLoadModel { } delegate: Rectangle { id: delegate required property int display width: parent.width height: 30 color: "white" Rectangle { id: bar
width: parent.width * delegate.display / 100.0 height: 30 color: "green" } Text { anchors.verticalCenter: parent.verticalCenter x: Math.min(bar.x + bar.width + 5, parent.width text: delegate.display + "%" } } } }
Ограничения На данный момент есть некоторые вещи, которые не так легко доступны. Одна из них заключается в том, что вы не можете легко создавать подключаемые модули QML с помощью Python. Вместо этого необходимо импортировать "модули" QML в программу на Python, а затем использовать qmlRegisterType, чтобы сделать возможным их импорт из QML.
Резюме В этой главе мы рассмотрели модуль PySide6 из проекта Qt for Python. После краткого ознакомления с установкой мы сосредоточились на том, как концепции Qt используются в Python. Сюда вошли слоты, сигналы и свойства. Мы также рассмотрели базовую модель списков и то, как отображать объекты и классы из Python в QML.
Qt для MCU Уведомление Qt for MCUs входит не в дистрибутив Qt с открытым исходным кодом, а в качестве коммерческого дополнения. Qt for MCUs - это версия Qt, предназначенная для платформ, которые слишком малы для работы с Linux. Вместо этого Qt for MCUs может работать поверх FreeRTOS или даже на "голом металле", т.е. без участия какой-либо операционной системы. Поскольку в этой книге основное внимание уделяется QML, мы более подробно рассмотрим Qt Quick Ultralite и сравним его с полноразмерным предложением Qt. Используя Qt for MCUs, вы можете создавать красивые, плавные графические интерфейсы пользователя для систем на базе микроконтроллеров. Qt для MCUs ориентирован на графический фронт-энд, поэтому вместо традиционных модулей Qt используются обычные типы C++. Это означает, что некоторые интерфейсы меняются. В первую очередь это касается того, как модели отображаются в QML. В этой главе мы рассмотрим это и многое другое.
Настройка Qt for MCUs (https://doc.qt.io/QtForMCUs/index.html) поставляется с поддержкой ряда оценочных плат от таких компаний, как NCP, Renesas, ST и Infinion/Cypress. Они хороши для начала работы и помогают опробовать интеграцию с конкретным MCU. В конечном итоге, скорее всего, придется настраивать конкретное определение платформы под конкретное оборудование, например, настраивать объем оперативной памяти, FLASH и конфигурацию экрана. Помимо поддержки нескольких MCU из коробки, Qt for MCUs также поддерживает работу либо с FreeRTOS, либо непосредственно на "голом металле", т.е. без операционной системы. Поскольку Qt for MCUs ориентирован на графический фронт-энд, в нем нет классов для файловых систем и т.п. Все это должно исходить от базовой системы. Поэтому, если вам нужна поддержка более сложных функций, то FreeRTOS - это один из вариантов.
Что касается среды разработки, то различные платы поставляются с различными компиляторами, поэтому настройка Qt for MCUs будет выглядеть несколько по-разному в зависимости от того, на какой MCU вы ориентируетесь, а также от того, какой компилятор вы выберете. Например, для плат от ST поддерживаются GCC и IAR, а для некоторых других плат используется Green Hills MULTI Compiler. Официально поддерживаемыми хостами разработки с точки зрения Qt являются Linux (Ubuntu 20.04 LTS на x86_64) или Windows (Windows 10 на x86_64). Для Windows обратите внимание, что поддерживаются компиляторы MSVC редакций 2017 и 2019 - не самые последние. Для получения рабочей среды обязательно следуйте последним инструкциям по настройке на сайте qt.io (https://doc.qt.io/QtForMCUs/qtul-setup- development-host.html). После настройки среды поддерживаемые платы можно найти в разделе Kits, а также в разделе Devices - MCU под пунктом меню Tools - Options... в Qt Creator.
СОВЕТ Если вы не нашли вкладку MCUs в разделе Tools, убедитесь, что подключаемые модули Qt for MCUs (McuSupport и BaremetalSupport) доступны и активированы в разделе Help About Plugins..... Ссылки Дополнительная информация на сайте qt.io: Поддерживаемые платы Руководствопо портированию платформ
Hello World - для микроконтроллеров Поскольку настройка Qt для MCU может быть несколько затруднительной, мы начнем с примера типа Hello World, чтобы убедиться в работоспособности инструментария и обсудить основные различия между Qt Quick Ultralite и стандартным Qt Quick. Прежде всего, нам необходимо создать проект Qt for MCUs в Qt Creator, чтобы получить точку входа в систему на языке C++. При работе с Qt Quick Ultralite мы не можем использовать обычную среду выполнения, такую как qml . Это связано с тем, что Qt Quick Ultralite транслируется на C++ вместе с оптимизированными версиями всех активов. Затем они встраиваются в целевой исполняемый файл. Это означает, что нет поддержки динамической загрузки QML и т.п., поскольку на целевом исполняемом файле не работает интерпретатор.
Я называю проект helloworld. Не стесняйтесь выбрать собственное имя. Единственное, что изменится, - это имя QML-файла проекта, являющегося точкой входа. Кроме того, при создании проекта обязательно выберите набор Qt for MCUs. Пройдя еще несколько страниц настройки, вы получите проект, как показано ниже.
После того как базовый проект настроен, запустите его на рабочем столе и убедитесь, что появилось окно, подобное показанному ниже. Теперь, когда мы знаем, что установка работает, замените QML в файле helloworld.qml на код, показанный ниже. Ниже мы рассмотрим этот пример построчно, но сначала соберите и запустите его для вашей целевой программы Qt for MCU Desktop. В результате должно появиться окно, похожее на скриншот, приведенный ниже.
import QtQuick import QtQuickUltralite.Extras Rectangle { width: 480 height: 272 Rectangle { id: rect anchors.fill: parent anchors.margins: 60 color: "orange" Behavior on color { ColorAnimation { duration: 400 } } MouseArea { anchors.fill: parent onClicked: { if (rect.color == "red") rect.color = "orange"; else rect.color = "red"; } } } StaticText { anchors.centerIn: parent color: "black" text: "Hello World!" font.pixelSize: 52 } } }
Щелкните на оранжевом прямоугольнике, и он станет красным. Щелкните его еще раз, и он снова станет оранжевым. Теперь давайте посмотрим на исходный код с точки зрения Qt Quick и сравним. Во-первых, Qt Quick Ultralight игнорирует номера версий после операторов импорта. Это поддерживается и в Qt Quick, начиная с Qt 6, за счет отсутствия
номер версии, поэтому если вы можете обойтись без него, но вам нужна совместимость, оставьте номер версии. import QtQuick import QtQuickUltralite.Extras В корне нашей сцены мы размещаем прямоугольник Rectangle . Это связано с тем, что Qt Quick Ultralite не предоставляет белый фон по умолчанию. Используя в качестве корня Rectangle, мы обеспечиваем контроль над цветом фона сцены. Rectangle { width: 480 height: 272 Следующая часть, прямоугольник с возможностью щелчка, представляет собой простой QML, с некоторым количеством Javascript, привязанным к событию onClicked. Qt для MCU имеет ограниченную поддержку Javascript, поэтому старайтесь, чтобы такие сценарии были простыми. Более подробно о конкретных ограничениях можно прочитать в ссылках в конце этого раздела. Rectangle { id: rect anchors.fill: parent anchors.margins: 60 color: "orange" Behavior on color { ColorAnimation { duration: 400 } } MouseArea { anchors.fill: parent onClicked: {
if (rect.color == "red") rect.color = "orange"; else rect.color = "red"; } } } Наконец, текст выводится с помощью элемента StaticText, который представляет собой версию элемента Text для статических текстов. Это означает, что текст может быть отрисован один раз или даже предварительно отрендерен, что позволяет сэкономить много ресурсов на небольшой системе на базе MCU. StaticText { anchors.centerIn: parent color: "black" text: "Hello World!" font.pixelSize: 52 } } В Qt Creator можно заметить, что появляются предупреждения вокруг Элемент StaticText. Это происходит потому, что Qt Creator предполагает, что вы работаете с Qt Quick. Для того чтобы Qt Creator знал Qt Quick Ultralite, необходимо установить параметр QML_IMPORT_PATH на путь к модулю совместимости с Qt for MCUs. Это можно сделать в файле CMakeLists.txt или в настройках проекта. Настройки проекта для стандартной установки Windows 10 приведены ниже.
Помимо того, что было сказано выше, есть и другие отличия. Например, в классе Qt Quick Ultralite Item, а значит, и в классе Rectangle, отсутствуют многие свойства, которые можно было бы найти в Qt Quick. Например, отсутствуют свойства масштабирования и вращения. Они доступны только для таких специфических элементов, как Image , и там вместо свойств используются типы Rotation и Scale. Если выйти за рамки приведенного примера, то в целом в Qt Quick Ultralite меньше QML-элементов, но поддерживаемые типы постоянны увеличивается. Мы стремимся к тому, чтобы предоставляемые типы покрывали все случаи использования целевых устройств. Более подробно об этом и об общих вопросах совместимости можно прочитать в приведенных ниже ссылках. Ссылки Дополнительная информация на сайте qt.io: Qt Quick Ultralite против Qt Quick Различия между Qt Quick Ultralite Controls и Qt Quick Controls Известные проблемы и ограничения
Интеграция с C++ C++ Для того чтобы продемонстрировать связь между C++ и QML в Qt for MCUs, мы создадим простой синглтон Counter, хранящий целочисленное значение. Обратите внимание, что мы начинаем со структуры, а не с класса. Это обычная практика в Qt Quick Ultralite. Синглтон будет использоваться из небольшого пользовательского интерфейса, как показано ниже. Структура Counter предоставляет свойство value, а также методы для изменения значения, increase и decrease, а также метод сброса. Кроме того, в ней предусмотрен сигнал hasBeenReset . #ifndef COUNTER_H #define COUNTER_H
#include <qul/singleton.h> #include <qul/property.h> #include <qul/signal.h> class Counter : public Qul::Singleton<Counter> { public: Counter(); Qul::Property<int> value; void increase(); void decrease(); void reset(); Qul::Signal<void(void)> hasBeenReset; }; #endif // COUNTER_H Со стороны Qt это выглядит странно. Именно здесь Qt для MCUs демонстрирует основные отличия. Здесь нет базового класса QObject или макроса Q_OBJECT, вместо этого используется новый набор классов из Qul. В данном конкретном случае базовым классом является класс Qul::Singleton, создающий глобально доступный синглтон в мире QML. Мы также используем класс Qul::Signal для создания сигнала и класс Qul::Property для создания свойства. Все публичные, не перегруженные функции-члены раскрываются в QML автоматически. СОВЕТ Для создания элемента, который может быть инстанцирован из QML, вместо синглтона используйте базовый класс Qul::Object.
Затем эта структура выводится в QML с помощью макроса CMake qul_target_generate_interfaces . Ниже приведен CMakeLists.txt, основанный на файле, сгенерированном Qt Creator, с добавленными файлами counter.h и counter.cpp. qul_target_generate_interfaces(cppintegration counter.h) Теперь продолжим реализацию структуры Counter. Во-первых, для свойства value мы используем функции value и setValue для доступа и модификации фактического значения. В нашем случае свойство содержит и int , но, как и для обычного движка QML, типы отображаются между C++ и QML (https://doc.qt.io/QtForMCUs/qtulintegratecppqml.html#type-mapping). Это используется в конструкторе, показанном ниже, который устанавливает начальное значение в ноль. Counter::Counter() { value.setValue(0); } Функции увеличения и уменьшения выглядят аналогично. Они используют геттер и сеттер вместо того, чтобы взаимодействовать непосредственно со значением. void Counter::increase() { value.setValue(value.value()+1); } void Counter::decrease() { value.setValue(value.value()-1); }
Счетчик также имеет сигнал. Сигнал представлен экземпляром Qul::Signal с именем hasReset . В качестве шаблонного аргумента сигнал принимает сигнатуру функции, поэтому для создания сигнала, несущего целое число, создать Qul::Signal<void(int)> . В данном случае сигнал не несет никаких значений, поэтому он определяется как void(void) . Чтобы издать сигнал, мы просто вызываем его, как если бы это была обычная функция, как показано в функции reset ниже. void Counter::reset() { std::cout << "Resetting from " << value.value() << std::end value.setValue(0); hasBeenReset(); } QML Код QML создает простой пользовательский интерфейс, показанный ниже.
Мы рассмотрим пользовательский интерфейс в трех частях. Вопервых, основная структура и привязки к Counter.value : import QtQuick Rectangle { width: 480 height: 272 Column { // Left buttons goes here } Column { // Right buttons goes here } Text { anchors.centerIn: parent text: Counter.value; } } Как видно, свойство text элемента Text привязано к элементу Счетчик.значение, как и во всех QML. Теперь рассмотрим левые боковые кнопки. Они используются для вызова методов C++, предоставляемых через синглтон Counter. PlainButton - это QML-элемент, который мы используем для создания этих простых кнопок. Он позволяет задать текст, цвет фона и обработчик сигнала нажатия. Как видно, каждая кнопка вызывает соответствующий метод синглтона Counter. Column { x: 10 y: 10
spacing: 5 PlainButton { text: "+" onClicked: Counter.increase() } PlainButton { text: "reset" onClicked: Counter.reset() } PlainButton { text: "-" onClicked: Counter.decrease() } } Кнопки справа изменяют значение Counter.value непосредственно из QML. Это возможно сделать, но невидимо для C++. В языке C++ нет простого способа отследить изменение свойства, поэтому, если необходима реакция C++, рекомендуется использовать метод сеттера, а не напрямую изменять значение свойства. Column { x: 350 y: 10 spacing: 5 PlainButton { color: "orange" text: "++" onClicked: Counter.value += 5; } PlainButton { color: "orange" text: "100" onClicked: Counter.value = 100; } PlainButton { color: "orange"
text: "--" onClicked: Counter.value -= 5; } } Здесь показано, как предоставить синглтон из C++ и как осуществлять вызовы функций, выдавать сигналы и обмениваться состоянием (свойствами) между C++ и QML. Повторный просмотр файла CMake Файл CMakeLists.txt может показаться вам знакомым, но в нем есть некоторые советы и хитрости, которые мы должны обсудить. Прежде всего, для того чтобы отобразить класс C++ в QML, необходимо воспользоваться функцией qul_target_generate_interfaces , например: qul_target_generate_interfaces(cppintegration counter.h) Вторая половина, QML-файлы, добавляется с помощью макроса qul_target_qml_sources. Если у вас несколько QML-файлов, просто перечислите их по очереди, как показано ниже: qul_target_qml_sources(cppintegration cppintegration.qml PlainB Еще один интересный момент заключается в том, что мы собираем проект на языке Си++ без написания функции main. Об этом позаботился макрос app_target_default_main, который добавляет в проект эталонную реализацию main. Разумеется, вы можете заменить его на собственную функцию main, если вам требуется больший контроль. app_target_default_main(cppintegration cppintegration)
И наконец, библиотеки, с которыми выполняется линковка, - это не стандартные библиотеки Qt, а Qul:: такие, как, например: target_link_libraries(cppintegration Qul::QuickUltralite Qul::QuickUltralitePlatform) Ссылки Дополнительная информация на сайте qt.io: Интеграция C++ и QML
Работа с моделями В Qt Quick Ultralite можно создавать модели на языке QML, используя элемент ListModel. Также возможно, и это несколько интереснее, создавать модели из C++. Это позволяет отображать списки данных из C++ в QML и инстанцировать элементы пользовательского интерфейса для каждого элемента списка. Настройка очень похожа на обычную Qt Quick, но базовые классы и интерфейсы более ограничены. В этой главе мы создадим список городов Европы, в котором будет указано название города и страна, в которой он расположен. Города будут отображаться в ListView, как показано ниже: C++ Для создания модели в Qt Quick Ultralite первым делом необходимо определить struct с данными каждого элемента списка. Для этой структуры мы также
необходимо предоставить оператор ==. Именно так мы и поступим со структурой CityData, показанной ниже. Обратите внимание, что в Qt Quick Ultralite мы используем std::string, а не QString. Предполагается, что используется кодировка UTF-8. #include <string> struct CityData { std::string name; std::string country; }; inline bool operator==(const CityData &l, const CityData &r) { return l.name == r.name && l.country == r.country; } После подготовки типа данных мы объявляем структуру CityModel, наследующую от Qul::ListModel . Это позволяет нам определить модель, к которой можно обращаться из QML. Мы должны реализовать методы count и data, которые аналогичны, но не идентичны соответствующим методам класса QAbstractListModel. Мы также используем макрос `CMake qul_target_generate_interfaces, чтобы сделать типы доступными для QML. #include <qul/model.h> #include <platforminterface/allocator.h> struct CityModel : Qul::ListModel<CityData> { private: Qul::PlatformInterface::Vector<CityData> m_data; public: CityModel();
int count() const override { return m_data.size(); } CityData data(int index) const override { return m_data[ind }; Мы также реализуем конструктор для структуры CityModel, который заполняет данными вектор m_data. #include "citymodel.h" CityModel::CityModel() { m_data.push_back(CityData {"Berlin", "Germany"}); m_data.push_back(CityData {"Copenhagen", "Denmark"}); m_data.push_back(CityData {"Helsinki", "Finland"}); m_data.push_back(CityData {"London", "England"}); m_data.push_back(CityData {"Oslo", "Norway"}); m_data.push_back(CityData {"Paris", "France"}); m_data.push_back(CityData {"Stockholm", "Sweden"}); } QML В примере мы показываем модель в виде прокручиваемого списка, как показано ниже.
Полностью код QML приведен ниже: import QtQuick 2.0 Rectangle { width: 480 height: 272 CityModel { id: cityModel } Component { id: cityDelegate Item { width: 480 height: 45 Column { spacing: 2 Text { text: model.name x: 10
font: Qt.font({ pixelSize: 24, unicodeCoverage: [Font.UnicodeBlock }) } Text { text: model.country x: 10 color: "gray" font: Qt.font({ pixelSize: 12, unicodeCoverage: [Font.UnicodeBlock }) } Rectangle { color: "lightGreen" x: 10 width: 460 height: 1 } } } } ListView { anchors.fill: parent model: cityModel delegate: cityDelegate spacing: 5 } } Пример начинается с инстанцирования модели cityModel . Поскольку модель не является синглтоном, ее необходимо инстанцировать из QML. Затем делегат cityDelegate реализуется как Component . Это означает, что он может быть многократно инстанцирован из QML. Данные модели
доступ к которому осуществляется через присоединенные свойства model.name и model.country. Наконец, элемент ListView объединяет модель и делегат, в результате чего получается список, показанный на скриншотах в этой главе. Интересным аспектом QML является то, как настраивается шрифт элементов Text. Свойство unicodeCoverage позволяет указать компилятору Qt Quick Ultralite, какие символы мы хотели бы иметь возможность отображать. При указании фиксированных строк инструментарий Qt Quick Ultralite генерирует минимальные шрифты, содержащие именно те глифы, которые мы собираемся использовать. Однако, поскольку модель будет предоставлять нам динамические данные, нам необходимо указать шрифту, какие символы мы предполагаем использовать. При рендеринге полного шрифта иногда возникают предупреждения следующего вида: [2/7 8.8/sec] Generating CMakeFiles/cppmodel.dir/qul_font_engin Warning: Glyph not found for character "\u0000" Warning: Glyph not found for character "\u0001" Warning: Glyph not found for character "\u0002" Warning: Glyph not found for character "\u0003" Их можно смело игнорировать, если только вы не рассчитываете показать данный персонаж.

Резюме В этой главе мы лишь поверхностно рассмотрели Qt for MCUs и Qt Quick Ultralite. Эти технологии переносят Qt на гораздо более компактные платформы и делают его по-настоящему встраиваемым. На протяжении всей главы мы использовали виртуальный рабочий стол, который позволяет быстро создавать прототипы. Нацеливание на конкретную плату не менее просто, но требует доступа к аппаратному обеспечению и соответствующим инструментам. Основные преимущества использования Qt Quick Ultralite заключаются в меньшем количестве встроенных элементов, а также в том, что некоторые API несколько ограничены. Но с учетом целевых систем это обычно не является препятствием. Qt Quick Ultralite также превращает QML в компилируемый язык, что, в общем-то, неплохо. Это позволяет отлавливать больше ошибок во время компиляции, а не тестировать их во время выполнения.