Qt6 QML Book(中文版本)之七 您所在的位置:网站首页 吃的津津有味的拼音是什么 Qt6 QML Book(中文版本)之七

Qt6 QML Book(中文版本)之七

2023-04-25 22:54| 来源: 网络整理| 查看: 265

《Qt6 QML Book》中文版

QtQuick 控件(QtQuick Controls)用户界面控件(UI Controls)

本章介绍如何使用 Qt Quick Controls 模块。 Qt Quick Controls 用于创建由标准组件(如按钮、标签、滑块等)构建的高级用户界面。

Qt Quick Controls 可以使用布局模块进行排列,并且易于设置样式。在深入定制样式之前,我们还将研究不同平台的各种样式。

控件简介

从头开始使用 Qt Quick 可以为您提供原始的图形和交互元素,您可以从中构建用户界面。使用 Qt Quick Controls,您可以从一组稍微结构化的控件开始构建。

控件范围从简单的文本标签和按钮到更复杂的控件,例如滑块和仪表盘。如果您想创建基于经典交互模式的用户界面,这些元素非常方便,因为它们提供了良好的基础。Qt Quick Controls 带有许多开箱即用的样式,如下表所示。 Basic 样式是基本的扁平样式。Universal 风格基于 Microsoft Universal Design Guidelines,Material 基于 Google 的 Material Design Guidelines,Fusion 风格是面向桌面的风格。

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

Basic

Fusion

macOS

Material

Imagine

Windows

Universal

Qt Quick Controls 2 可从 QtQuick.Controls 导入中获得。以下模块也很有趣:

QtQuick.Controls - 基本控件。QtQuick.Templates - 为控件提供行为的、非可视的基本类型。QtQuick.Controls.Imagine - 丰富风格主题支持。QtQuick.Controls.Material - 材质样式主题支持。QtQuick.Controls.Universal - 通用风格主题支持。Qt.labs.platform - 支持用于常见任务的平台原生对话框,例如选择文件、颜色等,以及系统托盘图标和标准路径。

Qt实验室

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

图像查看器

让我们看一个如何使用 Qt Quick Controls 的更大示例。为此,我们将创建一个简单的图像查看器。

首先,我们使用 Fusion 风格为桌面创建它,然后我们将重构它以获得移动体验,然后再查看最终代码库。

桌面版

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

我们使用用于空 Qt Quick 应用程序的 Qt Creator 项目模板作为起点。但是,我们将模板中的默认 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 元素作为内容来构建用户界面。当用户打开它们时,这个元素将用于显示图像,所以现在它只是一个占位符。背景属性用于向窗口提供一个元素以放置在内容后面。将在没有加载图像时显示,并且如果纵横比不允许它填充窗口的内容区域,则会显示为图像周围的边框。

ApplicationWindow {

// ...

background: Rectangle {

color: "darkGray"

}

Image {

id: image

anchors.fill: parent

fillMode: Image.PreserveAspectFit

asynchronous: true

}

// ...

}

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

ToolButton 有几个有趣的属性。文字是直截了当的。但是,icon.name 取自 http://freedesktop.org 图标命名规范。在该文档中,标准图标列表按名称列出。通过引用这样的名称,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 控件。文件对话框可用于打开或保存文件。

在代码中,我们首先指定一个标题。然后我们使用 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

}

}

// ...

}

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

在下面的代码中,我们创建了两个菜单,文件和帮助。在文件下,我们使用与工具栏中的工具按钮相同的图标和操作放置打开。在帮助下,您会找到关于触发对 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: 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的触发方法。这样,我们可以使用单个委托来触发不同的操作。

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 的标题中。我们添加了一个用于打开抽屉的按钮和一个用于应用程序标题的标签,而不是桌面样式的工具栏。

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

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

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 表示选择器的名称),然后可以将与要替换的文件同名的文件放置在目录中。当选择器弹出时,在目录中的文件将替代原始文件而被选取。

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

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

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

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 建立用户界面。然后我们将平台特定的部分添加到它,例如菜单栏和工具栏。对这些的唯一更改是打开相应对话框的调用是针对新功能而不是直接针对对话框控件进行的。

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 引擎自动创建的文件选择器可以选择正确的文件。

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

[Controls]

Style=Material

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

原生对话框

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

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

为了将原生文件对话框集成到图像查看器中,我们需要导入 Qt.labs.platform 模块。由于该模块与其替换的 QtQuick.Dialogs 模块有名称冲突,因此删除旧的导入语句很重要。在实际的文件对话框元素中,我们必须更改文件夹属性的设置方式,并确保 onAccepted 处理程序使用文件属性而不是 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 更改之外,我们还需要更改图像查看器的项目文件以包含小部件模块。

QT += quick quickcontrols2 widgets

我们需要更新 main.qml 来实例化一个 QApplication 对象而不是一个 QGuiApplication 对象。这是因为 QGuiApplication 类包含图形应用程序所需的最小环境,而 QApplication 使用支持 QtWidgets 所需的功能扩展了 QGuiApplication。

include

// ...

int main(int argc, char *argv[])

{

QApplication app(argc, argv);

// ...

}

通过这些更改,图像查看器现在将在大多数平台上使用本机对话框。支持的平台是 iOS、Linux(带有 GTK+ 平台主题)、macOS、Windows 和 WinRT。对于 Android,它将使用 QtWidgets 模块提供的默认 Qt 对话框。

常见模式(Common Patterns)

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

嵌套屏幕(Nested Screens)

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

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

应用程序的初始主屏幕如下图所示。

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

import QtQuick

import QtQuick.Controls

ApplicationWindow {

// ...

header: ToolBar {

// ...

}

Drawer {

// ...

}

StackView {

id: stackView

anchors.fill: parent

initialItem: Home {}

}

}

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

import QtQuick

import QtQuick.Controls

Page {

title: qsTr("Home")

Label {

anchors.centerIn: parent

text: qsTr("Home Screen")

}

}

回到 main.qml,我们现在看看抽屉部分。这是页面导航开始的地方。用户界面的活动部分是 ìtemDelegate 项。在 onClicked 处理程序中,下一页被推送到 stackView 上。

如以下代码所示,可以推送组件或对特定 QML 文件的引用。无论哪种方式都会导致创建一个新实例并将其推入堆栈。

ApplicationWindow {

// ...

Drawer {

id: drawer

width: window.width * 0.66

height: window.height

Column {

anchors.fill: parent

ItemDelegate {

text: qsTr("Profile")

width: parent.width

onClicked: {

stackView.push("Profile.qml")

drawer.close()

}

}

ItemDelegate {

text: qsTr("About")

width: parent.width

onClicked: {

stackView.push(aboutPage)

drawer.close()

}

}

}

}

// ...

Component {

id: aboutPage

About {}

}

// ...

}

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

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

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

ApplicationWindow {

// ...

header: ToolBar {

contentHeight: toolButton.implicitHeight

ToolButton {

id: toolButton

text: stackView.depth > 1 ? "\u25C0" : "\u2630"

font.pixelSize: Qt.application.font.pixelSize * 1.6

onClicked: {

if (stackView.depth > 1) {

stackView.pop()

} else {

drawer.open()

}

}

}

Label {

text: stackView.currentItem.title

anchors.centerIn: parent

}

}

// ...

}

Now we’ve seen how to reach the About and Profile pages, but we also want to make it possible to reach the Edit Profile page from the Profile page.这是通过个人资料页面上的按钮完成的。单击按钮时,EditProfile.qml 文件被推送到 StackView。

import QtQuick

import QtQuick.Controls

Page {

title: qsTr("Profile")

Column {

anchors.centerIn: parent

spacing: 10

Label {

anchors.horizontalCenter: parent.horizontalCenter

text: qsTr("Profile")

}

Button {

anchors.horizontalCenter: parent.horizontalCenter

text: qsTr("Edit");

onClicked: stackView.push("EditProfile.qml")

}

}

}

import QtQuick

import QtQuick.Controls

Page {

title: qsTr("Profile")

Column {

anchors.centerIn: parent

spacing: 10

Label {

anchors.horizontalCenter: parent.horizontalCenter

text: qsTr("Profile")

}

Button {

anchors.horizontalCenter: parent.horizontalCenter

text: qsTr("Edit");

onClicked: stackView.push("EditProfile.qml")

}

}

}

并排屏幕

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

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

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

import QtQuick

import QtQuick.Controls

ApplicationWindow {

visible: true

width: 640

height: 480

title: qsTr("Side-by-side")

SwipeView {

// ...

}

// ...

}

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

ApplicationWindow {

// ...

SwipeView {

id: swipeView

anchors.fill: parent

Current {

}

UserStats {

}

TotalStats {

}

}

// ...

}

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

ApplicationWindow {

// ...

SwipeView {

id: swipeView

// ...

}

PageIndicator {

anchors.bottom: parent.bottom

anchors.horizontalCenter: parent.horizontalCenter

currentIndex: swipeView.currentIndex

count: swipeView.count

}

}

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

import QtQuick

import QtQuick.Controls

Page {

header: Label {

text: qsTr("Community Stats")

font.pixelSize: Qt.application.font.pixelSize * 2

padding: 10

}

// ...

}

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

提示(TIP)

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

Page {

// ...

Column {

anchors.centerIn: parent

spacing: 10

Label {

anchors.horizontalCenter: parent.horizontalCenter

text: qsTr("Community statistics")

}

Button {

anchors.horizontalCenter: parent.horizontalCenter

text: qsTr("Back")

onClicked: swipeView.setCurrentIndex(0);

}

}

}

文档窗口

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

两个文档窗口和关闭警告对话框。

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

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

import QtQuick

import QtQuick.Controls

import Qt.labs.platform as NativeDialogs

ApplicationWindow {

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

DocumentWindow {

visible: true

}

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

该函数又依赖于 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 方法实例化一个新的文档窗口,并在窗口显示之前设置一个文件名。在这种情况下,不会发生真正的加载。

提示(TIP)

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

ApplicationWindow {

// ...

function openDocument(fileName)

{

openDialog.open();

}

NativeDialogs.FileDialog {

id: openDialog

title: "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 函数中,tryToClose 属性被选中。如果保存是用户想要在窗口关闭时保存文档的结果,则设置此标志。结果,在执行了保存操作后窗口关闭。同样,在此示例中没有实际保存。

ApplicationWindow {

// ...

property bool isDirty: true // Has the document got unsaved changes?

property string fileName // The filename of the document

property 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 here

console.log("Saving document")

root.isDirty = false;

if (root.tryingToClose)

root.close();

}

}

NativeDialogs.FileDialog {

id: saveAsDialog

title: "Save As"

folder: NativeDialogs.StandardPaths.writableLocation(NativeDialogs.StandardPaths.DocumentsLocation)

onAccepted: {

root.fileName = saveAsDialog.file

saveDocument();

}

onRejected: {

root.tryingToClose = false;

}

}

// ...

}

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

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

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

最后,当用户想要保存更改时,我们在调用 save 之前将 tryToClose 标志设置为 true。这将我们引向保存/另存为逻辑。

ApplicationWindow {

// ...

onClosing: {

if (root.isDirty) {

closeWarningDialog.open();

close.accepted = false;

}

}

NativeDialogs.MessageDialog {

id: closeWarningDialog

title: "Closing document"

text: "You have unsaved changed. Do you want to save your changes?"

buttons: NativeDialogs.MessageDialog.Yes | NativeDialogs.MessageDialog.No | NativeDialogs.MessageDialog.Cancel

onYesClicked: {

// Attempt to save the document

root.tryingToClose = true;

root.saveDocument();

}

onNoClicked: {

// Close the window

root.isDirty = false;

root.close()

}

onRejected: {

// Do nothing, aborting the closing of the window

}

}

}

关闭和保存/另存为逻辑的整个流程如下所示。系统在关闭状态下进入,而关闭和未关闭状态是结果。

与使用 Qt Widgets 和 C++ 实现相比,这看起来很复杂。这是因为对话框不会阻止 QML。这意味着我们不能在 switch 语句中等待对话的结果。相反,我们需要记住状态并在相应的 onYesClicked、onNoClicked、onAccepted 和 onRejected 处理程序中继续操作。

拼图的最后一块是窗口标题。它由 fileName 和 isDirty 属性组成。

ApplicationWindow {

// ...

title: (fileName.length===0?qsTr("Document"):fileName) + (isDirty?"*":"")

// ...

}

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

想象风格

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

想象风格基于 9-patch 图像。这允许图像携带有关它们如何被拉伸以及哪些部分被视为元素的一部分和哪些部分不是元素部分的信息;例如一个阴影。对于每个控件,样式支持多个元素,并且每个元素都有大量可用的状态。通过为这些元素和状态的某些组合提供资源,您可以详细控制每个控件的外观。

Imagine 样式文档 (opens new window) 中详细介绍了 9-patch 图像的详细信息,以及如何设置每个控件的样式。在这里,我们将为一个假想的设备界面创建一个自定义样式,以演示如何使用该样式。

应用程序的样式自定义了 ApplicationWindow 和 Button 控件。对于按钮,处理正常状态以及按下和选中状态。演示应用程序如下所示。

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

import QtQuick

import QtQuick.Controls

ApplicationWindow {

// ...

visible: true

width: 640

height: 480

title: qsTr("Hello World")

Column {

anchors.top: parent.top

anchors.left: parent.left

anchors.margins: 10

width: parent.width/2

spacing: 10

// ...

Repeater {

model: 5

delegate: Button {

width: parent.width

height: 70

text: qsTr("Click me!")

}

}

}

Grid {

anchors.top: parent.top

anchors.right: parent.right

anchors.margins: 10

columns: 2

spacing: 10

// ...

Repeater {

model: 10

delegate: Button {

height: 70

text: 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 样式元素参考表 (opens new window),按钮可以具有以下状态:

disabledpressedcheckedcheckablefocusedhighlightedflatmirroredhovered

所需的状态取决于您的用户界面。例如,悬停样式从不用于基于触摸的界面。

资源左侧和顶部的黑线标记了图像的可拉伸部分。这意味着当按钮被拉伸时,示例中的圆角和白色标记不受影响。

资源右侧和底部的黑线标记了用于控件内容的区域。这意味着在示例中用于文本的按钮部分。

资源标记插入区域右侧和底部的红线。这些区域是图像的一部分,但不被视为控件的一部分。对于上面选中的图像,这用于在按钮外部延伸的软光环。

button-background.9.png(下)和 button-background-checked.9.png(上)展示了插入区域的用法:图像似乎亮了起来,但没有移动。

总结(Summary)

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

我们已经演示了如何使用不同的样式,以及如何使用文件选择器开发通用代码库。这样,单个代码库可以处理具有不同用户交互和视觉风格的多个平台。

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

软件自由职业者:承接Qt/C++软件开发和定制(上海)



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有