Qt Quick 之动态旋转刻度盘(无人机中指南针 Demo )
文章目录
- 一、概览
- 二、关键实现技巧拆解
- 1. 文本“下边朝向圆心”的旋转
- 2. 主方向字母与普通数字差异化
- 三、指南针
- 四、UI 与交互增强建议(适用于无人机控制台)
- 五、代码
Gitee 源码: QmlLearningPro ,选择子工程 RotatingDial.pro
QML 其它文章请点击这里: QT QUICK QML 学习笔记
在无人机系统中,“航向”是一个核心的导航维度:你需要知道机头朝向哪里、当前航向与目标的偏差。
本文基于一个用 QtQuick Canvas + 2D 绘制 实现的旋转刻度圆 Demo,逐步拆解其设计原理、文字定向技巧。
一、概览
演示:
功能:
-
每 15° 一个刻度,其中 45° 的(即主方向 N, NE, E…)有加粗,并用字母标出方向(N, NE 等),其他标出角度数字。
-
所有文字都做了“下边朝向圆心”的旋转处理 —— 这样无论圆怎么转,外圈方向标注都是“朝外看”的自然视觉效果。
-
中心有一个固定指针,代表界面参考方向(通常是“机体前方”/北)。
-
模拟的 rotationAngle 每 50ms 递增一点,用来驱动圆整体的旋转(在真机中这会来自磁力计/融合后的航向)。
-
底部显示当前最接近的八个主方向(N, NE, …)。
二、关键实现技巧拆解
1. 文本“下边朝向圆心”的旋转
计算该标签位置向量 (lx, ly)(相对于中心)。
计算从标签指向圆心的向心向量 (-lx, -ly),得到它的角度 angleToCenter = atan2(-ly, -lx)。
文字默认“下边”是朝 +Y 方向,因此需要旋转 angleToCenter - 90°(即 angleToCenter - π/2)使文字底部对齐该向心方向。
通过 Canvas 的 ctx.rotate(…) 做局部变换然后绘制。
这个技巧避免了文字“倒着”或“横着”难以辨认的问题,即使盘在高速旋转,标签方向始终有一种“朝外”的、一致的视觉习惯。
2. 主方向字母与普通数字差异化
代码中用正则
判断是方向字母,对它们赋予不同颜色(例:绿色),使关键方向一目了然。
三、指南针
在真实无人机中,航向(heading)不是靠模拟自增给出的,而是来自一套传感器融合系统。要把这个 UI Demo 和实际传感器融合。
四、UI 与交互增强建议(适用于无人机控制台)
北向锁定切换:用户可以选择“磁北/真北/机头”模式,UI 上文字颜色/标签备注随之变化。
过渡动画:航向突变时用缓动处理防止跳变,避免飞控短时波动造成 UI 抖动。
偏差提示:例如显示“偏离目标航向 X°”,配合目标箭头指示(用另一层小箭头叠加)。
可配置刻度与单位:让用户切换 360°、16 分度、甚至自定义方向标签(比如航线编号)。
五、代码
具体见 QmlLearningPro ,选择子工程 RotatingDial.pro
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.11
import QtQml 2.15Window {id: rootwidth: 600height: 600visible: truecolor: "#202020"title: "旋转的刻度圆(文字下边朝向圆心)"property real rotationAngle: 0 // 当前旋转角度(模拟数据)property real radius: Math.min(width, height) * 0.3// 方向映射(假设 NW 是 315°)readonly property var directionMap: {"0": "N","45": "NE","90": "E","135": "SE","180": "S","225": "SW","270": "W","315": "NW","360": "N"}// 模拟数据:每 50ms 递增一点角度Timer {interval: 50repeat: truerunning: trueonTriggered: {root.rotationAngle = (root.rotationAngle + 0.5) % 360}}// 中心容器Item {id: dialanchors.centerIn: parentwidth: parent.widthheight: parent.heighttransform: Rotation {origin.x: root.width/2origin.y: root.height/2angle: root.rotationAngle}// 圆盘背景 + 刻度Canvas {id: circleCanvasanchors.centerIn: parentwidth: parent.widthheight: parent.heightonPaint: {var ctx = getContext("2d")ctx.reset()ctx.translate(width/2, height/2)// 画外圈ctx.beginPath()ctx.lineWidth = 6ctx.strokeStyle = "#555555"ctx.arc(0,0, root.radius + 10, 0, Math.PI*2)ctx.stroke()// 每 15° 画刻度for (var deg=0; deg<=360; deg += 15) {var rad = (deg - 90) * Math.PI / 180 // 0° 在顶部var innerR = root.radius - 10var outerR = root.radius + 5ctx.beginPath()ctx.lineWidth = (deg % 45 === 0) ? 3 : 1.5ctx.strokeStyle = "#888888"var x1 = Math.cos(rad) * innerRvar y1 = Math.sin(rad) * innerRvar x2 = Math.cos(rad) * outerRvar y2 = Math.sin(rad) * outerRctx.moveTo(x1, y1)ctx.lineTo(x2, y2)ctx.stroke()}// 每 15° 画标签,文字下边朝向圆心for (var deg=0; deg<=360; deg += 15) {var rad = (deg - 90) * Math.PI / 180 // 0° 在顶部var labelR = root.radius + 25var lx = Math.cos(rad) * labelRvar ly = Math.sin(rad) * labelR// 决定要显示什么:方向字母优先var text = ""if (directionMap.hasOwnProperty(deg.toString())) {text = directionMap[deg.toString()]} else if (deg === 360) {text = directionMap["360"]} else {text = deg.toString()}// 文字旋转:使“下边”朝向圆心// 向心向量是 (-lx, -ly),其角度:var angleToCenter = Math.atan2(-ly, -lx) // 弧度// 需要把文字的“下边”(正 y 方向,角度 90°)对齐到这个向量:var rotateRad = angleToCenter - Math.PI/2ctx.save()ctx.translate(lx, ly)ctx.rotate(rotateRad)// 文字样式ctx.font = "bold 14px Sans"ctx.textAlign = "center"ctx.textBaseline = "middle"if (/^[NSEW]{1,2}$/.test(text)) {ctx.fillStyle = "#00ff00" // 方向:绿} else {ctx.fillStyle = "#ffffff" // 数字:白}ctx.fillText(text, 0, 0)ctx.restore()}}onWidthChanged: requestPaint()onHeightChanged: requestPaint()Component.onCompleted: requestPaint()}// 中心指针(可选,标示当前 0 方向)Rectangle {width: 6height: root.radius * 0.6anchors.horizontalCenter: parent.horizontalCenteranchors.verticalCenter: parent.verticalCentercolor: "#ff5555"radius: 3y: parent.height/2 - height}// 当前角度显示(不随着圆盘转动,固定在界面)Text {id: angleLabeltext: "旋转角度: " + rotationAngle.toFixed(1) + "°"color: "#ffffff"font.pixelSize: 16anchors.horizontalCenter: parent.horizontalCenteranchors.top: parent.topanchors.topMargin: 10}}// 外圈当前方向提示Text {id: currentDiranchors.bottom: parent.bottomanchors.horizontalCenter: parent.horizontalCenteranchors.bottomMargin: 10font.pixelSize: 16color: "#ffffff"text: {var normalized = (rotationAngle % 360 + 360) % 360var closest = Math.round(normalized / 45) * 45if (closest === 360) closest = 0var dir = directionMap[closest.toString()] || closest + "°"return "当前最接近方向: " + dir}}
}