6.1 控件简介

从头开始使用 Qt Quick 为您提供了基本的图形和交互元素,您可以从中构建用户界面。使用 Qt Quick Controls 2,您可以从一组稍微结构化的控件开始构建。控件范围从简单的文本标签和按钮到更复杂的控件,例如滑块和转盘。如果您想创建基于经典交互模式的用户界面,这些元素非常方便,因为它们提供了立足的基础。

Qt Quick Controls 2 带有许多开箱即用的样式,如下表所示。默认样式是基本的平面样式。 Universal 风格基于 Microsoft Universal Design Guidelines,Material 基于 Google 的 Material Design Guidelines,Fusion 风格是面向桌面的风格。

可以通过调整使用的调色板来调整某些样式。 Imagine 是一种基于图像资产的样式,这允许图形设计师无需编写任何代码即可创建新样式,甚至无需编写调色板颜色代码。

Qt Quick Controls 2 可从 QtQuick.Controls 导入模块获得。 在本模块中,您将找到基本控件,例如按钮、标签、复选框、滑块等。 除了这些控件之外,以下模块也很重要:

模板 说明
QtQuick. Controls 基本控制。
QtQuick. Templates 提供控件的非可视部分。
QtQuick. Dialogs 提供用于显示消息、挑选文件、挑选颜色和挑选字体的标准对话框,以及自定义对话框的基础。
QtQuick. Controls. Universal 通用风格主题支持。
QtQuick. Controls. Material 材质风格主题支持
Qt.labs. calendar 支持日期选择和其他日历相关交互的控件。
Qt.labs. platform 支持用于常见任务的平台原生对话框,例如选择文件、颜色等,以及系统托盘图标和标准路径。

请注意,Qt.labs 模块是实验性的,这意味着它们的 API 可以在 Qt 版本之间进行重大更改。

注意:QtQuick.Dialogs 模块是 Qt Quick Controls 1 模块,但它也是在不依赖 QtWidgets 模块的情况下进行对话框的唯一方法。 请参阅下面的更多细节。

6.2 图像查看器

让我们看一个如何使用 Qt Quick Controls 2 的更大示例。 为此,我们将创建一个简单的图像查看器。
首先,我们使用 Fusion 风格为桌面创建它,然后我们将重构它以获得移动体验,然后再查看最终代码库。

6.2.1 桌面版

桌面版基于带有菜单栏、工具栏和文档区域的经典应用程序窗口。 该应用程序可以在下面看到。
我们使用用于空 Qt Quick 应用程序的 Qt Creator 项目模板作为起点。 但是,我们将模板中的默认 Window 元素替换为 QtQuick.Controls 模块中的 ApplicationWindow。 下面的代码显示了 main.qml,其中创建了窗口本身并设置了默认大小和标题。

import QtQuick 2.0
import QtQuick.Controls 2.4
import QtQuick.Dialogs 1.2ApplicationWindow {visible: truewidth: 640height: 480// ...
}

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

由于我们以桌面为目标,因此我们强制使用 Fusion 样式。 这可以通过环境变量、命令行参数或以编程方式在 C++ 代码中完成。 我们通过将以下行添加到 main.cpp 来执行后一种方式:

QQuickStyle::setStyle("Fusion");

然后我们开始在 main.qml 中通过添加一个 Image 元素作为内容来构建用户界面。 当用户打开它们时,这个元素将保存图像,所以现在它只是一个占位符。 背景属性用于向窗口提供一个元素以放置在内容后面。 这将在没有加载图像时显示,如果纵横比降低不让它填满窗口的内容区域,则会显示为图像周围的边框。

ApplicationWindow {// ...background: Rectangle {color: "darkGray"}Image {id: imageanchors.fill: parentfillMode: Image.PreserveAspectFitasynchronous: true}// ...
}

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

ToolButton 有几个有趣的属性。 文字是直截了当的。 但是,icon.name 取自 freedesktop.org 图标命名规范。 在该文档中,标准图标列表按名称列出。 通过引用这样的名称,Qt 将从当前桌面主题中挑选出正确的图标。

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

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

fileOpenDialog 元素是来自 QtQuick.Dialogs 模块的 FileDialog 控件。 文件对话框可用于打开或保存文件,以及选择目录。

注意:QtQuick.Dialogs 模块是 Qt Quick Controls 1 模块,但它也是在不依赖 QtWidgets 模块的情况下进行对话框的唯一方法。 进一步了解如何使用 Qt.labs.platform 实现本机对话框。

在代码中,我们首先指定一个标题。 然后我们使用快捷方式属性设置起始文件夹。 快捷方式属性包含指向常用文件夹的链接,例如用户的主页、文档等。 之后,我们设置一个名称过滤器来控制用户可以使用对话框查看和选择哪些文件。

最后,我们到达 onAccepted 信号处理程序,其中保存窗口内容的 Image 元素被设置为显示所选文件。 还有一个 onRejected 信号,但我们不需要在图像查看器应用程序中处理它。

ApplicationWindow {// ...FileDialog {id: fileOpenDialogtitle: "Select an image file"folder: shortcuts.documentsnameFilters: ["Image files (*.png *.jpeg *.jpg)",]onAccepted: {image.source = fileOpenDialog.fileUrl}}// ...
}

然后我们继续使用菜单栏。 要创建菜单,需要将 Menu 元素放在菜单栏内,然后用 MenuItem 元素填充每个 Menu。

在下面的代码中,我们创建了两个菜单,文件和帮助。 在文件下,我们使用与工具栏中的工具按钮相同的图标和操作放置打开。 在 Help 下,您可以找到 About 触发对 aboutDialog 元素的 open 方法的调用。

请注意,Menu 的 title 属性和 MenuItem 的 text 属性中的和号 (“&”) 将以下字符转换为键盘快捷键,例如 按 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 元素基于 QtQuick.Controls 模块中的 Dialog 控件,它是自定义对话框的基础。 我们即将创建的对话框如下图所示。
aboutDialog 的代码可以分为三个部分。 首先,我们设置带有标题的对话窗口。 然后我们为对话框提供一些内容——在本例中是一个标签控件。 最后,我们选择使用标准的 Ok 按钮来关闭对话框。

ApplicationWindow {// ...Dialog {id: aboutDialogtitle: qsTr("About")Label {anchors.fill: parenttext: qsTr("QML Image Viewer\nA part of the QmlBook\nhttp://qmlbook.org")horizontalAlignment: Text.AlignHCenter}standardButtons: StandardButton.Ok}// ...
}

所有这一切的最终结果是一个用于查看图像的简单的桌面应用程序。

6.2.2 移动到移动端

与桌面应用程序相比,预期用户界面在移动设备上的行为方式存在许多差异。 我们的应用程序最大的不同是如何访问操作。
我们将使用抽屉来代替菜单栏和工具栏,用户可以从中选择操作。 抽屉可以从侧面滑入,但我们还在标题中提供了一个汉堡按钮。 抽屉打开的结果应用程序如下所示。

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

QQuickStyle::setStyle("Material");

然后我们开始调整用户界面。 我们首先用抽屉替换菜单。 在下面的代码中,Drawer 组件作为子组件添加到 ApplicationWindow。 在抽屉内,我们放置了一个包含 ItemDelegate 实例的 ListView。 它还包含一个 ScrollIndicator,用于显示显示的长列表的哪一部分。 由于我们的列表仅包含两个项目,因此在此示例中不可见。

抽屉ListView是从一个ListModel中填充出来的,其中每个ListItem对应一个菜单项。每次点击一个item,在onClicked方法中,都会调用对应ListItem的触发方法。 这样,我们可以使用单个委托来触发不同的操作。

ApplicationWindow {// ...id: windowDrawer {id: drawerwidth: Math.min(window.width, window.height) / 3 * 2height: window.heightListView {focus: truecurrentIndex: -1anchors.fill: parentdelegate: ItemDelegate {width: parent.widthtext: model.texthighlighted: ListView.isCurrentItemonClicked: {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 中的标题。 我们添加了一个按钮来打开抽屉和应用程序的标题,而不是桌面样式的工具栏。

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

ToolButton 控件打开抽屉。 可以在 ListView 委托中找到相应的关闭调用。 选择项目后,抽屉关闭。 ToolButton 使用的图标来自 Material Design Icons 页面。

ApplicationWindow {// ...header: ToolBar {ToolButton {id: menuButtonanchors.left: parent.leftanchors.verticalCenter: parent.verticalCentericon.source: "images/baseline-menu-24px.svg"onClicked: drawer.open()}Label {anchors.centerIn: parenttext: "Image Viewer"font.pixelSize: 20elide: Label.ElideRight}}// ...
}

最后,我们使工具栏的背景变得漂亮——或者至少是橙色的。 为此,我们更改 Material.background 附加属性。 这来自 QtQuick.Controls.Material 2.1 模块,仅影响 Material 样式。

import QtQuick.Controls.Material 2.1ApplicationWindow {// ...header: ToolBar {Material.background: Material.Orange// ...
}

通过这些少量更改,我们将桌面图像查看器转换为适合移动设备的版本。

6.2.3 共享代码库

在过去的两节中,我们研究了为桌面使用而开发的图像查看器,然后将其改编为移动设备。

查看代码库,大部分代码仍然是共享的。 共享的部分主要与应用程序的文档相关联,即图像。 这些变化分别影响了桌面和移动端的个人交互模式。 自然,我们希望统一这些代码库。 QML 通过使用文件选择器来支持这一点。

文件选择器允许我们根据哪些选择器处于活动状态来替换单个文件。 Qt 文档在 QFileSelector 类的文档中维护了一个选择器列表(链接)。 在我们的例子中,我们将桌面版本设为默认版本,并在遇到 android 选择器时替换选定的文件。 在此期开发你可以将环境变量 QT_FILE_SELECTORS 设置为 android 来模拟这个。

注意:文件选择器通过在存在选择器时用替代文件替换文件来工作。
通过创建一个名为 +selector 的目录,其中 selector 表示选择器的名称,与要替换的文件并行,然后可以将与要替换的文件同名的文件放置在目录中。 当存在选择器时,将选择目录中的文件而不是原始文件。
选择器基于平台,例如 安卓、ios、osx、linux、qnx等。 它们还可以包括使用的 Linux 发行版的名称(如果标识),例如 debian、ubuntu、fedora。 最后,它们还包括语言环境,例如 en_US、sv_SE 等
也可以添加您自己的自定义选择器。

进行此更改的第一步是隔离共享代码。 我们通过创建 ImageViewerWindow 元素来做到这一点,该元素将用于我们的两个变体而不是 ApplicationWindow。 这将包括对话框、图像元素和背景。 为了使特定于平台的代码可以使用对话框的打开方法,我们需要通过函数 openFileDialog 和 openAboutDialog 公开它们。

import QtQuick 2.0
import QtQuick.Controls 2.4
import QtQuick.Dialogs 1.2ApplicationWindow {function openFileDialog() { fileOpenDialog.open(); }function openAboutDialog() { aboutDialog.open(); }visible: truetitle: qsTr("Image Viewer")background: Rectangle {color: "darkGray"}Image {id: imageanchors.fill: parentfillMode: Image.PreserveAspectFitasynchronous: true}FileDialog {id: fileOpenDialog// ...}Dialog {id: aboutDialog// ...}
}

接下来,我们为我们的默认样式 Fusion 创建一个新的 main.qml,即用户界面的桌面版本。
在这里,我们围绕 ImageViewerWindow 而不是 ApplicationWindow 建立用户界面。
然后我们将平台特定的部分添加到它,例如 菜单栏和工具栏。 对这些的唯一更改是打开相应对话框的调用是针对新功能而不是直接针对对话框控件进行的。

import QtQuick 2.0
import QtQuick.Controls 2.4ImageViewerWindow {id: windowwidth: 640height: 480menuBar: 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: parentToolButton {text: qsTr("Open")icon.name: "document-open"onClicked: window.openFileDialog()}}}
}

接下来,我们必须创建一个特定于移动设备的 main.qml。 这将基于 Material 主题。 在这里,我们保留了 Drawer 和特定于移动设备的工具栏。 同样,唯一的变化是对话框的打开方式。

import QtQuick 2.0
import QtQuick.Controls 2.4
import QtQuick.Controls.Material 2.1ImageViewerWindow {id: windowwidth: 360height: 520Drawer {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

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

6.2.4 原生对话框

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

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

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

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

import QtQuick 2.0
import QtQuick.Controls 2.4
import Qt.labs.platform 1.0ApplicationWindow {// ...FileDialog {id: fileOpenDialogtitle: "Select an image file"folder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)nameFilters: ["Image files (*.png *.jpeg *.jpg)",]onAccepted: {image.source = fileOpenDialog.file}}// ...
}

除了 QML 更改之外,我们还需要更改图像查看器的项目文件以包含小部件模块。

QT += quick quickcontrols2 widgets

我们需要更新 main.qml 来实例化一个 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 对话框。

6.3 常见模式

使用 Qt Quick Controls 2 可以实现许多常见的用户界面模式。在本节中,我们将尝试演示如何构建一些更常见的用户界面模式。

6.3.1 嵌套屏幕

对于这个例子,我们将创建一个可以从上一级屏幕访问的页面树。 结构如下图。

这种类型的用户界面中的关键组件是 StackView。 它允许我们将页面放置在堆栈上,然后可以在用户想要返回时弹出。 在此处的示例中,我们将展示如何实现这一点。

应用程序从 main.qml 开始,我们有一个 ApplicationWindow,其中包含一个 ToolBar、一个 Drawer、一个 StackView 和一个主页元素 Home。 我们将研究下面的每个组件。

import QtQuick 2.9
import QtQuick.Controls 2.2ApplicationWindow {// ...header: ToolBar {// ...}Drawer {// ...}StackView {id: stackViewanchors.fill: parentinitialItem: Home {}}
}

主页 Home.qml 包含一个 Page,它是一个支持页眉和页脚的控制元素。 在这个例子中,我们只是在页面上以文本 Home Screen 为标签居中。 这是有效的,因为 StackView 的内容会自动填充堆栈视图,因此页面具有正确的大小以使其正常工作

import QtQuick 2.9
import QtQuick.Controls 2.2Page {title: qsTr("Home")Label {anchors.centerIn: parenttext: qsTr("Home Screen")}
}

回到 main.qml,我们现在看看抽屉部分。 这是页面导航开始的地方。 用户界面的活动部分是 ÌtemDelegate 项。 在 onClicked 处理程序中,下一页被推送到 stackView 上。
如以下代码所示,可以推送组件或对特定 QML 文件的引用。 无论哪种方式都会导致创建一个新实例并将其推入堆栈。

ApplicationWindow {// ...Drawer {id: drawerwidth: window.width * 0.66height: window.heightColumn {anchors.fill: parentItemDelegate {text: qsTr("Profile")width: parent.widthonClicked: {stackView.push("Profile.qml")drawer.close()}}ItemDelegate {text: qsTr("About")width: parent.widthonClicked: {stackView.push(aboutPage)drawer.close()}}}}// ...Component {id: aboutPageAbout {}}// ...
}

拼图的另一半是工具栏。 这个想法是当 stackView 包含多个页面时显示一个后退按钮,否则显示一个菜单按钮。 其逻辑可以在 text 属性中看到,其中 "\u..." 字符串表示我们需要的 unicode 符号。

在 onClicked 处理程序中,我们可以看到当堆栈上的页面超过一页时,堆栈被弹出,即 首页被删除。 如果堆栈仅包含一项,即主屏幕,则打开抽屉。

在工具栏下方,您可以找到一个标签。 此元素在标题的中心显示每个页面的标题。

ApplicationWindow {// ...header: ToolBar {contentHeight: toolButton.implicitHeightToolButton {id: toolButtontext: stackView.depth > 1 ? "\u25C0" : "\u2630"font.pixelSize: Qt.application.font.pixelSize * 1.6onClicked: {if (stackView.depth > 1) {stackView.pop()} else {drawer.open()}}}Label {text: stackView.currentItem.titleanchors.centerIn: parent}}// ...
}

现在我们已经了解了如何访问 About 和 Profile 页面,但我们还希望能够从 Profile 页面访问 Edit Profile 页面。 这是通过个人资料页面上的按钮完成的。 单击按钮时,EditProfile.qml 文件被推送到 StackView。

import QtQuick 2.9
import QtQuick.Controls 2.2Page {title: qsTr("Profile")Column {anchors.centerIn: parentspacing: 10Label {anchors.horizontalCenter: parent.horizontalCentertext: qsTr("Profile")}Button {anchors.horizontalCenter: parent.horizontalCentertext: qsTr("Edit");onClicked: stackView.push("EditProfile.qml")}}
}

6.3.2 并排屏幕

对于此示例,我们创建了一个用户界面,该界面由用户可以切换的三个页面组成。 页面如下图所示。 这可能是健康跟踪应用程序的界面,跟踪当前状态、用户统计数据和整体统计数据。

下图显示了当前页面在应用程序中的外观。 屏幕的主要部分由 SwipeView 管理,这使得并排屏幕交互模式成为可能。 图中显示的标题和文字来自 SwipeView 内部的页面,而 PageIndicator(底部的三个点)来自 main.qml,位于 SwipeView 下方。 页面指示器向用户显示当前处于活动状态的页面,在导航时提供帮助。

深入到 main.qml,它由一个带有 SwipeView 的 ApplicationWindow 组成。

import QtQuick 2.9
import QtQuick.Controls 2.2ApplicationWindow {visible: truewidth: 640height: 480title: qsTr("Side-by-side")SwipeView {// ...}// ...
}

在 SwipeView 中,每个子页面都按照它们出现的顺序进行实例化。 它们是 Current、UserStats 和 TotalStats。

ApplicationWindow {// ...SwipeView {id: swipeViewanchors.fill: parentCurrent {}UserStats {}TotalStats {}}// ...
}

最后 SwipeView 的 count 和 currentIndex 属性绑定到 PageIndicator 元素。 这样就完成了页面周围的结构。

ApplicationWindow {// ...SwipeView {id: swipeView// ...}PageIndicator {anchors.bottom: parent.bottomanchors.horizontalCenter: parent.horizontalCentercurrentIndex: swipeView.currentIndexcount: swipeView.count}
}

每个页面由一个页面组成,其标题由标签和一些内容组成。 对于 Current 和 User Stats 页面,内容是一个简单的 Label,但对于 Community Stats 页面,包含一个后退按钮。

import QtQuick 2.9
import QtQuick.Controls 2.2Page {header: Label {text: qsTr("Community Stats")font.pixelSize: Qt.application.font.pixelSize * 2padding: 10}// ...
}

后退按钮显式调用 SwipeView 的 setCurrentIndex 将索引设置为零,将用户直接返回到当前页面。 在页面之间的每次转换期间,SwipeView 都会提供转换,因此即使显式更改索引,用户也会获得方向感。

注意:以编程方式在 SwipeView 中导航时,重要的是不要在 Javascript 中通过赋值来设置 currentIndex。 这是因为这样做会破坏它覆盖的任何 QML 绑定。 而是使用 setCurrentIndex、incrementCurrentIndex 和 decrementCurrentIndex 方法。 这保留了 QML 绑定。

Page {// ...Column {anchors.centerIn: parentspacing: 10Label {anchors.horizontalCenter: parent.horizontalCentertext: qsTr("Community statistics")}Button {anchors.horizontalCenter: parent.horizontalCentertext: qsTr("Back")onClicked: swipeView.setCurrentIndex(0);}}
}

6.3.3 文档窗口

这个例子展示了如何实现一个面向桌面的、以文档为中心的用户界面。 这个想法是每个文档有一个窗口。 打开新文档时,会打开一个新窗口。 对于用户来说,每个窗口都是一个独立的世界,只有一个文档。

代码从带有标准操作的文件菜单的应用程序窗口开始:新建、打开、保存和另存为。 我们把它放在文件 DocumentWindow.qml 中。

我们为原生对话框导入 Qt.labs.platform,并对项目文件和 main.cpp 进行了后续更改,如上面关于原生对话框的部分所述。

import QtQuick 2.0
import QtQuick.Controls 2.4
import Qt.labs.platform 1.0 as NativeDialogsApplicationWindow {id: root// ...menuBar: MenuBar {Menu {title: qsTr("&File")MenuItem {text: qsTr("&New")icon.name: "document-new"onTriggered: root.newDocument()}MenuSeparator {}MenuItem {text: qsTr("&Open")icon.name: "document-open"onTriggered: openDocument()}MenuItem {text: qsTr("&Save")icon.name: "document-save"onTriggered: saveDocument()}MenuItem {text: qsTr("Save &As...")icon.name: "document-save-as"onTriggered: saveAsDocument()}}}// ...
}

为了引导程序,我们从 main.qml 创建第一个 DocumentWindow 实例,它是应用程序的入口点。

import QtQuick 2.0DocumentWindow {visible: true
}

在本章开头的示例中,每个 MenuItem 在触发时都会导致调用相应的函数。 让我们从 New 项目开始,它在 newDocument 函数中结束。

反过来,该函数依赖于 _createNewDocument 函数,该函数从 DocumentWindow.qml 文件动态创建一个新元素实例,即一个新的 DocumentWindow 实例。 之所以把这部分新功能打出来,是因为我们在打开文档的时候也用到了。

请注意,使用 createObject 创建新实例时,我们不提供父元素。 这样,我们创建了新的顶级元素。 如果我们将当前文档作为父窗口提供给下一个,则父窗口的破坏将导致子窗口的破坏。

ApplicationWindow {// ...function _createNewDocument(){var component = Qt.createComponent("DocumentWindow.qml");var window = component.createObject();return window;}function newDocument(){var window = _createNewDocument();window.show();}// ...
}

查看 Open 项目会导致调用 openDocument 函数。 该函数只是打开 openDialog 让用户选择要打开的文件。 由于我们没有文档格式、文件扩展名或类似的东西,因此对话框的大多数属性都设置为默认值。 在现实世界的应用程序中,这将是更好的配置。
在 onAccepted 处理程序中,使用 _createNewDocument 方法实例化一个新的文档窗口,但是在显示窗口之前设置一个文件名。 在这种情况下,不会发生真正的加载。

注意:我们将 Qt.labs.platform 模块作为 NativeDialogs 导入。 这是因为它提供了一个与 QtQuick.Controls 模块提供的 MenuItem 冲突的 MenuItem。

ApplicationWindow {// ...function openDocument(fileName){openDialog.open();}NativeDialogs.FileDialog {id: openDialogtitle: "Open"folder:NativeDialogs.StandardPaths.writableLocation(NativeDialogs.StandardPaths.DocumentsLocation)onAccepted: {var window = root._createNewDocument();window._fileName = openDialog.file;window.show();}}// ...
}

文件名属于描述文档的一对属性:_fileName 和 _isDirty。 _fileName 保存文档名称的文件名,当文档有未保存的更改时设置 _isDirty。 这由保存和另存为逻辑使用,如下所示。

尝试保存没有名称的文档时,将调用 saveAsDocument。 这将导致 saveAsDialog 的往返,它设置一个文件名,然后尝试在 onAccepted 处理程序中再次保存。请注意,saveAsDocument 和 saveDocument 函数对应于 Save As 和 Save 菜单项。

保存文档后,在 saveDocument 函数中检查 _tryingToClose 属性。如果保存是用户想要在窗口关闭时保存文档的结果,则设置此标志。 结果,在执行了保存操作后窗口关闭。 同样,在此示例中没有实际保存。

ApplicationWindow {// ...property bool _isDirty: true // Has the document got unsaved changes?property string _fileName // The filename of the documentproperty bool _tryingToClose: false // Is the window trying to close (but needs a file name first)?// ...function saveAsDocument(){saveAsDialog.open();}function saveDocument(){if (_fileName.length === 0){root.saveAsDocument();}else{// Save document hereconsole.log("Saving document")root._isDirty = false;if (root._tryingToClose)root.close();}}NativeDialogs.FileDialog {id: saveAsDialogtitle: "Save As"folder: NativeDialogs.StandardPaths.writableLocation(NativeDialogs.StandardPaths.DocumentsLocation)onAccepted: {root._fileName = saveAsDialog.filesaveDocument();}onRejected: {root._tryingToClose = false;}}// ...
}

这导致我们关闭窗户。 关闭窗口时,将调用 onClosing 处理程序。在这里,代码可以选择不接受关闭请求。 如果文档有未保存的更改,我们打开 closeWarningDialog 并拒绝关闭请求。

closeWarningDialog 询问用户是否应该更改更改,但用户也可以选择取消关闭操作。 在 onRejected 中处理的取消是最简单的情况,因为我们在打开对话框时拒绝关闭。

当用户不想保存更改时,即在 onNoClicked 中,_isDirty 标志设置为 false 并再次关闭窗口。 这一次, onClosing 将接受关闭,因为 _isDirty 为假。

最后,当用户想要保存更改时,我们在调用 save 之前将 _tryingToClose 标志设置为 true。这导致我们进入保存 - 另存为逻辑。

ApplicationWindow {// ...onClosing: {if (root._isDirty) {closeWarningDialog.open();close.accepted = false;}}NativeDialogs.MessageDialog {id: closeWarningDialogtitle: "Closing document"text: "You have unsaved changed. Do you want to save your changes?"buttons: NativeDialogs.MessageDialog.Yes | NativeDialogs.MessageDialog.No| NativeDialogs.MessageDialog.CancelonYesClicked: {// Attempt to save the documentroot._tryingToClose = true;root.saveDocument();}onNoClicked: {// Close the windowroot._isDirty = false;root.close()}onRejected: {// Do nothing, aborting the closing of the window}}
}

关闭和保存的整个流程 - 另存为逻辑如下所示。 系统在关闭状态下进入,关闭和未关闭状态是结果。
与使用 QtWidgets 和 C++ 实现相比,这看起来很复杂。 这是因为对话框不会阻止 QML。 这意味着我们不能在 switch 语句中等待对话的结果。 相反,我们需要记住状态并在相应的 onYesClicked、onNoClicked、onAccepted 和 onRejected 处理程序中继续操作。

拼图的最后一块是窗口标题。 它由文件名和_isDirty 组成。

ApplicationWindow {// ...title: (_fileName.length===0?qsTr("Document"):_fileName) + (_isDirty?"*":"")// ...
}

这个例子远未完成。 例如,永远不会加载或保存文档。 另一个缺失的部分是处理一次关闭所有窗口的情况,即退出应用程序。 对于这个函数,需要一个维护所有当前 DocumentWindow 实例列表的单例。 但是,这只是触发关闭窗口的另一种方式,因此此处显示的逻辑流程仍然有效。

6.4 想象风格

Qt Quick Controls 2 的目标之一是将控件的逻辑与其外观分开。对于大多数样式,外观的实现由 QML 代码和图形资源的混合组成。但是,使用 Imagine 样式,可以仅使用图形资源自定义基于 Qt Quick Controls 2 的应用程序的外观。

想象风格基于 9-patch 图像。这允许图像携带有关它们如何被拉伸以及哪些部分被视为元素的一部分以及外部的信息,例如一个影子。对于每个控件,样式支持多个元素,并且每个元素都有大量可用的状态。通过为这些元素和状态的某些组合提供资产,您可以详细控制每个控件的外观。
Imagine style 文档中详细介绍了 9-patch 图像的详细信息,以及如何设置每个控件的样式。在这里,我们将为假想的设备界面创建自定义样式,以演示如何使用该样式。

该应用程序由自定义样式的 ApplicationWindow 和 Button 控件组成。对于按钮,处理正常情况,以及按下和检查。演示应用程序如下所示。

代码使用 Column 作为可点击按钮,使用 Grid 作为可检查按钮。可点击的按钮也随着窗口的宽度而伸展。

ApplicationWindow {// ...Column {// ...Repeater {model: 5delegate: Button {width: parent.widthheight: 70text: qsTr("Click me!")}}}Grid {// ...Repeater {model: 10delegate: Button {height: 70text: qsTr("Check me!")checkable: true}}}
}

当我们使用 Imagine 样式时,我们想要使用的所有控件都需要使用图形资源进行样式设置。 最简单的是 ApplicationWindow 的背景。 这是定义背景颜色的单像素纹理。 通过命名文件 applicationwindow-background.png 然后使用 qtquickcontrols2.conf 文件将样式指向它,该文件被拾取。
在下面显示的 qtquickcontrols2.conf 文件中,您可以看到我们如何将 Style 设置为 Imagine,然后为样式设置一个 Path,它可以在其中查找资产。 最后,我们还设置了一些调色板属性。
可用的调色板属性可以在调色板 QML 基本类型页面上找到。

[Controls]
Style=Imagine
[Imagine]
Path=:images/imagine
[Imagine\Palette]
Text=#ffffff
ButtonText=#ffffff
BrightText=#ffffff

Button 控件的资源是 button-background.9.png、button-background-pressed.9.png 和 button-background-checked.9.png。 这些遵循控制元素状态模式。 无状态文件 button-background.9.png 用于所有没有特定资产的状态。 根据 Imagine 样式元素参考表,按钮可以具有以下状态:

  • disabled
  • pressed
  • checked
  • checkable
  • focused
  • highlighted
  • flat
  • mirrored
  • hovered

需要哪些取决于您的用户界面。 例如,悬停样式从不用于基于触摸的界面。
查看上面 button-background-checked.9.png 的放大版本,您可以看到两侧的 9 补丁引导线。 出于可见性原因,添加了紫色背景。 该区域在示例中使用的资产中实际上是透明的。
图像边缘的像素可以是白色/透明、黑色或红色。 这些都有不同的含义,我们将一一介绍。

资产左侧和顶部的黑线标记了图像的可拉伸部分。 这意味着当按钮被拉伸时,示例中的圆角和白色标记不受影响。
资产右侧和底部的黑线标记了用于控件内容的区域。 这意味着按钮的哪一部分用于示例中的文本。
资产标记插入区域右侧和底部的红线。 这些区域是图像的一部分,但不被视为控件的一部分。 对于上面选中的图像,这用于在按钮外部延伸的软光环。

使用插入区域的演示是 button-background.9.png(下)和 button-background-checked.9.png(上)如何使图像看起来亮起来但不移动 这个例子。

6.5 总结

在本章中,我们查看了 Qt Quick Controls 2。它们提供了一组元素,这些元素提供了比基本 QML 元素更高级的概念。 对于大多数情况,您将通过使用 Qt Quick Controls 2 来节省内存并提高性能,因为它们基于优化的 C++ 逻辑而不是 Javascript 和 QML。

在本章中,我们演示了如何使用不同的样式,以及如何使用文件选择器开发通用代码库。 通过这种方式,单个代码库可以通过用户交互和视觉样式处理多个平台。

最后,我们查看了 Imagine 样式,它允许您通过使用图形资源完全自定义 QML 应用程序的外观。 通过这种方式,可以重新设计应用程序,而无需任何代码更改。

《Qt5 Cadaques》学习笔记(六):QT QUICK Controls 2相关推荐

  1. Qt文档阅读笔记-Qt Quick Controls - Wearable Demo实例解析

    程序运行截图如下: 这个实例主要是用qml玩界面. main.qml文件以及wearable.qml分别是由ApplicationWindow和StatckView.这个StackView用于做导航相 ...

  2. 10.QML Qt Quick Controls 2之imagine style

    Qt Quick Controls 2的目标之一是将控件的逻辑与外观分离.外观的实现由QML代码和图形资源组成.使用Imagine样式(imagine style),可以自定义应用程序的外观和图形资源 ...

  3. qml自定义switch_QML官方系列教程——Qt Quick Controls Styles

    附网址:http://qt-project.org/doc/qt-5/qtquickcontrolsstyles-index.html Qt Quick Controls Styles -- Qt Q ...

  4. Qt5官方demo解析集11——Qt Quick Particles Examples - Affectors

    本系列所有文章可以在这里查看http://blog.csdn.net/cloud_castle/article/category/2123873 接上文Qt5官方demo解析集10--Qt Quick ...

  5. Hello Qt(五十一)———Qt Quick Controls

    一.Qt Quick Controls基础 QT5.1发布了Qt Quick的一个全新模块:Qt Quick Controls.Qt Quick Controls模块提供了大量类似Qt Widgets ...

  6. Qt学习笔记,Qt国际化

    Qt学习笔记,Qt国际化 Qt国际化步骤: 第一步:设置.pro文件,加入TRANSLATIONS为国际化做准备 TRANSLATIONS = language/language_en.ts\     ...

  7. Qt学习笔记,Qt程序架构设计要旨

    Qt学习笔记,Qt程序架构设计要旨 时间过得很快,转眼学习Qt已经有一个多月了,对Qt的学习也在不断的深入中.自己手下的code也很多了,不过不得不说,还有很多的部分没有接触过,比如网络编程,2D,3 ...

  8. 7.QML Qt Quick——基于Qt Quick Controls 2实现图片浏览器

    Qt Quick Controls 2提供了一组UI控件,例如按钮,标签,复选框,滑块等(用之查之即可).用于在Qt Quick中创建用户界面.UI控件很多,这里通过一个图片浏览器的实现来逐步讲解 图 ...

  9. Ethernet/IP 学习笔记六

    Ethernet/IP 学习笔记六 EtherNet/IP defines two primary types of communications: explicit and implicit (Ta ...

最新文章

  1. 万维网源代码以 NFT 形式拍卖,价值或超 4.5 亿?
  2. sql语句的简单记录
  3. mysql cpu 内存占用_MySQL占用内存与CPU过高测试与解决办法
  4. 3.数据库操作相关术语,Oracle认证,insert into,批量插入,update tablename set,delete和truncate的区别,sql文件导入
  5. 简单理解计算机内存乱序
  6. 如何查看系统启动时间-
  7. mysql show sleep_mysq解决sleep进程过多的办法
  8. Python2.7-异常和工具
  9. 深入理解并行编程pdf
  10. Perl面向对象编程
  11. 业务系统--业务架构转化成为IT应用的中间桥梁
  12. Unity拓展——菜单栏拓展
  13. PHP 微信支付 企业支付个人 企业转账个人 企业转零钱
  14. 如何快速准确的验证QQ邮箱是否开通,是否存在?
  15. 广东理工学院计算机组成原理,20年广东理工学院成人高考期末考试 计算机组成原理 复习资料(7页)-原创力文档...
  16. Java 二十三种设计模式
  17. 搞定这套 Python 爬虫面试题,面试会 so easy
  18. 汉字转16进制 php,php中文汉字与16进制编码转换三种方法
  19. Jetson Xavier NX使用yolov5+deepsort实现CSI摄像头的目标跟踪
  20. 幼师计算机能力自我评价,幼师简历范文

热门文章

  1. 实时数据库简介和比较
  2. ORAN C平面协议的元素
  3. ORAN专题系列-29:运营商O-RAN扩展皮站测试的硬件架构
  4. 氨氮吹脱法脱与php有关系吗,吹脱法处理氨氮废水
  5. Java 上机----实训操作14----学校录取类
  6. SSL证书中的128位加密和256位加密有何区别?
  7. python 根据空格切割英文单词
  8. Error launching IEDA-启动IDEA报错解决(创建JVM失败提示)
  9. Learning Transferable Visual Models From Natural Language Supervision
  10. WPF遍历视觉树与逻辑树