- 示例工程
【免费下载链接】Windows-universal-samples
API samples for the Universal Windows Platform.
本文以 Windows-universal-samples 仓库 archived/Ink 目录下的 Inking sample 为对象,系统讲解如何基于Windows.UI.Input.InkingAPI,在 Universal Windows Platform(UWP)应用中用 JavaScript 完成墨迹笔画的渲染、选择、删除、复制粘贴、ISF 格式保存/加载,以及手写识别与识别结果内字符串搜索。读完本文,你将掌握InkManager(含 Reco 识别 API)与InkPresenter两套墨迹编程模型的核心用法,并了解本示例两个场景的完整实现细节。
示例概览:一套代码,两种墨迹编程模型
本示例位于 archived/Ink,是一个基于 WinJS 的 JavaScript UWP 应用,通过 Ink.sln 与 Ink.jsproj 组织工程。示例在 sample-configuration.js 中注册了两个可切换的场景:
- Scenario 1(/html/scenario1.html):演示Ink 与 Reco API,核心对象是
Windows.UI.Input.Inking.InkManager,覆盖渲染、选择、删除、复制粘贴、保存加载、手写识别、识别结果搜索等全部能力; - Scenario 2(/html/scenario2.html):演示Ink Presenter API,核心对象是
InkPresenter及其strokeContainer,提供铅笔、荧光笔、擦除三种笔尖模式,以及保存、加载、清除、撤销功能。
两个场景共用 default.html 作为宿主页面,墨迹相关的工具栏命令(颜色、宽度、模式切换)以 WinJSMenu/Flyout形式集中定义在宿主页面中。
Scenario 1:基于 InkManager 的完整墨迹工作流
Scenario 1 的实现全部位于 scenario1.js,它把InkManager作为墨迹数据的唯一事实来源(single source of truth),所有笔画操作(写入、选择、删除、识别、剪贴板、持久化)都围绕这一实例展开。
三层画布架构与渲染管线
页面 scenario1.html 在同一个canvasGroup容器中叠放了三个<canvas>:
HighlightCanvas(最底层):绘制荧光笔笔画与横线笔记纸背景;InkCanvas(中间层):绘制普通墨迹笔画;SelectCanvas(最顶层):绘制套索(lasso)选择框、擦除轨迹,并承接所有 Pointer 事件。
这种分层设计让墨迹、高亮和选择框可以独立重绘。renderAllStrokes()先清空三层画布,再调用renderPaper()绘制"笔记本"式背景(一条 FireBrick 左边距线 + 每 55px 一条蓝色横线),最后遍历inkManager.getStrokes(),依据drawingAttributes.drawAsHighlighter决定把笔画画到哪一层:
inkManager.getStrokes().forEach(function (stroke) { var att = stroke.drawingAttributes; var color = toColorString(att.color); var width = att.size.width; var hl = att.drawAsHighlighter; var ctx = hl ? hlContext : inkContext; if (stroke.selected) { renderStroke(stroke, color, width * 2, ctx); // 选中态加粗 var stripe = hl ? "Azure" : "White"; var w = width - (hl ? 3 : 1); renderStroke(stroke, stripe, w, ctx); // 中空条纹效果 } else { renderStroke(stroke, color, width, ctx); } });笔画的实际绘制使用stroke.getRenderingSegments()返回的贝塞尔段,通过bezierCurveTo逐段重建笔迹曲线——这正是fitToCurve = true时墨迹被平滑拟合为曲线的可视化结果。
指针事件驱动:从落笔到笔画入库
Pointer 事件统一注册在最顶层的SelectCanvas上(scenario1.js):
handlePointerDown:对 pen 或鼠标左键,调用inkManager.selectWithLine(pt, pt)清空旧选区,然后inkManager.processPointerDown(pt)开启新笔画;若pt.properties.isEraser为真(笔的橡皮端),则临时进入擦除模式;handlePointerMove:把evt.intermediatePoints(自上次移动以来积累的全部采样点)按时间顺序喂给inkManager.processPointerUpdate(pts[i]),同时在本地 2D context 上同步画线,保证"所见即所得";handlePointerUp/handlePointerOut:调用inkManager.processPointerUp(pt)收尾笔画,若当前处于 selecting 模式,用返回的包围矩形定位SelectionBox,最后整体重绘。
这里有一个值得注意的工程细节:代码用全局penID过滤鼠标误触产生的 move/up 事件——因为用户书写时偶尔会带动鼠标,导致产生坐标不一致的鼠标事件流。
绘制属性:颜色、宽度与高亮
墨迹外观由Windows.UI.Input.Inking.InkDrawingAttributes控制。初始化时示例设置了fitToCurve = true并通过inkManager.setDefaultDrawingAttributes(drawingAttributes)写入默认值。
由于 Stroke API 对drawingAttributes只支持"先 get、再改内部字段、最后 put 回去"的操作方式,示例专门封装了shapeStroke()(改宽度)和colorStroke()(改颜色)两个工具函数,并在setDefaults()中把当前 canvas 的lineWidth与strokeStyle同步为InkManager的新笔画默认值。颜色值在 HTML 十六进制字符串与Windows.UI.Color结构之间通过toColorString()/toColorStruct()互相转换(scenario1.js)。
工具栏按钮的 ID 直接映射为参数:墨迹宽度按钮 ID 为IW2~IW10,高亮宽度按钮 ID 为HW10~HW30,颜色按钮 ID 为颜色名,处理器用evt.currentTarget.id解析取值(见 default.html 中的InkWidthFlyout、HighlightWidthFlyout等菜单定义)。
四种操作模式:书写、高亮、选择、擦除
InkManager.mode取InkManipulationMode枚举,示例通过highlightMode()/inkMode()/selectMode()/eraseMode()四个函数切换:
- inking:正常书写或高亮,
context指向inkContext或hlContext; - selecting:
selContext.strokeStyle设为用 images/select.png 平铺出的虚线图案,绘制套索; - erasing:
selContext.strokeStyle设为全透明,套索路径变成擦除轨迹,任何被触碰的笔画整体删除。
对于带橡皮端的笔,示例实现了tempEraseMode():用saveMode()/restoreMode()维护一个深度为 1 的"模式栈",笔尾擦除结束后自动恢复之前的模式(书写/高亮/选择),而工具栏的 Erase 按钮则进入持续擦除模式直到用户切换其他功能。
选择、移动与删除
- 选择:
selectWithLine(pt, pt)用零长度线段清除选区;套索完成后processPointerUp返回选区包围矩形,detachSelection(rect)把一个透明的SelectionBoxdiv 精确覆盖到选中墨迹上; - 移动:
SelectionBox上注册MSGestureChange手势事件,handleSelectionBoxGestureChange中同步调用inkManager.moveSelected({x, y})移动笔画并重绘(scenario1.js); - 删除:
clear()依据anySelected()决定只删选中笔画(deleteSelected())还是全选后删除。
剪贴板:复制与粘贴墨迹
copySelectedToClipboard()把选中笔画(无选中时先selectAll())以 Windows 墨迹剪贴板格式写入系统剪贴板,可粘贴到支持该格式的应用(如 Windows Journal);paste()先通过inkManager.canPasteFromClipboard()检查可用性,再以{x: 100, y: 60}为插入点调用inkManager.pasteFromClipboard(insertionPoint);- 识别候选词也可复制:点击识别结果菜单中的候选按钮,
recoClipboard()用DataPackage.setText()把候选文本放入剪贴板。
ISF 持久化:保存与加载墨迹文件
保存流程(save()+writeInk())使用FileSavePicker,文件类型限定为 GIF(picker.fileTypeChoices.insert("GIF file", [".gif"])),因为 Ink Serialized Format(ISF)数据以元数据形式嵌入 GIF 文件;随后inkManager.saveAsync(saveStream)写入流并在完成后显式close()。
加载流程(load()+readInk())使用FileOpenPicker,起始位置为图片库,扩展名过滤[".gif"],通过inkManager.loadAsync(loadStream)恢复笔画并统计getStrokes().length。代码用openOperationRunning标志防止两个loadAsync并发执行,这正是异步 API 在 WinJS promise 链中常见的防重入模式。
手写识别与识别结果搜索
识别:recognize()调用inkManager.recognizeAsync(InkRecognitionTarget.selected),识别范围有三种模式——selected(仅选中笔画)、remaining(上次识别后新增的笔画)、all(全部笔画)。示例刻意不使用all模式,因为荧光笔笔画参与全量识别会得到大量星号等噪声结果:它先unselectHighlight()排除高亮,若无选中则用selectAllNoHighlight()选中全部非高亮笔画后再按selected模式识别。识别完成后调用inkManager.updateRecognitionResults(results)把结果写回 InkManager,供后续命中测试与搜索使用。
每个InkRecognitionResult代表一个"墨迹词",通过getTextCandidates()可取得最多 5 个候选文本(第 0 个为主候选)。hitTest(tx, ty)遍历inkManager.getRecognitionResults(),用boundingRect做矩形命中测试,命中后弹出RecoFlyout菜单展示该词的候选列表,点击任一候选即复制到剪贴板。
搜索:find()+findWord()实现"在识别结果中搜索字符串"。查找过程对用户输入按空格分词,逐词在所有识别结果的全部候选中做大小写不敏感匹配(alternates[j].toLowerCase() === target),命中后把该词对应笔画的selected置为 true 并高亮重绘。因此即使某个词被误识别(如把 "this" 识别成 "these"),只要 "this" 出现在任一候选中仍可被找到。
识别器选择:setRecognizerByName()遍历inkManager.getRecognizers()按名称查找,并通过setDefaultRecognizer()设为默认;示例在初始化时尝试加载 "Microsoft English (US) Handwriting Recognizer"。
快捷键与交互细节
keypress处理器为整个应用注册了快捷键:Esc依次"剥洋葱"式关闭识别菜单 → 取消选区 → 回到书写模式;Ctrl+C/Ctrl+V/Ctrl+F/Ctrl+O/Ctrl+S/Ctrl+R/Ctrl+Q分别对应复制、粘贴、查找、加载、保存、识别和退出(scenario1.js)。所有按钮与菜单均可通过鼠标、笔或触摸操作。
Scenario 2:基于 InkPresenter 的现代化墨迹编程
Scenario 2 的实现位于 scenario2.js,代表 Windows 10 引入的新一代墨迹 API:不再由应用自行管理笔画数据和事件分发,而是通过 Canvas 的ms-ink上下文直接获取系统托管的InkPresenter。
获取 InkPresenter 并配置输入设备
初始化流程(inkInitialize())先把 canvas 尺寸对齐到实际布局尺寸,然后:
inkContext = inkCanvas.getContext("ms-ink"); inkPresenter = inkContext.msInkPresenter; inkPresenter.inputDeviceTypes = Windows.UI.Core.CoreInputDeviceTypes.mouse | Windows.UI.Core.CoreInputDeviceTypes.pen;inputDeviceTypes显式声明接受鼠标与笔两种输入源,墨迹的采集、渲染全部由系统完成,应用无需再监听 Pointer 事件。
三种笔尖模式与绘制属性
inputProcessingConfiguration.mode取InkInputProcessingMode,可切换书写与擦除:
- 铅笔模式:
InkDrawingAttributes.createForPencil()创建铅笔属性,ignorePressure = false(启用压感)、fitToCurve = true、size = {width: 3, height: 3}; - 荧光笔模式:
drawAsHighlighter = true、penTip = PenTipShape.rectangle(矩形笔尖)、ignorePressure = true、size = {width: 10, height: 10},产生半透明宽笔画效果; - 擦除模式:直接置
mode = InkInputProcessingMode.erasing,笔画级擦除。
属性更新统一走inkPresenter.updateDefaultDrawingAttributes(drawingAttributes);颜色按钮(Black/Blue/Red/Green)则先copyDefaultDrawingAttributes()复制当前默认属性、修改color字段后再整体更新。
strokeContainer:撤销、保存、加载、清除
InkPresenter.strokeContainer是系统托管的笔画容器:
- 撤销:
undo()取getStrokes()最后一笔,置selected = true后deleteSelected(); - 保存/加载:对
ApplicationData.current.localFolder下的strokes.txt文件分别调用strokeContainer.saveAsync(stream)(先flushAsync再关流)和strokeContainer.loadAsync(stream),格式即 ISF; - 清除:
strokeContainer.clear()。
此外,场景 2 针对页面恢复后墨迹可能不刷新的已知问题,在visibilitychange事件中用requestAnimationFrame把 canvas 宽度 +1/-1 强制重绘(scenario2.js),是实战中值得借鉴的兼容处理。
使用说明与操作指南
依据 README 的说明,应用内操作方式如下:
- 使用平板笔绘制墨迹笔画;
- 工具栏提供颜色选择器、线宽选择器和多种功能按钮;
- Erase(擦除)模式:一条擦除轨迹会删除它所触及的全部笔画;若笔带有橡皮端,可用笔尾擦除(回到笔尖即退出擦除模式);
- Select(选择)模式:套索圈中的笔画全部被选中;点击工具栏其他功能即可退出选择模式。墨迹颜色/宽度会应用到所有被选中的普通笔画,高亮颜色/宽度会应用到所有被选中的高亮笔画;
- Recognition(识别)模式:对全部(或仅选中的)笔画执行手写识别,完成后在应用底部查看主候选文本;点击某个词可查看该词的全部候选;使用Find可在识别结果中搜索字符串;
- 按Erase按钮可删除全部(或仅选中)笔画;
- 其他命令:Copy把选中笔画复制到剪贴板,Paste从剪贴板粘贴笔画,Save/Load以 ISF 格式读写图片库(Pictures)或库(Libraries)文件夹中的墨迹文件。
构建与运行
- 若通过 ZIP 下载示例集合,务必解压整个压缩包(而不仅是本示例所在文件夹),以保证共享依赖可用;
- 启动 Microsoft Visual Studio 2017,选择File > Open > Project/Solution;
- 进入解压目录的
archived/Ink/js子文件夹,双击解决方案文件 Ink.sln; - 按
Ctrl+Shift+B或选择Build > Build Solution生成; - 按
F5(Debug > Start Debugging)调试运行,或按Ctrl+F5(Debug > Start Without Debugging)免调试运行。
运行环境要求:客户端 Windows 10、服务端 Windows Server 2016 Technical Preview、手机 Windows 10;示例清单 package.appxmanifest 声明目标设备族为Windows.Universal(MinVersion 10.0.17134.0),能力仅需internetClient。
与当前仓库中其他墨迹示例的关系
本示例是 Windows-universal-samples 墨迹系列的基础参考,仓库中还有两个更现代的墨迹示例可供对照学习:
- SimpleInk:演示基于
InkCanvas+InkPresenter的基础捕获与识别流程(含 C++、C# 多语言实现); - ComplexInk:演示选择、擦除、识别、剪贴板等更复杂的墨迹交互。
三者对比可以清晰看到墨迹 API 的演进脉络:从本示例(archived)中应用自管笔画数据与渲染的InkManager模型,到由系统托管渲染的InkPresenter模型,再到 XAMLInkCanvas控件层面的声明式用法。若需要为现代 UWP 应用引入墨迹功能,建议优先参考 SimpleInk 与 ComplexInk 的活跃版本实现。
- 示例工程
【免费下载链接】Windows-universal-samples
API samples for the Universal Windows Platform.
相关推荐
Windows-universal-samples SimpleInk 指南:用 Windows.UI.Input.Inking 实现 UWP 手写输入、识别与墨迹工具栏
Windows universal samples SimpleInk 指南:用 Windows.UI.Input.Inking 实现 UWP 手写输入、识别与
示例工程Rasa 多副本并行部署时如何配置 ConcurrentRedisLockStore
Rasa 多副本并行部署时如何配置 ConcurrentRedisLockStore 当 Rasa 以多个副本(replicated services)并行运行
示例工程Windows-universal-samples 中的 BasicFaceDetection 示例:使用 FaceDetector 在 UWP 应用中实现静态人脸检测
Windows universal samples 中的 BasicFaceDetection 示例:使用 FaceDetector 在 UWP 应用中实现静态
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考