Spec-Zone.ru › Qt 5.6

Начало работы с программированием в Qt Quick

Добро пожаловать в мир QML, декларативного языка для пользовательского интерфейса. В этом руководстве по началу работы мы создадим простое приложение текстового редактора с использованием QML. После прочтения этого руководства вы сможете разрабатывать свои собственные приложения, используя QML и Qt C++.

QML для создания пользовательских интерфейсов

Приложение, которое мы создаем, — это простой текстовый редактор, который будет загружать, сохранять и выполнять некоторые операции с текстом. Это руководство будет состоять из двух частей. В первой части мы разработаем макет и поведение приложения, используя декларативный язык QML. Во второй части загрузка и сохранение файлов будут реализованы с использованием Qt C++. С помощью системы метаобъектов Qt мы можем экспонировать функции C++ как свойства, которые могут использовать типы объектов QML. Использование QML и Qt C++ позволяет эффективно отделить логику интерфейса от логики приложения.

Полный исходный код находится в каталоге examples/quick/tutorials/gettingStartedQml. Если вы хотите увидеть, как выглядит завершенное приложение, вы можете перейти к главе Запуск текстового редактора.

Часть C++ этого учебника предполагает, что читатель обладает базовыми знаниями о процедурах компиляции Qt.

Главы учебника:

  1. Определение кнопки и меню
  2. Реализация меню
  3. Создание текстового редактора
  4. Декорирование текстового редактора
  5. Расширение QML с помощью Qt C++

Информация о QML, такая как синтаксис и функции, включена в Справочник по QML.

Определение кнопки и меню

Базовый компонент — кнопка

Мы начинаем создание нашего текстового редактора с создания кнопки. Функционально кнопка имеет чувствительную к мыши область и метку. Кнопки выполняют действия при нажатии пользователем.

В QML основным визуальным элементом является тип Прямоугольник. Тип Rectangle объекта QML имеет свойства QML для управления его внешним видом и расположением.

import QtQuick 2.3

Rectangle {
    id: simpleButton
    color: "grey"
    width: 150; height: 75

    Text {
        id: buttonLabel
        anchors.centerIn: parent
        text: "button label"
    }
}

Во-первых, оператор import QtQuick 2.3 позволяет инструменту qmlscene импортировать типы QML, которые мы будем использовать позже. Эта строка должна присутствовать в каждом файле QML. Обратите внимание, что версия модулей Qt включена в оператор импорта.

Этот простой прямоугольник имеет уникальный идентификатор simpleButton, который привязан к свойству id. Свойства объекта Rectangle привязываются к значениям путем перечисления свойства, за которым следует двоеточие, а затем значение. В приведенном коде цвет grey привязан к свойству color прямоугольника. Аналогично, мы привязываем свойства width и height прямоугольника.

Тип Текст — это поле текстового поля, которое нельзя редактировать. Мы называем этот объект buttonLabel. Для установки строкового содержимого поля Текст мы привязываем значение к свойству text. Метка находится внутри прямоугольника, и чтобы центрировать ее по центру, мы назначаем свойство anchors объекта Текст его родительскому элементу, который называется simpleButton. Якоря могут привязываться к якорям других элементов, что упрощает задания макета.

Мы сохраним этот код как SimpleButton.qml. Запуск qmlscene с файлом в качестве аргумента отобразит серый прямоугольник с текстовой меткой.

Для реализации функциональности нажатия кнопки мы можем использовать обработку событий QML. Обработка событий QML очень похожа на механизм сигналов и слотов Qt. Сигналы излучаются, и вызывается подключенный слот.

Rectangle {
    id: simpleButton
    ...

    MouseArea {
        id: buttonMouseArea

        // Anchor all sides of the mouse area to the rectangle's anchors
        anchors.fill: parent
        // onClicked handles valid mouse button clicks
        onClicked: console.log(buttonLabel.text + " clicked")
    }
}

Мы включаем объект MouseArea в нашу простую кнопку. Объекты MouseArea описывают интерактивную область, где обнаруживаются движения мыши. Для нашей кнопки мы привязываем весь MouseArea к его родителю, которым является simpleButton. Синтаксис anchors.fill — один из способов доступа к конкретному свойству, называемому fill, внутри группы свойств, называемых anchors. QML использует макеты на основе якорей, где элементы могут привязываться к другим элементам, создавая надежные макеты.

Объект MouseArea имеет множество обработчиков сигналов, которые вызываются во время перемещения мыши внутри указанных границ MouseArea. Одним из них является onClicked, который вызывается всякий раз, когда нажимается допустимая кнопка мыши, левая кнопка является по умолчанию. Мы можем привязать действия к обработчику onClicked. В нашем примере console.log() выводит текст всякий раз, когда щелкают на области мыши. Функция console.log() — полезный инструмент для отладки и вывода текста.

Код в SimpleButton.qml достаточно для отображения кнопки на экране и вывода текста всякий раз, когда по ней щелкают мышью.

Rectangle {
    id: button
    ...

    property color buttonColor: "lightblue"
    property color onHoverColor: "gold"
    property color borderColor: "white"

    signal buttonClick()

    onButtonClick: {
        console.log(buttonLabel.text + " clicked")
    }

    MouseArea{
        id: buttonMouseArea
        onClicked: buttonClick()
        hoverEnabled: true
        onEntered: parent.border.color = onHoverColor
        onExited:  parent.border.color = borderColor
    }

    // Determines the color of the button by using the conditional operator
    color: buttonMouseArea.pressed ? Qt.darker(buttonColor, 1.5) : buttonColor
}

Полностью работоспособная кнопка находится в Button.qml. В приведенных в этой статье фрагментах кода отсутствует часть кода, обозначенная многоточием, потому что она была либо введена ранее в предыдущих разделах, либо не имела отношения к текущему обсуждению кода.

Пользовательские свойства объявляются с помощью синтаксиса property type name. В коде свойство buttonColor, типа color, объявлено и привязано к значению "lightblue". Свойство buttonColor позже используется в условном операторе для определения цвета заливки кнопки. Обратите внимание, что присвоение значения свойства возможно с помощью знака = равенства, помимо привязки значения с помощью символа : двоеточия. Пользовательские свойства позволяют внешним элементам получить доступ к внутренним элементам за пределами области видимости прямоугольника. Существуют базовые типы QML, такие как int, string, real, а также тип, называемый variant.

Привязывая обработчики сигналов onEntered и onExited к цветам, граница кнопки станет желтой при наведении курсора на кнопку и вернет цвет при выходе курсора из области мыши.

Сигнал buttonClick() объявлен в Button.qml путем размещения ключевого слова signal перед именем сигнала. Все сигналы имеют автоматически созданные обработчики, имена которых начинаются с on. В результате onButtonClick — это обработчик buttonClick. Затем onButtonClick получает действие для выполнения. В нашем примере кнопки обработчик мыши onClicked просто вызывает onButtonClick, который отображает текст. onButtonClick позволяет внешним объектам легко получить доступ к области мыши Button . Например, у элементов может быть несколько объявлений MouseArea , и сигнал buttonClick может улучшить различение нескольких обработчиков сигналов MouseArea.

Теперь мы обладаем базовыми знаниями для реализации в QML элементов, которые могут обрабатывать основные движения мыши. Мы создали метку Text внутри Rectangle, настроили ее свойства и реализовали поведение, реагирующее на движения мыши. Эта идея создания объектов QML внутри объектов повторяется в приложении текстового редактора.

Эта кнопка бесполезна, если ее не использовать как компонент для выполнения действия. В следующем разделе мы вскоре создадим меню, содержащее несколько таких кнопок.

Создание страницы меню

До этого момента мы рассмотрели, как создавать объекты и назначать им поведение внутри одного файла QML. В этом разделе мы рассмотрим, как импортировать типы QML и как повторно использовать некоторые созданные компоненты для создания других компонентов.

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

import QtQuick 2.3             // Import the main Qt QML module
import "folderName"            // import the contents of a folder
import "script.js" as Script   // Import a Javascript file and name it as Script

Приведенный выше синтаксис демонстрирует использование ключевого слова import. Это необходимо для использования файлов JavaScript или файлов QML, которые не находятся в одной папке. Так как Button.qml находится в той же папке, что и FileMenu.qml, нам не нужно импортировать файл Button.qml для его использования. Мы можем напрямую создать объект Button путем объявления Button{}, аналогично объявлению Rectangle{}.

In FileMenu.qml:

    Row {
        anchors.centerIn: parent
        spacing: parent.width / 6

        Button {
            id: loadButton
            buttonColor: "lightgrey"
            label: "Load"
        }
        Button {
            buttonColor: "grey"
            id: saveButton
            label: "Save"
        }
        Button {
            id: exitButton
            label: "Exit"
            buttonColor: "darkgrey"

            onButtonClick: Qt.quit()
        }
    }

В FileMenu.qml мы объявляем три объекта Button . Они объявлены внутри типа Строка — позиционера, который расположит своих потомков в вертикальной строке. Объявление Button находится в Button.qml, что совпадает с тем, что мы использовали в предыдущем разделе. Новые привязки свойств могут быть объявлены внутри только что созданных кнопок, фактически перезаписывая свойства, заданные в Button.qml. Кнопка, называемая exitButton, будет закрывать окно при нажатии. Обратите внимание, что обработчик сигнала onButtonClick в Button.qml будет вызываться дополнительно к обработчику onButtonClick в exitButton.

Объявление Row объявлено в Rectangle, создавая контейнер прямоугольника для строки кнопок. Этот дополнительный прямоугольник создает косвенный способ организации строки кнопок внутри меню.

Объявление меню редактирования очень похоже на данном этапе. Меню содержит кнопки с метками: Copy, Paste, и Select All.

Вооруженные нашими знаниями об импорте и настройке ранее созданных компонентов, мы теперь можем объединить эти страницы меню, чтобы создать строку меню, содержащую кнопки для выбора меню и взглянуть на то, как мы можем структурировать данные с помощью QML.

Реализация строки меню

Наше приложение текстового редактора нуждается в способе отображения меню с помощью строки меню. Строка меню будет переключать различные меню, и пользователь может выбрать, какое меню отобразить. Переключение меню подразумевает, что меню нуждаются в более сложной структуре, чем просто их отображение в строке. QML использует модели и представления для структурирования данных и отображения структурированных данных.

Использование моделей и представлений данных

QML имеет различные представления данных, которые отображают модели данных. Наша строка меню будет отображать меню в виде списка с заголовком, который отображает строку имён меню. Список меню объявлен внутри ObjectModel. Тип ObjectModel содержит элементы, которые уже отображаются, такие как объекты Rectangle. Другие типы моделей, такие как тип ListModel, нуждаются в делегате для отображения своих данных.

Мы объявляем два визуальных элемента в menuListModel, FileMenu и EditMenu. Мы настраиваем два меню и отображаем их в ListView. Файл MenuBar.qml содержит объявления QML, и в нём определено простое меню «Правка» в EditMenu.qml.

ObjectModel {
    id: menuListModel

    FileMenu {
        width: menuListView.width
        height: menuBar.height
        color: fileColor
    }

    EditMenu {
        color: editColor
        width: menuListView.width
        height: menuBar.height
    }
}

Тип ListView отобразит модель в соответствии с делегатом. Делегат может отображать элементы модели в объекте Row или в таблице. В нашем menuListModel уже есть видимые элементы, поэтому нам не нужно объявлять делегат.

ListView {
    id: menuListView

    // Anchors are set to react to window anchors
    anchors.fill: parent
    anchors.bottom: parent.bottom
    width: parent.width
    height: parent.height

    // The model contains the data
    model: menuListModel

    // Control the movement of the menu switching
    snapMode: ListView.SnapOneItem
    orientation: ListView.Horizontal
    boundsBehavior: Flickable.StopAtBounds
    flickDeceleration: 5000
    highlightFollowsCurrentItem: true
    highlightMoveDuration: 240
    highlightRangeMode: ListView.StrictlyEnforceRange
}

Кроме того, ListView наследуется от Flickable, что делает список отзывчивым на перетаскивание мышью и другие жесты. Последняя часть кода выше устанавливает свойства Flickable для создания желаемого эффекта пролистывания в нашем представлении. В частности, свойство highlightMoveDuration изменяет продолжительность перехода пролистывания. Более высокое значение highlightMoveDuration приводит к более медленной смене меню.

ListView сохраняет элементы модели через index, и каждый визуальный элемент в модели доступен через index, в порядке объявления. Изменение currentIndex эффективно изменяет выделенный элемент в ListView. Заголовок нашей строки меню иллюстрирует этот эффект. В строке есть две кнопки, обе изменяющие текущее меню при нажатии. Кнопка fileButton переключает текущее меню на меню «Файл» при нажатии, поскольку index является 0, потому что FileMenu объявлена первой в menuListModel. Аналогично, кнопка editButton при нажатии переключит текущее меню на EditMenu.

Прямоугольник labelList имеет значение z равное 1, что означает, что он отображается поверх строки меню. Элементы с более высокими значениями z отображаются поверх элементов с меньшими значениями z. Значение по умолчанию для z равно 0.

Rectangle {
    id: labelList
    ...
    z: 1

    Row {
        anchors.centerIn: parent
        spacing: 40

        Button {
            label: "File"
            id: fileButton
            ...
            onButtonClick: menuListView.currentIndex = 0
        }

        Button {
            id: editButton
            label: "Edit"
            ...
            onButtonClick: menuListView.currentIndex = 1
        }
    }
}

Созданная нами строка меню может быть пролистана для доступа к меню или нажатием на имена меню в верхней части. Переключение экранов меню интуитивно понятно и отзывчиво.

Создание текстового редактора

Объявление текстовой области

Наш текстовый редактор не будет текстовым редактором, если в нём не будет области редактирования текста. Тип TextEdit QML позволяет объявить многострочную область редактирования текста. TextEdit отличается от типа Text, который не позволяет пользователю напрямую изменять текст.

TextEdit {
    id: textEditor
    anchors.fill: parent
    width: parent.width
    height: parent.height
    color: "midnightblue"
    focus: true

    wrapMode: TextEdit.Wrap

    onCursorRectangleChanged: flickArea.ensureVisible(cursorRectangle)
}

В редакторе установлено свойство шрифта color и включена функция wrapMode для обрезания текста. Область TextEdit находится внутри элемента пролистывания, который будет пролистывать текст, если курсор находится за пределами видимой области. Функция ensureVisible() проверяет, находится ли прямоугольник курсора за пределами видимых границ, и перемещает текстовую область соответственно. QML использует синтаксис JavaScript для своих скриптов, и, как уже упоминалось, файлы JavaScript могут быть импортированы и использованы в файле QML.

function ensureVisible(r) {
    if (contentX >= r.x)
        contentX = r.x;
    else if (contentX + width <= r.x + r.width)
        contentX = r.x + r.width - width;
    if (contentY >= r.y)
        contentY = r.y;
    else if (contentY + height <= r.y + r.height)
        contentY = r.y + r.height - height;
}

Объединение компонентов для текстового редактора

Теперь мы готовы создать макет нашего текстового редактора с использованием QML. Текстовый редактор состоит из двух компонентов: строки меню, которую мы создали, и области текста. QML позволяет нам повторно использовать компоненты, тем самым упрощая наш код, импортируя компоненты и при необходимости настраивая их. Наш текстовый редактор разделяет окно на две части; одна треть экрана предназначена для строки меню, а две трети экрана отображают область текста. Строка меню отображается поверх других объектов.

Rectangle {
    id: screen
    width: 1000
    height: 1000

    // The screen is partitioned into the MenuBar and TextArea.
    // One-third of the screen is assigned to the MenuBar
    property int partition: height / 3

    MenuBar {
        id: menuBar
        height: partition
        width: parent.width
        z: 1
    }

    TextArea {
        id: textArea
        anchors.bottom: parent.bottom
        y: partition
        color: "white"
        width: parent.width
        height: partition * 2
    }
}

Использование импортируемых компонентов значительно упрощает наш код TextEditor. Мы можем настроить основное приложение, не беспокоясь о свойствах, которые уже имеют определённое поведение. С помощью этого подхода макеты приложений и пользовательские интерфейсы можно создавать легко.

Декорирование текстового редактора

Реализация интерфейса выдвижного меню

Наш текстовый редактор выглядит просто, и его нужно украсить. Используя QML, мы можем объявить переходы и анимировать наш текстовый редактор. Наша строка меню занимает одну треть экрана, и было бы неплохо, чтобы она появлялась только тогда, когда нам это нужно.

Мы можем добавить интерфейс выдвижного меню, который будет скрывать или показывать строку меню при нажатии. В нашей реализации мы имеем узкий прямоугольник, реагирующий на щелчки мышью. drawer, а также приложение, имеет два состояния: «выдвижное меню открыто» и «выдвижное меню закрыто». Элемент drawer — это узкая полоска прямоугольника. Внутри выдвижного меню есть вложенный объект Image, который объявляет, что значок стрелки будет центрирован внутри выдвижного меню. Выдвижное меню назначает состояние всему приложению с идентификатором screen, каждый раз, когда пользователь нажимает на область.

Rectangle {
    id: drawer
    height: 15

    Image {
        id: arrowIcon
        source: "images/arrow.png"
        anchors.horizontalCenter: parent.horizontalCenter
    }

    MouseArea {
        id: drawerMouseArea
        anchors.fill: parent

        onClicked: {
            if (screen.state == "DRAWER_CLOSED")
                screen.state = "DRAWER_OPEN"
            else if (screen.state == "DRAWER_OPEN")
                screen.state = "DRAWER_CLOSED"
        }
        ...
    }
}

Состояние — это просто набор настроек, и оно объявляется с помощью типа State. Список состояний можно перечислить и привязать к свойству states. В нашем приложении два состояния называются DRAWER_CLOSED и DRAWER_OPEN. Настройки элементов объявляются в объектах PropertyChanges. В состоянии DRAWER_OPEN есть четыре элемента, которые получат изменения свойств. Первый целевой объект menuBar изменит своё свойство y на значение 0. Аналогично, textArea переместится в новое положение, когда состояние будет DRAWER_OPEN. textArea, drawer и значок выдвижного меню изменят свои свойства в соответствии с текущим состоянием.

states:[
    State {
        name: "DRAWER_OPEN"
        PropertyChanges { target: menuBar; y: 0 }
        PropertyChanges { target: textArea; y: partition + drawer.height }
        PropertyChanges { target: drawer; y: partition }
        PropertyChanges { target: arrowIcon; rotation: 180 }
    },
    State {
        name: "DRAWER_CLOSED"
        PropertyChanges { target: menuBar; y: -height; }
        PropertyChanges { target: textArea; y: drawer.height; height: screen.height - drawer.height }
        PropertyChanges { target: drawer; y: 0 }
        PropertyChanges { target: arrowIcon; rotation: 0 }
    }
]

Переходы между состояниями внезапны и требуют плавных переходов. Переходы между состояниями определяются с помощью типа Transition, который затем может быть привязан к свойству transitions элемента. В нашем текстовом редакторе переход между состояниями происходит при изменении состояния на DRAWER_OPEN или DRAWER_CLOSED. Важно, что переход должен иметь исходное и целевое состояние, но для наших переходов мы можем использовать символ подстановки * для обозначения того, что переход применяется ко всем изменениям состояния.

Во время переходов мы можем назначить анимации для изменений свойств. Наш menuBar переходит из положения y: 0 в положение y: -partition, и мы можем анимировать этот переход с помощью типа NumberAnimation. Мы объявляем, что свойства целевых объектов будут анимированы в течение определённого промежутка времени и с определённой кривой сглаживания. Кривая сглаживания управляет скоростью анимации и поведением интерполяции во время переходов между состояниями. Выбранная нами кривая сглаживания — Easing.OutExpo, которая замедляет движение в конце анимации. Более подробную информацию см. в статье QML об анимации.

transitions: [
    Transition {
        to: "*"
        NumberAnimation { target: textArea; properties: "y, height"; duration: 100; easing.type:Easing.OutExpo }
        NumberAnimation { target: menuBar; properties: "y"; duration: 100; easing.type: Easing.OutExpo }
        NumberAnimation { target: drawer; properties: "y"; duration: 100; easing.type: Easing.OutExpo }
    }
]

Другой способ анимировать изменения свойств — объявить тип Behavior. Переход работает только при изменении состояния, а Behavior может задать анимацию для изменения общего свойства. В текстовом редакторе у стрелки есть NumberAnimation, анимирующее свойство rotation при каждом изменении свойства.

В TextEditor.qml:

Behavior {
    NumberAnimation { property: "rotation"; easing.type: Easing.OutExpo }
}

Вернувшись к нашим компонентам со знаниями о состояниях и анимациях, мы можем улучшить внешний вид компонентов. В Button.qml мы можем добавить color и scale изменения свойств при нажатии на кнопку. Цвета анимируются с помощью ColorAnimation, а числа — с помощью NumberAnimation. Синтаксис on propertyName, показанный ниже, полезен при нацеливании на отдельное свойство.

В Button.qml:

...

color: buttonMouseArea.pressed ? Qt.darker(buttonColor, 1.5) : buttonColor
Behavior on color { ColorAnimation{ duration: 55 } }

scale: buttonMouseArea.pressed ? 1.1 : 1.0
Behavior on scale { NumberAnimation{ duration: 55 } }

Кроме того, мы можем улучшить внешний вид наших компонентов QML, добавив эффекты цвета, такие как градиенты и эффекты непрозрачности. Объявление объекта Gradient переопределит свойство color. Вы можете объявить цвет в градиенте с помощью типа GradientStop. Градиент позиционируется с помощью шкалы между 0.0 и 1.0.

В MenuBar.qml:

gradient: Gradient {
    GradientStop { position: 0.0; color: "#8C8F8C" }
    GradientStop { position: 0.17; color: "#6A6D6A" }
    GradientStop { position: 0.98; color: "#3F3F3F" }
    GradientStop { position: 1.0; color: "#0e1B20" }
}

Этот градиент используется строкой меню для отображения градиента, имитирующего глубину. Первый цвет начинается с 0.0, а последний цвет — с 1.0.

Куда двигаться дальше

Мы закончили создание пользовательского интерфейса очень простого текстового редактора. Далее пользовательский интерфейс готов, и мы можем реализовать логику приложения с помощью обычных Qt и C++. QML хорошо работает как инструмент прототипирования, отделяя логику приложения от дизайна пользовательского интерфейса.

Расширение QML с помощью Qt C++

Теперь, когда у нас есть макет текстового редактора, мы можем реализовать функциональность текстового редактора на C++. Использование QML с C++ позволяет нам создавать логику приложения с помощью Qt. Мы можем создать контекст QML в приложении C++ с помощью классов Quick от Qt и отобразить типы QML с помощью QQuickView. В качестве альтернативы, мы можем экспортировать наш код C++ в плагин расширения и сделать его доступным для QML как новый идентифицированный модуль. При запуске файлов QML с помощью qmlscene, нам нужно только убедиться, что наш модуль находится в одном из путей импорта, которые QML-движок использует для поиска модулей для импорта. Для нашего приложения мы будем использовать последний подход. Таким образом, мы можем загрузить файл QML непосредственно с помощью qmlscene вместо запуска исполняемого файла.

Использование классов C++ в QML

Мы будем реализовывать загрузку и сохранение файлов с помощью Qt и C++. Классы и функции C++ могут быть использованы в QML путём их регистрации. Они также должны быть скомпилированы как плагин Qt, а затем экспортированы как QML-модуль.

Для нашего приложения нам нужно создать следующие элементы:

  1. Directory класс, который будет обрабатывать операции, связанные с каталогами
  2. File класс, являющийся QObject, моделирующий список файлов в каталоге
  3. Класс плагина, который будет регистрировать классы в контексте QML
  4. Файл проекта Qt, который будет компилировать плагин
  5. Файл qmldir с определением модуля, который определяет идентификатор (URI импорта) и содержимое (в данном случае, наш плагин), которые должны быть доступны QML-модулем

Примечание: С Qt 5.1, модуль Qt Quick Dialogs предоставляет компонент диалога для выбора файлов из локальной файловой системы. В этом учебнике мы напишем свой собственный для иллюстративных целей.

Создание Qt-плагина

Для создания плагина нам нужно задать следующее в файле проекта Qt. Во-первых, необходимо добавить необходимые исходные файлы, заголовки и модули Qt в файл проекта. Весь код C++ и файлы проекта находятся в каталоге filedialog.

В filedialog.pro:

TEMPLATE = lib
CONFIG += qt plugin
QT += qml

DESTDIR +=  ../imports/FileDialog
OBJECTS_DIR = tmp
MOC_DIR = tmp

TARGET = filedialogplugin

HEADERS += \
        directory.h \
        file.h \
        dialogPlugin.h

SOURCES += \
        directory.cpp \
        file.cpp \
        dialogPlugin.cpp

В частности, мы связываем проект с модулем qml и настраиваем его как plugin, используя шаблон lib. Скомпилированный плагин мы поместим в родительский каталог imports/FileDialog.

Регистрация класса в QML

В dialogPlugin.h:

#include <QtQml/QQmlExtensionPlugin>

class DialogPlugin : public QQmlExtensionPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.QmlExtensionPlugin.FileDialog")

public:
    // registerTypes is inherited from QQmlExtensionPlugin
    void registerTypes(const char *uri);
};

Нам нужно экспортировать плагин с помощью макроса Q_PLUGIN_METADATA. Обратите внимание, что в нашем файле dialogPlugin.h, у нас есть макрос Q_OBJECT в верхней части нашего класса. Кроме того, нам нужно запустить qmake для файла проекта, чтобы сгенерировать необходимый мета-объектный код.

Наш класс плагина, DialogPlugin, является подклассом QQmlExtensionPlugin. Мы должны реализовать унаследованную функцию registerTypes().

В DialogPlugin.cpp:

#include "dialogPlugin.h"
#include "directory.h"
#include "file.h"
#include <QtQml>

void DialogPlugin::registerTypes(const char *uri)
{
    // Register the class Directory into QML as a "Directory" type version 1.0
    // @uri FileDialog
    qmlRegisterType<Directory>(uri, 1, 0, "Directory");
    qmlRegisterType<File>(uri, 1, 0, "File");
}

Функция registerTypes() регистрирует наши классы File и Directory в QML. Эта функция требует имени класса для шаблона, номера основной версии, номера дополнительной версии и имени наших классов. Комментарий // @uri <module identifier> позволяет Qt Creator распознавать зарегистрированные типы при редактировании файлов QML, которые импортируют этот модуль.

Создание свойств QML в классе C++

Мы можем создавать типы и свойства QML с помощью C++ и системы мета-объектов Qt. Мы можем реализовывать свойства с помощью слотов и сигналов, делая Qt-систему осведомленной о них. Эти свойства могут затем использоваться в QML.

Для текстового редактора нам нужно уметь загружать и сохранять файлы. Обычно эти функции находятся в диалоге выбора файла. К счастью, мы можем использовать QDir, QFile и QTextStream для реализации чтения каталогов и потоков ввода/вывода.

class Directory : public QObject {
    Q_OBJECT

    Q_PROPERTY (int filesCount READ filesCount CONSTANT)
    Q_PROPERTY (QString filename READ filename WRITE setFilename NOTIFY filenameChanged)
    Q_PROPERTY (QString fileContent READ fileContent WRITE setFileContent NOTIFY fileContentChanged)
    Q_PROPERTY (QQmlListProperty<File> files READ files CONSTANT)
    ...

Класс Directory использует систему мета-объектов Qt для регистрации свойств, необходимых для обработки файлов. Класс Directory экспортируется как плагин и доступен в QML как тип Directory. Каждое из перечисленных свойств, использующих макрос Q_PROPERTY(), является свойством QML.

Макрос Q_PROPERTY объявляет свойство, а также его функции чтения и записи в системе мета-объектов Qt. Например, свойство filename, типа QString, доступно для чтения с помощью функции filename() и для записи с помощью функции setFilename() . Кроме того, существует сигнал, связанный со свойством имени файла, с именем filenameChanged(), который испускается всякий раз, когда свойство изменяется. Функции чтения и записи объявляются как public в файле заголовков.

Аналогичным образом, у нас есть и другие свойства, объявленные в соответствии с их использованием. Свойство filesCount указывает количество файлов в каталоге. Свойство имени файла устанавливается в имя текущего выбранного файла, а содержимое загруженного/сохранённого файла хранится в свойстве fileContent.

Q_PROPERTY(QQmlListProperty<File> files READ files CONSTANT)

Свойство списка files — это список всех отфильтрованных файлов в каталоге. Класс Directory реализован для фильтрации невалидных текстовых файлов; только файлы с расширением .txt считаются валидными. Кроме того, списки QList могут быть использованы в файлах QML, объявив их как QQmlListProperty в C++. Шаблонный объект должен наследовать от QObject, следовательно, класс File также должен наследовать от QObject. В классе Directory список объектов File хранится в списке QList под именем m_fileList.

class File : public QObject{

    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)

    ...
};

Свойства могут быть использованы в QML в качестве части свойств объекта Directory. Обратите внимание, что нам не нужно создавать свойство с идентификатором id в нашем коде C++.

Directory {
    id: directory

    filesCount
    filename
    fileContent
    files

    files[0].name
}

Поскольку QML использует синтаксис и структуру JavaScript, мы можем перебирать список файлов и получать его свойства. Для получения свойства имени первого файла мы можем вызвать files[0].name.

Обычные функции C++ также доступны из QML. Функции загрузки и сохранения файлов реализованы на C++ и объявлены с использованием макроса Q_INVOKABLE. В качестве альтернативы, мы можем объявить функции как slot, и функции будут доступны из QML.

В directory.h:

Q_INVOKABLE void saveFile();
Q_INVOKABLE void loadFile();

Класс Directory также должен уведомлять другие объекты всякий раз, когда содержимое каталога изменяется. Эта функция выполняется с помощью signal. Как уже упоминалось, у QML-сигналов есть соответствующий обработчик с их именем, предваряемым on. Сигнал называется directoryChanged и испускается всякий раз, когда происходит обновление каталога. Обновление просто перезагружает содержимое каталога и обновляет список допустимых файлов в каталоге. Элементы QML могут быть уведомлены путём присоединения действия к обработчику сигнала onDirectoryChanged.

Свойства list требуют дальнейшего изучения. Это потому, что свойства списка используют обратные вызовы для доступа к содержимому списка и его изменения. Свойство списка имеет тип QQmlListProperty<File>. Всякий раз, когда к списку обращаются, функция доступа должна возвращать QQmlListProperty<File>. Шаблонный тип File должен быть производным от QObject . Кроме того, чтобы создать QQmlListProperty, функции доступа и изменения списка должны быть переданы в конструктор как указатели на функции. Список, в нашем случае QList, также должен быть списком указателей на File.

Конструктор QQmlListProperty объявлен следующим образом:

QQmlListProperty (QObject *object, void *data, AppendFunction append,
                  CountFunction count = 0, AtFunction at = 0, ClearFunction clear = 0);

Он принимает указатели на функции, которые будут добавлять в список, считать список, получать элемент по индексу и очищать список. Только функция append является обязательной. Обратите внимание, что указатели на функции должны соответствовать определению AppendFunction, CountFunction, AtFunction или ClearFunction.

Класс Directory создаёт экземпляр QQmlListProperty следующим образом:

QQmlListProperty<File>(this, &m_fileList, &appendFiles, &filesSize, &fileAt, &clearFilesPtr);

где параметрами являются указатели на следующие функции:

void appendFiles(QQmlListProperty<File> *property, File *file);
File* fileAt(QQmlListProperty<File> *property, int index);
int filesSize(QQmlListProperty<File> *property);
void clearFilesPtr(QQmlListProperty<File> *property);

Для упрощения диалога выбора файла класс Directory фильтрует недействительные текстовые файлы, то есть файлы, у которых нет расширения .txt. Если имя файла не имеет расширения .txt, то оно не будет отображаться в нашем диалоге выбора файла. Также реализация гарантирует, что сохранённые файлы имеют расширение .txt в имени файла. Directory использует QTextStream для чтения файла и вывода содержимого файла в файл.

С помощью нашего объекта Directory мы можем получить файлы в виде списка, узнать, сколько текстовых файлов находится в каталоге приложения, получить имя и содержимое файла как строку и быть уведомлёнными о любых изменениях в содержимом каталога.

Чтобы создать плагин, выполните qmake для файла проекта filedialog.pro, а затем выполните make для компиляции и переноса плагина в каталог plugins.

Импорт плагина в QML

Инструмент qmlscene импортирует файлы, находящиеся в той же директории, что и приложение. Мы также можем создать файл qmldir, содержащий расположения контента, который мы хотим импортировать. В данном случае, существует только плагин, но в файле qmldir могут быть определены и другие ресурсы (типы QML, файлы JavaScript).

Содержимое файла qmldir:

module FileDialog
plugin filedialogplugin

Модуль, который мы только что создали, называется FileDialog, и он предоставляет плагин под названием filedialogplugin, который соответствует полю TARGET в файле проекта. Поскольку мы не указали путь к плагину, QML-движок ожидает найти его в той же директории, что и файл qmldir.

Типы QML, зарегистрированные нашим плагином, теперь могут быть импортированы в QML:

import FileDialog 1.0

Directory {
    id: directory
}
...

Интеграция диалога выбора файла в меню Файл

Нашему FileMenu необходимо отобразить объект FileDialog, содержащий список текстовых файлов в каталоге, позволяя пользователю выбирать файл, щелкнув по списку. Также нам нужно назначить кнопки «Сохранить», «Загрузить» и «Новый» соответствующим действиям. Меню «Файл» содержит редактируемое текстовое поле для ввода имени файла с помощью клавиатуры.

Объект Directory используется в файле FileMenu.qml и уведомляет объект FileDialog о том, что каталог обновил свое содержимое. Это уведомление выполняется в обработчике сигнала onDirectoryChanged.

В FileMenu.qml:

Directory {
    id: directory
    filename: textInput.text
    onDirectoryChanged: fileDialog.notifyRefresh()
}

В соответствии с простотой нашего приложения, диалоговое окно «Файл» всегда будет отображаться и не будет отображать недопустимые текстовые файлы, у которых нет расширения .txt в их именах файлов.

В FileDialog.qml:

signal notifyRefresh()
onNotifyRefresh: dirView.model = directory.files

Объект FileDialog будет отображать содержимое каталога, считывая свойство списка, называемое files. Файлы используются в качестве модели для объекта GridView, который отображает элементы данных в сетке в соответствии с делегатом. Делегат обрабатывает отображение модели, и наше диалоговое окно «Файл» просто создаст сетку с центрированным текстом. Нажатие на имя файла приведет к появлению прямоугольника для выделения имени файла. Объект FileDialog уведомляется всякий раз, когда генерируется сигнал notifyRefresh, перезагружая файлы в каталоге.

В FileMenu.qml:

Button {
    id: newButton
    label: "New"
    onButtonClick: {
        textArea.textContent = ""
    }
}
Button {
    id: loadButton
    label: "Load"
    onButtonClick: {
        directory.filename = textInput.text
        directory.loadFile()
        textArea.textContent = directory.fileContent
    }
}
Button {
    id: saveButton
    label: "Save"
    onButtonClick: {
        directory.fileContent = textArea.textContent
        directory.filename = textInput.text
        directory.saveFile()
    }
}
Button {
    id: exitButton
    label: "Exit"
    onButtonClick: {
        Qt.quit()
    }
}

Наш объект FileMenu теперь может подключиться к соответствующим действиям. Объект saveButton передаст текст из TextEdit в свойство fileContent каталога, затем скопирует его имя файла из редактируемого текстового поля. Наконец, кнопка вызывает функцию saveFile(), сохраняя файл. У объекта loadButton аналогичное выполнение. Кроме того, действие New очистит содержимое TextEdit.

Далее, кнопки EditMenu подключены к функциям TextEdit для копирования, вставки и выбора всего текста в текстовом редакторе.

Приложение «Текстовый редактор»

Приложение может работать как простой текстовый редактор, способный принимать текст и сохранять его в файл. Оно также может загружать файл и выполнять манипуляции с текстом.

Запуск текстового редактора

Прежде чем текстовый редактор сможет запуститься, необходимо скомпилировать плагин C++ для диалогового окна «Файл». Для его компиляции перейдите в каталог filedialog, затем выполните qmake и выполните компиляцию с помощью make или nmake, в зависимости от вашей платформы.

Запустите текстовый редактор с помощью qmlscene, передавая каталог импортов в качестве параметра, чтобы движок QML знал, где искать модуль, который импортирует наш плагин диалогового окна «Файл»:

qmlscene -I ./imports texteditor.qml

Полный исходный код находится в каталоге examples/quick/tutorials/gettingStartedQml.

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/archives/qt-5.6/gettingstartedqml.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API