第七章:QtQuick控件


第七章:QtQuick控件

Qt,控件

freedesktop.org Icon Naming Specification (opens new window)。在本文档中,按名字列出了标准图标的列表。通过引用图标名,Qt将为当前桌面样式选择恰当的图标。
ToolButtononClicked信号处理函数中编写一段代码,它调用了fileOpenDialog元素的open函数。

  1. ApplicationWindow { 
  2.  
  3. // ... 
  4.  
  5. header: ToolBar { 
  6. Flow { 
  7. anchors.fill: parent 
  8. ToolButton { 
  9. text: qsTr("Open") 
  10. icon.name: "document-open" 
  11. onClicked: fileOpenDialog.open() 
  12. } 
  13. } 
  14. } 
  15.  
  16. // ... 
  17.  
  18. } 

fileOpenDialog 元素是来自 Qt.labs.platform 模块的 FileDialog 控件。文件对话框可用于打开或保存文件。
首先在代码指定一个标题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元素。
以下代码创建了两个菜单:FileHelp。在File下放一个Open菜单项,将图标和动作设置得与工具栏上的 打开 按钮一致。在Help下会看到一个About菜单,它会调用 aboutDialogOpen 方法。
请注意,Menutitle 属性和 MenuItemtext 属性中的逻辑与符号 (“&”) 将其后字符转换为键盘快捷键;例如按 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 控件,而它(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
    }

    // ...

}

以上便完成一个用于查看图像的简单可用的桌面应用程序。

材质设计图标页

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

    // ...

}

通过这些代码更改,我们将桌面版的图片查看器转化成了适合移动设备的版本。

类列表。本例中,我们将桌面版文件设为默认,当遇到Android选择器时,再替换成别的。开发时,可以把环境变量QT_FILE_SELECTORS设置为android来模拟适配。

文件选择器
通过selector,文件选择器可以将文件替换为备选文件。
通过在你想替换的文件的同一目录下创建一个名为+selector的目录(其中selector代表一个选择器的名称),然后你可以在该目录内放置与你想替换的文件同名的文件。当选择器出现时,该目录中的文件将被选中替换掉原始文件。
选择器是基于平台的:如安卓、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 建立用户界面。然后我们将平台特定的部分添加进来,例如菜单栏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。我们可以继续在main.cpp中使用条件表达式#ifdef来为不同的平台设置不同的样式。但我们要使用文件选择器通过选择配置文件来设置样式。下面你可以看到Material样式文件,而Fusion样式文件也同样简单。

[Controls]
Style=Material

通过这些变化,我们把所有可共享的代码整合起来,仅把用户交互有差异的代码单独处理。有多种实现方式,比如,将文档保存在包含特定平台接口的特定组件中,或者象本例那样,从不同平台中抽象出共同的代码。当你知道特定平台的样式且能够从特性中分离出共性时,就会做出最佳的路径来决定如何处理代码。

9-patch 图象。这允许图像携带有关它们如何被拉伸以及哪些部分被视为元素的一部分以及外部哪些部分的信息;比如,影子。对于每个控件,样式里支持几个元素,每个元素中都有大量的状态可用。通过向这些元素和状态提供一定的素材,你可以控制控件的样式细节。
Imagine 样式文档中详细介绍了 9-path 图像的详细信息,以及如何设置每个控件的样式。这里,我们将为一个假想的设备界面自定义一个样式,来展示风格如何使用。
应用程序的风格决定了ApplicationWindowButton控件。对这些按钮来说,其正常状态,以及按下选中状态都已经被处理了。演示程序效果如下:

代码为可点击按钮创建了一个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.pngbutton-background-checked.9.png。遵循control-element-state(控件-元素-状态)的规范模式。无状态的文件,象button-background.9.png用于没有素材附件的所有状态。根据想象风格元素引用表,按钮可以有如下状态:

  • disabled
  • pressed
  • checked
  • checkable
  • focused
  • highlighted
  • flat
  • mirrored
  • hovered
    是否需要这些状态有赖于你的用户界面。比如,悬空(hovered)样式在触摸交互型的用户界面里,永远不会用到。

    看下上面放大版的button-background-checked.9.png,可以看到两侧的指导线。出于视觉效果,添加了紫色背景。而本例所用到的素材附件实际上是透明的。
    图片边上的象素也可以是白的/透明的、黑的或红的,有着不同的意义,以下逐一说明:
  • 黑色 线在左边和上边标记图象的可拉伸部分。这意味着当按钮被拉伸时,示例中的圆角和白色标记不受影响。
  • 黑色 线在右边和下边,标记了控件的内容区域。这意味着在示例中用于文本的按钮部分。
  • 红色 线在右边和下边,标记了嵌入区域。这些区域是图像的一部分,但不被认为是控件的一部分。对于上面的可选图像,这是用在延伸到按钮外面的柔和光晕。

内嵌(inset)区域的使用演示如下button-background.9.png,而上面的button-background-checked.9.png:看起来象点亮,但没移动。

小结

本章介绍了Qt Quick Controls 2,涉及到比基础QML元素更高级的概念的一系列元素。多数场景下,会用到Qt Quick Controls 2以节省内存消耗,提高性能,因为它们是基于优化的 C++ 逻辑实现的,而非Javascript和QML。
我们已经演示了不同风格如何应用,以及一段可共用的代码如何通过文件选择器使用。这种方式,一段代码可以在不同的平台,以不同的交互方式和界面风格部署。
最后,我们一起学习了想象风格,它允许你使用图形素材来定制化一个基于QML的应用程序外观。这种方法,可以让一个应用在不改动任何代码的条件下更换皮肤。