QML предоставляет удобный способ разбиения кода под названием «Компоненты». Самым простым способом создания компонента, который можно будет в последствии использовать многократно, является добавление нового файла в рабочую директорию главного QML-файла.
Example.qml:
main.qml:
Также, компоненты можно упаковывать как модули (Qt Components являются таким модулем) и публиковать в виде плагинов. Этот пост посвящён использованию компонентов для написания чистого и легко поддерживаемого QML-кода.
Первый пример показал простоту создания дополнительных компонентов, так что не бойтесь их использовать.
Не делайте написание кода, пригодного для многократного использования, своей первоочередной целью. Стремитесь к инкапсулированию деталей реализации и уменьшению связности (decoupling) компонентов. Компоненты должны быть небольшими. Следуя этим правилам вы автоматически придёте к коду, который в последствии можно будет использовать многократно.
Давайте посмотрим на этот пример простых аналоговых часов:
скачать пример / посмотреть онлайн
main.qml:
Clock.qml:
Этот код содержит компонент Clock, который при запуске выглядит так, как показано на скриншоте снизу. Несмотря на то, что он используется в приложении единожды, был смысл выделить его из основного файла.
Во-первых, это делает простой и понятной оставшуюся в файле main.qml логику: таймер, обновляющий часы, минуты и секунды компонента Clock — это всё, что разработчику необходимо видеть в main.qml, если он захочет добавить ему функциональности.
Во-вторых, наш компонент Clock может не беспокоиться за собственное расположение в окне. Предположим, есть элемент Row, использующий наш компонент Clock N-раз. Если бы в корневом элементе компонента Clock был код 'anchors.fill: parent', мы бы не могли использовать его экземпляры в элементе Row: каждый экземпляр Clock занимал бы весь width_row, вместо width_row / N. Именно поэтому QML запрещает использование большинства якорей (anchors) в элементах, помещаемых в элемент Row. Если мы хотим чтобы наш компонент Clock оставался пригодным для многократного использования, мы не должны строить многочисленные предположения относительно его будущего использования. Резюмируя, корневой элемент компонента не должен содержать якоря к своему родителю или использовать жёстко заданные менеджеры размещения (layouts).
В то же время компонент Clock задает фиксированные значения своим длине и ширине. С семантической точки зрения, это размеры нашего компонента по умолчанию. Они не ограничивают использование нашего компонента, ведь размер его экземпляров можно изменить при необходимости. Компоненты с нулевыми размерами считаются невидимыми элементами, поэтому установка ненулевых размеров по умолчанию позволяет избежать глупых ошибок.
Также существуют другие, менее очевидные достоинства, такие как создание составных (composed) элементов и инкапсуляция деталей реализации.
Создание составного элемента (назовём его ComposedElement) из простых элементов ElementA, ElementB и ElementC упрощает добавление новых свойств и действий к элементам. Мы можем добавить новые элементы ElementD и ElementE к нашему ComposedElement без необходимости изменения ElementA, ElementB или ElementC. Наши простые элементы изолированы друг от друга и, поэтому, не могут просто так сломаться, если один из них вдруг поменяется.
Компоненты могут быть разделены на публичную и приватную части, так же как это делается в классах C++ и Java. Публичный API компонента есть сумма всех его свойств и методов, определённых в корневом элементе (включая унаследованные им свойства). Это значит, что такие свойства могут быть изменены, а такие методы могут быть вызваны пользователями компонента.
Любое свойство или метод, определённые во вложенном элементе (не корневом), могут считаться полностью приватным API. Это позволяет инкапсулировать детали реализации и должно, в итоге, стать обычным делом при создании компонентов разработчиками.
Чтобы доказать пользу от инкапсуляции, мы можем удалить внутренний элемент 'impl' из Clock.qml и запустить приложение вновь (например, через "$ qmiviewer main.qml"). Никаких новых ошибок не будет видно, так как публичный API компонента Clock не был изменён. Это значит, что мы можем свободно менять 'impl', зная, что никаких сторонних эффектов от таких изменений для других компонентов не появится.
Мы даже можем расширить эту идею и позволить Clock.qml загружать какой-либо элемент 'impl' динамически, в зависимости от ситуации. Это вводит концепцию полиморфизма в QML; реализация подобного механизма остаётся читателю в качестве упражнения.
Если у нас есть хорошо спроектированный, минимальный публичный API для каждого компонента, мы можем сосредоточиться на разработке интерфейсов, а не конкретной реализации.
Проверим, можем ли мы в действительности многократно использовать компонент Clock.qml. Он прекрасно подходит для создания часов, отображающих мировое время. В этом случае, нам не нужна секундная стрелка на них. Мы можем слегка изменить поведение Clock.qml, чтобы последний не отображал часы, минуты или секунды, если их значение меньше нуля. Для элемента Image с id = thinhand мы можем связать свойство visible с количеством секунд:
Мы не изменяли публичный API. Мы также полагаем, что использование в аналоговых часах отрицательных значений для секунд всё равно не имеет никакого смысла. Поэтому мы уверены, что это изменение не повлияет на работу существующих приложений, которые используют компонент Clock.
В нашем примере с мировыми часами мы, помимо всего прочего, можем отображать локальную информацию о погоде. Мы можем использовать один из доступных API погодных сервисов вместе с XmlListModel, который позволяет декларативно извлекать данные из API. Таймер, который ранее использовался только для обновления времени на часах, теперь будет использован и для обновления данных о погоде раз в час. Обратите внимание на то, как вводится сигнал обновления, который соединяется с функцией обновления данных XmlListModel.
Изменённый пример отображает мировые часы и локальную информацию о погоде для трёх городов.
Компонент Clock интегрируется тривиально. Мы отключили секундную стрелку и использовали UTC для значений времени вместе со смещением для конкретного города. Это сработало, так как Clock был спроектирован как простой элемент интерфейса. Если бы таймер у нас был внутри компонента (вместо того, чтобы давать изменять часы, минуты и секунды как свойства компонента), это бы сильно усложнило работу. К сожалению, main.qml значительно вырос в размере. Элемент Repeater внёс свой вклад в сложность проекта, равно как и массивы utcOffsets и cities.
скачать пример / посмотреть онлайн
main.qml:
Наша цель — сделать main.qml простым для понимания. В то же время, мы не хотим делать Clock.qml излишне сложным, потому что, в таком случае, его будет трудно использовать как простые аналоговые часы. Поэтому мы создали новый компонент, состоящий из сбора данных о погоде и компонента Clock. Он содержит таймер, логику обновления, XmlListModel и интеграцию Clock. Вместо того, чтобы напрямую задавать здесь массивы utcOffsets и cities, мы добавляем новые публичные свойства для компонента WeatherWorldClock:
Мы можем удалить эти массивы и Repeater из main.qml.
скачать пример / посмотреть онлайн
main.qml:
Компонент WeatherWorldClock изолирован от изменений в main.qml. Мы можем дополнять и изменять его свойства в любом файле и не беспокоиться о том, что что-то пойдёт не так. Если WeatherWorldClock станет слишком сложным для поддержки, его можно разделить на большее число компонентов. Именно поэтому очень важным является то, что основная логика нашего приложения выглядит сейчас крайне просто: мы инициализируем компоненты WeatherWorldClock и указываем город и его UTC-смещение. Вот и всё!
Данная статья показала, как можно поддерживать чистоту и логичность кода развивающегося и усложняющегося примера при помощи компонентов и применения всем известных принципов объектно-ориентированного проектирования. На один момент — когда мы добавляли возможность просмотра погоды к нашему примеру — мы перестали следить за нашими компонентами, поэтому логика приложения существенно усложнилась. Таким нехитрым способом было продемонстрировано, что поддержание чистоты и порядка в QML-коде является серьёзной работой и требует определённой дисциплинированности от разработчика.
Example.qml:
import QtQuick 1.0
Rectangle {
}
main.qml:
import QtQuick 1.0
Example {
}
Также, компоненты можно упаковывать как модули (Qt Components являются таким модулем) и публиковать в виде плагинов. Этот пост посвящён использованию компонентов для написания чистого и легко поддерживаемого QML-кода.
Создание новых компонентов
Первый пример показал простоту создания дополнительных компонентов, так что не бойтесь их использовать.
Не делайте написание кода, пригодного для многократного использования, своей первоочередной целью. Стремитесь к инкапсулированию деталей реализации и уменьшению связности (decoupling) компонентов. Компоненты должны быть небольшими. Следуя этим правилам вы автоматически придёте к коду, который в последствии можно будет использовать многократно.
Давайте посмотрим на этот пример простых аналоговых часов:
скачать пример / посмотреть онлайн
main.qml:
import QtQuick 1.0
// Покажем текущее время в аналоговых часах.
Rectangle {
id: root
width: 320
height: 320
property variant now: new Date()
Timer {
id: clockUpdater
interval: 1000 // обновляем часы каждую секунду
running: true
repeat: true
onTriggered: {
root.now = new Date()
}
}
Clock {
id: clock
anchors.centerIn: parent
hours: root.now.getHours()
minutes: root.now.getMinutes()
seconds: root.now.getSeconds()
}
}
Clock.qml:
import QtQuick 1.0
// Аналоговые часы, способные отображать часы, минуты и секунды.
Rectangle {
id: root
width: 262 // минимальная ширина
height: 262 // минимальная высота
// public:
property int hours: 0
property int minutes: 0
property int seconds: 0
// private:
Item {
id: impl
Image {
id: face
source: "images/face.png"
Image {
id: shorthand
source: "images/shorthand.png"
smooth: true
rotation: root.hours * 30
}
Image {
id: longhand
source: "images/longhand.png"
smooth: true
rotation: root.minutes * 6
}
Image {
id: thinhand
source: "images/thinhand.png"
smooth: true
rotation: root.seconds * 6
}
Image {
id: center
source: "images/knob.png"
}
}
}
}
Этот код содержит компонент Clock, который при запуске выглядит так, как показано на скриншоте снизу. Несмотря на то, что он используется в приложении единожды, был смысл выделить его из основного файла.
Во-первых, это делает простой и понятной оставшуюся в файле main.qml логику: таймер, обновляющий часы, минуты и секунды компонента Clock — это всё, что разработчику необходимо видеть в main.qml, если он захочет добавить ему функциональности.
Во-вторых, наш компонент Clock может не беспокоиться за собственное расположение в окне. Предположим, есть элемент Row, использующий наш компонент Clock N-раз. Если бы в корневом элементе компонента Clock был код 'anchors.fill: parent', мы бы не могли использовать его экземпляры в элементе Row: каждый экземпляр Clock занимал бы весь width_row, вместо width_row / N. Именно поэтому QML запрещает использование большинства якорей (anchors) в элементах, помещаемых в элемент Row. Если мы хотим чтобы наш компонент Clock оставался пригодным для многократного использования, мы не должны строить многочисленные предположения относительно его будущего использования. Резюмируя, корневой элемент компонента не должен содержать якоря к своему родителю или использовать жёстко заданные менеджеры размещения (layouts).
В то же время компонент Clock задает фиксированные значения своим длине и ширине. С семантической точки зрения, это размеры нашего компонента по умолчанию. Они не ограничивают использование нашего компонента, ведь размер его экземпляров можно изменить при необходимости. Компоненты с нулевыми размерами считаются невидимыми элементами, поэтому установка ненулевых размеров по умолчанию позволяет избежать глупых ошибок.
Также существуют другие, менее очевидные достоинства, такие как создание составных (composed) элементов и инкапсуляция деталей реализации.
Создание составного элемента (назовём его ComposedElement) из простых элементов ElementA, ElementB и ElementC упрощает добавление новых свойств и действий к элементам. Мы можем добавить новые элементы ElementD и ElementE к нашему ComposedElement без необходимости изменения ElementA, ElementB или ElementC. Наши простые элементы изолированы друг от друга и, поэтому, не могут просто так сломаться, если один из них вдруг поменяется.
Компоненты могут быть разделены на публичную и приватную части, так же как это делается в классах C++ и Java. Публичный API компонента есть сумма всех его свойств и методов, определённых в корневом элементе (включая унаследованные им свойства). Это значит, что такие свойства могут быть изменены, а такие методы могут быть вызваны пользователями компонента.
Любое свойство или метод, определённые во вложенном элементе (не корневом), могут считаться полностью приватным API. Это позволяет инкапсулировать детали реализации и должно, в итоге, стать обычным делом при создании компонентов разработчиками.
Чтобы доказать пользу от инкапсуляции, мы можем удалить внутренний элемент 'impl' из Clock.qml и запустить приложение вновь (например, через "$ qmiviewer main.qml"). Никаких новых ошибок не будет видно, так как публичный API компонента Clock не был изменён. Это значит, что мы можем свободно менять 'impl', зная, что никаких сторонних эффектов от таких изменений для других компонентов не появится.
Мы даже можем расширить эту идею и позволить Clock.qml загружать какой-либо элемент 'impl' динамически, в зависимости от ситуации. Это вводит концепцию полиморфизма в QML; реализация подобного механизма остаётся читателю в качестве упражнения.
Если у нас есть хорошо спроектированный, минимальный публичный API для каждого компонента, мы можем сосредоточиться на разработке интерфейсов, а не конкретной реализации.
Повторное использование компонентов
Проверим, можем ли мы в действительности многократно использовать компонент Clock.qml. Он прекрасно подходит для создания часов, отображающих мировое время. В этом случае, нам не нужна секундная стрелка на них. Мы можем слегка изменить поведение Clock.qml, чтобы последний не отображал часы, минуты или секунды, если их значение меньше нуля. Для элемента Image с id = thinhand мы можем связать свойство visible с количеством секунд:
visible: root.seconds > -1
Мы не изменяли публичный API. Мы также полагаем, что использование в аналоговых часах отрицательных значений для секунд всё равно не имеет никакого смысла. Поэтому мы уверены, что это изменение не повлияет на работу существующих приложений, которые используют компонент Clock.
В нашем примере с мировыми часами мы, помимо всего прочего, можем отображать локальную информацию о погоде. Мы можем использовать один из доступных API погодных сервисов вместе с XmlListModel, который позволяет декларативно извлекать данные из API. Таймер, который ранее использовался только для обновления времени на часах, теперь будет использован и для обновления данных о погоде раз в час. Обратите внимание на то, как вводится сигнал обновления, который соединяется с функцией обновления данных XmlListModel.
Изменённый пример отображает мировые часы и локальную информацию о погоде для трёх городов.
Компонент Clock интегрируется тривиально. Мы отключили секундную стрелку и использовали UTC для значений времени вместе со смещением для конкретного города. Это сработало, так как Clock был спроектирован как простой элемент интерфейса. Если бы таймер у нас был внутри компонента (вместо того, чтобы давать изменять часы, минуты и секунды как свойства компонента), это бы сильно усложнило работу. К сожалению, main.qml значительно вырос в размере. Элемент Repeater внёс свой вклад в сложность проекта, равно как и массивы utcOffsets и cities.
скачать пример / посмотреть онлайн
main.qml:
Rectangle {
...
property variant cities: ["Berlin", "Helsinki", "San Francisco"]
property variant utcOffsets: [1, 2, -8]
property variant now: new Date()
signal refresh()
Timer {
...
property int hours
onTriggered: {
hours = root.now.getHours()
root.now = new Date()
// Получение данных о погоде каждый час:
if (hours != root.now.getHours()) {
root.refresh()
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: root.cities
// Отображаем аналоговые часы с локальными временем и погодой для каждого города.
Rectangle {
id: current
width: 262 // минимальная ширина
height: 320 // минимальная высота
property string city: cities[index]
property int utcOffset: utcOffsets[index]
XmlListModel {
id: cityQuery
...
}
ListView {
model: cityQuery
anchors.fill: parent
delegate:
Item {
Clock {
id: clock
anchors.left: parent.left
anchors.top: parent.top
// Убедимся, что UTC со смещением никогда не станет отрицательным,
// иначе часы не будут отображаться:
hours: root.now.getUTCHours() + current.utcOffset + 24
minutes: root.now.getMinutes()
seconds: -1
}
Row {
...
Image {
id: icon
source: "http://www.google.com" + model.iconUrl
}
Text {
id: label
text: current.city + ", "+ model.temperature
+ "°C\n" + model.humidity
+ "\n" + model.windCondition
}
}
}
}
}
}
}
}
Наша цель — сделать main.qml простым для понимания. В то же время, мы не хотим делать Clock.qml излишне сложным, потому что, в таком случае, его будет трудно использовать как простые аналоговые часы. Поэтому мы создали новый компонент, состоящий из сбора данных о погоде и компонента Clock. Он содержит таймер, логику обновления, XmlListModel и интеграцию Clock. Вместо того, чтобы напрямую задавать здесь массивы utcOffsets и cities, мы добавляем новые публичные свойства для компонента WeatherWorldClock:
// public:
property string city: ""
property int utcOffset: 0
Мы можем удалить эти массивы и Repeater из main.qml.
скачать пример / посмотреть онлайн
main.qml:
import QtQuick 1.0
// Отображает время и погоду для выбранных городов.
Rectangle {
id: root
width: 786
height: 320
Row {
id: cities
anchors.fill: parent
anchors.horizontalCenter: parent.horizontalCenter
WeatherWorldClock {
city: "Berlin"
utcOffset: 1
}
WeatherWorldClock {
city: "Helsinki"
utcOffset: 2
}
WeatherWorldClock {
city: "San Francisco"
utcOffset: -8
}
}
}
Компонент WeatherWorldClock изолирован от изменений в main.qml. Мы можем дополнять и изменять его свойства в любом файле и не беспокоиться о том, что что-то пойдёт не так. Если WeatherWorldClock станет слишком сложным для поддержки, его можно разделить на большее число компонентов. Именно поэтому очень важным является то, что основная логика нашего приложения выглядит сейчас крайне просто: мы инициализируем компоненты WeatherWorldClock и указываем город и его UTC-смещение. Вот и всё!
Заключение
Данная статья показала, как можно поддерживать чистоту и логичность кода развивающегося и усложняющегося примера при помощи компонентов и применения всем известных принципов объектно-ориентированного проектирования. На один момент — когда мы добавляли возможность просмотра погоды к нашему примеру — мы перестали следить за нашими компонентами, поэтому логика приложения существенно усложнилась. Таким нехитрым способом было продемонстрировано, что поддержание чистоты и порядка в QML-коде является серьёзной работой и требует определённой дисциплинированности от разработчика.