- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF143:QML
Материал из Linuxformat.
- Виджеты QML Возьмем самый красивый и перетащим его в нашу программу на Qt
QML |
---|
|
Содержание |
QML: На службе приложений Qt
- Андрей Боровский готов поспорить, что QML стал любимой игрушкой кодеров Qt, и все их усилия нацелены на рисование красивых виджетов QML.
В прошлый раз мы познакомились с языком описания интерфейса QML и исследовали взаимодействие объектов QML между собой и с объектами Qt. На этом уроке мы воспользуемся QML для создания «настоящего» виджета, который можно будет использовать в программе на Qt. Этот виджет представляет собой переработанный пример QML Dial из дистрибутива Qt 4.7.1.
Сразу предупреждаю, что примеры из этой статьи будут работать с Qt 4.7.1, 4.7.2 и, надеюсь, с более поздними версиями Qt, но не с более ранними. Также хочу напомнить, что при работе с QML в файл проекта Qt нужно добавить строчку
QT += declarative
Демо QML Dial из дистрибутива Qt – это самостоятельная программа, написанная на QML (я уже писал о том, что QML можно использовать как самостоятельный язык программирования, наподобие JavaScript). Меня же, как программиста Qt, больше интересует применение QML для расширения возможностей интерфейсов программ Qt. Вот такое расширение мы сегодня и напишем, а заодно познакомимся с новыми возможностями Qt и некоторыми новыми методами взаимодействия Qt и QML. Сам объект QML Dial представляет собой почти фотореалистичную имитацию стрелочного индикатора, такого как спидометр или индикатор давления жидкости в трубе (рис. 1). Мы не только «перетащим» этот индикатор в свою программу Qt, но и дополним его некоторыми элементами (рис. 2).
Как мы уже знаем, важнейшей задачей QML является создание красивых графических интерфейсов, а это, как вы понимаете, невозможно без мощных средств работы с растровой графикой (для программиста рисовать красивые интерфейсы внутрипрограммно, векторными функциями, несколько утомительно). Для работы с изображениями, хранимыми в файлах, в языке QML есть объект Image{}. Этот объект подобен тэгу HTML <img…>, за исключением того, что он может гораздо больше. В простейшем варианте использование объекта Image{} выглядит так:
Image { source: “background.png” }
Эта конструкция загружает фон нашего индикатора (описание внешнего вида индикатора хранится в файле Dial.qml, расположенном в директории Dial и сопутствующих файлах с расширением png.) На первый взгляд тут все понятно без особых пояснений. Мы загружаем изображение из файла background.png... Стоп. А куда мы его загружаем? Где оно будет расположено? Вспомним, что визуальная часть QML основана на Qt Graphics View Framework, а эта система по умолчанию стремится расположить графические элементы так, чтобы геометрический центр сцены совпадал с центром окна вывода. Так что по умолчанию изображение background.png просто займет место в центре окна нашего виджета (что нам и требуется).
Помимо собственных свойств, к каковым относится, например, свойство source, объект Image{} унаследовал от базовых объектов QML такие свойства как x, y, scale, rotation, transform, anchors и многие другие. Сам движок, выполняющий отрисовку изображения, обладает многими полезными возможностями. Например, если загружаемый формат поддерживает альфа-канал, то при выводе изображения учитывается уровень прозрачности его элементов.
Рассмотрим некоторые свойства объекта Image{}. Если мы хотим выложить сцену фоновым рисунком как плиткой, надо добавить свойство
fillMode: Image.Tile
Свойства scale, rotation и transform, как вы уже догадались, позволяют выполнять преобразования изображения, такие как масштабирование и вращение. Вот как, например, выполняется вращение тени под стрелкой нашего индикатора:
transform: Rotation { origin.x: 9; origin.y: 67 angle: needleRotation.angle }
Вся эта конструкция находится в теле объекта image, отвечающего за вывод тени под стрелкой (у нас ведь почти фотореализм, так что тень должна двигаться вместе со стрелкой). В объекте Rotation мы задаем координаты центра вращения и угол поворота (если кто не понял, в этом чудесном языке двоеточие является оператором присваивания).
А как выполняется вращение самой стрелки? Тут все еще интереснее. Вот как выглядит описание стрелки:
Image { id: needle x: 98; y: 33 smooth: true source: “needle.png” transform: Rotation { id: needleRotation origin.x: 5; origin.y: 65 //! [needle angle]
angle: root.angle
Behavior on angle { SpringAnimation { spring: 1.4 damping: .15 } } //! [needle angle] } }
Большая часть этого описания должна быть вам уже понятна. Мы устанавливаем координаты стрелки; присвоение значения true свойству smooth приводит к тому, что при выполнении преобразований изображения стрелки (в нашем случае это вращение) выполняется специальная фильтрация, сглаживающая эффекты «лесенки», которые могут в ходе этих преобразований возникнуть. Сравните координаты стрелки и координаты тени. Между прочим, поскольку на изображении (файл needle.png) стрелка смотрит вверх, наш индикатор по умолчанию указывает на значение в середине диапазона. Можно было бы изменить это, повернув стрелку на картинке или выполнив необходимое вращение при инициализации, но мы оставим все как есть, потому что так интереснее.
Описание объекта Rotation начинается так же, как и в случае с тенью, однако дальше следует странное. Конструкция Behavior on angle {} указывает, что должна делать стрелка, когда ее угол поворота меняется. Объект SpringAnimation создает специальный анимационный эффект при движении стрелки – эффект затухающих колебаний. Свойство spring указывает, насколько велика должна быть изначальная амплитуда колебаний, а свойство damping определяет скорость их затухания. В результате стрелка нашего индикатора будет колебаться как настоящая стрелка на пружине.
С помощью свойства visible, которым обладают все объекты QML, в том числе и объект image, мы можем управлять видимостью этих объектов.
Возможно, вас удивляет, что свойство angle объекта needleRotation меняется всякий раз, когда меняется свойство root.angle. В таких языках, как C++, одна операция присваивания означает одно изменение значения; но в QML, похоже, операция присваивания продолжает работать постоянно, и изменение значения свойства в правой части выражения присваивания приводит к изменению значения свойства в левой части, когда бы оно ни случилось. И это действительно так. В QML этот механизм назван связыванием свойств [property binding]. Строка
angle: root.angle
связывает между собой свойства root.angle и angle, так что изменение значения первого свойства всегда будет приводить к изменению значения второго. Связывание свойств – это особая форма присваивания, которая используется всегда, когда слева от оператора присваивания указано свойство объекта, а справа – любое синтаксически корректное выражение языка JavaScript (свойство, функция и т. д). То есть фактически связывание свойств является стандартным методом присваивания в QML, при котором свойство, стоящее слева от оператора присваивания, становится псевдонимом выражения, стоящего справа (вот почему связывать можно не только свойства, но и такие «пассивные» объекты, как функции, которые сами не могут ничего инициировать). Связывание свойств – это тот механизм, который позволяет объектам QML обмениваться данными друг с другом, подобно тому, как объекты Qt обмениваются данными друг с другом с помощью сигналов и слотов. Точно так же, как в случае сигналов и слотов, связывание уже связанных свойств можно изменить с помощью элемента QML PropertyChanges {}. Мне очень нравится связывание свойств, и если бы я проектировал новый объектно-ориентированный язык программирования, то обязательно включил бы в него этот механизм.
Дальше в исходном примере на основание стрелки накладываются колпачок и стекло с бликом, изображения которых хранятся в файле overlay.png. Обратите внимание, что практически везде мы используем способность формата PNG создавать частично прозрачные изображения, иначе такой сложный элемент управления у нас просто не получился бы. Обратите внимание также на то, что порядок расположения изображений поверх друг друга соответствует порядку следования представляющих их объектов в тексте программы QML. В общем-то это естественно, если учесть, что виджет создается по мере выполнения программы.
Ко всему этому великолепию я добавил совсем немного.
С объектом Rectangle мы уже встречались:
Rectangle { x: 61 y: 118 width: 80 height: 36 color: “black” border.color: “#888888” }
Новое здесь – свойство border.color, позволяющее задать цвет границы прямоугольника. Объект Text дублирует значение, которое указывает индикатор.
Text { color: “green” text: root.angle/2 + 50 x: 80 y: 114 font.pointSize: 24; font.bold: true style: Text.Raised styleColor: “black” }
Здесь я хотел бы обратить ваше внимание только на один момент. Поскольку по умолчанию стрелка индикатора смотрит вверх, фактические углы поворота следует указывать относительно этого положения, причем повернутая на некоторый угол стрелка при новом повороте интерпретирует новый угол так, как если бы она находилась в исходном положении.
Создаем виджет
Перейдем ко второй части нашей работы – превращению модуля QML в виджет Qt. Необходимые для этого основные операции мы изучили в прошлый раз. Но и теперь нам есть что добавить. Представителем виджета QML в нашей программе Qt является класс Dial (файлы Dial.h, Dial.cpp). Здесь я приведу только объявление класса, остальное вы найдете на диске.
class Dial : public QObject { Q_OBJECT Q_PROPERTY(int angle READ angle WRITE setAngle NOTIFY angleChanged) public: Dial(); int angle(); void setAngle(int a); signals: void angleChanged(); private: int m_angle; };
Тем, кто читал предыдущую статью и знает Qt, тут все должно быть понятно. Класс экспортирует единственное свойство angle. Напомню только, что метод setAngle() должен явным образом эмитировать сигнал angleChanged(), иначе виджет QML никогда не узнает, что угол изменился. Знатоки сигналов и слотов Qt могут спросить, почему в сигнале angleChanged() не передается новое значение угла поворота. Если бы сигнал предназначался для других классов Qt, я бы так и сделал, но сигнал предназначен для виджета QML, а соответствующий объект этого виджета все равно прочитает значение свойства angle с помощью метода angle() (указанного после ключевого слова READ в макросе Q_PROPERTY). Так что передавать какой-либо параметр в сигнале Qt просто нет нужды.
Виджет в окне программы
Создание виджета в окне Qt тоже не представляет собой ничего особенно нового, за исключением одного момента, о котором будет сказано ниже. Вот как мы создаем виджет (это фрагмент файла dialcontrol.cpp):
QDeclarativeView *qmlView = new QDeclarativeView; dial = new Dial(); qmlView->rootContext()->setContextProperty(“Dial”, dial); qmlView->setSource(QUrl(“qrc:/Dial/Dial.qml”)); QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(qmlView);
Вы, наверное, сразу обратили внимание на то, какую ссылку мы используем для загрузки исходного текста модуля QML. В прошлый раз мы использовали ссылку на файл Linux. Это давало нам огромную свободу в плане модификации внешнего вида окна нашей программы, но делало ее зависимой от расположения файлов QML. Теперь мы поступаем иначе и включаем файл Dial.qml и все сопутствующие ему файлы в модуль ресурсов Qt.
В результате описание виджета QML станет частью программы Qt, и нам уже не придется беспокоиться о том, где хранятся соответствующие файлы. Обычно в модули ресурсов включают пиктограммы и элементы интернационализации приложения, но ничто не мешает нам включить в них модули QML, тем более что движок Qt QML умеет работать с пространством ресурсов приложения (и с другими пространствами URL) как с локальной файловой системой. Так что если включенному в модуль ресурсов модулю QML понадобится файл, тоже включенный в модуль ресурсов, модуль QML сможет без проблем загрузить его. Главное, чтобы ссылки на файлы были относительными, а не абсолютными.
Чтобы превратить модуль QML в ресурс программы Qt, нам понадобится файл описания ресурсов *.qrc. Работать с файлами с расширением qrc можно многими способами – с помощью дизайнера графических интерфейсов Qt или с помощью программы Qt Creator, но можно обойтись и текстовым редактором, ведь формат QRC основан на XML. Вот как, например, выглядит файл QRC для нашей программы (файл dialcontrol.qrc):
<!DOCTYPE RCC><RCC version=”1.0”> <qresource> <file>Dial/background.png</file> <file>Dial/Dial.qml</file> <file>Dial/DialControl.qrc</file> <file>Dial/needle.png</file> <file>Dial/needle_shadow.png</file> <file>Dial/overlay.png</file> </qresource> </RCC>
Внутри тэга <file> мы просто указываем путь к файлу ресурса относительно расположения файла QRC. Теперь при сборке приложения виджет QML будет включен в нашу программу. В результате наша программа больше не зависит от расположения файлов QML, но хорошо это или плохо?
С одной стороны, это упрощает установку программы: вам не нужно думать о том, где должны быть размещены файлы QML в соответствии со стандартом XDG, а если вы пишете кросс-платформенное приложение, ваша жизнь упрощается еще больше. С другой стороны, при таком подходе теряется одно из важнейших преимуществ QML как средства описания интерфейсов программ Qt: возможность радикально сменить интерфейс без повторной сборки приложения.
Существует еще своего рода компромиссный вариант: скомпилировать виджет QML как внешний двоичный ресурс программы Qt (для этого служит утилита rcc). С одной стороны, работать с внешним файлом ресурса в программе Qt не намного сложнее, чем со встроенным; с другой стороны, внешний файл ресурса может быть заменен без повторной сборки программы. Однако и у этого подхода есть свой минус: дизайнеру интерфейсов придется иметь дело со специальными инструментами Qt, такими как rcc, тогда как в случае расположения виджета QML целиком в собственных файлах дизайнеру для изменения интерфейса будет достаточно текстового редактора и редактора GIMP (или, на худой конец, Photoshop). В общем, наиболее разумный выбор зависит от конкретных целей – хотите ли вы, чтобы каждый пользователь, освоивший QML и растровую графику, мог «сшить новую одежку» для вашей программы, или нет.
Еще о методе setContextProperty()
До сих пор мы использовали метод setContextProperty() для передачи указателя на созданные нами объекты со свойствами. Но его возможности гораздо шире. Прежде всего, отметим, что существует и другой вариант метода setContextProperty():
void setContextProperty ( const QString & name, const QVariant & value )
То есть, со свойством контекста QML можно связать не только объект, но и любое значение, приводимое к типу QVariant. Например:
qmlView->rootContext()->setContextProperty(“HelloText”, trUtf8(“Hello World!”);
Объекты, которые мы передаем контекстам, могут экспортировать в QML не только свойства, но и методы. Чтобы модуль QML увидел метод объекта, этот метод должен быть объявлен в разделе public slots. Например, еслидобавить в класс Dial метод
public slots: int angleToPos(int angle) { … }
то в файле Dial.qml можно написать так:
property int angleToPos : dial.angleToPos(angle)
Сбор информации об ошибках
Поскольку наша сложная конструкция QML может нуждаться в отладке, в программе предусмотрен способ вывода информации об ошибках, которые возникают в ходе выполнения программы на QML. Вообще-то в ОС Linux все сообщения об ошибках QML выводятся в стандартный поток вывода той консоли, с которой была запущена программа. В ОС Windows Qt этого почему-то не делает: даже если запустить программу из окна командной строки Windows, никаких сообщений об ошибках мы не увидим (даже если ошибки есть). В любом случае, наше приложение графическое, и его не обязательно будут запускать из окна консоли, так что неплохо обзавестись собственным методом вывода информации об ошибках, основанном на графическом интерфейсе.
Список актуальных ошибок возвращается методом errors() QML-виджета в виде объекта
QList<QDeclarativeError>;
Объект класса QDeclarativeError включает несколько свойств и методов, из которых мы воспользуемся методом toString(). Этот метод возвращает информацию об ошибке на человеческом языке в переменной QString:
errors = qmlView->errors(); for(int i = 0; i < errors.count(); i++) MessageBox::critical(this, “QML Error”, errors.at(i).toString());
где errors – объект вышеуказанного класса, производного от Qlist.
Вы, наверное, обратили внимание: я написал, что метод errors() возвращает информацию об актуальных ошибках, то есть о тех, которые существуют на момент его вызова. Тем, кто привык к компилируемым языкам программирования, это может быть непривычно, но в динамически исполняемых программах QML ошибки могут возникать и исчезать динамически, и не каждая ошибка обязательно приводит к аварийному завершению программы.
В связи с этим полезно сравнить вывод программы на консоль Linux (куда поступает информация обо всех ошибках) с нашим выводом с помощью класса MessageBox. Таким образом, например, я узнал, что связывать корневой контекст с объектом dial лучше до загрузки текста программы QML, ведь выполнение программы начинается немедленно после вызова метода setSource(). Если сразу после вызова setSource() программа на QML не сможет инициализировать свой объект Dial, это вызовет ошибку, которая, однако будет устранена, как только мы укажем программе, чем именно следует инициализировать этот объект. Иначе говоря, инициализировать корневой контекст можно и до вызова setSource(), и после этого вызова, но первый способ работает чище.
Последние штрихи
Последнее, что мы сделаем для того, чтобы наш виджет выглядел, как любой другой виджет Qt – удалим белый фон, заботливо созданный для нас объектом QDeclarativeView (не забывайте, что это потомок QGraphicsView). Мы сделаем это так:
qmlView->setBackgroundRole(QPalette::Background);
Поскольку наш виджет только показывает значения, но не позволяет их вводить, нам следует добавить в окно программы еще один виджет, предназначенный для управления виджетом QML. Как и в исходном примере программы QML, мы воспользуемся для этого ползунком, только в нашем случае это будет объект класса QSlider. Наш виджет будет реагировать на сигнал sliderChanged() этого объекта. Можно было бы добавить в класс Dial слот и связать этот слот с сигналом sliderChanged() напрямую; я оставляю вам это в качестве домашнего задания.
В следующий раз мы, наконец, рассмотрим программу на чистом QML (для выполнения которой все равно требуется утилита Qt).