1. 项目概述与核心价值最近在做一个工业HMI的项目客户要求在设备启动自检的界面上用一个圆环形的进度条来展示自检进度而不是传统的长条状进度条。他们觉得圆环看起来更“高级”也更符合他们产品的整体UI风格。接到这个需求我第一反应就是去翻VisualTFT的控件库结果发现官方自带的进度条控件只有水平ProgressBar和垂直VProgressBar两种。这可就有点意思了官方没提供但需求又很明确那就得自己动手“造轮子”了。这个“自定义圆形进度条”的需求在工控、智能家居中控屏、医疗仪器面板等嵌入式GUI开发里其实挺常见的。它不仅仅是把形状从矩形变成圆形那么简单背后涉及到图形绘制、数值映射、动画平滑度、以及如何与底层硬件比如单片机高效交互等一系列问题。VisualTFT作为一款优秀的嵌入式UI设计工具其强大的自定义控件和脚本功能恰恰给了我们实现这种个性化需求的舞台。今天我就把自己从零开始在VisualTFT里实现一个美观、实用、可复用的圆形进度条的全过程包括设计思路、关键脚本编写、属性封装以及实际调试中遇到的坑毫无保留地分享出来。无论你是刚接触VisualTFT的新手还是想深化控件自定义能力的老鸟相信这篇内容都能给你带来直接的参考价值。2. 整体设计思路与方案选型在VisualTFT里实现一个控件尤其是这种图形化控件通常有几条路可以走。我们先来拆解一下并说说我为什么选择了最终这个方案。2.1 可行性路径分析纯位图叠加方案准备0%-100%共101张圆环进度图片通过脚本控制显示哪一张。这种方法最简单粗暴效果也稳定因为图片是美工做好的。但缺点极其明显资源占用巨大101张图片进度不连续只能以1%为步进修改样式比如颜色、粗细需要重做所有图片完全不灵活。PASS。使用“仪表”控件模拟VisualTFT有一个Meter仪表控件本身就是一个圆环或扇形。我们可以将其刻度隐藏指针改成一个不显示的标记然后通过设置其值来改变填充区域。这个方法比方案1好但Meter控件的重点在于模拟仪表盘其API和属性对于“进度条”这个应用场景来说并不直观定制填充样式如渐变色也比较麻烦。使用“画布”控件动态绘制这是最灵活、最专业也是我最终采用的方案。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关键点解析vtx:Arc()是绘图的核心。注意角度的单位是弧度不是角度。2 * math.pi就是一个完整的圆。我们绘制了两次第一次画一个完整的灰色圆环作为背景第二次根据进度画一个蓝色的圆弧作为前景。它们半径和宽度相同所以前景会覆盖背景形成进度效果。vtx:BeginPath()非常重要。它表示开始一条新的绘制路径。如果不调用第二次Arc会和第一次的路径连在一起导致绘制错误。进度计算(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里可以工作但不好复用。最佳实践是创建一个自定义控件。这样我们可以像使用标准控件一样拖拽它到界面上然后在属性窗口里设置最小值、最大值、当前值、颜色等甚至可以在其他项目中直接导入使用。创建自定义控件在VisualTFT的“资源”窗口右键“自定义控件”-“新建”。命名为CircleProgressBar。添加自定义属性在自定义控件的属性编辑器中添加我们需要的属性例如MinValue(整数 默认0)MaxValue(整数 默认100)CurrentValue(整数 默认0)LineWidth(整数 默认10)StartAngle(浮点数 默认-90 单位度 方便理解)ColorBackground(颜色)ColorForeground(颜色)ShowText(布尔 是否显示中间百分比文本)编写控件的绘制脚本在自定义控件的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提供设置进度的方法我们还需要一个接口让外部脚本比如定时器或数据解析回调能更新进度。可以在自定义控件中添加一个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绘制是性能热点。减少不必要的重绘只在CurrentValue真正改变时调用Invalidate()。避免在循环或高频定时器中无条件重绘。简化绘图操作如果不需要背景圆环就不画。如果文本不常变可以考虑将其画在另一个Label控件上而不是每次在Canvas里绘制。固定大小与坐标如果Canvas控件大小和位置不变避免在onPaint中进行复杂的布局计算。可以将centerX,centerY,radius等计算一次后缓存起来注意当控件大小改变时需要重新计算可监听onResize事件。慎用渐变和透明度渐变色计算和Alpha混合透明度会消耗更多CPU资源。如果性能吃紧优先使用纯色。5. 常见问题与调试技巧在实际开发中你可能会遇到下面这些问题。5.1 圆环绘制不完整或位置不对现象圆环只显示一部分或者偏离中心。排查检查坐标和半径确保centerX, centerY是sender.Width/2和sender.Height/2。radius不能大于min(centerX, centerY) - lineWidth/2否则部分圆弧会画到控件区域外被裁剪。检查角度确认startAngle和endAngle的单位是弧度。一个常见错误是直接用了角度值。使用math.rad(角度)进行转换。验证Canvas大小在界面上检查Canvas控件是否被其他控件遮挡或其Width和Height属性是否设置正确。5.2 进度更新后界面无变化现象修改了CurrentValue但屏幕上进度条没动。排查是否调用了Invalidate()修改属性后必须调用控件的Invalidate()方法来请求重绘。直接改属性值是不会刷新屏幕的。作用域问题确保你修改的是正确的控件对象。在Lua脚本中使用控件的准确名称如CanvasProgress.CurrentValue 50。数值范围检查CurrentValue是否在MinValue和MaxValue之间。我们的绘图计算依赖这个范围。5.3 自定义控件属性不生效现象在属性窗口改了自定义控件的颜色、宽度但运行时没变化。排查属性读取在onPaint脚本中你是否通过self.PropertyName正确读取了自定义属性属性名必须完全匹配。属性默认值在自定义控件编辑器中检查属性是否设置了有效的默认值。运行时与设计时有时设计时属性面板的更改需要重新编译或下载工程到模拟器/实机才能生效。确保你执行了完整的“生成代码”-“下载”流程。5.4 实机运行闪烁或卡顿现象在PC模拟器上流畅下载到真机如STM32上画面闪烁或更新很慢。排查帧率过高检查动画定时器的间隔是否太短。对于许多工控MCU50ms20FPS已经是比较高的要求了可以尝试调整到100ms甚至200ms。绘图复杂度关闭渐变、圆角等高级效果看是否改善。如果改善明显说明需要优化绘图指令或降低效果。MCU性能与内存确认目标MCU的Flash和RAM资源是否充足。使用VisualTFT的性能分析工具如果有或查看编译报告了解资源占用情况。双缓冲检查VisualTFT工程或底层GUI库是否开启了双缓冲。双缓冲可以极大减少闪烁。通常这是在工程配置或底层驱动中设置的。调试技巧善用“输出窗口”在关键位置使用print(“当前值”, currentValue)将变量打印出来这是最直接的调试方法。分步绘制在onPaint函数中先注释掉绘制前景或背景的代码只画一样看是否正确。逐步增加功能定位问题代码。模拟器优先绝大部分逻辑和显示问题都在PC模拟器上解决再下载到真机调试硬件相关问题能大大提高效率。最后这个自定义圆形进度条控件完成后它的价值不仅仅在于完成了一个任务。它更是一个模板你可以基于它轻松修改出“扇形进度条”、“仪表盘指针”、“速度表”等各种各样的自定义图形控件。掌握Canvas绘图和自定义控件封装你在VisualTFT上的开发能力就上了一个大台阶面对各种奇葩的UI需求时心里都会更有底气。
VisualTFT自定义圆形进度条:Canvas绘图与嵌入式GUI开发实践
1. 项目概述与核心价值最近在做一个工业HMI的项目客户要求在设备启动自检的界面上用一个圆环形的进度条来展示自检进度而不是传统的长条状进度条。他们觉得圆环看起来更“高级”也更符合他们产品的整体UI风格。接到这个需求我第一反应就是去翻VisualTFT的控件库结果发现官方自带的进度条控件只有水平ProgressBar和垂直VProgressBar两种。这可就有点意思了官方没提供但需求又很明确那就得自己动手“造轮子”了。这个“自定义圆形进度条”的需求在工控、智能家居中控屏、医疗仪器面板等嵌入式GUI开发里其实挺常见的。它不仅仅是把形状从矩形变成圆形那么简单背后涉及到图形绘制、数值映射、动画平滑度、以及如何与底层硬件比如单片机高效交互等一系列问题。VisualTFT作为一款优秀的嵌入式UI设计工具其强大的自定义控件和脚本功能恰恰给了我们实现这种个性化需求的舞台。今天我就把自己从零开始在VisualTFT里实现一个美观、实用、可复用的圆形进度条的全过程包括设计思路、关键脚本编写、属性封装以及实际调试中遇到的坑毫无保留地分享出来。无论你是刚接触VisualTFT的新手还是想深化控件自定义能力的老鸟相信这篇内容都能给你带来直接的参考价值。2. 整体设计思路与方案选型在VisualTFT里实现一个控件尤其是这种图形化控件通常有几条路可以走。我们先来拆解一下并说说我为什么选择了最终这个方案。2.1 可行性路径分析纯位图叠加方案准备0%-100%共101张圆环进度图片通过脚本控制显示哪一张。这种方法最简单粗暴效果也稳定因为图片是美工做好的。但缺点极其明显资源占用巨大101张图片进度不连续只能以1%为步进修改样式比如颜色、粗细需要重做所有图片完全不灵活。PASS。使用“仪表”控件模拟VisualTFT有一个Meter仪表控件本身就是一个圆环或扇形。我们可以将其刻度隐藏指针改成一个不显示的标记然后通过设置其值来改变填充区域。这个方法比方案1好但Meter控件的重点在于模拟仪表盘其API和属性对于“进度条”这个应用场景来说并不直观定制填充样式如渐变色也比较麻烦。使用“画布”控件动态绘制这是最灵活、最专业也是我最终采用的方案。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关键点解析vtx:Arc()是绘图的核心。注意角度的单位是弧度不是角度。2 * math.pi就是一个完整的圆。我们绘制了两次第一次画一个完整的灰色圆环作为背景第二次根据进度画一个蓝色的圆弧作为前景。它们半径和宽度相同所以前景会覆盖背景形成进度效果。vtx:BeginPath()非常重要。它表示开始一条新的绘制路径。如果不调用第二次Arc会和第一次的路径连在一起导致绘制错误。进度计算(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里可以工作但不好复用。最佳实践是创建一个自定义控件。这样我们可以像使用标准控件一样拖拽它到界面上然后在属性窗口里设置最小值、最大值、当前值、颜色等甚至可以在其他项目中直接导入使用。创建自定义控件在VisualTFT的“资源”窗口右键“自定义控件”-“新建”。命名为CircleProgressBar。添加自定义属性在自定义控件的属性编辑器中添加我们需要的属性例如MinValue(整数 默认0)MaxValue(整数 默认100)CurrentValue(整数 默认0)LineWidth(整数 默认10)StartAngle(浮点数 默认-90 单位度 方便理解)ColorBackground(颜色)ColorForeground(颜色)ShowText(布尔 是否显示中间百分比文本)编写控件的绘制脚本在自定义控件的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提供设置进度的方法我们还需要一个接口让外部脚本比如定时器或数据解析回调能更新进度。可以在自定义控件中添加一个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绘制是性能热点。减少不必要的重绘只在CurrentValue真正改变时调用Invalidate()。避免在循环或高频定时器中无条件重绘。简化绘图操作如果不需要背景圆环就不画。如果文本不常变可以考虑将其画在另一个Label控件上而不是每次在Canvas里绘制。固定大小与坐标如果Canvas控件大小和位置不变避免在onPaint中进行复杂的布局计算。可以将centerX,centerY,radius等计算一次后缓存起来注意当控件大小改变时需要重新计算可监听onResize事件。慎用渐变和透明度渐变色计算和Alpha混合透明度会消耗更多CPU资源。如果性能吃紧优先使用纯色。5. 常见问题与调试技巧在实际开发中你可能会遇到下面这些问题。5.1 圆环绘制不完整或位置不对现象圆环只显示一部分或者偏离中心。排查检查坐标和半径确保centerX, centerY是sender.Width/2和sender.Height/2。radius不能大于min(centerX, centerY) - lineWidth/2否则部分圆弧会画到控件区域外被裁剪。检查角度确认startAngle和endAngle的单位是弧度。一个常见错误是直接用了角度值。使用math.rad(角度)进行转换。验证Canvas大小在界面上检查Canvas控件是否被其他控件遮挡或其Width和Height属性是否设置正确。5.2 进度更新后界面无变化现象修改了CurrentValue但屏幕上进度条没动。排查是否调用了Invalidate()修改属性后必须调用控件的Invalidate()方法来请求重绘。直接改属性值是不会刷新屏幕的。作用域问题确保你修改的是正确的控件对象。在Lua脚本中使用控件的准确名称如CanvasProgress.CurrentValue 50。数值范围检查CurrentValue是否在MinValue和MaxValue之间。我们的绘图计算依赖这个范围。5.3 自定义控件属性不生效现象在属性窗口改了自定义控件的颜色、宽度但运行时没变化。排查属性读取在onPaint脚本中你是否通过self.PropertyName正确读取了自定义属性属性名必须完全匹配。属性默认值在自定义控件编辑器中检查属性是否设置了有效的默认值。运行时与设计时有时设计时属性面板的更改需要重新编译或下载工程到模拟器/实机才能生效。确保你执行了完整的“生成代码”-“下载”流程。5.4 实机运行闪烁或卡顿现象在PC模拟器上流畅下载到真机如STM32上画面闪烁或更新很慢。排查帧率过高检查动画定时器的间隔是否太短。对于许多工控MCU50ms20FPS已经是比较高的要求了可以尝试调整到100ms甚至200ms。绘图复杂度关闭渐变、圆角等高级效果看是否改善。如果改善明显说明需要优化绘图指令或降低效果。MCU性能与内存确认目标MCU的Flash和RAM资源是否充足。使用VisualTFT的性能分析工具如果有或查看编译报告了解资源占用情况。双缓冲检查VisualTFT工程或底层GUI库是否开启了双缓冲。双缓冲可以极大减少闪烁。通常这是在工程配置或底层驱动中设置的。调试技巧善用“输出窗口”在关键位置使用print(“当前值”, currentValue)将变量打印出来这是最直接的调试方法。分步绘制在onPaint函数中先注释掉绘制前景或背景的代码只画一样看是否正确。逐步增加功能定位问题代码。模拟器优先绝大部分逻辑和显示问题都在PC模拟器上解决再下载到真机调试硬件相关问题能大大提高效率。最后这个自定义圆形进度条控件完成后它的价值不仅仅在于完成了一个任务。它更是一个模板你可以基于它轻松修改出“扇形进度条”、“仪表盘指针”、“速度表”等各种各样的自定义图形控件。掌握Canvas绘图和自定义控件封装你在VisualTFT上的开发能力就上了一个大台阶面对各种奇葩的UI需求时心里都会更有底气。