From 97e88dbd6f96074fd5085064bca2b29e1b24bfaa Mon Sep 17 00:00:00 2001
From: Polaris-Night <158275221@qq.com>
Date: Sat, 17 May 2025 08:25:17 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20FluTour=E5=A2=9E=E5=8A=A0=E6=8C=87?=
=?UTF-8?q?=E7=A4=BA=E5=99=A8=E5=92=8C=E5=8A=A8=E7=94=BB=E6=95=88=E6=9E=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
example/example_en_US.ts | 23 +-
example/example_zh_CN.ts | 29 ++-
example/qml/page/T_Tour.qml | 30 ++-
src/Qt5/imports/FluentUI/Controls/FluTour.qml | 229 +++++++++++++++---
src/Qt5/imports/FluentUI/plugins.qmltypes | 2 +
src/Qt6/imports/FluentUI/Controls/FluTour.qml | 229 +++++++++++++++---
6 files changed, 449 insertions(+), 93 deletions(-)
diff --git a/example/example_en_US.ts b/example/example_en_US.ts
index a4886c29..15e18788 100644
--- a/example/example_en_US.ts
+++ b/example/example_en_US.ts
@@ -2737,49 +2737,60 @@ Some contents...
T_Tour
+
Upload File
+
Put your files here.
-
-
+
+
+
Save
+
Save your changes.
+
Other Actions
+
Click to see other actions.
-
+
Begin Tour
-
-
+
+ Begin Tour with custom indicator
+
+
+
+
+
Upload
-
+
More
diff --git a/example/example_zh_CN.ts b/example/example_zh_CN.ts
index 5d1d3c5d..f2be137d 100644
--- a/example/example_zh_CN.ts
+++ b/example/example_zh_CN.ts
@@ -552,7 +552,7 @@
Tour
- 游览
+ 漫游式引导
@@ -2938,56 +2938,67 @@ Some contents...
+
Upload File
上传文件
+
Put your files here.
把你的文件放在这里
-
-
+
+
+
Save
保存
+
Save your changes.
保存更改
+
Other Actions
其他操作
+
Click to see other actions.
点击查看其他操作
-
+
Begin Tour
- 开始游览
+ 开始导览
-
-
+
+ Begin Tour with custom indicator
+ 以自定义指示器开始导览
+
+
+
+
Upload
上传
-
+
More
更多
Tour
- 游览
+ 漫游式引导
diff --git a/example/qml/page/T_Tour.qml b/example/qml/page/T_Tour.qml
index e87e5827..f7ce16df 100644
--- a/example/qml/page/T_Tour.qml
+++ b/example/qml/page/T_Tour.qml
@@ -17,20 +17,42 @@ FluScrollablePage{
{title:qsTr("Other Actions"),description: qsTr("Click to see other actions."),target:()=>btn_more}
]
}
+ FluTour{
+ id:tour_custom_indicator
+ steps:[
+ {title:qsTr("Upload File"),description: qsTr("Put your files here."),target:()=>btn_upload},
+ {title:qsTr("Save"),description: qsTr("Save your changes."),target:()=>btn_save},
+ {title:qsTr("Other Actions"),description: qsTr("Click to see other actions."),target:()=>btn_more}
+ ]
+ indicator: Component{
+ FluText {
+ text: "%1 / %2".arg(current + 1).arg(total)
+ }
+ }
+ }
FluFrame{
Layout.fillWidth: true
Layout.preferredHeight: 130
padding: 10
- FluFilledButton{
+ Row{
anchors{
top: parent.top
topMargin: 14
}
- text: qsTr("Begin Tour")
- onClicked: {
- tour.open()
+ spacing: 20
+ FluFilledButton{
+ text: qsTr("Begin Tour")
+ onClicked: {
+ tour.open()
+ }
+ }
+ FluFilledButton{
+ text: qsTr("Begin Tour with custom indicator")
+ onClicked: {
+ tour_custom_indicator.open()
+ }
}
}
diff --git a/src/Qt5/imports/FluentUI/Controls/FluTour.qml b/src/Qt5/imports/FluentUI/Controls/FluTour.qml
index d8ff7027..b73b4853 100644
--- a/src/Qt5/imports/FluentUI/Controls/FluTour.qml
+++ b/src/Qt5/imports/FluentUI/Controls/FluTour.qml
@@ -7,8 +7,10 @@ import FluentUI 1.0
Popup{
property var steps : []
property int targetMargins: 5
+ property int targetRadius: 2
property Component nextButton: com_next_button
property Component prevButton: com_prev_button
+ property Component indicator: com_indicator
property int index : 0
property string finishText: qsTr("Finish")
property string nextText: qsTr("Next")
@@ -22,12 +24,12 @@ Popup{
contentItem: Item{}
onVisibleChanged: {
if(visible){
+ d.animationEnabled = false
control.index = 0
+ d.updatePos()
+ d.animationEnabled = true
}
}
- onIndexChanged: {
- canvas.requestPaint()
- }
Component{
id: com_next_button
FluFilledButton{
@@ -50,10 +52,32 @@ Popup{
}
}
}
+ Component{
+ id: com_indicator
+ Row{
+ spacing: 10
+ Repeater{
+ model: total
+ delegate: Rectangle{
+ width: 8
+ height: 8
+ radius: 4
+ scale: current === index ? 1.2 : 1
+ color:{
+ if(current === index){
+ return FluTheme.primaryColor
+ }
+ return FluTheme.dark ? Qt.rgba(99/255,99/255,99/255,1) : Qt.rgba(214/255,214/255,214/255,1)
+ }
+ }
+ }
+ }
+ }
Item{
id:d
property var window: Window.window
property point pos: Qt.point(0,0)
+ property bool animationEnabled: true
property var step: steps[index]
property var target: {
if(steps[index]){
@@ -73,15 +97,22 @@ Popup{
}
return control.width
}
+ function updatePos(){
+ if(d.target && d.window){
+ d.pos = d.target.mapToGlobal(0,0)
+ d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y)
+ }
+ }
+ onTargetChanged: {
+ updatePos()
+ }
}
Connections{
target: d.window
function onWidthChanged(){
- canvas.requestPaint()
timer_delay.restart()
}
function onHeightChanged(){
- canvas.requestPaint()
timer_delay.restart()
}
}
@@ -89,39 +120,128 @@ Popup{
id: timer_delay
interval: 200
onTriggered: {
- canvas.requestPaint()
+ d.updatePos()
}
}
- Canvas{
- id: canvas
- anchors.fill: parent
- onPaint: {
- d.pos = d.target.mapToGlobal(0,0)
- d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y)
- var ctx = canvas.getContext("2d")
- ctx.clearRect(0, 0, canvasSize.width, canvasSize.height)
- ctx.save()
- ctx.fillStyle = "#88000000"
- ctx.fillRect(0, 0, canvasSize.width, canvasSize.height)
- ctx.globalCompositeOperation = 'destination-out'
- ctx.fillStyle = 'black'
- var rect = Qt.rect(d.pos.x-control.targetMargins,d.pos.y-control.targetMargins, d.target.width+control.targetMargins*2, d.target.height+control.targetMargins*2)
- drawRoundedRect(rect,2,ctx)
- ctx.restore()
+ Item{
+ id: targetRect
+ x: d.pos.x - control.targetMargins
+ y: d.pos.y - control.targetMargins
+ width: d.target ? d.target.width + control.targetMargins * 2 : 0
+ height: d.target ? d.target.height + control.targetMargins * 2 : 0
+ Behavior on x {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
}
- function drawRoundedRect(rect, r, ctx) {
- ctx.beginPath();
- ctx.moveTo(rect.x + r, rect.y);
- ctx.lineTo(rect.x + rect.width - r, rect.y);
- ctx.arcTo(rect.x + rect.width, rect.y, rect.x + rect.width, rect.y + r, r);
- ctx.lineTo(rect.x + rect.width, rect.y + rect.height - r);
- ctx.arcTo(rect.x + rect.width, rect.y + rect.height, rect.x + rect.width - r, rect.y + rect.height, r);
- ctx.lineTo(rect.x + r, rect.y + rect.height);
- ctx.arcTo(rect.x, rect.y + rect.height, rect.x, rect.y + rect.height - r, r);
- ctx.lineTo(rect.x, rect.y + r);
- ctx.arcTo(rect.x, rect.y, rect.x + r, rect.y, r);
- ctx.closePath();
- ctx.fill()
+ Behavior on y {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ Behavior on width {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ Behavior on height {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ }
+ Shape {
+ anchors.fill: parent
+ layer.enabled: true
+ layer.samples: 4
+ layer.smooth: true
+ ShapePath {
+ fillColor: "#88000000"
+ strokeWidth: 0
+ strokeColor: "transparent"
+
+ // draw background
+ PathMove {
+ x: 0
+ y: 0
+ }
+ PathLine {
+ x: control.width
+ y: 0
+ }
+ PathLine {
+ x: control.width
+ y: control.height
+ }
+ PathLine {
+ x: 0
+ y: control.height
+ }
+ PathLine {
+ x: 0
+ y: 0
+ }
+
+ // draw highlight
+ PathMove {
+ x: targetRect.x + control.targetRadius
+ y: targetRect.y
+ }
+ PathLine {
+ x: targetRect.x + targetRect.width - control.targetRadius
+ y: targetRect.y
+ }
+ PathArc {
+ x: targetRect.x + targetRect.width
+ y: targetRect.y + control.targetRadius
+ radiusX: control.targetRadius
+ radiusY: control.targetRadius
+ useLargeArc: false
+ direction: PathArc.Clockwise
+ }
+
+ PathLine {
+ x: targetRect.x + targetRect.width
+ y: targetRect.y + targetRect.height - control.targetRadius
+ }
+ PathArc {
+ x: targetRect.x + targetRect.width - control.targetRadius
+ y: targetRect.y + targetRect.height
+ radiusX: control.targetRadius
+ radiusY: control.targetRadius
+ useLargeArc: false
+ direction: PathArc.Clockwise
+ }
+
+ PathLine {
+ x: targetRect.x + control.targetRadius
+ y: targetRect.y + targetRect.height
+ }
+ PathArc {
+ x: targetRect.x
+ y: targetRect.y + targetRect.height - control.targetRadius
+ radiusX: control.targetRadius
+ radiusY: control.targetRadius
+ useLargeArc: false
+ direction: PathArc.Clockwise
+ }
+
+ PathLine {
+ x: targetRect.x
+ y: targetRect.y + control.targetRadius
+ }
+ PathArc {
+ x: targetRect.x + control.targetRadius
+ y: targetRect.y
+ radiusX: control.targetRadius
+ radiusY: control.targetRadius
+ useLargeArc: false
+ direction: PathArc.Clockwise
+ }
}
}
FluFrame{
@@ -151,6 +271,18 @@ Popup{
return 0
}
border.width: 0
+ Behavior on x {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ Behavior on y {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
FluShadow{
radius: 5
}
@@ -193,10 +325,21 @@ Popup{
leftMargin: 15
}
}
+ FluLoader{
+ readonly property int total: steps.length
+ readonly property int current: control.index
+ sourceComponent: control.indicator
+ anchors{
+ bottom: parent.bottom
+ left: parent.left
+ bottomMargin: 15
+ leftMargin: 15
+ }
+ }
FluLoader{
id: loader_next
property bool isEnd: control.index === steps.length-1
- sourceComponent: com_next_button
+ sourceComponent: control.nextButton
anchors{
top: text_desc.bottom
topMargin: 10
@@ -207,7 +350,7 @@ Popup{
FluLoader{
id: loader_prev
visible: control.index !== 0
- sourceComponent: com_prev_button
+ sourceComponent: control.prevButton
anchors{
right: loader_next.left
top: loader_next.top
@@ -246,5 +389,17 @@ Popup{
}
return 0
}
+ Behavior on x {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ Behavior on y {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
}
}
diff --git a/src/Qt5/imports/FluentUI/plugins.qmltypes b/src/Qt5/imports/FluentUI/plugins.qmltypes
index ffeabf87..17c84d82 100644
--- a/src/Qt5/imports/FluentUI/plugins.qmltypes
+++ b/src/Qt5/imports/FluentUI/plugins.qmltypes
@@ -4328,8 +4328,10 @@ Module {
defaultProperty: "contentData"
Property { name: "steps"; type: "QVariant" }
Property { name: "targetMargins"; type: "int" }
+ Property { name: "targetRadius"; type: "int" }
Property { name: "nextButton"; type: "QQmlComponent"; isPointer: true }
Property { name: "prevButton"; type: "QQmlComponent"; isPointer: true }
+ Property { name: "indicator"; type: "QQmlComponent"; isPointer: true }
Property { name: "index"; type: "int" }
Property { name: "finishText"; type: "string" }
Property { name: "nextText"; type: "string" }
diff --git a/src/Qt6/imports/FluentUI/Controls/FluTour.qml b/src/Qt6/imports/FluentUI/Controls/FluTour.qml
index fea6368b..5049c9cc 100644
--- a/src/Qt6/imports/FluentUI/Controls/FluTour.qml
+++ b/src/Qt6/imports/FluentUI/Controls/FluTour.qml
@@ -7,8 +7,10 @@ import FluentUI
Popup{
property var steps : []
property int targetMargins: 5
+ property int targetRadius: 2
property Component nextButton: com_next_button
property Component prevButton: com_prev_button
+ property Component indicator: com_indicator
property int index : 0
property string finishText: qsTr("Finish")
property string nextText: qsTr("Next")
@@ -22,12 +24,12 @@ Popup{
contentItem: Item{}
onVisibleChanged: {
if(visible){
+ d.animationEnabled = false
control.index = 0
+ d.updatePos()
+ d.animationEnabled = true
}
}
- onIndexChanged: {
- canvas.requestPaint()
- }
Component{
id: com_next_button
FluFilledButton{
@@ -50,10 +52,32 @@ Popup{
}
}
}
+ Component{
+ id: com_indicator
+ Row{
+ spacing: 10
+ Repeater{
+ model: total
+ delegate: Rectangle{
+ width: 8
+ height: 8
+ radius: 4
+ scale: current === index ? 1.2 : 1
+ color:{
+ if(current === index){
+ return FluTheme.primaryColor
+ }
+ return FluTheme.dark ? Qt.rgba(99/255,99/255,99/255,1) : Qt.rgba(214/255,214/255,214/255,1)
+ }
+ }
+ }
+ }
+ }
Item{
id:d
property var window: Window.window
property point pos: Qt.point(0,0)
+ property bool animationEnabled: true
property var step: steps[index]
property var target: {
if(steps[index]){
@@ -73,15 +97,22 @@ Popup{
}
return control.width
}
+ function updatePos(){
+ if(d.target && d.window){
+ d.pos = d.target.mapToGlobal(0,0)
+ d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y)
+ }
+ }
+ onTargetChanged: {
+ updatePos()
+ }
}
Connections{
target: d.window
function onWidthChanged(){
- canvas.requestPaint()
timer_delay.restart()
}
function onHeightChanged(){
- canvas.requestPaint()
timer_delay.restart()
}
}
@@ -89,39 +120,128 @@ Popup{
id: timer_delay
interval: 200
onTriggered: {
- canvas.requestPaint()
+ d.updatePos()
}
}
- Canvas{
- id: canvas
- anchors.fill: parent
- onPaint: {
- d.pos = d.target.mapToGlobal(0,0)
- d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y)
- var ctx = canvas.getContext("2d")
- ctx.clearRect(0, 0, canvasSize.width, canvasSize.height)
- ctx.save()
- ctx.fillStyle = "#88000000"
- ctx.fillRect(0, 0, canvasSize.width, canvasSize.height)
- ctx.globalCompositeOperation = 'destination-out'
- ctx.fillStyle = 'black'
- var rect = Qt.rect(d.pos.x-control.targetMargins,d.pos.y-control.targetMargins, d.target.width+control.targetMargins*2, d.target.height+control.targetMargins*2)
- drawRoundedRect(rect,2,ctx)
- ctx.restore()
+ Item{
+ id: targetRect
+ x: d.pos.x - control.targetMargins
+ y: d.pos.y - control.targetMargins
+ width: d.target ? d.target.width + control.targetMargins * 2 : 0
+ height: d.target ? d.target.height + control.targetMargins * 2 : 0
+ Behavior on x {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
}
- function drawRoundedRect(rect, r, ctx) {
- ctx.beginPath();
- ctx.moveTo(rect.x + r, rect.y);
- ctx.lineTo(rect.x + rect.width - r, rect.y);
- ctx.arcTo(rect.x + rect.width, rect.y, rect.x + rect.width, rect.y + r, r);
- ctx.lineTo(rect.x + rect.width, rect.y + rect.height - r);
- ctx.arcTo(rect.x + rect.width, rect.y + rect.height, rect.x + rect.width - r, rect.y + rect.height, r);
- ctx.lineTo(rect.x + r, rect.y + rect.height);
- ctx.arcTo(rect.x, rect.y + rect.height, rect.x, rect.y + rect.height - r, r);
- ctx.lineTo(rect.x, rect.y + r);
- ctx.arcTo(rect.x, rect.y, rect.x + r, rect.y, r);
- ctx.closePath();
- ctx.fill()
+ Behavior on y {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ Behavior on width {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ Behavior on height {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ }
+ Shape {
+ anchors.fill: parent
+ layer.enabled: true
+ layer.samples: 4
+ layer.smooth: true
+ ShapePath {
+ fillColor: "#88000000"
+ strokeWidth: 0
+ strokeColor: "transparent"
+
+ // draw background
+ PathMove {
+ x: 0
+ y: 0
+ }
+ PathLine {
+ x: control.width
+ y: 0
+ }
+ PathLine {
+ x: control.width
+ y: control.height
+ }
+ PathLine {
+ x: 0
+ y: control.height
+ }
+ PathLine {
+ x: 0
+ y: 0
+ }
+
+ // draw highlight
+ PathMove {
+ x: targetRect.x + control.targetRadius
+ y: targetRect.y
+ }
+ PathLine {
+ x: targetRect.x + targetRect.width - control.targetRadius
+ y: targetRect.y
+ }
+ PathArc {
+ x: targetRect.x + targetRect.width
+ y: targetRect.y + control.targetRadius
+ radiusX: control.targetRadius
+ radiusY: control.targetRadius
+ useLargeArc: false
+ direction: PathArc.Clockwise
+ }
+
+ PathLine {
+ x: targetRect.x + targetRect.width
+ y: targetRect.y + targetRect.height - control.targetRadius
+ }
+ PathArc {
+ x: targetRect.x + targetRect.width - control.targetRadius
+ y: targetRect.y + targetRect.height
+ radiusX: control.targetRadius
+ radiusY: control.targetRadius
+ useLargeArc: false
+ direction: PathArc.Clockwise
+ }
+
+ PathLine {
+ x: targetRect.x + control.targetRadius
+ y: targetRect.y + targetRect.height
+ }
+ PathArc {
+ x: targetRect.x
+ y: targetRect.y + targetRect.height - control.targetRadius
+ radiusX: control.targetRadius
+ radiusY: control.targetRadius
+ useLargeArc: false
+ direction: PathArc.Clockwise
+ }
+
+ PathLine {
+ x: targetRect.x
+ y: targetRect.y + control.targetRadius
+ }
+ PathArc {
+ x: targetRect.x + control.targetRadius
+ y: targetRect.y
+ radiusX: control.targetRadius
+ radiusY: control.targetRadius
+ useLargeArc: false
+ direction: PathArc.Clockwise
+ }
}
}
FluFrame{
@@ -151,6 +271,18 @@ Popup{
return 0
}
border.width: 0
+ Behavior on x {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ Behavior on y {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
FluShadow{
radius: 5
}
@@ -193,10 +325,21 @@ Popup{
leftMargin: 15
}
}
+ FluLoader{
+ readonly property int total: steps.length
+ readonly property int current: control.index
+ sourceComponent: control.indicator
+ anchors{
+ bottom: parent.bottom
+ left: parent.left
+ bottomMargin: 15
+ leftMargin: 15
+ }
+ }
FluLoader{
id: loader_next
property bool isEnd: control.index === steps.length-1
- sourceComponent: com_next_button
+ sourceComponent: control.nextButton
anchors{
top: text_desc.bottom
topMargin: 10
@@ -207,7 +350,7 @@ Popup{
FluLoader{
id: loader_prev
visible: control.index !== 0
- sourceComponent: com_prev_button
+ sourceComponent: control.prevButton
anchors{
right: loader_next.left
top: loader_next.top
@@ -246,5 +389,17 @@ Popup{
}
return 0
}
+ Behavior on x {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
+ Behavior on y {
+ enabled: d.animationEnabled && FluTheme.animationEnabled
+ NumberAnimation {
+ duration: 167
+ }
+ }
}
}