Приложение просмотрщика изображений на QML
Давайте рассмотрим более крупный пример использования Qt Quick Controls. Для этого мы создадим простой просмотрщик изображений.
Сначала мы создадим его для десктопных компьютеров, используя стиль Fusion, а затем выполним его рефакторинг для мобильных устройств.
Десктопная версия
Десктопная версия основана на классическом окне приложения с панелью меню, панелью инструментов и областью документа. Приложение в действии можно увидеть ниже.
В качестве отправной точки мы используем шаблон проекта Qt Creator для пустого приложения Qt Quick. Однако мы заменяем элемент Window
по умолчанию из шаблона на ApplicationWindow
из модуля QtQuick.Controls
. В приведенном ниже коде файла main.qml показано, где создается само окно, и настраиваются размер по умолчанию и заголовок.
import QtQuick
import QtQuick.Controls
import Qt.labs.platform
ApplicationWindow {
visible: true
width: 640
height: 480
// ...
}
ApplicationWindow
состоит из четырех основных областей, как показано ниже: строка меню, панель инструментов и строка состояния обычно заполняются экземплярами элементов управления MenuBar
, ToolBar
или TabBar
, а область контента – это место, куда попадают дочерние элементы окна. Обратите внимание, что в приложении для просмотра изображений нет строки состояния; вот, почему она отсутствует в приведенном здесь коде, а также на рисунке выше.
Поскольку мы ориентируемся на десктоп, то принудительно включаем использование стиля Fusion. Это можно сделать с помощью файла конфигурации, переменных среды, аргументов командной строки или программно в коде C++. Мы делаем это вторым способом, добавляя следующую строку в main.cpp:
QQuickStyle::setStyle("Fusion");
Затем мы начинаем создавать пользовательский интерфейс в main.qml, добавляя элемент Image
в качестве контента. Этот элемент будет содержать изображения, когда пользователь их открывает, так что пока это просто заполнитель. Свойство background
используется для предоставления окну элемента для размещения за контентом. Этот элемент будет отображаться, когда изображение не загружено, и в виде границ вокруг изображения, если соотношение сторон не позволяет ему заполнить область контента окна.
ApplicationWindow {
// ...
background: Rectangle {
color: "darkGray"
}
Image {
id: image
anchors.fill: parent
fillMode: Image.PreserveAspectFit
asynchronous: true
}
// ...
}
Затем мы продолжаем, добавляя панель инструментов ToolBar
. Это делается с помощью свойства header
окна. Внутри панели инструментов мы добавляем элемент Flow
, который позволит содержимому заполнить ширину элемента управления, прежде чем перейти на новую строку. Внутри Flow
мы размещаем кнопку ToolButton
.
У ToolButton
есть пара интересных свойств. Со свойством text
всё понятно. Однако icon.name
взято из «Спецификации именования иконок freedesktop.org». В этом документе список стандартных иконок перечислен по именам. Ссылаясь на такое имя, Qt выберет правильную иконку из текущей темы рабочего стола.
В обработчике сигнала onClicked
кнопки ToolButton
находится последний фрагмент кода. Он вызывает метод open
элемента fileOpenDialog
.
ApplicationWindow {
// ...
header: ToolBar {
Flow {
anchors.fill: parent
ToolButton {
text: qsTr("Open")
icon.name: "document-open"
onClicked: fileOpenDialog.open()
}
}
}
// ...
}
Элемент fileOpenDialog
– это элемент управления FileDialog
из модуля Qt.labs.platform
. Диалоговое окно файла можно использовать для открытия или сохранения файлов.
В коде мы начинаем с присвоения заголовка title
. Затем мы устанавливаем начальную папку с помощью класса StandardsPaths
. Класс StandardsPaths
содержит ссылки на распространенные папки, такие как домашний каталог пользователя, документы и т. д. После этого мы устанавливаем фильтр имен, который контролирует, какие файлы пользователь может видеть и выбирать с помощью данного диалогового окна.
Наконец, мы доходим до обработчика сигнала onAccepted
, где элемент Image
, содержащий контент окна, настраивается на отображение выбранного файла. Также существует сигнал onRejected
, но в приложении для просмотра изображений нам не нужно его обрабатывать.
ApplicationWindow {
// ...
FileDialog {
id: fileOpenDialog
title: "Select an image file"
folder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
nameFilters: [
"Image files (*.png *.jpeg *.jpg)",
]
onAccepted: {
image.source = fileOpenDialog.fileUrl
}
}
// ...
}
Затем мы продолжаем работу с MenuBar
. Чтобы создать меню, нужно поместить элементы Menu
в строку меню, а затем заполнить каждое Menu
элементами MenuItem
.
В приведенном ниже коде мы создаем два меню: File и Help. В разделе File мы размещаем Open, используя ту же иконку и действие, что и у кнопки на панели инструментов. В разделе Help вы найдете пункт About, который запускает вызов метода open
элемента aboutDialog
.
Обратите внимание, что амперсанд (&
) в свойстве title
элемента Menu
и в свойстве text
элемента MenuItem
превращает следующий символ в сочетание клавиш; например, вы попадете в меню файла, нажав Alt+F, а затем Alt+O, чтобы открыть элемент.
ApplicationWindow {
// ...
menuBar: MenuBar {
Menu {
title: qsTr("&File")
MenuItem {
text: qsTr("&Open...")
icon.name: "document-open"
onTriggered: fileOpenDialog.open()
}
}
Menu {
title: qsTr("&Help")
MenuItem {
text: qsTr("&About...")
onTriggered: aboutDialog.open()
}
}
}
// ...
}
Элемент aboutDialog
основан на элементе управления Dialog
из модуля QtQuick.Controls
, который является основой для пользовательских диалогов. Диалог, который мы собираемся создать, показан на рисунке ниже.
Код aboutDialog
можно разделить на три части. Во-первых, мы настраиваем диалоговое окно с заголовком. Затем мы предоставляем некоторое содержимое для диалогового окна – в данном случае это элемент управления Label
. Наконец, мы решили использовать стандартную кнопку ОК, чтобы закрыть диалоговое окно.
ApplicationWindow {
// ...
Dialog {
id: aboutDialog
title: qsTr("About")
Label {
anchors.fill: parent
text: qsTr("QML Image Viewer\nA part of the QmlBook\nhttp://qmlbook.org")
horizontalAlignment: Text.AlignHCenter
}
standardButtons: StandardButton.Ok
}
// ...
}
Конечным результатом всего этого является функциональное, хотя и простое десктопное приложение для просмотра изображений.
Переход на мобильные устройства
Существует ряд различий в том, как пользовательский интерфейс должен выглядеть и вести себя на мобильном устройстве по сравнению с десктопным приложением. Самая большая разница для нашего приложения заключается в том, как осуществляется доступ к действиям. Вместо строки меню и панели инструментов мы будем использовать выдвижной элемент (drawer), в котором пользователь может выбирать действия. Этот выдвижной элемент можно выдвигать сбоку, но мы также предлагаем кнопку с иконкой гамбургера в верхней части. Получившееся приложение с открытым выдвижным элементом можно увидеть ниже.
Прежде всего, нам нужно изменить стиль, установленный в main.cpp, с Fusion на Material:
QQuickStyle::setStyle("Material");
Затем приступаем к адаптации пользовательского интерфейса. Начнем с замены меню на выдвижной элемент. В приведенном ниже коде компонент Drawer
добавляется в качестве дочернего элемента к ApplicationWindow
. Внутри выдвижного элемента мы помещаем ListView
, содержащий экземпляры ItemDelegate
. Он также содержит ScrollIndicator
, используемый для отображения того, какая часть длинного списка показывается в настоящий момент. Поскольку наш список состоит всего из двух элементов, этот индикатор в этом примере не виден.
ListView
выдвижного элемента заполняется из ListModel
, где каждый ListItem
соответствует пункту меню. При клике на любом элемента в методе onClicked
вызывается метод соответствующего ListItem
, указанный в свойстве triggered
. Таким образом, мы можем использовать одного делегата для запуска различных действий.
ApplicationWindow {
// ...
id: window
Drawer {
id: drawer
width: Math.min(window.width, window.height) / 3 * 2
height: window.height
ListView {
focus: true
currentIndex: -1
anchors.fill: parent
delegate: ItemDelegate {
width: parent.width
text: model.text
highlighted: ListView.isCurrentItem
onClicked: {
drawer.close()
model.triggered()
}
}
model: ListModel {
ListElement {
text: qsTr("Open...")
triggered: function() { fileOpenDialog.open(); }
}
ListElement {
text: qsTr("About...")
triggered: function() { aboutDialog.open(); }
}
}
ScrollIndicator.vertical: ScrollIndicator { }
}
}
// ...
}
Следующее изменение находится в свойстве header
элемента ApplicationWindow
. Вместо панели инструментов в десктопном стиле мы добавляем кнопку для открытия выдвижного элемента и метку для заголовка нашего приложения.
Панель инструментов ToolBar
содержит два дочерних элемента: ToolButton
и Label
.
Элемент управления ToolButton
открывает выдвижной контейнер. Соответствующий вызов закрытия можно найти в делегате ListView
. Когда элемент выбран, выдвижной контейнер закрывается. Иконка, используемая для ToolButton
, взята со страницы иконок для дизайна в стиле Material.
ApplicationWindow {
// ...
header: ToolBar {
ToolButton {
id: menuButton
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
icon.source: "images/baseline-menu-24px.svg"
onClicked: drawer.open()
}
Label {
anchors.centerIn: parent
text: "Image Viewer"
font.pixelSize: 20
elide: Label.ElideRight
}
}
// ...
}
Наконец, мы делаем фон панели инструментов красивым – или хотя бы оранжевым. Для этого мы изменим прикрепленное свойство Material.background
. Оно идет из модуля QtQuick.Controls.Material
и влияет только на стиль Material.
import QtQuick.Controls.Material
ApplicationWindow {
// ...
header: ToolBar {
Material.background: Material.Orange
// ...
}
Благодаря этим небольшим изменениям мы преобразовали нашу программу просмотра изображений для десктопных компьютеров в мобильную версию.
Общая кодовая база
В последних двух разделах мы рассмотрели просмотрщик изображений, разработанный для десктопных компьютеров, а затем адаптировали его для мобильных устройств.
Судя по кодовой базе, большая часть кода по-прежнему используется совместно. Части, которые являются общими, в основном связаны с документом приложения, то есть с изображением. Изменения учитывали разные модели взаимодействия на десктопных и мобильных устройствах соответственно. Естественно, мы хотели бы унифицировать эти кодовые базы. QML поддерживает это за счет использования селекторов файлов.
Селектор файлов позволяет нам заменять отдельные файлы в зависимости от того, какие селекторы активны. Документация Qt содержит список селекторов в документации для класса QFileSelector
(ссылка). В нашем случае мы сделаем десктопную версию версией по умолчанию и заменим выбранные файлы при встрече с селектором android. Во время разработки вы можете установить для переменной среды QT_FILE_SELECTORS
значение android
, чтобы имитировать это поведение.
Селектор файлов
Селекторы файлов при работе заменяют файлы альтернативой, когда присутствует селектор.
Создав каталог с именем +selector
(где selector
представляет имя селектора) в том же каталоге, что и файлы, которые вы хотите заменить, вы можете поместить в этот каталог файлы с теми же именами, что и файлы, которые хотите заменить. Когда селектор присутствует, файл в этом каталоге будет выбран вместо исходного файла.
Селекторы основываются на платформе: например. android, ios, osx, linux, qnx и так далее. Они также могут включать имя используемого дистрибутива Linux (если он указан), например, debian, ubuntu, fedora. Наконец, они также включают локаль, например, en_US, sv_SE и т.д.
Также можно добавлять свои собственные селекторы.
Первым шагом для внесения этого изменения является изоляция общего кода. Мы выполним это, создав элемент ImageViewerWindow
, который будет использоваться вместо ApplicationWindow
для обоих наших вариантов. Он будет состоять из диалогов, элемента Image
и фона. Чтобы сделать открытые методы диалогов доступными для специфичного для платформы кода, нам нужно предоставить их через функции openFileDialog
и openAboutDialog
.
// ImageViewerWindow.qml
import QtQuick
import QtQuick.Controls
import Qt.labs.platform
ApplicationWindow {
function openFileDialog() { fileOpenDialog.open(); }
function openAboutDialog() { aboutDialog.open(); }
visible: true
title: qsTr("Image Viewer")
background: Rectangle {
color: "darkGray"
}
Image {
id: image
anchors.fill: parent
fillMode: Image.PreserveAspectFit
asynchronous: true
}
FileDialog {
id: fileOpenDialog
// ...
}
Dialog {
id: aboutDialog
// ...
}
}
Затем мы создаем новый файл main.qml для нашего стиля Fusion по умолчанию, т.е. десктопной версии пользовательского интерфейса.
Здесь мы строим пользовательский интерфейс на основе ImageViewerWindow
, а не на ApplicationWindow
. Затем мы добавляем к нему специфичные для платформы части, например, MenuBar
и ToolBar
. Единственным изменением в них является то, что вызовы для открытия соответствующих диалогов выполняются для новых функций, а не непосредственно для элементов управления диалоговых окон.
// main.qml
import QtQuick
import QtQuick.Controls
ImageViewerWindow {
id: window
width: 640
height: 480
menuBar: MenuBar {
Menu {
title: qsTr("&File")
MenuItem {
text: qsTr("&Open...")
icon.name: "document-open"
onTriggered: window.openFileDialog()
}
}
Menu {
title: qsTr("&Help")
MenuItem {
text: qsTr("&About...")
onTriggered: window.openAboutDialog()
}
}
}
header: ToolBar {
Flow {
anchors.fill: parent
ToolButton {
text: qsTr("Open")
icon.name: "document-open"
onClicked: window.openFileDialog()
}
}
}
}
Далее нам нужно создать файл main.qml
для мобильных устройств. Он будет основан на теме Material. Здесь мы сохраняем Drawer
и панель инструментов для мобильных устройств. Опять же, единственное изменение заключается в том, как открываются диалоги.
// +android/main.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material
ImageViewerWindow {
id: window
width: 360
height: 520
Drawer {
id: drawer
// ...
ListView {
// ...
model: ListModel {
ListElement {
text: qsTr("Open...")
triggered: function(){ window.openFileDialog(); }
}
ListElement {
text: qsTr("About...")
triggered: function(){ window.openAboutDialog(); }
}
}
// ...
}
}
header: ToolBar {
// ...
}
}
Два файла main.qml размещаются в файловой системе, как показано ниже. Это позволяет селектору файлов, автоматически создаваемому механизмом QML, выбирать нужный файл. По умолчанию загружается файл main.qml Fusion. Если присутствует селектор android
, вместо него загружается Material main.qml.
До сих пор стиль устанавливался в main.cpp. Мы могли бы продолжить это и использовать выражения #ifdef
для установки разных стилей для разных платформ. Но вместо этого мы снова воспользуемся механизмом селекторов файлов и установим стиль с помощью файла конфигурации. Ниже вы можете увидеть файл qtquickcontrols2.conf стиля Material, но файл Fusion так же прост.
[Controls]
Style=Material
Эти изменения дали нам объединенную кодовую базу, в которой весь код документа является общим, и различаются только различия в шаблонах взаимодействия с пользователем. Есть разные способы сделать это, например, сохранение документа в определенном компоненте, включенном в интерфейсы конкретной платформы, или, как в этом примере, путем создания общей базы, расширяемой каждой платформой. Лучший подход лучше всего определяется, когда вы знаете, как выглядит ваша конкретная кодовая база, и можете решить, как отделить общее от уникального.
Нативные диалоги
При использовании средства просмотра изображений вы заметите, что оно использует нестандартный диалог выбора файлов. Из-за этого оно выглядит неуместно.
Модуль Qt.labs.platform
может помочь нам решить эту проблему. Он обеспечивает привязку QML к собственным диалоговым окнам платформы, таким как диалоговое окно файла, диалоговое окно шрифта и диалоговое окно цвета. Он также предоставляет API для создания иконок на панели задач, а также глобальных системных меню, которые находятся поверх экрана (например, как в OS X). Стоимость этого зависит от модуля QtWidgets
, поскольку диалоговое окно на основе виджета используется в качестве запасного варианта, когда отсутствует собственная поддержка.
Чтобы интегрировать нативный диалог файлов в наш просмотрщик изображений, нам нужно импортировать модуль Qt.labs.platform
. Поскольку имя этого модуля не совпадает с именем модуля QtQuick.Dialogs
, который он заменяет, важно удалить старую инструкцию импорта.
В самом элементе диалогового окна файла мы должны изменить способ установки свойства folder
и убедиться, что обработчик onAccepted
использует свойство file
вместо свойства fileUrl
. Помимо этих деталей, использование идентично FileDialog
из QtQuick.Dialogs
.
import QtQuick
import QtQuick.Controls
import Qt.labs.platform
ApplicationWindow {
// ...
FileDialog {
id: fileOpenDialog
title: "Select an image file"
folder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
nameFilters: [
"Image files (*.png *.jpeg *.jpg)",
]
onAccepted: {
image.source = fileOpenDialog.file
}
}
// ...
}
В дополнение к изменениям QML нам также необходимо изменить файл проекта просмотрщика изображений, чтобы включить модуль widgets
.
QT += quick quickcontrols2 widgets
И нам нужно обновить main.cpp, чтобы создать экземпляр объекта QApplication
вместо объекта QGuiApplication
. Это связано с тем, что класс QGuiApplication
содержит минимум, необходимый для графического приложения, в то время как QApplication
расширяет класс QGuiApplication
функциями, необходимыми для поддержки QtWidgets
.
include <QApplication>
// ...
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// ...
}
Благодаря этим изменениям просмотрщик изображений теперь будет использовать нативные диалоговые окна на большинстве платформ. Поддерживаемые платформы: iOS, Linux (с темой платформы GTK+), macOS, Windows и WinRT. Для Android будет использоваться диалоговое окно Qt по умолчанию, предоставляемое модулем QtWidgets
.