Использование Loader в QML

Добрый день! В этой статье я расскажу про такой компонент из QML как Loader.

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

Я рассматриваю компонент из QtQuick 2.0 которая входит в Qt пятой версии. В более ранней версии этот компонент также есть, но функционал немного меньше.

Задание содержимого для 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 такой способ может помочь обойти ограничения библиотеки.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 10

    +1
    Спасибо за статью. Много узнал нового.
      0
      Вопрос относительно loader'ов и делегатов в списках. Часто хочется в списке отображать сложную информацию и получается, что каждый элемент списка имеет свою высоту, от этого бывают тупняки в скроллинге и опять же от этого высота скроллбара начинает плясать, есть какие-то рецепты на этот счет?
        0
        Я сам использую Loader в списках и элементы разной высоты. Возможно, дело в самом скроллбаре и он некорректно обрабатывает изменение размера элементов. Могу сказать, что свой скроллбар я чинил неоднократно, пока он не стал работать во всех ситуациях как надо )
        На какой версии Qt получаются лаги?
          0
          Лаги вылезают на Qt 4.7.4 на MeeGo, приходится задирать до предела cache buffer. На Qt5 вроде плавнее, но тут и видео гораздо мощнее, может просто эффект незаметен.
            0
            Посмотрел старую версию своего проекта, которая на Qt 4.8.3. Действительно скролл лагает нещадно, в пятой такого и близко нет.

            В пятой версии немного поменяли поведение:
            «The cacheBuffer property now has a non-zero default and delegates in the cacheBuffer are created asynchronously» источник

            Я посмотрел, в пятой версии по умолчанию cacheBuffer устанавливается в 320, в четвертой — 0. Поставил в четвертой 320 и стало гораздо лучше, почти без лагов.
          • UFO just landed and posted this here
              0
              — Если у вас в одной модели элементы разных типов и должны отображаться, соответственно, по разному. Например контакт-лист в IM-клиенте и в нем как сами контакты, так и группы контактов. Для контактов в Loader загружается один компонент, для групп — другой.
              — Если в зависимости от состояния делегата он выглядит сильно по разному, может быть удобно сделать два разных компонента и переключать их в Loader'е, в зависимости от состояния. Например, по умолчанию элемент максимально простой и легкий (именно так и рекомендуется делать), но есть возможность его «развернуть» и он превращается в другой виджет, содержащий кучу всякой дополнительной информации, кнопок, картинок и т.п. Можно все запихнуть в один виджет, но стоит учитывать, что делегаты создаются/уничтожаются на лету. При скролле ListView то, что находится далеко за областью видимостью удаляется, затем создается снова когда туда попадает. Если забить его «тяжелыми» делагатами, то скролл может тормозить.
              • UFO just landed and posted this here
                  0
                  Можно и так, конечно. Это как раз то, о чем я говорил, когда писал про императивный подход.
                  Впрочем, в Qt5 таким методом можно не только асинхронно создать компонент, но и создать из него объект. Для особо тяжелых объектов разнца видна невооруженным глазом.
          0

          Спасибо, интересно, может пригодиться. Скажите, а как выглядел бы код, если qml-файл в source нужно было бы обновить:


          • по сигналу от объекта QObject типа: load(const QString &newFileNme) — т.е. загружаем другой файл
          • по сигналу от QFileWatcher, например, при изменении текущего файла?

          Благодарю!

          Only users with full accounts can post comments. Log in, please.