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

VisualTFT自定义圆形进度条:Canvas绘图与嵌入式GUI开发实践

1. 项目概述与核心价值

最近在做一个工业HMI的项目,客户要求在设备启动自检的界面上,用一个圆环形的进度条来展示自检进度,而不是传统的长条状进度条。他们觉得圆环看起来更“高级”,也更符合他们产品的整体UI风格。接到这个需求,我第一反应就是去翻VisualTFT的控件库,结果发现官方自带的进度条控件只有水平(ProgressBar)和垂直(VProgressBar)两种。这可就有点意思了,官方没提供,但需求又很明确,那就得自己动手“造轮子”了。

这个“自定义圆形进度条”的需求,在工控、智能家居中控屏、医疗仪器面板等嵌入式GUI开发里其实挺常见的。它不仅仅是把形状从矩形变成圆形那么简单,背后涉及到图形绘制、数值映射、动画平滑度、以及如何与底层硬件(比如单片机)高效交互等一系列问题。VisualTFT作为一款优秀的嵌入式UI设计工具,其强大的自定义控件和脚本功能,恰恰给了我们实现这种个性化需求的舞台。今天,我就把自己从零开始,在VisualTFT里实现一个美观、实用、可复用的圆形进度条的全过程,包括设计思路、关键脚本编写、属性封装以及实际调试中遇到的坑,毫无保留地分享出来。无论你是刚接触VisualTFT的新手,还是想深化控件自定义能力的老鸟,相信这篇内容都能给你带来直接的参考价值。

2. 整体设计思路与方案选型

在VisualTFT里实现一个控件,尤其是这种图形化控件,通常有几条路可以走。我们先来拆解一下,并说说我为什么选择了最终这个方案。

2.1 可行性路径分析

  1. 纯位图叠加方案:准备0%-100%共101张圆环进度图片,通过脚本控制显示哪一张。这种方法最简单粗暴,效果也稳定,因为图片是美工做好的。但缺点极其明显:资源占用巨大(101张图片),进度不连续(只能以1%为步进),修改样式(比如颜色、粗细)需要重做所有图片,完全不灵活。PASS。

  2. 使用“仪表”控件模拟:VisualTFT有一个Meter(仪表)控件,本身就是一个圆环或扇形。我们可以将其刻度隐藏,指针改成一个不显示的标记,然后通过设置其值来改变填充区域。这个方法比方案1好,但Meter控件的重点在于模拟仪表盘,其API和属性对于“进度条”这个应用场景来说并不直观,定制填充样式(如渐变色)也比较麻烦。

  3. 使用“画布”控件动态绘制:这是最灵活、最专业,也是我最终采用的方案。VisualTFT提供了Canvas(画布)控件,它就像一块空白的画布,我们可以通过Lua脚本,使用其提供的绘图API(如画弧、画线、填充)在上面动态地绘制出我们想要的任何图形。圆形进度条本质上就是一个不断变长的圆弧,用Canvas来实现再合适不过。

2.2 为什么选择“Canvas动态绘制”方案

选择这个方案,是基于以下几个核心考量:

  • 极致灵活:进度条的宽度、颜色(包括静态色和渐变色)、起始角度、绘制方向(顺时针/逆时针)、是否显示中心文本等,全部可以通过属性或脚本参数控制,无需修改资源。
  • 资源占用极小:只需要一个Canvas控件,几乎不占用额外的Flash存储空间(图片资源),特别适合资源紧张的嵌入式平台。
  • 平滑连续:由于是实时计算并绘制图形,进度可以非常平滑地变化,甚至可以配合定时器实现动画过渡效果。
  • 技能复用:掌握Canvas绘图,就等于掌握了在VisualTFT中创建任何不规则图形控件的能力,价值远超实现一个进度条本身。

这个方案的核心在于编写正确的绘图逻辑。接下来,我们就深入到Canvas控件的脚本中,看看如何用代码“画”出这个圆环。

3. 核心实现:Canvas绘图脚本详解

假设我们在VisualTFT的窗体上放置了一个Canvas控件,命名为CanvasProgress。我们所有的魔法都将发生在它的onPaint事件回调函数里。这个函数会在控件需要重绘时(如初始显示、值改变后)被自动调用。

3.1 绘图坐标与参数计算

在屏幕上绘图,首先要建立坐标系和理解关键参数。Canvas控件的左上角是坐标原点(0,0),向右为x轴正方向,向下为y轴正方向。

绘制一个圆环进度条,我们需要以下几个核心参数:

  • centerX, centerY: 圆环的中心点坐标。通常就是Canvas宽度和高度的一半。
  • radius: 圆环的半径。
  • lineWidth: 圆环的粗细(宽度)。
  • startAngle: 进度条开始的弧度角。数学上,0弧度指向正右方(3点钟方向)。
  • currentAngle: 当前进度对应的弧度角。这由当前进度值currentValue、最大值maxValue和最小值minValue计算得出。
  • colorStart,colorEnd: 如果使用渐变色,这是起始和结束颜色。

让我们在onPaint函数中实现它。首先,我们需要获取或定义这些参数。一种好的实践是将可配置的参数放在脚本的开头,或者通过控件的自定义属性来设置(后面会讲)。这里我们先在脚本内定义。

function CanvasProgress.onPaint(sender, vtx, paintParam) -- 1. 定义配置参数(后续可改为从属性读取) local minValue = 0 local maxValue = 100 local currentValue = 75 -- 示例:当前进度75% local centerX = sender.Width / 2 local centerY = sender.Height / 2 local radius = math.min(centerX, centerY) * 0.8 -- 半径为Canvas大小的80%,留出边距 local lineWidth = radius * 0.2 -- 圆环宽度为半径的20% local startAngle = -math.pi / 2 -- 从顶部(-90度,即12点钟方向)开始,更符合视觉习惯 local endAngle = startAngle + (currentValue - minValue) / (maxValue - minValue) * (2 * math.pi) -- 计算结束弧度 local colorBack = 0xCCCCCC -- 背景圆环颜色(灰色) local colorFore = 0x007ACC -- 前景进度颜色(蓝色) -- 2. 绘制底层背景圆环 vtx:BeginPath() vtx:Arc(centerX, centerY, radius, 0, 2 * math.pi, false) -- 绘制一个完整的圆 vtx:SetLineWidth(lineWidth) vtx:SetStrokeColor(colorBack) vtx:Stroke() -- 3. 绘制上层进度圆环 vtx:BeginPath() -- 开始新路径 -- 绘制圆弧。参数:中心x, 中心y, 半径, 起始角, 结束角, 是否逆时针(false为顺时针) vtx:Arc(centerX, centerY, radius, startAngle, endAngle, false) vtx:SetLineWidth(lineWidth) vtx:SetStrokeColor(colorFore) vtx:Stroke() -- 4. (可选)绘制中心文本 local text = string.format("%d%%", currentValue) vtx:SetFont(sender.FontName, sender.FontSize, vtx.FONT_BOLD) -- 使用控件字体或自定义 vtx:SetTextAlign(vtx.TEXT_ALIGN_CENTER, vtx.TEXT_ALIGN_MIDDLE) vtx:SetFillColor(0x000000) -- 文本颜色黑色 vtx:FillText(text, centerX, centerY) end

关键点解析

  1. vtx:Arc()是绘图的核心。注意角度的单位是弧度,不是角度。2 * math.pi就是一个完整的圆。
  2. 我们绘制了两次:第一次画一个完整的灰色圆环作为背景,第二次根据进度画一个蓝色的圆弧作为前景。它们半径和宽度相同,所以前景会覆盖背景,形成进度效果。
  3. vtx:BeginPath()非常重要。它表示开始一条新的绘制路径。如果不调用,第二次Arc会和第一次的路径连在一起,导致绘制错误。
  4. 进度计算:(currentValue - minValue) / (maxValue - minValue)得到进度比例,再乘以2 * math.pi(整个圆的弧度),得到进度对应的弧度跨度。

3.2 实现渐变色与圆角端点

基础的圆环有了,但产品经理可能想要更炫酷的效果,比如渐变色和圆润的线条端点。VisualTFT的Canvas绘图上下文也支持这些。

添加渐变色: 渐变色需要先创建一个线性或径向渐变对象,然后将其设置为描边或填充样式。

-- 在绘制前景进度圆环的部分替换掉单色设置 -- 创建线性渐变(从进度起点到终点) local gradient = vtx:CreateLinearGradient( centerX + radius * math.cos(startAngle), -- 起点x centerY + radius * math.sin(startAngle), -- 起点y centerX + radius * math.cos(endAngle), -- 终点x centerY + radius * math.sin(endAngle) -- 终点y ) gradient:AddColorStop(0.0, 0xFF0000) -- 0%位置为红色 gradient:AddColorStop(1.0, 0x0000FF) -- 100%位置为蓝色 vtx:BeginPath() vtx:Arc(centerX, centerY, radius, startAngle, endAngle, false) vtx:SetLineWidth(lineWidth) vtx:SetStrokeStyle(gradient) -- 使用渐变样式替代单一颜色 vtx:Stroke()

设置圆角线帽: 默认的线条端点是方形的(vtx.LINE_CAP_BUTT)。要让圆环的末端看起来圆润,可以设置线帽为圆形。

vtx:SetLineCap(vtx.LINE_CAP_ROUND) -- 在调用Stroke之前设置 vtx:Stroke()

这样,进度条的头部(当前进度端点)就会呈现一个半圆形,视觉效果更加柔和。

3.3 封装为可复用的自定义控件

把上面的代码直接写在onPaint里可以工作,但不好复用。最佳实践是创建一个自定义控件。这样我们可以像使用标准控件一样,拖拽它到界面上,然后在属性窗口里设置最小值最大值当前值颜色等,甚至可以在其他项目中直接导入使用。

  1. 创建自定义控件:在VisualTFT的“资源”窗口,右键“自定义控件”->“新建”。命名为CircleProgressBar
  2. 添加自定义属性:在自定义控件的属性编辑器中,添加我们需要的属性,例如:
    • MinValue(整数, 默认0)
    • MaxValue(整数, 默认100)
    • CurrentValue(整数, 默认0)
    • LineWidth(整数, 默认10)
    • StartAngle(浮点数, 默认-90, 单位度, 方便理解)
    • ColorBackground(颜色)
    • ColorForeground(颜色)
    • ShowText(布尔, 是否显示中间百分比文本)
  3. 编写控件的绘制脚本:在自定义控件的onPaint事件中,编写与我们之前类似的脚本,但关键参数不再写死,而是从self(控件对象)的属性中读取。
function self.onPaint(sender, vtx, paintParam) -- 从自定义属性读取值 local minValue = self.MinValue or 0 local maxValue = self.MaxValue or 100 local currentValue = self.CurrentValue or 0 -- 确保当前值在范围内 currentValue = math.max(minValue, math.min(maxValue, currentValue)) local centerX = sender.Width / 2 local centerY = sender.Height / 2 local radius = math.min(centerX, centerY) - (self.LineWidth or 10) / 2 -- 将角度从度转换为弧度 local startAngleRad = math.rad(self.StartAngle or -90) local endAngleRad = startAngleRad + (currentValue - minValue) / (maxValue - minValue) * (2 * math.pi) -- ... 后续绘制代码与之前类似,但颜色等使用 self.ColorBackground, self.ColorForeground ... -- 文本显示根据 self.ShowText 属性判断 end
  1. 提供设置进度的方法:我们还需要一个接口,让外部脚本(比如定时器或数据解析回调)能更新进度。可以在自定义控件中添加一个Lua函数。
-- 在自定义控件的脚本中,可以定义全局函数 function SetProgress(newValue) self.CurrentValue = newValue self:Invalidate() -- 标记控件需要重绘,触发onPaint end

这样,在其他地方就可以用CircleProgressBar.SetProgress(50)来更新进度了。

4. 高级功能与性能优化

一个基本的圆形进度条已经完成。但在实际工业项目中,我们往往需要更多功能和考虑性能。

4.1 动画平滑过渡

直接从0%跳到100%会很生硬。我们可以实现一个平滑的动画过渡。

思路:使用一个Timer(定时器)控件。当目标进度改变时,启动定时器。在定时器的onTimer事件中,让CurrentValue逐步逼近TargetValue,每次逼近都调用Invalidate()重绘。

-- 在自定义控件或窗体脚本中 local targetValue = 0 local animationSpeed = 2 -- 每帧变化的单位值 function StartAnimationTo(newTarget) targetValue = newTarget -- 启动一个间隔50ms的定时器 TimerAnimation.Enabled = true end function TimerAnimation.onTimer(sender) local current = CircleProgressBar.CurrentValue if math.abs(current - targetValue) < 0.5 then -- 接近目标,停止动画 sender.Enabled = false CircleProgressBar.CurrentValue = targetValue CircleProgressBar:Invalidate() else -- 向目标值移动 if current < targetValue then CircleProgressBar.CurrentValue = current + animationSpeed else CircleProgressBar.CurrentValue = current - animationSpeed end CircleProgressBar:Invalidate() end end

注意:动画会频繁触发重绘,对性能有影响。在低性能MCU上需谨慎使用,或降低动画帧率(增大定时器间隔)。

4.2 多段颜色与阈值警示

在工业场景,进度可能代表温度、压力等。我们常常需要根据不同的值域显示不同颜色(如正常蓝色、警告黄色、危险红色)。

实现方法:在onPaint绘制前景圆弧前,根据currentValue判断所在区间,动态决定使用的颜色。

local function getColorByValue(val) if val < 60 then return 0x00CC00 -- 绿色,安全 elseif val < 85 then return 0xFFAA00 -- 黄色,警告 else return 0xFF0000 -- 红色,危险 end end local progressColor = getColorByValue(currentValue) vtx:SetStrokeColor(progressColor)

更复杂的可以配置一个颜色阈值表,实现高度可配置化。

4.3 性能优化要点

在资源受限的嵌入式设备上,GUI绘制是性能热点。

  1. 减少不必要的重绘:只在CurrentValue真正改变时调用Invalidate()。避免在循环或高频定时器中无条件重绘。
  2. 简化绘图操作:如果不需要背景圆环,就不画。如果文本不常变,可以考虑将其画在另一个Label控件上,而不是每次在Canvas里绘制。
  3. 固定大小与坐标:如果Canvas控件大小和位置不变,避免在onPaint中进行复杂的布局计算。可以将centerX,centerY,radius等计算一次后缓存起来(注意,当控件大小改变时需要重新计算,可监听onResize事件)。
  4. 慎用渐变和透明度:渐变色计算和Alpha混合(透明度)会消耗更多CPU资源。如果性能吃紧,优先使用纯色。

5. 常见问题与调试技巧

在实际开发中,你可能会遇到下面这些问题。

5.1 圆环绘制不完整或位置不对

  • 现象:圆环只显示一部分,或者偏离中心。
  • 排查
    1. 检查坐标和半径:确保centerX, centerYsender.Width/2sender.Height/2radius不能大于min(centerX, centerY) - lineWidth/2,否则部分圆弧会画到控件区域外被裁剪。
    2. 检查角度:确认startAngleendAngle的单位是弧度。一个常见错误是直接用了角度值。使用math.rad(角度)进行转换。
    3. 验证Canvas大小:在界面上检查Canvas控件是否被其他控件遮挡,或其WidthHeight属性是否设置正确。

5.2 进度更新后界面无变化

  • 现象:修改了CurrentValue,但屏幕上进度条没动。
  • 排查
    1. 是否调用了Invalidate():修改属性后,必须调用控件的Invalidate()方法来请求重绘。直接改属性值是不会刷新屏幕的。
    2. 作用域问题:确保你修改的是正确的控件对象。在Lua脚本中,使用控件的准确名称(如CanvasProgress.CurrentValue = 50)。
    3. 数值范围:检查CurrentValue是否在MinValueMaxValue之间。我们的绘图计算依赖这个范围。

5.3 自定义控件属性不生效

  • 现象:在属性窗口改了自定义控件的颜色、宽度,但运行时没变化。
  • 排查
    1. 属性读取:在onPaint脚本中,你是否通过self.PropertyName正确读取了自定义属性?属性名必须完全匹配。
    2. 属性默认值:在自定义控件编辑器中,检查属性是否设置了有效的默认值。
    3. 运行时与设计时:有时设计时属性面板的更改需要重新编译或下载工程到模拟器/实机才能生效。确保你执行了完整的“生成代码”->“下载”流程。

5.4 实机运行闪烁或卡顿

  • 现象:在PC模拟器上流畅,下载到真机(如STM32)上画面闪烁或更新很慢。
  • 排查
    1. 帧率过高:检查动画定时器的间隔是否太短。对于许多工控MCU,50ms(20FPS)已经是比较高的要求了,可以尝试调整到100ms甚至200ms。
    2. 绘图复杂度:关闭渐变、圆角等高级效果,看是否改善。如果改善明显,说明需要优化绘图指令或降低效果。
    3. MCU性能与内存:确认目标MCU的Flash和RAM资源是否充足。使用VisualTFT的性能分析工具(如果有)或查看编译报告,了解资源占用情况。
    4. 双缓冲:检查VisualTFT工程或底层GUI库是否开启了双缓冲。双缓冲可以极大减少闪烁。通常这是在工程配置或底层驱动中设置的。

调试技巧

  • 善用“输出窗口”:在关键位置使用print(“当前值:”, currentValue)将变量打印出来,这是最直接的调试方法。
  • 分步绘制:在onPaint函数中,先注释掉绘制前景或背景的代码,只画一样,看是否正确。逐步增加功能,定位问题代码。
  • 模拟器优先:绝大部分逻辑和显示问题都在PC模拟器上解决,再下载到真机调试硬件相关问题,能大大提高效率。

最后,这个自定义圆形进度条控件完成后,它的价值不仅仅在于完成了一个任务。它更是一个模板,你可以基于它轻松修改出“扇形进度条”、“仪表盘指针”、“速度表”等各种各样的自定义图形控件。掌握Canvas绘图和自定义控件封装,你在VisualTFT上的开发能力就上了一个大台阶,面对各种奇葩的UI需求时,心里都会更有底气。

http://www.jsqmd.com/news/864784/

相关文章:

  • 终极指南:如何在Windows 11任务栏上免费显示歌词
  • 市面上有哪些是真正靠谱的降AI率工具(顺利通过高校AIGC审核)
  • Kali与Windows靶机网络连通避坑指南:仅主机模式实操配置
  • 基于FPGA的嵌入式频谱分析仪设计:低功耗实时信号处理方案
  • TypeScript装饰器与元编程实战
  • 武汉地坪施工厂家优选的行业逻辑与武汉顽固地坪工程建设有限公司的专注实践 - 品牌评测官
  • 范式级升级!2026理解生成一体大模型推荐排行 原生统一架构/模态协同/端到端智能 - 极欧测评
  • AI 伦理安全指引 1.0 发布:严控违规智能应用,划定行业伦理安全红线
  • 2026年济南儿童康复与融合教育完全指南:从评估到入园的专业路径 - 企业名录优选推荐
  • Linux下实现Everything级文件搜索:inotify与Shell脚本实战
  • 深入解析Linux内核sk_buff:网络数据包的内存布局与核心操作
  • 微信聊天记录导出终极指南:三步实现数据永久保存
  • 上海鸿泰黄金回收2026年5月变现攻略:金价高位运行,这样卖才不亏 - 润富黄金珠宝行
  • Taotoken用量看板与账单追溯功能带来的成本管理清晰度
  • 告别重复点击疲劳:MouseClick鼠标连点器让你的工作效率翻倍
  • Selenium反爬实战:从WebDriver识别到人类行为模拟
  • 山东一卡通回收最全攻略|2026三种正规渠道、价格行情与操作指南 - 可可收公众号
  • 新手渗透测试实战指南:48小时可控流程与合法边界
  • Selenium浏览器指纹识别原理与分层对抗实战
  • 重磅盘点!企业布局 AI 搜索营销前必看:2026年5月GEO公司排名十强出炉,附选型指南 - 速递信息
  • Unity轻量动画方案:iTween安装避坑与To/By API原理详解
  • 2026招投标行业AI工具深度评测:云境标书AI凭什么问鼎排名前列? - 陈工0237
  • 深入解析Linux内核sk_buff内存布局与核心操作原理
  • 3大核心模块深度解析:Win11Debloat如何让Windows系统重获新生
  • Windows HEIC缩略图扩展:iPhone照片在Windows完美预览终极指南
  • 温州本地黄金回收门店盘点 全城区域均可上门变现 - 润富黄金珠宝行
  • 开源依赖引发线上性能风暴:JVM内存泄漏排查与解决方案
  • 数控双头打孔机怎么选?2026行业趋势与选型避坑指南 - 品牌优选官
  • 解决.net 7.0接入 Sqlserver 2008R2低版本数据库的问题
  • 图文详解Spring Boot整合MyBatis(附源码)