В этой статье я расскажу про такой компонент из QML как Loader.
Он позволяет создать контейнер, в который затем можно вложить необходимый qml-элемент, использовать разные элементы в зависимости от состояния программы, а также сделать редко используемые части загружаемыми по требованию и сэкономить ресурсы. Loader является контейнером для QML-компонента и сам по себе не отображается.
Я рассматриваю компонент из QtQuick 2.0 который входит в Qt пятой версии (Qt 5.x). В более ранней версии этот компонент также есть, но функционал немного меньше.
Задание содержимого для Loader
В качестве содержимого Loader можно задать либо путь к qml-файлу либо компонент. Рассмотрим эти способы поподробнее.
1. Путь к qml-файлу
Путь задается через параметр source. Это может быть как путь к локальному файлу, так и url файла из сети. Путь также может быть как абсолютным, так и относительным.
Рассмотрим пример, меняющий фон по клику мышью в области окна.
main.qml:
import QtQuick 2.0
Item {
id: main
property int backgroundNumber: 1
width: 360
height: 360
Loader {
id: background
anchors.fill: parent
source: "Background_1.qml"
}
MouseArea {
anchors.fill: parent
onClicked: {
background.source = (backgroundNumber == 1 ? "Background_2.qml" : "Background_1.qml")
backgroundNumber = (backgroundNumber == 1 ? 2 : 1)
}
}
}
Background_1.qml
import QtQuick 2.0
Rectangle {
color: "black"
}
Background_2 .qml:
import QtQuick 2.0
Rectangle {
color: "yellow"
}
Такой способ имеет одно преимущество. Перед созданием объекта из qml-компонента, движок qml должен сначала его скомпилировать. Если мы не установим source, ничего компилироваться не будет, соответственно для объектов, которые нужны не всегда и по умолчанию не создаются, такой способ подходит лучше всего.
2. Компонент
Можно определить компонент в области видимости Loader и указать его в качестве свойства sourceComponent. Поскольку мы определяем компонент, движок qml его скомпилирует, даже если мы за все время работы программы ни разу не создадим с него объект, что является недостатком. В то же время, поскольку компонент уже скомпилирован, нужно будет только создать объект, что быстрее. Если требуется часто переключать состояния между разными компонентами, то такой способ хорошо подходит. При этом, не нужно загружать файл компонента, что может быть еще одним ощутимым преимуществом, если компонент загружается не из локального файла на диске, а качается из сети.
Еще одной особенностью является то, что компонент определяется в контексте данного qml-файла, а значит в его видимости будет и содержимое файла и он может получить доступ к свойствам и функциям объектов из этого файла.
main.qml:
import QtQuick 2.0
Item {
id: main
property int backgroundNumber: 1
property color backgroundColor1: "black"
property color backgroundColor2: "yellow"
width: 360
height: 360
Loader {
id: background
anchors.fill: parent
sourceComponent: background_1
}
MouseArea {
anchors.fill: parent
onClicked: {
background.sourceComponent = (backgroundNumber == 1 ? background_2 : background_1)
backgroundNumber = (backgroundNumber == 1 ? 2 : 1)
}
}
Component {
id: background_1
Rectangle {
color: main.backgroundColor1
}
}
Component {
id: background_2
Rectangle {
color: main.backgroundColor2
}
}
}
В этом примере из компонентов background_1 и background_2 виден родительский компонент (main) и его свойства.
3. setSource()
Описывая объект в qml, можно задать его начальные параметры, а так же использовать привязки (bindings). Загружая компонент при помощи Loader двумя вышеперечисленными способами, сделать этого нельзя.
В QtQuick 2.0 (Qt 5.0) появился метод setSource. Он позволяет задать свойства создаваемого объекта. Для этого, нужно в качестве второго аргумента функции передать обычный JavaScript-объект с необходимыми параметрами.
В качестве примера установим opacity, т.е. прозрачность (точнее, непрозрачность).
main.qml:
import QtQuick 2.0
Item {
id: main
property int backgroundNumber: 1
width: 360
height: 360
Loader {
id: background
anchors.fill: parent
Component.onCompleted: setSource("Background_1.qml", { "opacity": 0.5 })
}
MouseArea {
anchors.fill: parent
onClicked: {
background.setSource(backgroundNumber == 1 ? "Background_2.qml" : "Background_1.qml",
{ "opacity": 0.5 })
backgroundNumber = (backgroundNumber == 1 ? 2 : 1)
}
}
}
Синхронная и асинхронная загрузка
По умолчанию, компоненты, которым в качестве source задан путь локальный путь к файлу на диске загружаются синхронно. Если же указать url файла из сети, то статус меняется на Loader.Loading и загрузка ведется асинхронно.
Асинхронный режим загрузки можно активировать и вручную, установив свойству asynchronous значение true.
Если время загрузки может быть значительным, может пригодится информация о прогрессе загрузки. Свойство progress будет отображать, насколько загружен компонент (от 0 до 1).
Доступ к объекту
К созданному объекту можно получить доступ, используя свойство item. Таким образом, можно обращаться к свойствам объекта (если объект создавался не при помощи setSource, то это единственный способ задать нужные свойства объекту).
Можно не только задавать свойства объекту но и присоединить к его сигналам обработчики и вызывать его методы. Единственное, стоит дождаться, пока компонент загрузится. Нужно дождаться, пока параметр status не будет равен Loader.Ready либо поставит обработчик на сигнал loaded.
Рассмотрим небольшой пример.
main.qml:
import QtQuick 2.0
Item {
id: main
width: 360
height: 360
Loader {
id: background
anchors.fill: parent
asynchronous: true
source: "Background_1.qml"
onStatusChanged: console.log("status", status, "item", item)
onLoaded: item.color = "green"
}
}
Вот, что мы получим в качестве вывода:
status 2 item null
status 2 item null
status 1 item QQuickRectangle(0x95436f8)
Число 2 соответствует значению Loader.Loading, 1 — Loader.Ready. Так же видно, что item указывает на созданный объект, когда завершилась загрузка объекта. После завершения загрузки цвет фона изменится на зеленый.
Работа с сигналами
Для того, чтобы установить обработчик на сигнал объекта, загруженного в Loader, нужно вызвать метод connect() у этого сигнала и в качестве параметра передать-функцию обработчик. Обработчик можно убрать при помощи соответствующего метода disconnect().
Рассмотрим пример.
main.qml:
import QtQuick 2.0
Item {
id: main
width: 360
height: 360
Loader {
id: container
anchors.fill: parent
source: "Mouse.qml"
onLoaded: item.mouseClicked.connect(processMouse)
Component.onDestruction: {
if (item) {
item.mouseClicked.disconnect(processMouse)
}
}
}
function processMouse() {
console.log("mouse clicked!")
}
}
Mouse.qml:
import QtQuick 2.0
Item {
id: m
signal mouseClicked()
MouseArea {
anchors.fill: parent
onClicked: m.mouseClicked()
}
}
По клику на окне в консоли будет выводиться текст: «mouse clicked!»
Этот способ является мощным и позволяет устанавливать/убирать обработчики в зависимости от состояния программы. Ценой за это являются возрастающие сложность, количество кода и меньшая надежность. Когда ручное управление сигналами не нужно, мы можем использовать qml-компонент Connections. В качестве свойства target мы указываем item и устанавливаем обработчики так, как если бы мы их определяли в самом элементе.
main.qml:
import QtQuick 2.0
Item {
id: main
width: 360
height: 360
Loader {
id: container
anchors.fill: parent
source: "Mouse.qml"
}
Connections {
target: container.item
onMouseClicked: processMouse()
}
function processMouse() {
console.log("mouse clicked!")
}
}
Как видим, этот способ проще, удобнее и менее склонен к ошибкам. Более того, такой способ сделан в декларативном стиле и является более естественным для qml, в отличие от императивного, когда мы соединяем сигналы с соответствующими обработчиками вручную.
Про декларативный и императивный подходы
В приведенных выше примерах ничего особо сложного нет, но если нужно соединять между собой объекты, загружающиеся асинхронно, то это может быть нетривиально. Не стоит рассчитывать, что движок qml загружает их в какой-то определенной последовательности, особенно, если они загружаются по сети.
По опыту, если есть возможность использовать декларативные методы, то лучше использовать именно их. Это касается не только сигналов, но и свойств объектов, которые можно устанавливать при помощи setSource, вместо того, чтобы дожидаться загрузки компонента и устанавливать их вручную через свойство item (в Qt четвертой версии этого метода нет, там только так).
Загрузка невизуальных элементов
В QtQuick 2.0 в Loader добавили возможность загружать не только визуальные элементы. Например, можно таким образом загрузить элемент, основанные на QtObject.
main.qml:
import QtQuick 2.0
Item {
id: main
width: 360
height: 360
Loader {
id: container
anchors.fill: parent
source: "Element.qml"
onLoaded: console.log(item.text)
}
}
Element.qml:
import QtQuick 2.0
QtObject {
property string text: "hello!"
}
При загрузке, в консоль выведется «hello!».
Небольшое резюме
Loader может как загружать qml-компоненты из других файлов, так и использовать компоненты, определенные в области текущего qml-файла. В зависимости от выбранного метода, можно отложить выполнить часть процесса создания однократно и тем самым ускорить создание объектов либо отложить все операции по созданию объекта до тех пор, пока он не понадобится.
Несмотря на то, что qml является декларативным языком, есть возможность делать многие вещи императивно, что может в отдельных ситуациях оказаться полезным, хотя и ценой некоторого усложнения и увеличения количества кода. В случае с не самой последней версией Qt такой способ может помочь обойти ограничения библиотеки.