Pull to refresh

Лучшие приёмы Qt Quick: Компоненты

Reading time7 min
Views19K
QML предоставляет удобный способ разбиения кода под названием «Компоненты». Самым простым способом создания компонента, который можно будет в последствии использовать многократно, является добавление нового файла в рабочую директорию главного 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-коде является серьёзной работой и требует определённой дисциплинированности от разработчика.
Tags:
Hubs:
Total votes 17: ↑16 and ↓1+15
Comments5

Articles

Information

Website
www.microsoft.com
Registered
Founded
Employees
1,001–5,000 employees
Location
Финляндия