Skip to content

Latest commit

 

History

History
890 lines (672 loc) · 40.8 KB

File metadata and controls

890 lines (672 loc) · 40.8 KB

三、用户界面

在本章中,我们将更详细地了解 QML,并勾画出我们的用户界面布局。我们将为所有屏幕创建占位符视图,并实现一个在它们之间导航的框架。我们还将讨论这些视图中的内容,特别是如何以灵活和响应的方式锚定和调整元素的大小。我们将讨论这些主题:

  • 用户界面设计
  • 创建视图
  • StackView 组件
  • 锚定元件
  • 尺寸元素
  • 在视图之间导航

UX

如果您曾经使用过其他声明式用户界面技术,如 HTML 和 XAML,它们通常采用父/子方法来处理用户界面,也就是说,有一个父视图或根视图一直存在,并包含全局功能,如顶级导航。然后,它有动态内容或子视图,可以根据需要切换,并在必要时提供上下文相关的命令。

我们将采取同样的方法,我们的主视图是我们的用户界面的根。我们将添加一个全局导航栏和一个内容窗格,我们可以根据需要在其中添加和删除内容。子视图将可选地呈现命令栏,用于执行操作,例如,将记录保存到数据库。

让我们看看我们的目标是什么:

导航栏( 1 )将一直存在,并包含将用户导航到应用内关键区域的按钮。默认情况下,栏会很窄,与按钮相关的命令会用图标表示;但是,按下切换按钮将扩展栏,为每个按钮显示附带的描述性文本。

内容窗格( 2 )将是一堆子视图。导航到应用的不同区域将通过替换内容窗格中的子视图来实现。例如,如果我们在导航栏上添加一个新客户端按钮并按下它,我们将把新客户端视图推到内容框架堆栈上。

命令栏( 3 )是一个可选元素,将用于向用户呈现更多命令按钮。导航栏的主要区别在于,这些命令与当前视图相关,对上下文敏感。例如,在创建新客户端时,我们需要一个“保存”按钮,但是在搜索客户端时,“保存”按钮没有任何意义。每个子视图将可选地呈现其自己的命令栏。这些命令将由图标表示,下面有一个简短的描述。

现在让我们计划一下屏幕流,或者我们称之为视图的流:

创建视图

cm-ui 中,右键点击views.qrc,选择【新增】。选择 Qt > QML 文件,点击选择...:

cm-ui/ui/views中创建SplashView.qml文件。重复此过程,直到创建完以下所有视图:

| 文件 | 目的 | | SplashView.qml | 加载用户界面时显示的占位符视图。 | | DashboardView.qml | 中央“家”观。 | | CreateClientView.qml | 用于输入新客户端详细信息的视图。 | | EditClientView.qml | 用于读取/更新现有客户端详细信息的视图。 | | FindClientView.qml | 用于搜索现有客户端的视图。 |

如前所述,在纯文本编辑器中编辑views.qrc。您将看到我们的新视图已添加到新的qresource块中,默认前缀如下:

<RCC>
    <qresource prefix="/views">
        <file alias="MasterView">views/MasterView.qml</file>
    </qresource>
    <qresource prefix="/">
        <file>views/SplashView.qml</file>
        <file>views/DashboardView.qml</file>
        <file>views/CreateClientView.qml</file>
        <file>views/EditClientView.qml</file>
        <file>views/FindClientView.qml</file>
    </qresource>
</RCC>

还要注意项目导航器有点乱:

将所有新文件移入“/views”前缀块,并移除“/”块。为每个新文件添加别名:

<RCC>
    <qresource prefix="/views">
        <file alias="MasterView.qml">views/MasterView.qml</file>
        <file alias="SplashView.qml">views/SplashView.qml</file>
        <file alias="DashboardView.qml">views/DashboardView.qml</file>
        <file alias="CreateClientView.qml">views/CreateClientView.qml</file>
        <file alias="EditClientView.qml">views/EditClientView.qml</file>
        <file alias="CreateAppointmentView.qml">views/CreateAppointmentView.qml</file>
        <file alias="FindClientView.qml">views/FindClientView.qml</file>
    </qresource>
</RCC>

一旦保存这些更改,您应该会看到导航器被清理:

StackView 先生

我们的子视图将通过 StackView 组件呈现,该组件提供了一个基于堆栈的内置历史导航模型。新视图(在本文中,视图意味着几乎所有的 QML)在显示时被推送到堆栈上,并且可以从堆栈中弹出,以便返回到上一个视图。我们不需要使用历史功能,但是它们是非常有用的特性。

要访问组件,我们首先需要引用模块,因此将导入添加到主视图:

import QtQuick.Controls 2.2

完成后,让我们用一个StackView替换包含欢迎信息的文本元素:

StackView {
    id: contentFrame
    initialItem: "qrc:/views/SplashView.qml"
}

我们给组件分配一个唯一的标识符contentFrame,这样我们就可以在 QML 的其他地方引用它,并且我们指定默认情况下我们想要加载哪个子视图——新的SplashView

接下来,编辑SplashView。将QtQuick模块版本更新为 2.9,使其与主视图相匹配(如果没有明确说明,请对所有其他 QML 文件执行此操作)。这并不是严格必要的,但是避免视图之间的不一致是一个很好的做法。在 Qt 的小版本中,通常没有太多的方法来破坏变化,但是引用不同版本 QtQuick 的两个视图上的相同代码可能会表现出不同的行为,从而导致问题。

目前,我们要做的就是用这个视图制作一个 400 像素宽、200 像素高的矩形,它有一个“充满活力”的背景色,这样我们就可以看到它已经加载了:

import QtQuick 2.9

Rectangle {
    width: 400
    height: 200
    color: "#f4c842"
}

颜色可以使用十六进制 RGB 值来指定,就像我们在这里所做的那样,或者命名为 SVG 颜色。我通常觉得十六进制更容易,因为我永远记不住颜色的名字!

If you hover your cursor over the hex string in Qt Creator, you get a really useful little pop-up color swatch.

现在运行应用,您应该会看到欢迎消息不再显示,取而代之的是一个光荣的橙黄色矩形,这是我们的 SplashView

我们的精彩新 SplashView 的一个小问题是,它实际上没有填满窗口。当然,我们可以将 400 x 200 的尺寸更改为 1024 x 768,以便与主视图相匹配,但是如果用户调整窗口大小会发生什么?现代用户界面都是响应性设计——动态内容可以适应它所呈现的显示,因此只适合一个平台的硬编码属性并不理想。幸运的是,主播们来救我们了。

让我们把我们值得信赖的旧的草稿栏项目投入使用,看看锚在行动。

右键单击qml.qrc,在scratchpad文件夹中的现有main.qml文件旁边添加一个新的AnchorsDemo.qml QML 文件。不要担心子文件夹或.qrc前缀、别名或任何类似的东西。

浏览main.cpp并加载我们的新文件,而不是main.qml:

engine.load(QUrl(QStringLiteral("qrc:/AnchorsDemo.qml")));

接下来,将以下代码粘贴到AnchorsDemo中:

import QtQuick 2.9
import QtQuick.Window 2.2

Window {
    visible: true
    width: 1024
    height: 768
    title: qsTr("Scratchpad")
    color: "#ffffff"
    Rectangle {
        id: paleYellowBackground
        anchors.fill: parent
        color: "#cece9e"
    }
    Rectangle {
        id: blackRectangleInTheCentre
        width: 120
        height: 120
        anchors.centerIn: parent
        color: "#000000"
    }
    Rectangle {
        id: greenRectangleInTheCentre
        width: 100
        height: 100
        anchors.centerIn: parent
        anchors.verticalCenterOffset: 20
        color: "#008000"
    }
    Rectangle {
        id: redRectangleTopLeftCorner
        width: 100
        height: 100
        anchors {
            top: parent.top
            left: parent.left
        }
        color: "#800000"
    }
    Rectangle {
        id: blueRectangleTopLeftCorner
        width: 100
        height: 100
        anchors{
            top: redRectangleTopLeftCorner.bottom
            left: parent.left
        }
        color: "#000080"
    }
    Rectangle {
        id: purpleRectangleTopLeftCorner
        width: 100
        height: 100
        anchors{
            top: blueRectangleTopLeftCorner.bottom
            left: parent.left
            leftMargin: 20
        }
        color: "#800080"
    }
    Rectangle {
        id: turquoiseRectangleBottomRightCorner
        width: 100
        height: 100
        anchors{
            bottom: parent.bottom
            right: parent.right
            margins: 20
        }
        color: "#008080"
    }
}

构建并运行该应用,您将看到这个令人困惑的场景:

起初,这一切可能看起来有点混乱,如果你的颜色感知不是最佳的,我很抱歉,但我们所做的只是绘制了一系列不同锚点值的花哨的彩色矩形。让我们逐一浏览每个矩形,看看发生了什么:

Rectangle {
    id: paleYellowBackground
    anchors.fill: parent
    color: "#cece9e"
}

我们的第一个矩形是暗黄棕色背景;anchors.fill: parent告诉矩形填充它的父矩形,不管它有多大。任何给定 QML 组件的父组件都是包含它的 QML 组件,这是层次结构中的下一级。在这种情况下,是窗口元素。窗口元素是 1024 x 768 像素,这就是矩形的大小。请注意,我们不需要为矩形指定宽度和高度属性,因为它们是从锚点推断出来的。

这正是我们想要的 SplashView 的行为,但是在回到我们的主项目之前,让我们看看主播的一些其他能力:

Rectangle {
    id: blackRectangleInTheCentre
    width: 120
    height: 120
    anchors.centerIn: parent
    color: "#000000"
}
Rectangle {
    id: greenRectangleInTheCentre
    width: 100
    height: 100
    anchors.centerIn: parent
    anchors.verticalCenterOffset: 20
    color: "#008000"
}

我们将一起看下两个矩形。首先,我们有一个 120 像素见方的黑色矩形;anchors.centerIn: parent将其定位在其父对象的中心。我们必须指定宽度高度,因为我们只是定位它,而不是确定它的尺寸。

接下来,我们有一个稍微小一点的绿色矩形,也以它的父矩形为中心。然后,我们使用anchors.verticalCenterOffset属性将它向屏幕下方移动 20 个像素。用于定位的 xy 坐标系的根(0,0)在屏幕左上角;verticalCenterOffset添加到 y 坐标。正数将项目向下移动到屏幕上,负数将项目向上移动到屏幕上。其姊妹属性—horizontalCenterOffset—用于在 x 轴上进行调整。

这里要注意的最后一点是,矩形是重叠的,绿色矩形胜出并显示为完整。黑色矩形被向后推并被遮挡。同样,我们所有的小矩形都位于大背景矩形的前面。QML 是以自上而下的方式渲染的,所以当根元素( Window )被绘制时,它的子元素会从文件的顶部到底部被逐个处理。因此,文件底部的项目将呈现在文件顶部呈现的项目之前。同样的道理,如果你把一面墙涂成白色,然后再涂成黑色,墙就会变成黑色,因为那是最后涂(渲染)的:

Rectangle {
    id: redRectangleTopLeftCorner
    width: 100
    height: 100
    anchors {
        top: parent.top
        left: parent.left
    }
    color: "#800000"
}

接下来,我们画一个红色的矩形,而不是一次定位或调整整个矩形的大小,我们只是锚定某些边。我们取其顶部侧的锚,并将其对准其母公司(窗口)的顶部侧的锚。我们将它的 l eft 侧固定在它父母的左侧侧。因此,它被“附加”到左上角。

我们必须键入以下内容:

anchors.top: parent.top
anchors.left: parent.left

这里另一个有用的语法糖是,与其这样做,我们可以删除重复,并在花括号内设置anchors组的子属性:

anchors {
    top: parent.top
    left: parent.left
}

接下来,蓝色矩形:

Rectangle {
    id: blueRectangleTopLeftCorner
    width: 100
    height: 100
    anchors{
        top: redRectangleTopLeftCorner.bottom
        left: parent.left
    }
    color: "#000080"
}

这遵循相同的模式,尽管这一次不是只附加到它的父节点,我们还锚定到一个兄弟节点(红色矩形),我们可以通过id属性引用它:

Rectangle {
    id: purpleRectangleTopLeftCorner
    width: 100
    height: 100
    anchors{
        top: blueRectangleTopLeftCorner.bottom
        left: parent.left
        leftMargin: 20
    }
    color: "#800080"
}

紫色矩形锚定在蓝色矩形的底部和窗口的左侧,但这里我们介绍我们的第一个边距。每一面都有自己的边界,在这种情况下,我们使用leftMargin给我们一个从左锚的偏移量,与我们之前使用verticalCenterOffset看到的方式完全相同:

Rectangle {
    id: turquoiseRectangleBottomRightCorner
    width: 100
    height: 100
    anchors{
        bottom: parent.bottom
        right: parent.right
        margins: 20
    }
    color: "#008080"
}

最后,我们的绿松石矩形使用了屏幕右侧的一些空白空间,并演示了如何使用margins属性同时设置所有四个边的边距。

请注意,所有这些绑定都是动态的。尝试调整窗口大小,所有的矩形会自动适应。锚点是响应用户界面设计的一个很好的工具。

让我们回到我们的cm-ui项目中的SplashView,应用我们刚刚学到的知识。将固定的宽度高度属性替换为更动态的anchors.fill属性:

Rectangle {
    anchors.fill: parent
    color: "#f4c842"
}

现在,SplashView将填充它的父元素。构建并运行,您会看到,它并没有像我们预期的那样充满屏幕,而是完全消失了。让我们看看为什么会这样。

胶料

我们的矩形将填充其父矩形,因此矩形的大小完全取决于其父矩形的大小。在 QML 层次结构中,包含矩形的组件是回到主视图中的StackView元素:

StackView {
    id: contentFrame
    initialItem: Qt.resolvedUrl("qrc:/views/SplashView.qml")
}

通常,QML 组件足够聪明,可以根据他们的孩子来确定自己的尺寸。之前,我们已经将矩形设置为 400 x 200 的固定大小。StackView可以看着它说“我需要包含一个 400 x 200 的单个矩形,所以我也要做 400 x 200。轻松!”。我们总是可以否决它,并使用它的宽度高度属性将其设置为其他尺寸,但是它可以计算出它想要的尺寸。

回到scratchpad中,创建一个新的SizingDemo.qml视图并编辑main.cpp以在启动时加载它,就像我们对AnchorsDemo所做的那样。编辑SizingDemo如下:

import QtQuick 2.9
import QtQuick.Window 2.2

Window {
    visible: true
    width: 1024
    height: 768
    title: qsTr("Scratchpad")
    color: "#ffffff"
    Column {
        id: columnWithText
        Text {
            id: text1
            text: "Text 1"
        }
        Text {
            id: text2
            text: "Text 2"
            width: 300
            height: 20
        }
        Text {
            id: text3
            text: "Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3"
        }
        Text {
            id: text4
            text: "Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4"
            width: 300
        }
        Text {
            id: text5
            text: "Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5"
            width: 300
            wrapMode: Text.Wrap
        }
    }
    Column {
        id: columnWithRectangle
        Rectangle {
            id: rectangle
            anchors.fill: parent
        }
    }
    Component.onCompleted: {
        console.log("Text1 - implicitWidth:" + text1.implicitWidth + " implicitHeight:" + text1.implicitHeight + " width:" + text1.width + " height:" + text1.height)
        console.log("Text2 - implicitWidth:" + text2.implicitWidth + " implicitHeight:" + text2.implicitHeight + " width:" + text2.width + " height:" + text2.height)
        console.log("Text3 - implicitWidth:" + text3.implicitWidth + " implicitHeight:" + text3.implicitHeight + " width:" + text3.width + " height:" + text3.height)
        console.log("Text4 - implicitWidth:" + text4.implicitWidth + " implicitHeight:" + text4.implicitHeight + " width:" + text4.width + " height:" + text4.height)
        console.log("Text5 - implicitWidth:" + text5.implicitWidth + " implicitHeight:" + text5.implicitHeight + " width:" + text5.width + " height:" + text5.height)
        console.log("ColumnWithText - implicitWidth:" + columnWithText.implicitWidth + " implicitHeight:" + columnWithText.implicitHeight + " width:" + columnWithText.width + " height:" + columnWithText.height)
        console.log("Rectangle - implicitWidth:" + rectangle.implicitWidth + " implicitHeight:" + rectangle.implicitHeight + " width:" + rectangle.width + " height:" + rectangle.height)
        console.log("ColumnWithRectangle - implicitWidth:" + columnWithRectangle.implicitWidth + " implicitHeight:" + columnWithRectangle.implicitHeight + " width:" + columnWithRectangle.width + " height:" + columnWithRectangle.height)
    }
}

运行这个,你会得到另一个充满废话的屏幕:

我们更感兴趣的是输出到控制台的内容:

qml: Text1 - implicitWidth:30 implicitHeight:13 width:30 height:13

qml: Text2 - implicitWidth:30 implicitHeight:13 width:300 height:20

qml: Text3 - implicitWidth:1218 implicitHeight:13 width:1218 height:13

qml: Text4 - implicitWidth:1218 implicitHeight:13 width:300 height:13

qml: Text5 - implicitWidth:1218 implicitHeight:65 width:300 height:65

qml: ColumnWithText - implicitWidth:1218 implicitHeight:124 width:1218 height:124

qml: Rectangle - implicitWidth:0 implicitHeight:0 width:0 height:0

qml: ColumnWithRectangle - implicitWidth:0 implicitHeight:0 width:0 height:0

发生什么事了?我们已经创建了两个元素,它们是垂直排列子元素的不可见布局组件。我们在第一列填充了各种文本元素,并在第二列添加了一个单独的矩形。在视图的底部是一个 JavaScript 函数,当窗口组件完成时(即加载完成),该函数将执行。所有的功能就是在视图上写出各个元素的implicitWidthimplicitHeightwidthheight属性。

让我们浏览一下元素和相应的控制台行:

Text {
    id: text1
    text: "Text 1"
}

qml: Text1 - implicitWidth:30 implicitHeight:13 width:30 height:13

这个文本元素包含一小段文本,我们没有指定任何大小。它的implicitWidthimplicitHeight属性是元素希望基于其内容的大小。它的widthheight属性是元素实际的大小。在这种情况下,它想怎么定就怎么定,因为我们没有另外说明,所以它的width / height和它的implicitWidth / implicitHeight是一样的:

Text {
    id: text2
    text: "Text 2"
    width: 300
    height: 20
}

qml: Text2 - implicitWidth:30 implicitHeight:13 width:300 height:20

对于text2,隐式大小与text1相同,因为内容实际上是相同的。然而,这一次,我们明确告诉它是 300 宽,20 高。控制台告诉我们,元素正在按照它被告知的那样工作,并且确实是这个大小:

Text {
    id: text3
    text: "Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3"
}

qml: Text3 - implicitWidth:1218 implicitHeight:13 width:1218 height:13

这个text3采取了与text1相同的不干涉方法,但是它的内容是一段更长的文本。这一次,implicitWidth要大得多,因为这是容纳长文本所需的空间。请注意,这实际上比窗口更宽,文本被剪掉了。同样,我们没有另外指示它,所以它会自己调整大小:

Text {
    id: text4
    text: "Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4"
    width: 300
}

qml: Text4 - implicitWidth:1218 implicitHeight:13 width:300 height:13

text4有同样长的文本块,但是我们已经告诉它这次我们想要的宽度。您会在屏幕上注意到,即使元素只有 300 像素宽,文本在整个窗口中都是可见的。内容溢出了容器的边界。您可以将clip属性设置为true来防止这种情况,但我们不太关心这一点:

Text {
    id: text5
    text: "Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 
    5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5   
    Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 
    5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5"
    width: 300
    wrapMode: Text.Wrap
}

qml: Text5 - implicitWidth:1218 implicitHeight:65 width:300 height:65

text5重复相同的长文本块,并将宽度限制为 300,但是这一次,我们通过将wrapMode属性设置为Text.Wrap来为程序带来一点秩序。使用此设置,启用的行为更像是您对文本块的期望——它填满了可用的宽度,然后换行到下一行。元素的implicitHeightheight也随之增加,以容纳内容物。但是注意implicitHeight还是和之前一样;考虑到我们已经定义的约束,并且我们没有定义高度约束,这仍然是控件想要的宽度,以便适合它的所有内容。

然后,我们打印出包含所有这些文本的列的属性:

qml: ColumnWithText - implicitWidth:1218 implicitHeight:124 width:1218 height:124

需要注意的重要一点是,该列能够计算出它需要多宽多高来容纳它的所有子级。

接下来,我们回到我们在SplashView中遇到的问题:

Column {
    id: columnWithRectangle
    Rectangle {
        id: rectangle
        anchors.fill: parent
    }
}

这里,我们有一个先有鸡还是先有蛋的场景。Column试图计算出它需要多大才能容纳它的孩子,所以它看了一下RectangleRectangle没有明确的大小信息,也没有自己的子代,它只是设置为填充其父代Column。两个元素都无法计算出它们应该有多大,所以它们都默认为 0x0,这使得它们不可见。

qml: Rectangle - implicitWidth:0 implicitHeight:0 width:0 height:0

qml: ColumnWithRectangle - implicitWidth:0 implicitHeight:0 width:0 height:0

Sizing of elements is probably the thing that has caught me out the most with QML over the years. As a general guideline, if you write some QML but then can’t see it rendered on screen, it’s probably a sizing issue. I usually find that giving everything an arbitrary fixed width and height is a good start when debugging, and then one by one, make the sizes dynamic until you recreate the problem.

有了这些知识,让我们回到MasterView并解决我们之前的问题。

anchors.fill: parent添加到StackView组件中:

StackView {
    id: contentFrame
    anchors.fill: parent
    initialItem: Qt.resolvedUrl("qrc:/views/SplashView.qml")
}

StackView现在将填充其父窗口,我们已经明确给出了 1024 x 768 的固定大小。再次运行该应用,您现在应该有一个可爱的橙黄色SplashView,如果您调整窗口大小,它会填充屏幕并愉快地调整大小:

航行

让我们快速补充一下我们的SplashView:

Rectangle {
    anchors.fill: parent
    color: "#f4c842"
    Text {
        anchors.centerIn: parent
        text: "Splash View"
    }
}

这只是将视图的名称添加到屏幕上,因此当我们开始在视图之间移动时,我们知道我们在看哪个视图。完成后,将SplashView的内容复制到所有其他新视图中,更新每个视图中的文本以反映视图的名称,例如,在DashboardView中,文本可以是“仪表板视图”。

我们要做的第一个导航是当MasterView完成加载,我们准备好行动时,加载DashboardView。我们使用我们刚刚看到的一个 QML 组件插槽来实现这一点— Component.onCompleted()

MasterView的根Window组件中添加以下行:

Component.onCompleted: contentFrame.replace("qrc:/views/DashboardView.qml");

现在当您构建并运行时,一旦MasterView完成加载,它就会将子视图切换到DashboardView。这可能发生得太快了,以至于你再也看不到SplashView,但它仍然存在。如果您有一个需要进行大量初始化的应用,并且您不能真正拥有非阻塞式用户界面,那么拥有这样的闪屏视图是非常好的。这是一个方便放置公司标志和“网状样条线”的地方正在加载消息。是的,那是一个模拟人生参考!

StackView 就像网页浏览器中的历史。如果你先去www.google.com再去www.packtpub.com,你就是www.packtpub.com推到栈上。如果您点击浏览器上的返回,您将返回到www.google.com。该历史可以由几个页面(或视图)组成,您可以在其中前后导航。有时你不需要历史,有时你又不希望用户能够回到过去。顾名思义,我们调用的replace()方法会将新视图推送到堆栈上,并清除任何历史记录,这样您就无法返回。

Component.onCompleted槽中,我们看到了一个如何直接从 QML 在视图之间导航的例子。我们可以将这种方法用于所有的应用导航。例如,我们可以为用户添加一个按钮来创建一个新的客户端,当它被点击时,将CreateClientView直接推到堆栈上,如下所示:

Button {
    onClicked: contentFrame.replace("qrc:/views/CreateClientView.qml")
}

对于 UX 设计或简单的用户界面繁重的应用,几乎没有业务逻辑,这是一个完全有效的方法。问题在于,您的 QML 视图和组件变得非常紧密地耦合在一起,业务逻辑层不知道用户在做什么。通常,移动到应用的新屏幕并不像显示新视图那么简单。您可能需要更新一个状态机,设置一些模型,或者从以前的视图中清除一些数据。通过我们的主控制器交换机路由我们所有的导航请求,我们分离我们的组件,并为我们的业务逻辑获得一个截取点,以采取它需要的任何行动,并验证请求是否合适。

我们将通过从业务逻辑层发出信号并让我们的主视图响应这些信号并执行转换来请求导航到这些视图。与其弄乱我们的主控制器,我们将把导航的责任委托给cm-lib中的新控制器,所以创建一个新的头文件(没有这样的实现,所以我们不需要cm/cm-lib/source/controllers中名为navigation-controller.h.cpp文件),并添加以下代码:

#ifndef NAVIGATIONCONTROLLER_H
#define NAVIGATIONCONTROLLER_H

#include <QObject>

#include <cm-lib_global.h>
#include <models/client.h>

namespace cm {
namespace controllers {

class CMLIBSHARED_EXPORT NavigationController : public QObject
{
    Q_OBJECT

public:
    explicit NavigationController(QObject* _parent = nullptr)
        : QObject(_parent)
    {}

signals:
    void goCreateClientView();
    void goDashboardView();
    void goEditClientView(cm::models::Client* client);
    void goFindClientView();
};

}
}
#endif

我们已经创建了一个继承自QObject的最小类,并为我们的每个新视图实现了一个信号。请注意,我们不需要导航到主视图飞溅视图,因此没有相应的信号。当我们导航到EditClientView时,我们需要通知用户界面我们想要编辑哪个客户端,所以我们将把它作为参数传递。从我们的业务逻辑代码中的任何地方调用其中的一个方法,都会向以太发出一个请求,说“我想去某某视图,请”。然后由用户界面层的主视图来监控这些请求并做出相应的响应。请注意,业务逻辑层仍然对 UI 实现一无所知。如果没人回应信号也没关系;这不是双向交流。

Whenever you inherit from QObject, always remember the Q_OBJECT macro and also an overloaded constructor that takes a QObject parent. As we want to use this class outside of this project (in the UI project), we must also remember the CMLIBSHARED_EXPORT macro.

我们在这里向前看了一点,并假设我们的 Client 类将位于cm::models命名空间中,但是 Qt 在我们创建项目时为我们添加的默认Client类不是,所以让我们在继续之前修复它:

客户端. h :

#ifndef CLIENT_H
#define CLIENT_H

#include "cm-lib_global.h"

namespace cm {
namespace models {

class CMLIBSHARED_EXPORT Client
{
public:
    Client();
};

}}

#endif

client.cpp:

#include "client.h"

namespace cm {
namespace models {

Client::Client()
{
}

}}

我们需要能够创建一个导航控制器的实例,并让我们的用户界面与之交互。出于单元测试的原因,将对象创建隐藏在某种对象工厂接口之后是一个很好的做法,但是我们在这个阶段并不关心这个,所以我们将简单地在主控制器中创建对象。让我们借此机会也给我们的主控制器添加私有实现(PImpl)这个成语。如果您以前没有遇到过 PImpl,它只是一种将所有私有实现细节移出头文件并放入定义中的技术。这有助于保持头文件尽可能的短和干净,只包含公共 API 的使用者所必需的内容。将宣言和执行改为:

master-controller.h:

#ifndef MASTERCONTROLLER_H
#define MASTERCONTROLLER_H

#include <QObject>
#include <QScopedPointer>
#include <QString>

#include <cm-lib_global.h>
#include <controllers/navigation-controller.h>

namespace cm {
namespace controllers {

class CMLIBSHARED_EXPORT MasterController : public QObject
{
    Q_OBJECT
    Q_PROPERTY( QString ui_welcomeMessage READ welcomeMessage CONSTANT )
    Q_PROPERTY( cm::controllers::NavigationController* ui_navigationController READ navigationController CONSTANT )

public:
    explicit MasterController(QObject* parent = nullptr);
    ~MasterController();

    NavigationController* navigationController();
    const QString& welcomeMessage() const;

private:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};

}}
#endif

master-controller.cpp:

#include "master-controller.h"

namespace cm {
namespace controllers {

class MasterController::Implementation
{
public:
    Implementation(MasterController* _masterController)
        : masterController(_masterController)
    {
        navigationController = new NavigationController(masterController);
    }

    MasterController* masterController{nullptr};
    NavigationController* navigationController{nullptr};
    QString welcomeMessage = "This is MasterController to Major Tom";
};

MasterController::MasterController(QObject* parent)
    : QObject(parent)
{
    implementation.reset(new Implementation(this));
}

MasterController::~MasterController()
{
}

NavigationController* MasterController::navigationController()
{
    return implementation->navigationController;
}

const QString& MasterController::welcomeMessage() const
{
    return implementation->welcomeMessage;
}

}}

You may have noted that we don’t specify the cm::controllers namespace for the NavigationController accessor method, but we do for the Q_PROPERTY. This is because the property is accessed by the UI QML, which is not executing within the scope of the cm namespace, so we have to explicitly specify the fullyqualified name. As a general rule of thumb, be explicit about namespaces for anything that QML interacts with directly, including parameters in signals and slots.

接下来,我们需要在 cm-ui 项目中向 QML 系统注册新的NavigationController类,因此在main.cpp中,在现有的主控制器类旁边添加以下注册:

qmlRegisterType<cm::controllers::NavigationController>("CM", 1, 0, "NavigationController");

我们现在准备架设主视图对这些导航信号做出反应。在StackView前增加以下元素:

Connections {
    target: masterController.ui_navigationController
    onGoCreateClientView: contentFrame.replace("qrc:/views/CreateClientView.qml")
    onGoDashboardView: contentFrame.replace("qrc:/views/DashboardView.qml")
    onGoEditClientView: contentFrame.replace("qrc:/views/EditClientView.qml", {selectedClient: client})
    onGoFindClientView: contentFrame.replace("qrc:/views/FindClientView.qml")
}

我们正在创建一个连接组件,绑定到我们的新实例导航控制器,它对我们添加的每个 go 信号做出反应,并通过contentFrame导航到相关视图,使用与我们之前移动到仪表板相同的replace()方法。因此,每当goCreateClientView()信号在导航控制器上被触发时,onGoCreateClientView()槽被调用到我们的Connections组件上,并且CreateClientView被加载到名为contentFrame堆栈视图中。在onGoEditClientView的情况下,从信号中传递一个client参数,我们将该对象传递给一个名为selectedClient的属性,稍后我们会将其添加到视图中。

Some signals and slots in QML components are automatically generated and connected for us and are convention based. Slots are named on[CapitalisedNameOfRelatedSignal]. So, for example, if you have a signal called mySplendidSignal(), then the corresponding slot will be named onMySplendidSignal. These conventions are in play with our NavigationController and Connections components.

接下来,让我们在主视图中添加一个带有一些占位符按钮的导航栏,这样我们就可以尝试这些信号了。

添加一个Rectangle来形成我们酒吧的背景:

Rectangle {
    id: navigationBar
    anchors {
        top: parent.top
        bottom: parent.bottom
        left: parent.left
    }
    width: 100
    color: "#000000"
}

这将在视图的左侧绘制一个 100 像素宽的黑色条。

我们还需要调整我们的StackView,以便它为我们的酒吧留出一些空间。与其填充它的父项,不如将它的四个边中的三个锚定到它的父项,但是将左侧附加到我们的栏的右侧:

StackView {
    id: contentFrame
    anchors {
        top: parent.top
        bottom: parent.bottom
        right: parent.right
        left: navigationBar.right
    }
    initialItem: Qt.resolvedUrl("qrc:/views/SplashView.qml")
}

现在,让我们在导航Rectangle中添加一些按钮:

 Rectangle {
    id: navigationBar
    …

    Column {
        Button {
            text: "Dashboard"
            onClicked: masterController.ui_navigationController.goDashboardView()
        }
        Button {
            text: "New Client"
            onClicked: masterController.ui_navigationController.goCreateClientView()
        }
        Button {
            text: "Find Client"
            onClicked: masterController.ui_navigationController.goFindClientView()
        }
    }

}

我们使用Column组件来为我们布局按钮,而不是必须单独将按钮相互锚定。每个按钮显示一些文本,当点击时,在导航控制器上调用一个信号。我们的Connection组件对信号做出反应,并为我们执行视图转换:

好东西,我们有一个功能导航框架!但是,当您单击其中一个导航按钮时,导航栏会暂时消失,然后再次出现。在我们的应用输出控制台中,我们也收到了“锚冲突”的消息,这表明我们正在做一些不太正确的事情。让我们在继续前进之前解决这些问题。

修复冲突

导航栏问题很简单。如前所述,QML 在结构上是分等级的。这体现在元素的呈现方式上——首先出现的子元素首先呈现。在我们的例子中,我们绘制导航栏,然后绘制内容框架。当 StackView 组件加载新内容时,默认情况下,它会应用时髦的过渡来使其看起来不错。这些转换会导致内容移出控件边界,并覆盖其下的任何内容。有几种方法可以解决这个问题。

首先,我们可以重新排列组件的呈现顺序,并将导航栏放在内容框架之后。这将在StackView的顶部绘制导航条,不管它是怎么回事。第二个选项,也是我们将要实现的选项,是简单地设置 StackViewclip属性:

clip: true

这将剪辑任何与控件边界重叠的内容,并且不会呈现它。

下一个问题有点深奥。正如我们已经讨论过的,在过去几年的 QML 开发中,我遇到的最大的困惑是组件的尺寸。我们使用的一些组件,如矩形,本质上是视觉元素。如果没有定义它们的大小,无论是直接用width/height属性还是间接用锚点,那么它们都不会渲染。其他元素如连接根本不可见,尺寸属性是多余的。像这样的布局元素在一个轴上可能有固定的大小,但在另一个轴上本质上是动态的。

大多数组件的一个共同点是它们继承自,而项又直接继承自 QtObject ,而后者只是一个普通的 QObject 。就像 C++ 端的 Qt Framework 为普通旧的 QObject *实现了很多默认行为一样,QML 组件经常为组件实现默认行为,我们可以在这里利用这些默认行为。

在我们的子视图中,我们使用了矩形作为根对象。这很有意义,因为我们想要显示一个固定大小和颜色的矩形。然而,这给 StackView 带来了问题,因为它不知道应该是什么尺寸。为了提供这些信息,我们尝试将它锚定到它的父视图(T4)上,但是当我们切换视图时,由于与堆栈视图试图执行的转换冲突,这会导致它自己的问题。

我们摆脱这种困境的方法是让我们孩子观点的根源成为一个普通的旧StackView 组件有处理组件的内部逻辑,只会为我们调整大小。然后,我们的矩形组件成为已经自动调整大小的项目组件的子组件,我们可以锚定到该子组件:

Item {
    Rectangle {
        ...
    }
}

这一切都有点令人困惑,感觉像巫毒教,但这里的要点是,将物品作为 QML 习俗的根元素通常是一件好事。继续以这种方式向所有子视图添加根组件(但不添加主视图)。

再次运行该应用,您现在应该有很好的平滑过渡,并且控制台中没有警告消息。

摘要

我们有一个灵活的、解耦的导航机制,并且正在不同的视图之间成功地转换。我们已经准备好了导航栏的基础知识和本章开头设计的工作内容窗格。

让用户界面调用业务逻辑层来发出一个信号,然后用户界面对该信号做出反应,这看起来像是一种在视图之间导航的迂回方式,但是这种业务逻辑信号/用户界面槽设计带来了好处。它保持了用户界面的模块化,因为视图不需要相互了解。它将导航逻辑保留在业务逻辑层中,并使该层能够请求用户界面将用户导航到特定的视图,而无需了解用户界面或视图本身的任何信息。至关重要的是,它还为我们提供了拦截点,以便当用户请求导航到给定视图时,我们可以处理它并执行任何我们需要的附加处理,例如状态管理或清理。

第 4 章 *【样式】*中,我们将介绍一个共享样式组件,以及 QML 模块和图标,然后我们用一个动态命令栏完成我们的 UI 设计。