# 图片浏览器

看一个大一些的例子,来说明如何使用Qt Quick Controls。 为此,将创建一个简单的图像查看器。

首先,使用Fusion风格创建桌面版本,之后重构以提供移动设备的体验,最后查看最终代码库。

# 桌面版本

桌面版基于经典的应用程序窗口,带有菜单栏、工具栏和文档区域。 可以在下面看到该应用程序的运行情况。

最开始,使用Qt Creator项目模板创建一个空的 Qt Quick 应用程序。 但是,将模板中默认的Window元素替换为QtQuick.Controls模块中的ApplicationWindow元素。 下面展示了main.qml的代码,创建了窗口本身,并设置了默认的大小和标题。

import QtQuick
import QtQuick.Controls
import Qt.labs.platform

ApplicationWindow {
    visible: true
    width: 640
    height: 480

    // ...

}

ApplicationWindow由四个主要区域组成,菜单栏、工具栏和状态栏通常由MenuBarToolBarTabBar控件的实例填充,而内容区域是窗口子项所在的位置。 请注意,图像查看器应用程序没有状态栏,这就是为什么此处显示的代码以及上图中缺少它的原因。

由于目标是桌面版本,强制使用Fusion样式。 这可以通过配置文件、环境变量、命令行参数或在C++代码中以编程方式实现。 采用后一种方式,通过将以下代码添加到main.cpp实现:

QQuickStyle::setStyle("Fusion");

然后,通过在main.qml中添加一个Image元素作为内容来构建用户界面。 当用户打开图像时,这个元素将持有图像,因此现在它只是一个占位符。 background属性用于为窗口提供一个元素,这个元素放在内容的后面。 background将在没有加载图像时显示,如果图像的纵横比不允许它充满窗口的内容区域,background会显示为图像周围的边框。

ApplicationWindow {
    
    // ...
    
    background: Rectangle {
        color: "darkGray"
    }

    Image {
        id: image
        anchors.fill: parent
        fillMode: Image.PreserveAspectFit
        asynchronous: true
    }

    // ...

}

然后,继续添加ToolBar,使用窗口的toolBar属性完成。 在工具栏中,添加了一个Flow元素,它让内容在填充满控件的宽度后才溢出到新的一行。 在这个流元素中,放置了一个 ToolButton

ToolButton 有几个有趣的属性。 text 是能够见名知义的。 但是,icon.name 取自 freedesktop.org图标命名规范(freedesktop.org Icon Naming Specification) (opens new window)。 在该文档中,按名称列出了标准图标列表。 通过引用这样的名称,Qt会从当前桌面主题中挑选出正确的图标。

ToolButton的最后一段代码是onClicked信号处理器,它调用fileOpenDialog元素的open方法。

ApplicationWindow {
    
    // ...
    
    header: ToolBar {
        Flow {
            anchors.fill: parent
            ToolButton {
                text: qsTr("Open")
                icon.name: "document-open"
                onClicked: fileOpenDialog.open()
            }
        }
    }

    // ...

}

fileOpenDialog元素是来自Qt.labs.platform模块的FileDialog(文件对话框)控件,文件对话框可用于打开或保存文件。

在下面代码中,首先分配一个title(标题)。 然后使用StandardsPaths(标准路径)类设置起始文件夹。 StandardsPaths类包含指向常用文件夹的链接,例如用户的Home路径、文档路径等。 之后,设置了一个名称过滤器,用于控制用户可以使用对话框查看和选择哪些文件。

最后,是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元素放在菜单栏中,然后用MenuItem元素填充每个Menu

在下面的代码中,创建了两个菜单,FileHelp。 在File下,使用与工具栏中的工具按钮相同的图标和操作放置Open菜单项。 在Help下,有About菜单项,它会触发对 aboutDialog元素的open方法的调用。

请注意,Menutitle属性和MenuItemtext属性中的“&”符号将接下来紧挨着的字符转换为键盘快捷键; 例如,可以通过按Alt+F导航文件(File)菜单,然后按Alt+O触发打开(Open)项。

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元素基于QtQuick.Controls模块中的Dialog控件,该模块是自定义对话框的基础。 即将创建如下图所示的对话框。

aboutDialog的代码可以分为三部分。 首先,设置对话框窗口的标题; 然后,为对话框提供一些内容——在本例中,是一个Label控件; 最后,选择使用标准的Ok按钮,用来关闭对话框。

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
    }

    // ...

}

所有这一切的最终结果是一个用于查看图像的桌面应用程序,尽管它简单,但很实用。

# 迁移到移动设备

与桌面应用程序的用户界面的外观和行为相比,在移动设备上方式存在许多差异。 应用程序最大的不同在于操作的方式。 它将使用一个抽屉页面,而不是菜单栏和工具栏,用户可以从中选择要操作的行为。 抽屉页面可以从侧面滑入,但还在标题中提供了一个汉堡按钮(按钮上有几条横线,长得像个汉堡)。 在下面,可以看到打开抽屉页面后的应用程序。

首先,需要将main.cpp中设置的样式从Fusion更改为Material

QQuickStyle::setStyle("Material");

然后开始调整用户界面。 先用抽屉替换菜单。 在下面的代码中,将Drawer组件添加为ApplicationWindow的子组件。 在抽屉里,放了一个包含ItemDelegate(项代理)实例的ListView(列表视图);它还包含一个 ScrollIndicator(滚动条),用于体现正在显示的长列表的哪一部分。 由于此列表仅包含两个项,因此该指标在此示例中不可见。

抽屉的ListView由一个ListModel(列表模型)填充,其中每个ListItem(列表项)对应一个菜单项。 每点击一个item,在它的onClicked方法中,会调用对应ListItemtriggered方法。 这样,就可以使用单个委托来触发不同的操作。

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 { }
        }
    }

    // ...

}

下一个更改是在ApplicationWindowheader中。 不再使用桌面样式的工具栏,而是添加了一个按钮来打开抽屉,并为应用程序的标题添加一个标签。

ToolBar包含两个子元素:一个ToolButton和一个Label

ToolButton控件打开抽屉,可以在ListView委托中找到相应的close调用。 选择一个项后,抽屉将关闭。 ToolButton使用的图标来自Material设计图标页面(Material Design Icons page) (opens new window)

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通过使用文件选择器(file selectors)来支持这一点。

文件选择器可以根据哪些选择器处于活动状态来替换单个文件。 Qt 文档在QFileSelector类(链接 (opens new window))的文档中维护了一个选择器列表 。在子中,将桌面版本设为默认版本并在遇到android选择器时替换所选文件。 在开发过程中,您可以将环境变量QT_FILE_SELECTORS设置为android来模拟这一点。

文件选择器

selector(选择器)存在时,文件选择器通过用替换文件来工作。

通过在要替换的文件所在的目录中创建一个名为+selector(其中,selector表示选择器的名称)的目录,就可以在目录里面放置与要替换文件同名的文件。 当选择器存在时,将拾取目录中的文件而不是原始文件。

选择器基于平台:例如 android、ios、osx、linux、qnx等。 它们还可以包括所使用的Linux 发行版的名称(如果已确定的话),例如debian、ubuntu、fedora。 最后,它们还可以包括语言环境,例如en_US、sv_SE等。

也可以添加自定义的选择器。

进行此更改的第一步是隔离共享代码,通过创建ImageViewerWindow元素来实现这一点,该元素将用于代替前面两个变体的ApplicationWindow。 这将由对话框、Image元素和背景组成。 为了使对话框的打开方法可用于特定平台的代码,需要通过函数openFileDialogopenAboutDialog暴露它们。

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

        // ...

    }
}

接下来,为默认样式Fusion(即用户界面的桌面版本)创建一个新的main.qml

在这里,基于ImageViewerWindow(而不是ApplicationWindow)构建用户界面。 然后向它添加平台特定的部分,如MenuBarToolBar。 对这些的唯一的更改是打开相应对话框的调用是对新的函数进行的,而不是直接对对话框控件进行。

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和特定于移动设备的工具栏。 同样,唯一的变化是打开对话框的方式。

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引擎自动创建的文件选择器选择正确的文件。 默认情况下,Fusionmain.qml被加载。 如果存在android选择器,则加载 Materialmain.qml

到目前为止,已在main.cpp中设置样式。可以继续这样做,并使用#ifdef表达式为不同平台设置不同样式。将使用配置文件设置样式替代再次使用文件选择器机制。下面,可以看到设置Material样式的文件,设置Fusion样式也同样简单。

[Controls]
Style=Material

这些变化提供了一个联合代码库,其中所有文档代码都是共享的,只有用户交互模式存在差异。 有不同的方法可以处理这一点,例如:将文档保存在包含在平台特定接口中的特定组件中,或者如同本示例中,通过创建由每个平台扩展的公共基础。 当知道特定代码库的外观并可以决定如何区分通用代码和独特代码时,能决定最好的方法。

# 原生对话框

使用图像查看器时,会注意到它使用了一个非标准的文件选择器对话框,这使它看起来与周围格格不入。

Qt.labs.platform模块可以帮助解决这个问题。它为本地对话框(如文件对话框、字体对话框和颜色对话框)提供QML绑定。它还提供API来创建系统托盘图标,以及位于屏幕顶部的系统全局菜单(例如,在OS X中)。这样做的代价是依赖于QtWidgets模块,因为基于对话框的Widget被用作缺少原生支持的后备。

为了将本机文件对话框集成到图像查看器中,需要导入Qt.labs.platform模块。由于此模块与它替换的QtQuick.Dialogs模块名称冲突,因此删除旧的导入语句很重要。

在实际的文件对话框元素中,必须更改设置folder属性的方式,并确保onAccepted处理器使用file属性而不是fileUrl属性。除了这些细节之外,用法与QtQuick.Dialogs中的FileDialog相同。

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而言,它将使用QtWidgets模块提供的默认Qt对话框。

最后更新: 12/1/2021, 11:34:47 PM