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

05手写画布实现-鸿蒙PC端Electron开发

欢迎加入开源鸿蒙 PC社区

https://harmonypc.csdn.net/

效果截图

第5篇:手写画布实现

系列教程导航

篇号标题状态
01环境搭建与项目创建
02数据模型与单词仓库
03主入口页面与导航结构
04极速划词页面实现
05手写画布实现本篇
06百度OCR手写识别接入下一篇
07答案比对与反馈UI
08单词切换与底部导航
09词根分解与水印展示
10项目总结与优化方向

源码仓库:https://atomgit.com/qq_33247427/englishProject

一、Canvas 组件基础

1.1 创建 Canvas

ArkUI 提供了Canvas组件用于 2D 绑图,API 与 Web 标准的 Canvas 2D Context 基本一致:

// 1. 创建渲染上下文privatesettings:RenderingContextSettings=newRenderingContextSettings(true);privatecanvasCtx:CanvasRenderingContext2D=newCanvasRenderingContext2D(this.settings);// 2. 在 UI 中使用Canvas(this.canvasCtx).id('speedDictCanvas')// 必须设置 id,后续截图需要.width('100%').height('100%').onReady(()=>{// Canvas 准备就绪,可以开始绑图this.setupBrush();}).onTouch((event:TouchEvent)=>{// 处理触摸事件})
1.2 RenderingContextSettings
newRenderingContextSettings(true)// true = 开启抗锯齿

开启抗锯齿后,线条边缘更平滑,手写体验更好。

1.3 Canvas 的 id 属性

.id('speedDictCanvas')非常重要——后续使用componentSnapshot.get('speedDictCanvas')截图时需要通过这个 id 找到组件。

二、画笔配置

2.1 基本画笔设置
setupBrush(){this.canvasCtx.strokeStyle='#1A1A1A';// 笔色(近黑色)this.canvasCtx.lineWidth=4;// 笔宽 4pxthis.canvasCtx.lineCap='round';// 线条端点圆形this.canvasCtx.lineJoin='round';// 线条连接处圆形}
2.2 各属性效果对比
属性效果
lineCap'butt'方形端点(默认)
lineCap'round'圆形端点(推荐)
lineCap'square'方形但多出半个线宽
lineJoin'miter'尖角连接(默认)
lineJoin'round'圆角连接(推荐)
lineJoin'bevel'斜角连接

对于手写场景,round+round的组合最自然,笔画起止和转折处都是圆润的。

2.3 笔宽选择
场景推荐笔宽说明
英文单词3-5 px字母笔画较细
中文汉字5-8 px笔画需要更粗
签名2-3 px流畅细线

三、触摸绑图(核心:防卡顿)

3.1 错误实现(会越来越卡)

很多教程给出的写法:

// ❌ 错误写法.onTouch((event:TouchEvent)=>{consttouch=event.touches[0];switch(event.type){caseTouchType.Down:this.canvasCtx.beginPath();this.canvasCtx.moveTo(touch.x,touch.y);break;caseTouchType.Move:this.canvasCtx.lineTo(touch.x,touch.y);this.canvasCtx.stroke();// 每次都重绘整条路径!break;caseTouchType.Up:break;}})

问题stroke()会绘制当前 path 中的所有线段。随着 Move 事件不断触发,path 越来越长(可能几百上千个点),每次stroke()都要重绘从起点到当前点的所有线段,导致帧率急剧下降。

3.2 正确实现(每段独立绘制)
// ✅ 正确写法.onTouch((event:TouchEvent)=>{consttouch=event.touches[0];switch(event.type){caseTouchType.Down:this.isDrawing=true;this.canvasCtx.beginPath();this.canvasCtx.moveTo(touch.x,touch.y);break;caseTouchType.Move:if(this.isDrawing){this.canvasCtx.lineTo(touch.x,touch.y);this.canvasCtx.stroke();// 关键:重新开始路径this.canvasCtx.beginPath();this.canvasCtx.moveTo(touch.x,touch.y);}break;caseTouchType.Up:this.isDrawing=false;break;}})

原理:每次 Move 后立即beginPath()+moveTo(),将当前点作为新路径的起点。这样每次stroke()只绘制最新的一小段线条(从上一个点到当前点),性能恒定,不会随书写时间增长而变慢。

3.3 性能对比
方案书写 100 个点后的 stroke 开销书写 1000 个点后
错误写法绘制 100 段线条绘制 1000 段线条
正确写法绘制 1 段线条绘制 1 段线条

四、三层 Stack 结构

4.1 为什么需要三层

默写单词时,画布上需要同时展示:

  1. 白色背景— OCR 截图需要白底才能识别
  2. 水印文字— 显示当前单词供参考
  3. 手写笔迹— 用户的书写内容

如果只用一个 Canvas:

  • Canvas 背景白色 → 水印被遮住
  • Canvas 背景透明 → OCR 截图没有对比度

解决方案:三层 Stack 叠加。

4.2 层级结构
Stack({alignContent:Alignment.TopStart}){// 第 1 层:白色底(最底层)Column().width('100%').height('100%').backgroundColor('#FFFFFF')// 第 2 层:水印文字(中间层)if(this.showWatermark&&this.currentWord!==null){Column({space:12}){Text(this.currentWord.english).fontSize(48).fontColor('#E8EDE0')// 极浅色.fontWeight(FontWeight.Bold).fontStyle(FontStyle.Italic)Text(this.currentWord.phonetic).fontSize(18).fontColor('#D0D8C8')}.width('100%').height('100%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).hitTestBehavior(HitTestMode.Transparent)// 关键!}// 第 3 层:Canvas(最顶层,透明背景)Canvas(this.canvasCtx).id('speedDictCanvas').width('100%').height('100%').backgroundColor(Color.Transparent)// 透明!.onReady(()=>{this.setupBrush();}).onTouch(...)}
4.3 关键属性解析

hitTestBehavior(HitTestMode.Transparent)

水印层在 Canvas 下方,但 Stack 中后面的子元素在上层。如果水印层拦截了触摸事件,Canvas 就收不到了。HitTestMode.Transparent让触摸事件穿透水印层,传递给下方的 Canvas。

等等——Canvas 在最顶层(Stack 中最后声明的在最上面),水印在中间,所以触摸事件先到 Canvas,不需要穿透。那为什么还要加hitTestBehavior

答:防止水印层在某些情况下(如条件渲染重新插入时)意外拦截事件。加上这个属性是防御性编程。

backgroundColor(Color.Transparent)

Canvas 默认背景是白色。设为透明后,Canvas 只显示笔迹,水印文字从下层透出来。

4.4 视觉效果
用户看到的效果: ┌─────────────────────────────┐ │ │ │ electrical │ ← 浅色水印(可见) │ /ɪˈlektrɪkl/ │ │ │ │ ████████ │ ← 用户笔迹(覆盖在水印上方) │ ████ │ │ │ └─────────────────────────────┘

五、清空画布

5.1 clearCanvas 方法
clearCanvas(){constw=this.canvasCtx.width;consth=this.canvasCtx.height;this.canvasCtx.clearRect(0,0,w,h);// 清除所有像素this.setupBrush();// 重新设置画笔this.feedbackText='';// 清除反馈this.showAnswer=false;}
5.2 为什么不用 fillRect 填白?

因为 Canvas 背景是透明的,clearRect会将像素清除为透明,水印自然从下层显示出来。如果用fillRect('#FFFFFF')填白,会遮住水印。

六、componentSnapshot 截图

6.1 截图原理

componentSnapshot.get(id)会将指定 id 的组件及其所有子层渲染为一张 PixelMap 图片。由于我们的 Stack 包含白底 + 水印 + 笔迹,截图结果是一张白底上有笔迹的图片(水印颜色极浅,不影响 OCR)。

import{componentSnapshot}from'@kit.ArkUI';import{image}from'@kit.ImageKit';constpixelMap:image.PixelMap=awaitcomponentSnapshot.get('speedDictCanvas');
6.2 注意事项
  • 截图是异步操作,需要await
  • 截图分辨率取决于设备像素密度(高分屏可能很大)
  • 截图包含 Canvas 的透明背景——但因为 Stack 下面有白底层,最终截图是白底的

七、水印开关

7.1 状态控制
@StateshowWatermark:boolean=true;// 在工具栏中Row({space:6}){Text('水印').fontSize(12).fontColor('#6B7280')Toggle({type:ToggleType.Switch,isOn:this.showWatermark}).selectedColor('#8B9D6B').width(40).height(22).onChange((isOn:boolean)=>{this.showWatermark=isOn;})}
7.2 条件渲染
if(this.showWatermark&&this.currentWord!==null){// 渲染水印}

showWatermark为 false 时,水印层不渲染,用户看到的是纯白画布。

八、画布容器样式

Stack(...).layoutWeight(1)// 占满剩余垂直空间.width('100%').backgroundColor('#FFFFFF').borderRadius(4)// 微圆角.clip(true)// 裁剪超出内容.border({width:1,color:'#E5E7EB'})
  • borderRadius(4):微圆角,不要太圆,保持工具感
  • clip(true):笔迹画到边缘时不会溢出
  • 整个 DictationContent 有padding({ left: 30, right: 30 }),画布距离页面边缘 30px

九、本篇小结

通过本篇教程,我们完成了:

  • 理解了 Canvas 组件的创建和配置
  • 掌握了画笔属性设置(lineCap、lineJoin、lineWidth)
  • 解决了画笔卡顿问题(每段独立 stroke)
  • 实现了三层 Stack 结构(白底 + 水印 + 透明画布)
  • 理解了 hitTestBehavior 触摸穿透机制
  • 实现了清空画布和水印开关
  • 了解了 componentSnapshot 截图原理

下一篇预告

第 6 篇:百度 OCR 手写识别接入— 我们将把画布截图发送到百度手写文字识别 API,获取识别结果,实现"写完即识别"的核心功能。

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

相关文章:

  • 2026年评价高的双法兰伸缩接头/双法兰限位伸缩接头深度厂家推荐 - 行业平台推荐
  • 数据库缓冲池优化:数组翻译技术的原理与实践
  • TestDisk与PhotoRec:免费开源的数据恢复双雄终极指南
  • 14 - AI新物种设计罗盘:从“填表”到“意图瞬移”的六把密钥
  • 纸箱破洞湿水检测数据集3322张VOC+YOLO格式
  • NoFences:你的Windows桌面整理革命,告别杂乱无章的终极方案
  • 通过用量看板直观对比不同模型调用的延迟与花费
  • AI视频工业化革命(Sora 2×TikTok创作闭环全拆解):实测单日产出47条自然流量破10w+视频的私有工作流
  • 国内外AI都搞不定----看来要我出马了
  • UVA10341 Solve It 题解
  • 蜂群协议深度解析:构建高弹性分布式系统的核心原理与实践
  • Day08 用户下单
  • 基于LLM视觉的智能家居自动化:ha-llmvision集成部署与实战指南
  • YoungsDB:为什么它能同时扛住持续写入与高频分析?
  • 别再傻傻分不清了!用Python和NumPy实战理解概率论中的‘相关’与‘独立’
  • AMD NPU加速GPT-2微调:边缘AI训练实战解析
  • 搞定了-----
  • 2026年质量好的江苏球型伸缩接头厂家综合对比分析 - 品牌宣传支持者
  • 3分钟搞定!WarcraftHelper终极指南:让魔兽争霸3在现代电脑上完美运行
  • CRUD 入门:数据的增、查、改、删
  • 湖南防火门技术选型指南:国曼消防工艺解析与新国标验收要点
  • Ai小程序入门06-数据绑定(小白入门:从静态到动态,让页面数据显示得活灵活现)
  • AI教材生成秘籍:利用AI写教材,轻松实现低查重与高质量内容!
  • LeRobot SO-ARM101机械臂教程:三、遥感操作
  • 基于CRICKIT与CircuitPython的蛇形机器人避障项目实践
  • 数据不出本机、全程离线运行,这个AI工具让我告别手动办公
  • AI进阶,韧性必修:从传统灾备到数据韧性“变形记”
  • 15种logo检测数据集9626张VOC+YOLO格式
  • 从图论到解析分子结构的应用:Floyd-Warshall 算法
  • 强化学习如何优化大语言模型:TextRL实战指南