Приложение просмотрщика изображений на QML

Добавлено 15 марта 2022 в 05:35

Давайте рассмотрим более крупный пример использования 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, который является основой для пользовательских диалогов. Диалог, который мы собираемся создать, показан на рисунке ниже.

диалог about

Код 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.

Теги

GUI / Графический интерфейс пользователяQMLQtQtQuickQtQuick ControlsПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.