当前位置: 首页 > news >正文

Qt Quick 之动态旋转刻度盘(无人机中指南针 Demo )

文章目录

  • 一、概览
  • 二、关键实现技巧拆解
    • 1. 文本“下边朝向圆心”的旋转
    • 2. 主方向字母与普通数字差异化
  • 三、指南针
  • 四、UI 与交互增强建议(适用于无人机控制台)
  • 五、代码


Gitee 源码:     QmlLearningPro选择子工程 RotatingDial.pro

QML 其它文章请点击这里:     QT QUICK QML 学习笔记


在无人机系统中,“航向”是一个核心的导航维度:你需要知道机头朝向哪里、当前航向与目标的偏差。

本文基于一个用 QtQuick Canvas + 2D 绘制 实现的旋转刻度圆 Demo,逐步拆解其设计原理、文字定向技巧。

一、概览

演示:

在这里插入图片描述

功能:

  1. 每 15° 一个刻度,其中 45° 的(即主方向 N, NE, E…)有加粗,并用字母标出方向(N, NE 等),其他标出角度数字。

  2. 所有文字都做了“下边朝向圆心”的旋转处理 —— 这样无论圆怎么转,外圈方向标注都是“朝外看”的自然视觉效果。

  3. 中心有一个固定指针,代表界面参考方向(通常是“机体前方”/北)。

  4. 模拟的 rotationAngle 每 50ms 递增一点,用来驱动圆整体的旋转(在真机中这会来自磁力计/融合后的航向)。

  5. 底部显示当前最接近的八个主方向(N, NE, …)。

二、关键实现技巧拆解

1. 文本“下边朝向圆心”的旋转

计算该标签位置向量 (lx, ly)(相对于中心)。

计算从标签指向圆心的向心向量 (-lx, -ly),得到它的角度 angleToCenter = atan2(-ly, -lx)。

文字默认“下边”是朝 +Y 方向,因此需要旋转 angleToCenter - 90°(即 angleToCenter - π/2)使文字底部对齐该向心方向。

通过 Canvas 的 ctx.rotate(…) 做局部变换然后绘制。

这个技巧避免了文字“倒着”或“横着”难以辨认的问题,即使盘在高速旋转,标签方向始终有一种“朝外”的、一致的视觉习惯。

2. 主方向字母与普通数字差异化

代码中用正则 ^[NSEW]{1,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}}
}
http://www.dtcms.com/a/316647.html

相关文章:

  • 400V降24V,200mA,应用领域:从生活到工业的 “全能电源管家”WD5208
  • 华清远见25072班C语言学习day2
  • Z20K118库中寄存器及其库函数封装-CLOCK库
  • Boosting 知识点整理:机制、对比与应用场景
  • TDengine 中 TDgp 中部署时序基础模型
  • Android10 系统休眠调试相关
  • 力扣热题100-------169.多数元素
  • 工作相关: 预刷真值与人工标注的真值之间的关系 以及 真值与原始数据的关系,
  • 站在JS的角度,看鸿蒙中的ArkTs
  • 从汇编角度揭秘C++构造函数(1)
  • 数据安全——解读大数据安全架构设计方案【附全文阅读】
  • 力扣-283.移动零
  • Claude Code实战体验:AI智能编程助手如何重塑开发工作流?
  • MyBatis-Plus主键回填详解:插入数据后自动获取主键值
  • WSN - Wirth syntax notation 沃斯语法符号
  • 模板的进阶
  • JAVA,ThreadLocal
  • 浮动路由和BFD配置
  • 瀑布模型与敏捷开发的选择分析
  • 如何在nuxt项目中使用scss
  • 数据库SQL高阶操作1——Mysql8转换DM8相关sql语法写法优化调整,包括递归,函数,以及方言等特殊操作的改造支持
  • vue2 vue3 区别
  • PostGIS面试题及详细答案120道之 (101-110 )
  • 解决微信小程序中camera组件被view事件穿透触发对焦以及camera的bindtap事件
  • 猜数字游戏 Java
  • 并发编程的三要素是什么
  • Docker Desktop
  • 实战项目3-工控软件-2.0- 自定义控件HMILabel的创建
  • 用 Spark 找出最大值:高性能计算的正确姿势
  • 线性筛和os数组(牛客多校25年#7-G)