开源 C++ QT QML 开发(七)自定义控件--仪表盘

文章的目的为了记录使用QT QML开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。

相关链接:

开源 C++ QT QML 开发(一)基本介绍

开源 C++ QT QML 开发(二)工程结构

开源 C++ QT QML 开发(三)常用控件

开源 C++ QT QML 开发(四)复杂控件--Listview

开源 C++ QT QML 开发(五)复杂控件--Gridview

开源 C++ QT QML 开发(六)自定义控件--波形图

开源 C++ QT QML 开发(七)自定义控件--仪表盘

推荐链接:

开源 C# 快速开发(一)基础知识

开源 C# 快速开发(二)基础控件

开源 C# 快速开发(三)复杂控件

开源 C# 快速开发(四)自定义控件--波形图

开源 C# 快速开发(五)自定义控件--仪表盘

开源 C# 快速开发(六)自定义控件--圆环

开源 C# 快速开发(七)通讯--串口

开源 C# 快速开发(八)通讯--Tcp服务器端

开源 C# 快速开发(九)通讯--Tcp客户端

开源 C# 快速开发(十)通讯--http客户端

开源 C# 快速开发(十一)线程

开源 C# 快速开发(十二)进程监控

开源 C# 快速开发(十三)进程--管道通讯

开源 C# 快速开发(十四)进程--内存映射

开源 C# 快速开发(十五)进程--windows消息

开源 C# 快速开发(十六)数据库--sqlserver增删改查

本章节主要内容是:仪表盘实现了一个汽车速度表风格自定义控件,参数可设置,数据可实时更新。

1.代码分析

2.所有源码

3.效果演示

一、代码分析

  1. 角度计算函数

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°

  1. 绘图函数分析

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:自动闭合形成三角形

  1. 主窗口控制函数

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 = [ ... ]  // 自定义颜色段
}
  1. 数学原理总结

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。

相关推荐
奔跑吧邓邓子3 小时前
【C++实战(71)】解锁C++音视频开发:FFmpeg从入门到实战
c++·ffmpeg·实战·音视频
笑口常开xpr3 小时前
【c++】面 向 对 象 与 抽 象 数 据 类 型
开发语言·c++·抽象数据类型
tt5555555555553 小时前
嵌入式开发面试八股文详解教程
linux·c++·驱动开发·面试
Flower#3 小时前
【算法】树上启发式合并 (CCPC2020长春 F. Strange Memory)
c++·算法
奔跑吧邓邓子4 小时前
【C++实战(75)】筑牢安全防线,攻克漏洞难题
c++·安全·实战·漏洞
胖咕噜的稞达鸭4 小时前
二叉树进阶面试题:最小栈 栈的压入·弹出序列 二叉树层序遍历
开发语言·c++
wzg20164 小时前
pyqt5 简易入门教程
开发语言·数据库·qt
橘颂TA5 小时前
【剑斩OFFER】算法的暴力美学——将 x 减到零的最小操作数
c++·算法·leetcode·动态规划
拾光Ծ5 小时前
【数据结构】二叉搜索树 C++ 简单实现:增删查改全攻略
数据结构·c++·算法