文章的目的为了记录使用QT QML开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。
相关链接:
开源 C++ QT QML 开发(四)复杂控件--Listview
开源 C++ QT QML 开发(五)复杂控件--Gridview
推荐链接:
开源 C# 快速开发(十六)数据库--sqlserver增删改查
本章节主要内容是:仪表盘实现了一个汽车速度表风格自定义控件,参数可设置,数据可实时更新。
1.代码分析
2.所有源码
3.效果演示
一、代码分析
- 角度计算函数
angleRange 计算属性
readonly property real angleRange: {
if (endAngle > startAngle) {
return endAngle - startAngle
} else {
return 360 - startAngle + endAngle
}
}
功能:计算仪表盘的总角度范围
逻辑:
正常情况:endAngle - startAngle
跨0度情况:360 - startAngle + endAngle(如135°到45°)
示例:startAngle=135, endAngle=45 → 角度范围 = 270°
valueToAngle(val) 函数
function valueToAngle(val) {
var normalizedValue = (val - minValue) / (maxValue - minValue)
return startAngle + normalizedValue * angleRange
}
功能:将数值转换为对应的角度
计算步骤:
(val - minValue) / (maxValue - minValue) → 归一化到[0,1]
normalizedValue * angleRange → 映射到角度范围
startAngle + ... → 加上起始角度偏移
示例:val=50, min=0, max=100, start=135, angleRange=270
→ normalizedValue = 0.5
→ 角度 = 135 + 0.5 × 270 = 270°
- 绘图函数分析
2.1 背景圆弧绘制
PathAngleArc {
centerX: root.width / 2
centerY: root.height / 2
radiusX: Math.min(root.width, root.height) / 2 - 10
radiusY: Math.min(root.width, root.height) / 2 - 10
startAngle: root.startAngle
sweepAngle: root.angleRange
}
参数说明:
centerX/Y:圆弧中心点(仪表盘中心)
radiusX/Y:半径(取宽高最小值,-10留边距)
startAngle:起始角度
sweepAngle:扫过的角度范围
2.2 阶段颜色段绘制
PathAngleArc {
centerX: root.width / 2
centerY: root.height / 2
radiusX: Math.min(root.width, root.height) / 2 - 25 // 更小的半径
radiusY: Math.min(root.width, root.height) / 2 - 25
startAngle: valueToAngle(modelData.from) // 段起始角度
sweepAngle: valueToAngle(modelData.to) - valueToAngle(modelData.from) // 段角度范围
}
关键点:
半径比背景小15像素,形成环形效果
每个颜色段独立计算起始和结束角度
2.3 刻度生成系统
主刻度生成
transform: Rotation {
origin.x: majorTick.width / 2
origin.y: Math.min(root.width, root.height) / 2 - 5
angle: root.startAngle + index * (root.angleRange / root.scaleMajor)
}
旋转计算:
origin:旋转原点(刻度底部中心)
angle:每个刻度的角度 = 起始角度 + 索引 × 角度间隔
次刻度生成
angle: root.startAngle + index * (root.angleRange / (root.scaleMajor * root.scaleMinor))
间隔计算:总刻度数 = 主刻度数 × 次刻度数
2.4 刻度值定位函数
x: root.width / 2 - width / 2 +
(Math.min(root.width, root.height) / 2 + 20) *
Math.sin((root.startAngle + index * (root.angleRange / root.scaleMajor)) * Math.PI / 180)
y: root.height / 2 - height / 2 -
(Math.min(root.width, root.height) / 2 + 20) *
Math.cos((root.startAngle + index * (root.angleRange / root.scaleMajor)) * Math.PI / 180)
极坐标转换公式:
x = centerX + radius × sin(angle)
y = centerY - radius × cos(angle)(Y轴向下为正)
参数:
半径:Math.min(width,height)/2 + 20(在圆环外20像素)
角度转换为弧度:angle * Math.PI / 1802.5 指针绘制函数
PathLine {
x: root.width / 2 +
(Math.min(root.width, root.height) / 2 - 30) *
Math.sin(valueToAngle(root.value) * Math.PI / 180)
y: root.height / 2 -
(Math.min(root.width, root.height) / 2 - 30) *
Math.cos(valueToAngle(root.value) * Math.PI / 180)
}
PathLine {
x: root.width / 2 - 5 *
Math.cos(valueToAngle(root.value) * Math.PI / 180)
y: root.height / 2 - 5 *
Math.sin(valueToAngle(root.value) * Math.PI / 180)
}
指针三角形计算:
指针尖端:在半径-30的位置
指针尾部点1:从中心向左偏移5像素,垂直于指针方向
指针尾部点2:自动闭合形成三角形
- 主窗口控制函数
3.1 速度控制函数
onClicked: {
currentSpeed = Math.max(0, currentSpeed - 10) // 减速,不低于0
if (dashboardLoader.item) {
dashboardLoader.item.value = currentSpeed // 更新仪表盘
}
}
3.2 预设值函数
onClicked: {
currentSpeed = 50 // 中间值
if (dashboardLoader.item) {
dashboardLoader.item.value = currentSpeed
}
}
3.3 仪表盘加载回调
onLoaded: {
item.value = currentSpeed
item.minValue = 0
item.maxValue = 100
item.title = "阶段速度仪表盘"
item.segments = [ ... ] // 自定义颜色段
}
- 数学原理总结
4.1 极坐标系统
x = centerX + r × cos(θ)
y = centerY + r × sin(θ)
但代码中使用的是:
x = centerX + r × sin(θ) // 因为0°指向右侧
y = centerY - r × cos(θ) // Y轴反转,-cos使0°向上
4.2 线性映射公式
角度 = startAngle + (value - minValue) / (maxValue - minValue) × angleRange
4.3 边界处理
Math.min(width, height):确保圆形适应各种容器
Math.max(0, ...) / Math.min(100, ...):限制速度范围
这个仪表盘通过精密的数学计算实现了完整的极坐标可视化系统,每个组件都基于中心点和半径进行精确定位。
二、所有源码
dashboard.qml文件源码
import QtQuick 2.12
import QtQuick.Shapes 1.12
Item {
id: root
// 可配置属性
property real value: 0 // 当前值
property real minValue: 0 // 最小值
property real maxValue: 100 // 最大值
property real startAngle: 135 // 起始角度
property real endAngle: 45 // 结束角度
property real scaleMajor: 10 // 主刻度数量
property real scaleMinor: 5 // 每个主刻度中的次刻度数量
property string title: "速度" // 标题
// 阶段颜色配置
property var segments: [
{from: 0, to: 30, color: "#91c7ae"},
{from: 30, to: 60, color: "#63869e"},
{from: 60, to: 90, color: "#c23531"}
]
// 文本颜色
property color textColor: "white"
property color scaleColor: "white"
width: 300
height: 300
// 计算角度范围
readonly property real angleRange: {
if (endAngle > startAngle) {
return endAngle - startAngle
} else {
return 360 - startAngle + endAngle
}
}
// 将值转换为角度
function valueToAngle(val) {
var normalizedValue = (val - minValue) / (maxValue - minValue)
return startAngle + normalizedValue * angleRange
}
// 绘制仪表盘背景
Shape {
id: background
anchors.fill: parent
ShapePath {
id: backgroundPath
strokeWidth: 0
fillColor: "transparent"
startX: root.width / 2
startY: root.height / 2
PathAngleArc {
centerX: root.width / 2
centerY: root.height / 2
radiusX: Math.min(root.width, root.height) / 2 - 10
radiusY: Math.min(root.width, root.height) / 2 - 10
startAngle: root.startAngle
sweepAngle: root.angleRange
}
}
}
// 绘制阶段颜色
Repeater {
model: root.segments
Shape {
id: segmentShape
anchors.fill: parent
ShapePath {
strokeWidth: 20
strokeColor: modelData.color
capStyle: ShapePath.FlatCap
fillColor: "transparent"
PathAngleArc {
centerX: root.width / 2
centerY: root.height / 2
radiusX: Math.min(root.width, root.height) / 2 - 25
radiusY: Math.min(root.width, root.height) / 2 - 25
startAngle: valueToAngle(modelData.from)
sweepAngle: valueToAngle(modelData.to) - valueToAngle(modelData.from)
}
}
}
}
// 绘制刻度
Repeater {
model: root.scaleMajor + 1
// 主刻度
Rectangle {
id: majorTick
x: root.width / 2 - width / 2
y: 10
width: 2
height: 10
color: root.scaleColor
transform: Rotation {
origin.x: majorTick.width / 2
origin.y: Math.min(root.width, root.height) / 2 - 5
angle: root.startAngle + index * (root.angleRange / root.scaleMajor)
}
}
}
Repeater {
model: root.scaleMajor * root.scaleMinor
// 次刻度
Rectangle {
id: minorTick
x: root.width / 2 - width / 2
y: 10
width: 1
height: 5
color: root.scaleColor
transform: Rotation {
origin.x: minorTick.width / 2
origin.y: Math.min(root.width, root.height) / 2 - 5
angle: root.startAngle + index * (root.angleRange / (root.scaleMajor * root.scaleMinor))
}
}
}
// 绘制刻度值
Repeater {
model: root.scaleMajor + 1
Text {
text: Math.round(root.minValue + index * (root.maxValue - root.minValue) / root.scaleMajor)
color: root.textColor
font.pixelSize: 12
x: root.width / 2 - width / 2 +
(Math.min(root.width, root.height) / 2 + 20) *
Math.sin((root.startAngle + index * (root.angleRange / root.scaleMajor)) * Math.PI / 180)
y: root.height / 2 - height / 2 -
(Math.min(root.width, root.height) / 2 + 20) *
Math.cos((root.startAngle + index * (root.angleRange / root.scaleMajor)) * Math.PI / 180)
}
}
// 绘制指针
Shape {
id: pointer
anchors.fill: parent
ShapePath {
id: pointerPath
strokeWidth: 0
fillColor: "#fff"
startX: root.width / 2
startY: root.height / 2
PathLine {
x: root.width / 2 +
(Math.min(root.width, root.height) / 2 - 30) *
Math.sin(valueToAngle(root.value) * Math.PI / 180)
y: root.height / 2 -
(Math.min(root.width, root.height) / 2 - 30) *
Math.cos(valueToAngle(root.value) * Math.PI / 180)
}
PathLine {
x: root.width / 2 - 5 *
Math.cos(valueToAngle(root.value) * Math.PI / 180)
y: root.height / 2 - 5 *
Math.sin(valueToAngle(root.value) * Math.PI / 180)
}
}
}
// 中心圆点
Rectangle {
width: 10
height: 10
radius: 5
color: "#fff"
anchors.centerIn: parent
}
// 显示当前值
Text {
id: valueText
text: root.value.toFixed(1)
color: root.textColor
font.pixelSize: 24
font.bold: true
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.verticalCenter
topMargin: 20
}
}
// 显示标题
Text {
text: root.title
color: root.textColor
font.pixelSize: 16
anchors {
horizontalCenter: parent.horizontalCenter
top: valueText.bottom
topMargin: 5
}
}
// 显示单位
Text {
text: "km/h"
color: root.textColor
font.pixelSize: 12
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.bottom
topMargin: -30
}
}
}
main.qml文件源码
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
Window {
id: mainWindow
width: 800
height: 600
visible: true
title: "自定义仪表盘 - 刻度在外"
color: "#1e1e1e"
// 当前速度值
property real currentSpeed: 0
// 定时器,每1秒更新数据
Timer {
id: dataTimer
interval: 1000
running: true
repeat: true
onTriggered: {
console.log("当前速度:", currentSpeed.toFixed(1) + " km/h")
}
}
Column {
anchors.centerIn: parent
spacing: 30
// 仪表盘 - 使用加载器动态加载
Loader {
id: dashboardLoader
source: "dashboard.qml"
width: 350 // 稍微加大以容纳外部刻度
height: 350
onLoaded: {
item.value = currentSpeed
item.minValue = 0
item.maxValue = 100
item.title = "阶段速度仪表盘"
// 可以自定义阶段颜色
item.segments = [
{from: 0, to: 30, color: "#91c7ae"},
{from: 30, to: 60, color: "#63869e"},
{from: 60, to: 90, color: "#c23531"},
{from: 90, to: 100, color: "#d48265"}
]
}
}
// 控制按钮区域
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 20
// 减速按钮
Button {
text: "-"
font.pixelSize: 24
width: 60
height: 60
background: Rectangle {
color: parent.down ? "#555555" : "#333333"
radius: 30
border.color: "#666666"
border.width: 2
}
contentItem: Text {
text: parent.text
font: parent.font
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
currentSpeed = Math.max(0, currentSpeed - 10)
if (dashboardLoader.item) {
dashboardLoader.item.value = currentSpeed
}
}
}
// 当前速度显示
Rectangle {
width: 180
height: 50
color: "transparent"
border.color: "#444444"
border.width: 2
radius: 8
Text {
anchors.centerIn: parent
text: "当前速度: " + currentSpeed.toFixed(1) + " km/h"
color: "white"
font.pixelSize: 16
font.bold: true
}
}
// 加速按钮
Button {
text: "+"
font.pixelSize: 24
width: 60
height: 60
background: Rectangle {
color: parent.down ? "#555555" : "#333333"
radius: 30
border.color: "#666666"
border.width: 2
}
contentItem: Text {
text: parent.text
font: parent.font
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
currentSpeed = Math.min(100, currentSpeed + 10)
if (dashboardLoader.item) {
dashboardLoader.item.value = currentSpeed
}
}
}
}
// 控制面板
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 15
// 重置按钮
Button {
text: "重置"
width: 80
height: 40
background: Rectangle {
color: parent.down ? "#555555" : "#333333"
radius: 5
border.color: "#666666"
border.width: 2
}
contentItem: Text {
text: parent.text
font.pixelSize: 14
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
currentSpeed = 0
if (dashboardLoader.item) {
dashboardLoader.item.value = currentSpeed
}
}
}
// 设置最大值按钮
Button {
text: "最大值"
width: 80
height: 40
background: Rectangle {
color: parent.down ? "#555555" : "#333333"
radius: 5
border.color: "#666666"
border.width: 2
}
contentItem: Text {
text: parent.text
font.pixelSize: 14
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
currentSpeed = 100
if (dashboardLoader.item) {
dashboardLoader.item.value = currentSpeed
}
}
}
// 中间值按钮
Button {
text: "中间值"
width: 80
height: 40
background: Rectangle {
color: parent.down ? "#555555" : "#333333"
radius: 5
border.color: "#666666"
border.width: 2
}
contentItem: Text {
text: parent.text
font.pixelSize: 14
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
currentSpeed = 50
if (dashboardLoader.item) {
dashboardLoader.item.value = currentSpeed
}
}
}
}
}
// 状态显示
Text {
anchors {
bottom: parent.bottom
right: parent.right
margins: 10
}
text: "仪表盘大小: " + dashboardLoader.width + "x" + dashboardLoader.height +
" | 量程: 0-100 | 刻度位置: 外部"
color: "#888888"
font.pixelSize: 12
}
// 说明文本
Text {
anchors {
bottom: parent.bottom
left: parent.left
margins: 10
}
text: "刻度外部显示"
color: "#91c7ae"
font.pixelSize: 12
}
}
三、效果演示
每1s回刷新速度,点击+-,速度会+-10。