Spec-Zone.ru › Qt 5.9

Начало работы с программированием 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 в наш simpleButton. Объекты 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. Важно, что переход требует состояний from и to, но для наших переходов мы можем использовать символ подстановки *, чтобы указать, что переход применяется ко всем изменениям состояния.

Во время переходов мы можем назначить анимации для изменений свойств. Наш 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/qt-5.9/gettingstartedqml.html

Spec-Zone.ru

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