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

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

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

 相关链接:

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

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

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

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

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

推荐链接:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

本章节主要内容是:介绍自定义控件的方法,自定义可设置参数的波形图为例,实现了可配置参数(数据点数量、Y轴范围)、支持多条曲线的波形显示功能,每1s对波形图控件进行数据更新。

1.代码分析

2.所有源码

3.效果演示

一、代码分析1. WaveformChart.qml 详细分析
1.1 属性定义部分
 

// 公共属性 - 可自定义
property int dataCount: 100  // 显示的数据点数量
property real yMin: -1.0     // Y轴最小值
property real yMax: 1.0      // Y轴最大值// 三条曲线的配置
property var curves: [{name: "曲线1",color: "#FF6B9C",data: [],visible: true},// ... 其他曲线
]


功能分析:

dataCount: 控制显示的数据点数量,实现"到达显示数量后,只显示最近的数量的数据"

yMin/yMax: 定义Y轴量程,实现"量程可定义"

curves: 数组定义三条曲线,每条曲线包含名称、颜色、数据数组和可见性,实现"3条曲线,颜色可定义"


1.2 坐标系统属性
 

// 内边距 - 为坐标轴留出空间
property real paddingLeft: 60
property real paddingRight: 30
property real paddingTop: 40
property real paddingBottom: 50// 计算实际绘图区域
property real plotWidth: width - paddingLeft - paddingRight
property real plotHeight: height - paddingTop - paddingBottom
property real xScale: plotWidth / Math.max(1, dataCount - 1)


功能分析:

内边距系统确保坐标轴标签不被裁剪

plotWidth/plotHeight: 计算实际可用于绘制波形的区域

xScale: 根据数据点数量计算X轴的缩放比例

1.3 数据管理函数
addDataPoint(index, value) 函数

function addDataPoint(index, value) {if (index < 0 || index >= curves.length) {console.warn("曲线索引超出范围")return}var curve = curves[index]curve.data.push(value)// 保持数据数量不超过设定值if (curve.data.length > dataCount) {curve.data.shift() // 移除最旧的数据}canvas.requestPaint()
}


详细分析:

参数验证: 检查曲线索引是否有效

数据添加: 使用 push() 将新数据添加到数组末尾

数据限制: 当数据量超过 dataCount 时,使用 shift() 移除数组开头的旧数据

重绘请求: 调用 requestPaint() 触发波形重绘

addDataPoints(index, values) 函数
 

function addDataPoints(index, values) {// ... 参数验证var curve = curves[index]curve.data = curve.data.concat(values)// 保持数据数量不超过设定值if (curve.data.length > dataCount) {curve.data = curve.data.slice(-dataCount)}canvas.requestPaint()
}


详细分析:

使用 concat() 批量添加数据

使用 slice(-dataCount) 保留最后 dataCount 个数据点

适用于需要一次性添加多个数据点的场景

clearData() 函数
 

function clearData() {for (var i = 0; i < curves.length; i++) {curves[i].data = []}canvas.requestPaint()
}


详细分析:

遍历所有曲线,清空数据数组

调用重绘以显示空白图表

setCurveVisibility(index, visible) 函数
 

function setCurveVisibility(index, visible) {if (index >= 0 && index < curves.length) {curves[index].visible = visiblecanvas.requestPaint()}
}


详细分析:

设置指定曲线的可见性

在重绘时根据 visible 属性决定是否绘制该曲线

1.4 绘图系统
网格绘制 (gridCanvas)
 

onPaint: {var ctx = getContext("2d")ctx.clearRect(0, 0, width, height)ctx.strokeStyle = gridColorctx.lineWidth = 0.8ctx.beginPath()// 水平网格线var horizontalLines = 5for (var i = 0; i <= horizontalLines; i++) {var y = paddingTop + i * plotHeight / horizontalLinesctx.moveTo(paddingLeft, y)ctx.lineTo(width - paddingRight, y)}// ... 垂直网格线绘制ctx.stroke()
}


详细分析:

clearRect(): 清空画布

计算网格线位置基于内边距和绘图区域

使用循环绘制等间距的网格线

坐标轴绘制 (axisCanvas)
 

// Y轴刻度和标签
var ySteps = 5
ctx.textAlign = "right"
for (var i = 0; i <= ySteps; i++) {var yValue = yMax - (yMax - yMin) * i / yStepsvar yPos = paddingTop + i * plotHeight / ySteps// 绘制刻度线ctx.strokeStyle = axisColorctx.lineWidth = 1ctx.beginPath()ctx.moveTo(paddingLeft - 6, yPos)ctx.lineTo(paddingLeft, yPos)ctx.stroke()// 绘制刻度值ctx.fillText(yValue.toFixed(1), paddingLeft - 12, yPos)
}


详细分析:

计算刻度值:yMax - (yMax - yMin) * i / ySteps

计算刻度位置:paddingTop + i * plotHeight / ySteps

使用 fillText() 绘制刻度标签

toFixed(1) 控制数值显示精度

波形绘制 (canvas)
 

onPaint: {var ctx = getContext("2d")ctx.clearRect(0, 0, width, height)for (var i = 0; i < curves.length; i++) {var curve = curves[i]if (!curve.visible || curve.data.length === 0) continuectx.strokeStyle = curve.colorctx.lineWidth = 2.5ctx.lineJoin = "round"ctx.lineCap = "round"ctx.beginPath()// 计算第一个点的位置var startX = 0var startY = plotHeight - ((curve.data[0] - yMin) / (yMax - yMin)) * plotHeightctx.moveTo(startX, startY)// 绘制曲线for (var j = 1; j < curve.data.length; j++) {var x = j * xScalevar y = plotHeight - ((curve.data[j] - yMin) / (yMax - yMin)) * plotHeight// 限制Y坐标在绘图区域内y = Math.max(0, Math.min(plotHeight, y))ctx.lineTo(x, y)}ctx.stroke()}
}


详细分析:

坐标计算:


y = plotHeight - ((dataValue - yMin) / (yMax - yMin)) * plotHeight
将数据值映射到画布坐标:(dataValue - yMin) / (yMax - yMin) 将数据归一化到 [0,1] 范围

plotHeight - ... 翻转Y轴(画布坐标系Y轴向下)

数据点处理:

ctx.moveTo(startX, startY): 移动到第一个数据点

ctx.lineTo(x, y): 连接到后续数据点

Math.max(0, Math.min(plotHeight, y)): 限制Y坐标在有效范围内

绘图样式:

lineJoin: "round": 线条连接处圆角

lineCap: "round": 线条端点圆角

lineWidth: 2.5: 线条粗细

2. main.qml 详细分析
2.1 数据更新系统
定时器 (timer)
 

Timer {id: timerinterval: 40  // 25Hz 更新,更流畅running: truerepeat: trueonTriggered: {updateWaveformData()}
}


详细分析:

interval: 40: 40毫秒间隔,实现25Hz刷新率

repeat: true: 重复执行

onTriggered: 每次触发时调用 updateWaveformData()

updateWaveformData() 函数
 

function updateWaveformData() {time += 0.08// 正弦波数据var sineValue = Math.sin(time) * 2.0waveformChart.addDataPoint(0, sineValue)// 余弦波数据  var cosineValue = Math.cos(time) * 1.5waveformChart.addDataPoint(1, cosineValue)// 随机信号数据var randomValue = (Math.random() - 0.5) * 3waveformChart.addDataPoint(2, randomValue)
}


详细分析:

time += 0.08: 时间累加,控制波形变化速度

正弦波:Math.sin(time) * 2.0,幅度为2.0

余弦波:Math.cos(time) * 1.5,幅度为1.5

随机信号:(Math.random() - 0.5) * 3,范围[-1.5, 1.5]

2.2 布局系统
 

Column {anchors.fill: parentanchors.margins: 15spacing: 15WaveformChart {id: waveformChartwidth: parent.widthheight: parent.height - controlPanel.height - parent.spacing// ... 配置}Rectangle {id: controlPanelwidth: parent.widthheight: 90// ... 控制面板}
}


详细分析:

使用 Column 布局确保控制面板不会遮挡波形图

height: parent.height - controlPanel.height - parent.spacing: 动态计算波形图高度

这种布局方式解决了控制面板遮挡横坐标的问题

二、所有源码

WaveformChart.qml文件源码

// WaveformChart.qml
import QtQuick 2.12
import QtQuick.Shapes 1.12Item {id: chartRoot// 公共属性 - 可自定义property int dataCount: 100  // 显示的数据点数量property real yMin: -1.0     // Y轴最小值property real yMax: 1.0      // Y轴最大值// 三条曲线的配置property var curves: [{name: "曲线1",color: "#FF6B9C",  // 柔和的粉色data: [],visible: true},{name: "曲线2",color: "#4CDBC4",  // 清新的青绿色data: [],visible: true},{name: "曲线3",color: "#FFD166",  // 明亮的黄色data: [],visible: true}]// 背景属性 - 新的配色方案property color backgroundColor: "#1A1F2B"  // 深蓝灰色property color gridColor: "#2D3748"        // 中等蓝灰色property color axisColor: "#E2E8F0"        // 浅灰色property color textColor: "#CBD5E0"        // 中灰色property bool showGrid: true// 内边距 - 为坐标轴留出空间property real paddingLeft: 60property real paddingRight: 30property real paddingTop: 40property real paddingBottom: 50// 计算实际绘图区域property real plotWidth: width - paddingLeft - paddingRightproperty real plotHeight: height - paddingTop - paddingBottomproperty real xScale: plotWidth / Math.max(1, dataCount - 1)// 添加数据点function addDataPoint(index, value) {if (index < 0 || index >= curves.length) {console.warn("曲线索引超出范围")return}var curve = curves[index]curve.data.push(value)// 保持数据数量不超过设定值if (curve.data.length > dataCount) {curve.data.shift() // 移除最旧的数据}canvas.requestPaint()}// 批量添加数据function addDataPoints(index, values) {if (index < 0 || index >= curves.length) {console.warn("曲线索引超出范围")return}var curve = curves[index]curve.data = curve.data.concat(values)// 保持数据数量不超过设定值if (curve.data.length > dataCount) {curve.data = curve.data.slice(-dataCount)}canvas.requestPaint()}// 清空数据function clearData() {for (var i = 0; i < curves.length; i++) {curves[i].data = []}canvas.requestPaint()}// 设置曲线可见性function setCurveVisibility(index, visible) {if (index >= 0 && index < curves.length) {curves[index].visible = visiblecanvas.requestPaint()}}// 背景Rectangle {id: backgroundanchors.fill: parentcolor: chartRoot.backgroundColorradius: 12// 网格线Canvas {id: gridCanvasanchors.fill: parentvisible: showGridonPaint: {var ctx = getContext("2d")ctx.clearRect(0, 0, width, height)ctx.strokeStyle = gridColorctx.lineWidth = 0.8ctx.beginPath()// 水平网格线var horizontalLines = 5for (var i = 0; i <= horizontalLines; i++) {var y = paddingTop + i * plotHeight / horizontalLinesctx.moveTo(paddingLeft, y)ctx.lineTo(width - paddingRight, y)}// 垂直网格线var verticalLines = 10for (var j = 0; j <= verticalLines; j++) {var x = paddingLeft + j * plotWidth / verticalLinesctx.moveTo(x, paddingTop)ctx.lineTo(x, height - paddingBottom)}ctx.stroke()}}// 坐标轴和刻度Canvas {id: axisCanvasanchors.fill: parentonPaint: {var ctx = getContext("2d")ctx.clearRect(0, 0, width, height)// 设置文本样式ctx.fillStyle = textColorctx.font = "13px Arial"ctx.textAlign = "center"ctx.textBaseline = "middle"// 绘制坐标轴线ctx.strokeStyle = axisColorctx.lineWidth = 1.5ctx.beginPath()// X轴 (底部)ctx.moveTo(paddingLeft, height - paddingBottom)ctx.lineTo(width - paddingRight, height - paddingBottom)// Y轴 (左侧)ctx.moveTo(paddingLeft, paddingTop)ctx.lineTo(paddingLeft, height - paddingBottom)ctx.stroke()// Y轴刻度和标签var ySteps = 5ctx.textAlign = "right"for (var i = 0; i <= ySteps; i++) {var yValue = yMax - (yMax - yMin) * i / yStepsvar yPos = paddingTop + i * plotHeight / ySteps// 绘制刻度线ctx.strokeStyle = axisColorctx.lineWidth = 1ctx.beginPath()ctx.moveTo(paddingLeft - 6, yPos)ctx.lineTo(paddingLeft, yPos)ctx.stroke()// 绘制刻度值ctx.fillText(yValue.toFixed(1), paddingLeft - 12, yPos)}// X轴刻度和标签var xSteps = 8ctx.textAlign = "center"ctx.textBaseline = "top"for (var j = 0; j <= xSteps; j++) {var xPos = paddingLeft + j * plotWidth / xSteps// 绘制刻度线ctx.strokeStyle = axisColorctx.lineWidth = 1ctx.beginPath()ctx.moveTo(xPos, height - paddingBottom)ctx.lineTo(xPos, height - paddingBottom + 6)ctx.stroke()// 绘制刻度值 (显示时间或数据点索引)var labelValue = Math.round(j * dataCount / xSteps)ctx.fillText(labelValue.toString(), xPos, height - paddingBottom + 12)}// 坐标轴标题ctx.textAlign = "center"ctx.font = "bold 14px Arial"// X轴标题 - 上移避免被遮挡ctx.fillText("数据点序列", width / 2, height - 25)// Y轴标题 - 垂直显示ctx.save()ctx.translate(20, height / 2)ctx.rotate(-Math.PI / 2)ctx.fillText("信号幅值", 0, 0)ctx.restore()}}// 波形绘制区域Canvas {id: canvasanchors {left: parent.leftright: parent.righttop: parent.topbottom: parent.bottomleftMargin: paddingLeftrightMargin: paddingRighttopMargin: paddingTopbottomMargin: paddingBottom}onPaint: {var ctx = getContext("2d")ctx.clearRect(0, 0, width, height)// 绘制每条曲线for (var i = 0; i < curves.length; i++) {var curve = curves[i]if (!curve.visible || curve.data.length === 0) continuectx.strokeStyle = curve.colorctx.lineWidth = 2.5ctx.lineJoin = "round"ctx.lineCap = "round"ctx.beginPath()// 计算第一个点的位置var startX = 0var startY = plotHeight - ((curve.data[0] - yMin) / (yMax - yMin)) * plotHeightctx.moveTo(startX, startY)// 绘制曲线for (var j = 1; j < curve.data.length; j++) {var x = j * xScalevar y = plotHeight - ((curve.data[j] - yMin) / (yMax - yMin)) * plotHeight// 限制Y坐标在绘图区域内y = Math.max(0, Math.min(plotHeight, y))ctx.lineTo(x, y)}ctx.stroke()}}}// 图例 - 移到左上角Rectangle {id: legendBackgroundanchors {top: parent.topleft: parent.leftmargins: 15}width: legend.width + 20height: legend.height + 12color: "#60000000"radius: 8border {width: 1color: "#40000000"}Column {id: legendanchors.centerIn: parentspacing: 6padding: 5Repeater {model: curvesRow {spacing: 8Rectangle {width: 18height: 3color: modelData.colorradius: 1.5anchors.verticalCenter: parent.verticalCenter}Text {text: modelData.namecolor: textColorfont {pixelSize: 12}anchors.verticalCenter: parent.verticalCenter}}}}}}
}

main.qml文件源码

// main.qml
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12Window {width: 1200height: 700visible: truetitle: "高级波形图显示器"color: "#0F172A"  // 深蓝色背景// 主布局Column {anchors.fill: parentanchors.margins: 15spacing: 15// 波形图控件WaveformChart {id: waveformChartwidth: parent.widthheight: parent.height - controlPanel.height - parent.spacingdataCount: 200yMin: -2.5yMax: 2.5curves: [{name: "正弦波",color: "#FF6B9C",data: [],visible: true},{name: "余弦波",color: "#4CDBC4",data: [],visible: true},{name: "随机信号",color: "#FFD166",data: [],visible: true}]}// 控制面板 - 现在不会遮挡图表Rectangle {id: controlPanelwidth: parent.widthheight: 90color: "#1E293B"radius: 10Column {anchors.fill: parentanchors.margins: 10spacing: 8// 第一行:主要控制按钮Row {spacing: 15anchors.horizontalCenter: parent.horizontalCenterButton {text: timer.running ? "暂停" : "开始"background: Rectangle {color: timer.running ? "#EF4444" : "#10B981"radius: 18}contentItem: Text {text: parent.textcolor: "white"font.bold: truehorizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenter}onClicked: timer.running = !timer.running}Button {text: "清空数据"background: Rectangle {color: "#6B7280"radius: 18}contentItem: Text {text: parent.textcolor: "white"font.bold: truehorizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenter}onClicked: waveformChart.clearData()}}// 第二行:曲线控制和设置Row {spacing: 20anchors.horizontalCenter: parent.horizontalCenterRepeater {model: waveformChart.curves.lengthRow {spacing: 8Rectangle {width: 16height: 3color: waveformChart.curves[index].colorradius: 1.5anchors.verticalCenter: parent.verticalCenter}Text {text: waveformChart.curves[index].namecolor: "#CBD5E0"font.pixelSize: 12anchors.verticalCenter: parent.verticalCenter}Switch {checked: waveformChart.curves[index].visibleonCheckedChanged: waveformChart.setCurveVisibility(index, checked)}}}// 网格开关Row {spacing: 8Text {text: "显示网格"color: "#CBD5E0"font.pixelSize: 12anchors.verticalCenter: parent.verticalCenter}Switch {checked: waveformChart.showGridonCheckedChanged: waveformChart.showGrid = checked}}}}}}// 状态信息 - 右上角Rectangle {anchors {top: parent.topright: parent.rightmargins: 20}width: statusText.width + 20height: statusText.height + 12color: "#60000000"radius: 6Text {id: statusTextanchors.centerIn: parenttext: "数据点: " + (waveformChart.curves[0].data ? waveformChart.curves[0].data.length : 0) +" | 量程: [" + waveformChart.yMin + ", " + waveformChart.yMax + "]"color: "#94A3B8"font.pixelSize: 12}}// 标题Text {anchors {top: parent.topleft: parent.leftmargins: 20}text: "实时波形监控系统"color: "#F1F5F9"font {pixelSize: 18bold: true}}// 数据更新定时器Timer {id: timerinterval: 40  // 25Hz 更新,更流畅running: truerepeat: trueonTriggered: {updateWaveformData()}}// 时间计数器property real time: 0// 更新波形数据function updateWaveformData() {time += 0.08// 正弦波数据var sineValue = Math.sin(time) * 2.0waveformChart.addDataPoint(0, sineValue)// 余弦波数据var cosineValue = Math.cos(time) * 1.5waveformChart.addDataPoint(1, cosineValue)// 随机信号数据var randomValue = (Math.random() - 0.5) * 3waveformChart.addDataPoint(2, randomValue)}Component.onCompleted: {console.log("高级波形图系统初始化完成")}
}

三、效果演示

http://www.dtcms.com/a/449250.html

相关文章:

  • excel-mcp-server 安装
  • Axios快速上手
  • 中国建网站报价电子商务网站的建设流程图
  • 某单位固态硬盘站点备份至固态硬盘的站点备份方案
  • 高级经济师资源合集
  • 【数据结构】数据结构考研核心:树形查找算法对比与应用场景全指南
  • 做公司网站需要了解哪些东西体育网站的制作哪里可以做
  • 【Docker】解决Docker中“exec format error”错误:架构不匹配的完整指南
  • 如何自己开个网站平台成立网站建设公司要求
  • [嵌入式embed]RT-ThreadStudio-STM32F103C8T6(江协科技)+移植RT-Thread v4.11模版
  • 元宇宙的科幻预言:影视作品中的元宇宙畅想
  • Day07_刷题niuke20251007
  • 厦门湖里区建设局网站深圳宝安美容医院网站建设
  • 人机共生:生成式 AI 重构内容创作的机遇与博弈
  • 正规手表回收网站如何自己制作网站
  • 6.基本查询
  • python高校心理健康服务小程序(源码+文档+调试+基础修改+答疑)
  • vscode code-server
  • MySQL 8.0性能优化终极指南
  • 做网站有什么优势做外贸开店用哪个网站
  • 射频前端国产替代:昂瑞微扮演重要角色
  • 工程师 - 配置Raspberry Pi Pico W 开发板
  • 在vscode下的cmake项目里传参调试c++命令行程序
  • 2025.2.X 版本 IDEA maven 打包乱码问题的解决
  • 应用lbfgs的sklearn神经网络
  • mysql学习---事务
  • 网站建设情况通报公司注册资金需要多少
  • 【C++实战(75)】筑牢安全防线,攻克漏洞难题
  • 西安网站托管排名网站建设技术支持有什么
  • 【OTA升级】英飞凌TC397实现刷写失败回滚(A/B分区)