惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Register - Security
The Register - Security
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
博客园 - 叶小钗
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
SegmentFault 最新的问题
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
The Cloudflare Blog
T
The Blog of Author Tim Ferriss
Apple Machine Learning Research
Apple Machine Learning Research
C
Check Point Blog
Microsoft Azure Blog
Microsoft Azure Blog
V
V2EX
量子位
F
Fortinet All Blogs
Martin Fowler
Martin Fowler
大猫的无限游戏
大猫的无限游戏
Last Week in AI
Last Week in AI
博客园 - 聂微东
美团技术团队
Stack Overflow Blog
Stack Overflow Blog
Google DeepMind News
Google DeepMind News
宝玉的分享
宝玉的分享
M
MIT News - Artificial intelligence
U
Unit 42
罗磊的独立博客
MyScale Blog
MyScale Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
V
Visual Studio Blog
Jina AI
Jina AI
Recorded Future
Recorded Future
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
MongoDB | Blog
MongoDB | Blog
N
Netflix TechBlog - Medium
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
B
Blog
J
Java Code Geeks
爱范儿
爱范儿
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
aimingoo的专栏
aimingoo的专栏
B
Blog RSS Feed
月光博客
月光博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Privacy International News Feed
Know Your Adversary
Know Your Adversary
雷峰网
雷峰网

涛哥的博客

玩转QtQuick(2)-默认渲染器 | 涛哥的博客 玩转QtQuick(2)-默认渲染器 | 涛哥的博客 玩转QtQuick(1)-SceneGraph场景图简介 | 涛哥的博客 玩转QtQuick(1)-SceneGraph场景图简介 | 涛哥的博客 QQuickWidget中文输入法问题的正确解法 | 涛哥的博客 玩转Qml(18)-用户向导 | 涛哥的博客 玩转Qml(17)-树组件的定制 | 涛哥的博客 玩转Qml(17)-树组件的定制 | 涛哥的博客 玩转Qt(15)-操控Web小车案例 | 涛哥的博客 玩转Qt(15)-操控Web小车案例 | 涛哥的博客 玩转Qt(14)-Qt与Web混合开发 | 涛哥的博客 玩转Qt(14)-Qt与Web混合开发 | 涛哥的博客 玩转Qt(13)-安卓5.12.4环境搭建 | 涛哥的博客 玩转Qt(13)-安卓5.12.4环境搭建 | 涛哥的博客 玩转Qt(12)-github-Actions缓存优化 | 涛哥的博客 玩转Qt(12)-github-Actions缓存优化 | 涛哥的博客 玩转Qt(11)-github-Actions自动化发行 | 涛哥的博客 玩转Qt(11)-github-Actions自动化发行 | 涛哥的博客 玩转Qt(10)-github-Actions自动化编译 | 涛哥的博客 玩转Qt(10)-github-Actions自动化编译 | 涛哥的博客 玩转Qt(9)-程序发布指南 | 涛哥的博客 玩转Qt(9)-程序发布指南 | 涛哥的博客 玩转Qt(8)-掌握信号槽使用细节 | 涛哥的博客 玩转Qt(8)-掌握信号槽使用细节 | 涛哥的博客 玩转Qt(7)-窥探信号槽的实现细节 | 涛哥的博客 玩转Qt(7)-窥探信号槽的实现细节 | 涛哥的博客 玩转Qt(6)-认清信号槽的本质 | 涛哥的博客 玩转Qt(6)-认清信号槽的本质 | 涛哥的博客 玩转Qt(5)-理解事件循环 | 涛哥的博客 玩转Qt(5)-理解事件循环 | 涛哥的博客 玩转Qml(16)-移植ShaderToy | 涛哥的博客 玩转Qml(16)-移植ShaderToy | 涛哥的博客 玩转Qml(15)-着色器效果ShaderEffect | 涛哥的博客 玩转Qml(15)-着色器效果ShaderEffect | 涛哥的博客 玩转Qml(14)-动画特效-梯度 | 涛哥的博客 玩转Qml(14)-动画特效-梯度 | 涛哥的博客 玩转Qml(13)-动画特效-飞入 | 涛哥的博客 玩转Qml(13)-动画特效-飞入 | 涛哥的博客
玩转Qml(18)-用户向导 | 涛哥的博客
2020-11-10 · via 涛哥的博客
  • 简介
  • 效果预览
  • 源码
  • 原理说明
    • 需求说明
    • 原理
      • Qml对象作用域
      • QObject对象树
    • 实现
      • 最小改动
      • 获取焦点区域
        • 找到对象
        • 取坐标
        • 向导页
        • 向导组件
        • 向导数据源

简介

很多现代化的软件,都会有向导功能,相信大家并不陌生。

“用户向导”的作用,可以帮助新用户快速了解产品,也可以用来提醒用户该如何操作。

这次涛哥就分享一个Qml制作“用户向导”的方案。

效果预览

看一下最终效果

预览
预览

在整个软件界面上,覆盖一层遮罩,只保留一部分镂空区域,并用箭头指向镂空区域,

以此,将用户的眼球聚焦到镂空区域,并用适当的文字,说明镂空区域的作用。

这个遮罩层是不能操作到软件界面的,点击任意区域,会转到下一步的向导,直到最后向导退出。

源码

《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick

github https://github.com/jaredtao/TaoQuick

访问不了或者速度太慢,可以用国内的镜像网站gitee

https://gitee.com/jaredtao/TaoQuick

原理说明

向导的实现方案有很多,比如贴图之类的。按不同的需求,方案也不尽相同。

这里先列一个涛哥的需求,再讨论方案才有意义。

需求说明

  • 窗口大小不固定

  • 焦点区域可以是Qml界面中的任意可视组件

  • 向导功能要独立实现,且对已有代码改动最小

原理

遮罩的制作,在Qml中就是几个半透明的Rectangle拼在一起,附带一个顶层MouseArea,

将所有鼠标事件都过滤掉,这些都比较简单。

焦点区域才是本文的核心。

Qml对象作用域

先来说一下Qml中的对象作用域吧。

如果你阅读过《Qml Book》或者有一定的Qml经验,就知道Qml中的对象,都是通过id相互引用的。

像html / Android,都有类似findElementById这样的接口,获取任意的组件。

在Qml,省去了find这一步,可以直接使用id进行访问。

不过呢,Qml这个id的可访问性,不是任何场景都能直接用的,有诸多情况下,不能直接使用, 例如:

  • 平级的两个组件,它们的子组件不能互相访问到(同一个文件作用域中的除外)

  • 各种View的delegate中的组件id,view外部不能直接访问(这是因为delegate有多个实例的情况下,id不能唯一标识)

  • Loader/Component动态加载后的子组件,不能被直接访问(动态加载,都不知道它什么时候创建,当然不能用它)

诸如此类,都不能直接访问。

扩展说明:

顶层组件的id,可以被所有子组件访问,因此可以用来存储全局对象;

Qml单例组件,或者C++上下文对象,也可以被所有子组件访问。

这两种方式也可以用来实现向导功能,有兴趣的读者可自己探索。

QObject对象树

Qml中的所有可视化组件,大都是继承于Item,Item的继承关系是 Item ==> QQuickItem ==> QObject

因此呢,Qml中的所有对象,都在一个QObject树上,具有父子关系。

我们可以从任意节点,向上找到根节点,也可以通过根节点的findChild的方式,找到任意一个带objectName的节点。

涛哥的向导方案,便是基于此实现。

实现

最小改动

在已有的Qml代码中,对于想当作焦点区域的组件,最小改动是增加一个objectName

例如涛哥想把已有的标题栏按钮,作为焦点区域:

改动前:

1
2
3
4
5
6
7
...
SkinBtn {
width: 24
height: 24
anchors.verticalCenter: parent.verticalCenter
}
...

改动后:

1
2
3
4
5
6
7
8
...
SkinBtn {
width: 24
height: 24
anchors.verticalCenter: parent.verticalCenter
objectName: "skinBtn"
}
...

获取焦点区域

找到对象

像前面的组件,只要有了objectName,就可以从任意位置获取到它了

这需要一点C++代码扩展

1
2
3
4
5
6
7
QQuickItem *getObject(QObject * pRootObject, const QString &targetObjName)
{
if (!pRootObject) {
return nullptr;
}
return pRootObject->findChild<QQuickItem *>(targetObjName);
}

通过findChild就可以拿到目标对象了, rootObject是根节点,可以从任意节点向上找到,

也可以通过QQmlContex直接获取到。

取坐标

找到了对象,就可以计算它的坐标,用于向导了。

为了方便计算,同时避免处理繁杂的父子关系,可以直接把坐标映射到顶层窗口。向导的坐标也以顶层窗口为准即可。

这里进一步封装了C++代码,直接计算好坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
QRect getItemGeometryToScene(const QString &targetObjName) const
{
if (!pRootObject) {
return {};
}
auto pItem = pRootObject->findChild<QQuickItem *>(targetObjName);
if (pItem) {
if (pItem->parentItem()) {

auto pos = pItem->parentItem()->mapToScene(pItem->position());
return QRectF { pos.x(), pos.y(), pItem->width(), pItem->height() }.toRect();
} else {

return pItem->boundingRect().toRect();
}
}
return {};
}

完整的代码,可以在TaoQuick项目中看到, 封装了一个QuickTool类,

所在路径是TaoQuick/3rdparty/TaoCommon/QuickTool

向导页

接下来就是封装一个向导页面了,遮罩围绕在焦点四周,同时带上箭头和文字说明即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175


import QtQuick 2.9
import QtQuick.Controls 2.2
import QtGraphicalEffects 1.0
import ".."
import "../.."

Item {
id: pageItem
z: 998
anchors.fill: parent
property string wizardName
property string wizardDescript
property string targetObjectName
property int pageType: pageTypeDown

property color maskColor: "black"
property real maskOpacity: 0.75

property rect focusRect


readonly property int pageTypeDown: Qt.DownArrow
readonly property int pageTypeUp: Qt.UpArrow
readonly property int pageTypeLeft: Qt.LeftArrow
readonly property int pageTypeRight: Qt.RightArrow


Component.onCompleted: {

var rect = quickTool.getItemGeometryToScene(targetObjectName)
focusRect = rect
}

Item {
id: focusItem
x: focusRect.x
y: focusRect.y
width: focusRect.width
height: focusRect.height
RadialGradient {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 0.6; color: maskColor }
}
opacity: maskOpacity / 2
}
}

Rectangle {
x: 0
y: 0
width: focusRect.x
height: parent.height
color: maskColor
opacity: maskOpacity
}

Rectangle {
x: focusRect.x + focusRect.width
y: 0
width: pageItem.width - x
height: parent.height
color: maskColor
opacity: maskOpacity
}

Rectangle {
x: focusRect.x
width: focusRect.width
y: 0
height: focusRect.y
color: maskColor
opacity: maskOpacity
}

Rectangle {
x: focusRect.x
width: focusRect.width
y: focusRect.y + focusRect.height
height: pageItem.height - y
color: maskColor
opacity: maskOpacity
}
Row {
id: leftRow
spacing: 10
visible: pageType === pageTypeLeft
z: 998
anchors {
left: focusItem.right
leftMargin: 5
verticalCenter: focusItem.verticalCenter
}
CusImage {
source: CusConfig.imagePathPrefix + "arrow-left.png"
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: rightRow
spacing: 10
layoutDirection: Qt.RightToLeft
visible: pageType === pageTypeRight
z: 998
anchors {
right: focusItem.left
rightMargin: 5
verticalCenter: focusItem.verticalCenter
}
CusImage {
source: CusConfig.imagePathPrefix + "arrow-right.png"
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
id: downColumn
spacing: 10
visible: pageType === pageTypeDown
width: 300
z: 998
anchors {
bottom: focusItem.top
bottomMargin: 5
horizontalCenter: focusItem.horizontalCenter
}
CusImage {
source: CusConfig.imagePathPrefix + "arrow-down.png"
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
id: upColumn
spacing: 10
visible: pageType === pageTypeUp
width: 300
z: 998
anchors {
top: focusItem.bottom
topMargin: 5
horizontalCenter: focusItem.horizontalCenter
}
CusImage {
source: CusConfig.imagePathPrefix + "arrow-up.png"
anchors.horizontalCenter: parent.horizontalCenter
}
}
CusLabel {
id: wizardDescriptLabel
z: 998
text: qsTr(wizardDescript) + CusConfig.transString
font.pixelSize: 16
color: "white"
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
verticalCenterOffset: 50
horizontalCenterOffset: -50
}
}
CusLabel {
z: 998
text: qsTr(wizardName) + CusConfig.transString
font.pixelSize: 26
color: "white"
anchors {
left: wizardDescriptLabel.left
bottom: wizardDescriptLabel.top
bottomMargin: 30
}
}

}
向导组件

向导一般不止一页,而是很多页,这里就封装了一个向导组件。

使用model-view的方式,数据源由外部设置,组件只管按照model去实例化对应的向导页即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59


import QtQuick 2.9
import QtQuick.Controls 2.2
import ".."
import "../.."

Item {
id: cusWizardItem
signal wizardFinished
property string totlaString: qsTr("Wizard %1/%2 >").arg(currentIndex + 1).arg(count) + CusConfig.transString
property string operatorString: qsTr("Click any area to show next") + CusConfig.transString
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
currentIndex++
if (currentIndex >= count) {
wizardFinished()
}
}
}
property var model
property int count: model.count
property int currentIndex: 0
Repeater {
model: cusWizardItem.model
delegate: CusWizardPage {
anchors.fill: parent
visible: index === currentIndex
wizardName: model.name
wizardDescript: model.descript
targetObjectName: model.targetObjectName
pageType: model.arrowType
}
}
CusLabel {
z: 999
id: centerLabel
anchors {
centerIn: parent
horizontalCenterOffset: 300
verticalCenterOffset: 150
}
text: totlaString
font.pixelSize: 22
color: "white"
}
CusLabel {
z: 999
anchors {
centerIn: parent
horizontalCenterOffset: 300
verticalCenterOffset: 150 + centerLabel.height
}
text: operatorString
color: "white"
}
}
向导数据源

最后来看一下,TaoQuick项目的首页,提供的向导数据model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
ListModel {
id:wizardModel
ListElement {
name: "TitleBar"
descript: "drag change window pos, double click change window state"
targetObjectName: "blankItem"
arrowType: Qt.UpArrow
}
ListElement {
name: "Control Buttons"
descript: "minimize, maximize, normal or close main window"
targetObjectName: "controlButtonsRow"
arrowType: Qt.UpArrow
}
ListElement {
name: "Skin Button"
descript: "switch theme"
targetObjectName: "skinBtn"
arrowType: Qt.UpArrow
}
ListElement {
name: "Language Button"
descript: "switch language"
targetObjectName: "langBtn"
arrowType: Qt.UpArrow
}
ListElement {
name: "Left Pane"
descript: "show content list"
targetObjectName: "leftPane"
arrowType: Qt.LeftArrow
}
ListElement {
name: "Drawer Button"
descript: "show or hide left pane"
targetObjectName: "menuBtn"
arrowType: Qt.LeftArrow
}
ListElement {
name: "Search Input"
descript: "search content"
targetObjectName: "searchInput"
arrowType: Qt.LeftArrow
}
ListElement {
name: "Home Button"
descript: "go back home page"
targetObjectName: "homeBtn"
arrowType: Qt.LeftArrow
}
ListElement {
name: "Content List"
descript: "switch content"
targetObjectName: "contentListView"
arrowType: Qt.LeftArrow
}
ListElement {
name: "Content Pane"
descript: "show current selected content by list"
targetObjectName: "contentRect"
arrowType: Qt.RightArrow
}
ListElement {
name: "Tool Buttons"
descript: "special function buttons such as Wizard, View Source Code and so on"
targetObjectName: "wizardBtn"
arrowType: Qt.RightArrow
}
}