Skip to content

Commit ce1a95d

Browse files
committed
[新增QtQuick圆形进度条控件并优化电池控件]: 实现CircularProgressQuick圆形进度条QtQuick组件,提供完整的动画和主题系统,同时修复BatteryQuick控件的布局和动画问题
- **新增CircularProgressQuick控件**: 创建完整的QtQuick圆形进度条组件,包含进度动画、颜色定制、角度调节和多种主题支持,提供丰富的交互体验 - **优化BatteryQuick控件**: 修复电池头部位置偏移问题,移除多余圆角样式,改进动画逻辑防止值更新循环,优化布局适配不同屏幕尺寸 - **完善构建系统**: 在CMakeLists.txt中添加CircularProgressQuick子目录配置,确保新组件正确集成到项目构建流程中 - **更新项目文档**: 在README.md中添加CircularProgressQuick控件介绍和截图展示,保持文档与代码同步更新 - **提供完整示例**: 实现功能丰富的演示程序,包含数值控制、颜色选择、动画调节和快速主题切换,展示控件的全部特性
1 parent 7054c4c commit ce1a95d

9 files changed

Lines changed: 864 additions & 11 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
- 包含快速主题切换功能(经典、暗色、现代)
7676
- <img src="src/CircularProgress/images/circular_progress.png" width="800" alt="圆形进度条控件截图">
7777

78+
### [CircularProgressQuick](src/CircularProgressQuick/) - 圆形进度条控件(QtQuick版本)
79+
80+
- <img src="src/CircularProgressQuick/images/circular_progress.png" width="800" alt="圆形进度条控件截图">
81+
7882
### [Clock](src/Clock/) - 模拟时钟控件
7983

8084
- 支持秒针平滑动画效果

src/BatteryQuick/Battery.qml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,8 @@ Item {
141141
id: rootHead
142142
width: Math.max(5, parent.width * 0.07)
143143
height: Math.max(8, rootBody.height * 0.35)
144-
x: rootBody.width
144+
x: rootBody.width - 1
145145
y: (parent.height - height) / 2
146-
radius: height * 0.15
147146
color: root.borderColor
148147
}
149148

@@ -164,7 +163,9 @@ Item {
164163
if (clampedValue !== value) {
165164
privateData.previousValue = value;
166165
privateData.animationRunning = true;
166+
privateData.updatingFromExternal = true;
167167
value = clampedValue;
168+
privateData.updatingFromExternal = false;
168169
}
169170
}
170171

src/BatteryQuick/Main.qml

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,12 @@ ApplicationWindow {
2323
anchors.margins: 10
2424

2525
// === 顶部:电池显示区域 ===
26-
Item {
26+
Battery {
27+
id: battery
2728
Layout.fillWidth: true
28-
Layout.preferredHeight: 120
29-
30-
Battery {
31-
id: battery
32-
anchors.centerIn: parent
33-
width: 200
34-
height: 80
35-
}
29+
Layout.fillHeight: true
30+
Layout.leftMargin: 100
31+
Layout.rightMargin: 100
3632
}
3733

3834
// === 控制面板区域 ===

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ add_subdirectory(Carousel3DView)
77
# add_subdirectory(Chart)
88
add_subdirectory(CheckableTreeItem)
99
add_subdirectory(CircularProgress)
10+
add_subdirectory(CircularProgressQuick)
1011
add_subdirectory(Clock)
1112
add_subdirectory(DashBoard)
1213
add_subdirectory(GridViewModel)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
qt_add_executable(appCircularProgressQuick main.cc)
2+
target_link_libraries(appCircularProgressQuick PRIVATE Qt::Quick)
3+
4+
qt_add_qml_module(
5+
appCircularProgressQuick
6+
URI
7+
CircularProgressQuick
8+
VERSION
9+
1.0
10+
QML_FILES
11+
Main.qml
12+
CircularProgress.qml
13+
OUTPUT_DIRECTORY
14+
"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/CircularProgressQuick")
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import QtQuick
2+
import QtQuick.Shapes
3+
4+
Item {
5+
id: root
6+
7+
// === 公共属性 ===
8+
property real value: 0
9+
property real minValue: 0
10+
property real maxValue: 100
11+
property real startAngle: 130
12+
property real endAngle: 410
13+
property bool showPercent: true
14+
property string title: "Progress"
15+
16+
// 颜色属性
17+
property color arcColor: "#4da1ff"
18+
property color textColor: "#4da1ff"
19+
property color titleColor: "#505050"
20+
property color baseColor: "#b3b3b3"
21+
property color backgroundColor: "transparent"
22+
23+
// 动画属性
24+
property int animationDuration: 500
25+
26+
// 尺寸属性
27+
property real arcWidth: Math.min(width, height) * 0.1
28+
29+
// === 计算属性 ===
30+
readonly property bool isAnimating: privateData.animationRunning
31+
readonly property real progress: (value - minValue) / (maxValue - minValue)
32+
readonly property real totalAngle: endAngle - startAngle
33+
readonly property real progressAngle: totalAngle * progress
34+
readonly property real currentStartAngle: endAngle - progressAngle
35+
36+
// === 信号定义 === (只定义QML不会自动生成的信号)
37+
signal valueIncreased(real newValue)
38+
signal valueDecreased(real newValue)
39+
signal valueReset
40+
signal animationStarted(real oldValue, real newValue)
41+
signal animationFinished(real value)
42+
43+
// === 私有数据 ===
44+
QtObject {
45+
id: privateData
46+
property bool animationRunning: false
47+
property real previousValue: root.value
48+
property bool updatingFromExternal: false
49+
}
50+
51+
// === 隐式尺寸 ===
52+
implicitWidth: 200
53+
implicitHeight: 200
54+
55+
// === 动画系统 ===
56+
Behavior on value {
57+
id: valueAnimation
58+
enabled: root.animationDuration > 0 && privateData.animationRunning
59+
NumberAnimation {
60+
duration: root.animationDuration
61+
easing.type: Easing.OutCubic
62+
63+
onStarted: {
64+
root.animationStarted(privateData.previousValue, root.value);
65+
}
66+
67+
onStopped: {
68+
privateData.animationRunning = false;
69+
root.animationFinished(root.value);
70+
}
71+
}
72+
}
73+
74+
// === 值变化处理 ===
75+
onValueChanged: {
76+
if (!privateData.updatingFromExternal) {
77+
// 直接设置值,无动画
78+
privateData.previousValue = value;
79+
80+
// 检查数值增减
81+
if (value > privateData.previousValue) {
82+
valueIncreased(value);
83+
} else if (value < privateData.previousValue) {
84+
valueDecreased(value);
85+
}
86+
}
87+
}
88+
89+
// === 外观组件 ===
90+
91+
// 背景
92+
Rectangle {
93+
id: backgroundRect
94+
anchors.fill: parent
95+
color: root.backgroundColor
96+
radius: Math.min(width, height) * 0.5
97+
}
98+
99+
// 使用 Shape 绘制圆弧
100+
Shape {
101+
id: progressShape
102+
anchors.fill: parent
103+
vendorExtensionsEnabled: true
104+
105+
// 背景圆弧
106+
ShapePath {
107+
id: baseArc
108+
strokeColor: root.baseColor
109+
fillColor: "transparent"
110+
strokeWidth: root.arcWidth
111+
capStyle: ShapePath.RoundCap
112+
113+
PathAngleArc {
114+
centerX: root.width / 2
115+
centerY: root.height / 2
116+
radiusX: Math.min(root.width, root.height) / 2 - root.arcWidth / 2
117+
radiusY: Math.min(root.width, root.height) / 2 - root.arcWidth / 2
118+
startAngle: root.startAngle
119+
sweepAngle: root.totalAngle
120+
}
121+
}
122+
123+
// 进度圆弧
124+
ShapePath {
125+
id: progressArc
126+
strokeColor: root.arcColor
127+
fillColor: "transparent"
128+
strokeWidth: root.arcWidth
129+
capStyle: ShapePath.RoundCap
130+
131+
PathAngleArc {
132+
centerX: root.width / 2
133+
centerY: root.height / 2
134+
radiusX: Math.min(root.width, root.height) / 2 - root.arcWidth / 2
135+
radiusY: Math.min(root.width, root.height) / 2 - root.arcWidth / 2
136+
startAngle: root.startAngle
137+
sweepAngle: root.progressAngle
138+
}
139+
}
140+
}
141+
142+
// 数值文本
143+
Text {
144+
id: valueText
145+
anchors.centerIn: parent
146+
anchors.verticalCenterOffset: parent.height * 0.1
147+
text: {
148+
if (root.showPercent) {
149+
var percent = ((root.value - root.minValue) / (root.maxValue - root.minValue)) * 100;
150+
return percent.toFixed(2) + "%";
151+
} else {
152+
return root.value.toFixed(2);
153+
}
154+
}
155+
color: root.textColor
156+
font.pixelSize: Math.min(root.width, root.height) / 10.0
157+
font.bold: true
158+
horizontalAlignment: Text.AlignHCenter
159+
verticalAlignment: Text.AlignVCenter
160+
}
161+
162+
// 标题文本
163+
Text {
164+
id: titleText
165+
anchors {
166+
top: valueText.bottom
167+
horizontalCenter: parent.horizontalCenter
168+
topMargin: Math.min(root.width, root.height) * 0.05
169+
}
170+
text: root.title
171+
color: root.titleColor
172+
font.pixelSize: Math.min(root.width, root.height) / 15.0
173+
horizontalAlignment: Text.AlignHCenter
174+
verticalAlignment: Text.AlignVCenter
175+
}
176+
177+
// === 公共方法 ===
178+
function setValue(newValue) {
179+
var clampedValue = Math.max(root.minValue, Math.min(root.maxValue, newValue));
180+
if (clampedValue !== root.value) {
181+
privateData.previousValue = root.value;
182+
privateData.animationRunning = false;
183+
privateData.updatingFromExternal = true;
184+
root.value = clampedValue;
185+
privateData.updatingFromExternal = false;
186+
}
187+
}
188+
189+
function setValueAnimated(newValue) {
190+
var clampedValue = Math.max(root.minValue, Math.min(root.maxValue, newValue));
191+
if (clampedValue !== root.value) {
192+
privateData.previousValue = root.value;
193+
privateData.animationRunning = true;
194+
privateData.updatingFromExternal = true;
195+
root.value = clampedValue;
196+
privateData.updatingFromExternal = false;
197+
}
198+
}
199+
200+
function increaseValue(increment) {
201+
var actualIncrement = increment || 1.0;
202+
if (actualIncrement <= 0)
203+
return;
204+
205+
var newValue = Math.min(root.maxValue, root.value + actualIncrement);
206+
if (newValue !== root.value) {
207+
root.setValueAnimated(newValue);
208+
}
209+
}
210+
211+
function decreaseValue(decrement) {
212+
var actualDecrement = decrement || 1.0;
213+
if (actualDecrement <= 0)
214+
return;
215+
216+
var newValue = Math.max(root.minValue, root.value - actualDecrement);
217+
if (newValue !== root.value) {
218+
root.setValueAnimated(newValue);
219+
}
220+
}
221+
222+
function reset() {
223+
if (root.value !== root.minValue) {
224+
root.setValueAnimated(root.minValue);
225+
root.valueReset();
226+
}
227+
}
228+
229+
// === 组件完成时初始化 ===
230+
Component.onCompleted: {
231+
// 初始化动画值
232+
privateData.previousValue = root.value;
233+
}
234+
}

0 commit comments

Comments
 (0)