# 图片浏览器
看一个大一些的例子,来说明如何使用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
由四个主要区域组成,菜单栏、工具栏和状态栏通常由MenuBar
、ToolBar
、TabBar
控件的实例填充,而内容区域是窗口子项所在的位置。 请注意,图像查看器应用程序没有状态栏,这就是为什么此处显示的代码以及上图中缺少它的原因。
由于目标是桌面版本,强制使用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
。
在下面的代码中,创建了两个菜单,File和Help。 在File下,使用与工具栏中的工具按钮相同的图标和操作放置Open菜单项。 在Help下,有About菜单项,它会触发对 aboutDialog
元素的open
方法的调用。
请注意,Menu
的title
属性和MenuItem
的text
属性中的“&”符号将接下来紧挨着的字符转换为键盘快捷键; 例如,可以通过按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
方法中,会调用对应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 { }
}
}
// ...
}
下一个更改是在ApplicationWindow
的header
中。 不再使用桌面样式的工具栏,而是添加了一个按钮来打开抽屉,并为应用程序的标题添加一个标签。
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
元素和背景组成。 为了使对话框的打开方法可用于特定平台的代码,需要通过函数openFileDialog
和openAboutDialog
暴露它们。
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
)构建用户界面。 然后向它添加平台特定的部分,如MenuBar
和ToolBar
。 对这些的唯一的更改是打开相应对话框的调用是对新的函数进行的,而不是直接对对话框控件进行。
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引擎自动创建的文件选择器选择正确的文件。 默认情况下,Fusion的main.qml
被加载。 如果存在android
选择器,则加载 Material的main.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对话框。