Начало работы с программированием Qt Quick
Добро пожаловать в мир QML, декларативного языка для интерфейсов. В этом руководстве по началу работы мы создадим простое приложение текстового редактора с использованием QML. После прочтения этого руководства вы будете готовы разрабатывать собственные приложения с использованием QML и Qt C++.
QML для создания пользовательских интерфейсов
Приложение, которое мы создаём, представляет собой простой текстовый редактор, который будет загружать, сохранять и выполнять некоторые операции с текстом. Это руководство будет состоять из двух частей. В первой части мы разработаем макет и поведение приложения, используя декларативный язык QML. Во второй части мы реализуем загрузку и сохранение файлов с помощью Qt C++. С помощью системы метаобъектов Qt мы можем экспонировать функции C++ в качестве свойств, которые могут использовать типы объектов QML. Использование QML и Qt C++ позволяет эффективно отделить логику интерфейса от логики приложения.
Полный исходный код находится в каталоге examples/quick/tutorials/gettingStartedQml. Если вы хотите увидеть, как выглядит готовое приложение, вы можете перейти к главе Запуск текстового редактора.
Часть C++ этого учебника предполагает, что читатель обладает базовыми знаниями о процедурах компиляции Qt.
Главы учебника:
- Определение кнопки и меню
- Реализация меню
- Создание текстового редактора
- Декорация текстового редактора
- Расширение 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 . Они объявлены внутри типа Row, позиционера, который разместит своих потомков в вертикальном ряду. Объявление 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 находится внутри элемента flickable, который будет прокручивать текст, если указатель курсора находится вне видимой области. Функция ensureVisible() проверит, находится ли прямоугольник курсора за пределами видимых границ, и соответственно переместит текстовую область.
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++ с помощью классов Qt Quick и отображать типы QML с помощью QQuickView. В качестве альтернативы, мы можем экспортировать наш код C++ в плагин расширения и сделать его доступным для QML как новый идентифицируемый модуль. При запуске QML-файлов с помощью qmlscene, нам нужно только убедиться, что наш модуль находится в одном из путей импорта, которые ищет QML-движок для импорта модулей. Для нашего приложения мы воспользуемся последним подходом. Таким образом, мы можем загрузить QML-файл непосредственно с помощью qmlscene вместо запуска исполняемого файла.
Экспонирование классов C++ в QML
Мы будем реализовывать загрузку и сохранение файлов с помощью Qt и C++. Классы и функции C++ могут использоваться в QML путём их регистрации. Они также должны быть скомпилированы как плагин Qt, а затем экспонированы как QML-модуль.
Для нашего приложения нам нужно создать следующие элементы:
-
Directoryкласс, который будет обрабатывать операции, связанные с каталогом -
Fileкласс, который является QObject, моделирующий список файлов в каталоге - Класс плагина, который будет регистрировать классы в контексте QML
- Файл проекта Qt, который будет компилировать плагин
- файл определения модуля 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, содержащий местоположения содержимого, которое мы хотим импортировать. В данном случае, есть только плагин, но другие ресурсы (QML-типы, JavaScript-файлы) также могут быть определены в файле qmldir.
Содержимое файла 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.11/gettingstartedqml.html