- 示例工程
【免费下载链接】Windows-universal-samples
API samples for the Universal Windows Platform.
本文围绕 Windows-universal-samples 仓库中的 POS 打印机示例(PosPrinter README)展开,系统讲解基于Windows.Devices.PointOfService.PosPrinterAPI 在 UWP 应用中完成收据打印机接入的完整链路:从设备枚举查找、认领(Claim)与启用(Enable),到提交打印作业(文本、位图、条码)、处理切纸时序,再到多应用竞争同一台打印机时的"保留/释放"仲裁。读者读完本文后,将能够独立实现一个可用的 UWP 收据打印功能模块,并理解 PointOfService 设备访问模型的核心机制。
示例概览:它演示了什么
该示例是 UWP 功能示例大集合中的一员,专门演示Windows.Devices.PointOfService.PosPrinterAPI 的典型用法。按 README 的说明,它聚焦四个能力点:
- 查找(Find):发现系统上可用的 POS 打印机设备;
- 认领(Claim)与启用(Enable):独占式认领收据打印机并使其进入可用状态;
- 打印(Print):向已认领的收据打印机提交打印作业;
- 切纸保护(Paper Cutter):确保切纸器不会切到小票上已打印的内容;
- 竞争认领管理(Competing Claims):处理多客户端(多个应用实例)同时对同一台打印机发起认领的竞争场景。
示例工程同时提供了C#(Samples/PosPrinter/cs)与C++/CX(Samples/PosPrinter/cpp)两个实现版本,两者的界面 XAML 是共享的(Samples/PosPrinter/shared),业务逻辑一一对应,方便对照学习两种语言下 WinRT 异步 API 的写法差异。
从代码结构看,整个示例由"一个共享的主页面状态容器 + 三个独立场景页面"构成:
| 场景 | C# 实现 | C++ 实现 | 共享 XAML | 演示内容 |
|---|---|---|---|---|
| 场景一 | Scenario1_FindClaimEnable.xaml.cs | Scenario1_FindClaimEnable.xaml.cpp | Scenario1_FindClaimEnable.xaml | 查找、认领、启用收据打印机 |
| 场景二 | Scenario2_PrintReceipt.xaml.cs | Scenario2_PrintReceipt.xaml.cpp | Scenario2_PrintReceipt.xaml | 打印文本、整张小票、位图与条码 |
| 场景三 | Scenario3_MultipleClaims.xaml.cs | Scenario3_MultipleClaims.xaml.cpp | Scenario3_MultipleClaims.xaml | 对同一打印机发起第二次认领 |
主页面(C# 版 SampleConfiguration.cs、C++ 版 SampleConfiguration.cpp)在MainPage上集中保存了跨场景共享的运行状态:PosPrinter Printer、ClaimedPosPrinter ClaimedPrinter、DeviceInformation deviceInfo,以及"是否为重要交易"标志IsAnImportantTransaction,三个场景页通过它协调工作。
系统要求与构建运行
按 README 的约定,示例的硬性环境要求是Windows 10(可从清单文件进一步确认:C# 与 C++ 的 Package.appxmanifest 均声明TargetDeviceFamily为Windows.Universal,MinVersion="10.0.15063.0",即最低支持 Windows 10 创意者更新)。开发构建需要Visual Studio。
构建步骤(与仓库中其他 UWP 示例一致):
- 如果以 ZIP 方式下载了整个示例集合,务必解压整个压缩包,而不是只解压目标示例所在的文件夹——示例之间共享
SharedContent等公共依赖,解压不完整会导致编译失败; - 启动 Visual Studio,选择File → Open → Project/Solution;
- 进入示例的
Samples\PosPrinter子目录,再进入你偏好的语言子目录(cs或cpp),双击其中的解决方案文件(PosPrinter.sln)打开工程; - 按Ctrl+Shift+B,或选择Build → Build Solution编译。
运行阶段分两种情况:
- 仅部署:选择Build → Deploy Solution;
- 部署并运行:按F5(或Debug → Start Debugging)以调试方式运行;按Ctrl+F5(或Debug → Start Without Debugging)则免调试运行。
需要说明的是,POS 打印机示例的完整功能需要在接入真实收据打印机(或支持 PointOfService 协议的模拟设备)的环境中验证;仅靠模拟器无法观察到真实的打印与切纸行为。
场景一:查找、认领与启用收据打印机
这是其余所有场景的前置条件,界面说明(Scenario1_FindClaimEnable.xaml)明确写着"该场景是其他场景的前提"。页面提供五个控件:Find receipt printer(查找)、Claim and enable(认领并启用)、Retain device复选框(重要交易时保留设备)、Release claim(释放认领)、Release printer(释放打印机)。
第一步:用 DevicePicker 查找设备
查找过程使用系统级设备选择器DevicePicker,而不是自行枚举设备列表:
// 选择一个 POS 打印机设备(C#,见 Scenario1_FindClaimEnable.xaml.cs 的 FindPrinter_Click) DevicePicker devicePicker = new DevicePicker(); devicePicker.Filter.SupportedDeviceSelectors.Add(PosPrinter.GetDeviceSelector()); // 将选择器锚定在 Find 按钮上,使其弹出位置贴近按钮 GeneralTransform ge = FindButton.TransformToVisual(Window.Current.Content as UIElement); Rect rect = ge.TransformBounds(new Rect(0, 0, FindButton.ActualWidth, FindButton.ActualHeight)); DeviceInformation deviceInfo = await devicePicker.PickSingleDeviceAsync(rect);关键 API 是PosPrinter.GetDeviceSelector()——它返回一个 AQS(Advanced Query Syntax)选择器字符串,只筛选出 POS 打印机类设备。拿到DeviceInformation.Id后,通过PosPrinter.FromIdAsync(deviceInfo.Id)建立设备对象。
值得注意的细节:查找动作之前会先调用rootPage.ReleaseAllPrinters()释放上一次持有的打印机,保证每次查找都从干净状态开始;如果用户取消了选择器(deviceInfo == null),则不会创建打印机对象。
第二步:校验设备能力
示例并不直接接受任意 POS 设备,而是先校验其能力:
if (printer != null && printer.Capabilities.Receipt.IsPrinterPresent) { rootPage.Printer = printer; rootPage.NotifyUser("Found receipt printer.", NotifyType.StatusMessage); } else { printer?.Dispose(); // 清理无法使用的设备对象 rootPage.NotifyUser("Please select a device whose printer is present.", NotifyType.ErrorMessage); }Capabilities.Receipt.IsPrinterPresent是PosPrinterCapabilities中收据子模块的能力标志,表明该设备确实带有一个收据打印单元。能力校验失败的设备会被立即Dispose()释放——这是 WinRT 设备对象的标准清理方式,防止句柄泄漏。C++/CX 版本中对应delete printer;(见 Scenario1_FindClaimEnable.xaml.cpp)。
第三步:认领并启用
POS 打印机的访问模型是"独占认领":应用必须通过ClaimPrinterAsync()获得独占所有权后,才能提交打印作业;认领成功后还需EnableAsync()将设备置为可用状态:
rootPage.ClaimedPrinter = await rootPage.Printer.ClaimPrinterAsync(); if (rootPage.ClaimedPrinter == null) { rootPage.NotifyUser("Unable to claim printer", NotifyType.ErrorMessage); } else { // 注册 ReleaseDeviceRequested 事件,以便在其他客户端想抢走打印机时得到通知 rootPage.SubscribeToReleaseDeviceRequested(); if (await rootPage.ClaimedPrinter.EnableAsync()) { rootPage.NotifyUser("Enabled printer.", NotifyType.StatusMessage); } else { rootPage.NotifyUser("Could not enable printer", NotifyType.ErrorMessage); rootPage.ReleaseClaimedPrinter(); } }认领失败(返回null)或启用失败时,示例都会向用户报错,并在启用失败时主动释放认领,避免残留无效状态。
认领后的界面状态机
场景一的UpdateButtons()方法(C# 与 C++ 实现一致)根据"是否忙碌、是否已找到打印机、是否已认领"三组状态,动态启用/禁用五个按钮,直观展示了认领状态流转:
| 状态 | Find | Claim and enable | Release claim | Release printer |
|---|---|---|---|---|
| 忙碌中(异步操作进行中) | 禁用 | 禁用 | 禁用 | 禁用 |
| 未找到打印机 | 启用 | 禁用 | 禁用 | 禁用 |
| 已找到、未认领 | 禁用 | 启用 | 禁用 | 启用 |
| 已认领 | 禁用 | 禁用 | 启用 | 启用 |
竞争通知:ReleaseDeviceRequested 与保留设备
认领成功后,示例立即订阅ClaimedPosPrinter.ReleaseDeviceRequested事件(见 SampleConfiguration.cs)。该事件在其他客户端试图认领这台打印机时触发,收到通知的持有者必须迅速决策:保留或让出。
// 如果勾选了 "Retain device"(重要交易),保留设备;否则让出认领 private async void ClaimedPrinter_ReleaseDeviceRequested(ClaimedPosPrinter sender, PosPrinterReleaseDeviceRequestedEventArgs args) { if (IsAnImportantTransaction) { await sender.RetainDeviceAsync(); } else { await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { NotifyUser("Lost printer claim.", NotifyType.ErrorMessage); ReleaseClaimedPrinter(); }); } }这里体现了 POS 设备共享的核心仲裁机制:RetainDeviceAsync()让当前持有者在指定时间内响应并保留设备(适合正在打印重要交易小票的场景);若持有者选择不保留,则立即在 UI 线程上释放认领并提示"Lost printer claim."。C++/CX 版本通过create_task(sender->RetainDeviceAsync())与Dispatcher->RunAsync完成同样的逻辑(SampleConfiguration.cpp)。
释放逻辑同样有讲究:ReleaseClaimedPrinter()先退订ReleaseDeviceRequested事件再Dispose()并置空引用;ReleaseAllPrinters()则在释放认领之后,进一步Dispose()掉PosPrinter对象本身(SampleConfiguration.cs)。每次状态变化都会触发StateChanged事件,驱动场景页刷新按钮状态。
场景二:向已认领的收据打印机提交打印作业
场景二提供四种打印能力(Scenario2_PrintReceipt.xaml):打印单行文本、打印示例小票、打印位图(Logo)、打印 UPCA 条码。所有入口的第一步都是IsPrinterClaimed()守卫——若尚未认领打印机,提示"Use scenario 1 to find, claim, and enable a receipt printer."。
打印的基本模型是:从ClaimedPrinter.Receipt创建ReceiptPrintJob作业对象 → 向作业追加打印指令 → 调用ExecuteAsync()一次性执行。
打印单行文本
ReceiptPrintJob job = rootPage.ClaimedPrinter.Receipt.CreateJob(); job.PrintLine(PrintLineTextBox.Text); await ExecuteJobAndReportResultAsync(job);PrintLine()打印一行文本并自动换行,是最基础的指令。
打印完整小票并处理切纸时序
示例的"打印示例小票"演示了更接近真实收银场景的流程:一次作业包含两份小票(商户联与顾客联),并且在切纸前补足足够的空行,确保切纸器不会切到已打印的内容:
string receiptString = "======================\n" + "| Sample Header |\n" + "======================\n" + "Item Price\n" + "----------------------\n" + "Books 10.40\n" + "Games 9.60\n" + "----------------------\n" + "Total 20.00\n"; ReceiptPrintJob job = rootPage.ClaimedPrinter.Receipt.CreateJob(); PrintLineFeedAndCutPaper(job, receiptString + GetMerchantFooter()); PrintLineFeedAndCutPaper(job, receiptString + GetCustomerFooter()); await ExecuteJobAndReportResultAsync(job);切纸保护的实现是本节的核心技巧(Scenario2_PrintReceipt.xaml.cs):
private void PrintLineFeedAndCutPaper(ReceiptPrintJob job, string receipt) { // 将多行字符串一次性传给 Print,比多次调用 PrintLine 走纸更平滑 string feedString = ""; for (uint n = 0; n < rootPage.ClaimedPrinter.Receipt.LinesToPaperCut; n++) { feedString += "\n"; } job.Print(receipt + feedString); if (rootPage.Printer.Capabilities.Receipt.CanCutPaper) { job.CutPaper(); } }要点有三:
LinesToPaperCut:ClaimedReceiptPrinter暴露的、从最后一打印行到切纸器之间所需的空行数。示例按该值生成等量换行符,把内容"送"过切纸器刀口后再执行切割;- 多行字符串用
Print()而非多次PrintLine():源码注释明确说明,这样走纸更平滑; CanCutPaper能力门控:只有设备能力报告支持切纸时才调用CutPaper(),避免对无切纸器的设备发送无效指令。
打印位图
打印位图前先开启信函质量模式,再从应用包资源加载图片解码为BitmapFrame,以居中对齐方式打印:
rootPage.ClaimedPrinter.Receipt.IsLetterQuality = true; ReceiptPrintJob job = rootPage.ClaimedPrinter.Receipt.CreateJob(); BitmapFrame logoFrame = await LoadLogoBitmapAsync(); job.PrintBitmap(logoFrame, PosPrinterAlignment.Center); await ExecuteJobAndReportResultAsync(job);LoadLogoBitmapAsync()展示了从ms-appx:///Assets/coffee-logo.png读取应用内图片的完整链路:StorageFile.GetFileFromApplicationUriAsync→OpenReadAsync→BitmapDecoder.CreateAsync→GetFrameAsync(0)(取第一帧)。
打印条码
job.PrintBarcode(BarcodeText.Text, BarcodeSymbologies.Upca, 60, 3, PosPrinterBarcodeTextPosition.Below, PosPrinterAlignment.Center);PrintBarcode()的参数依次为:条码内容、条码符号体系(此处为BarcodeSymbologies.Upca,UPC-A 标准)、条码高度(60)、条码宽度(3)、可读文本位置(Below表示打印在条码下方)、对齐方式(Center)。场景界面中条码文本框默认内容为012345678912(12 位 UPC-A 码)。
作业失败的诊断
ExecuteJobAndReportResultAsync()在所有打印路径中共用。执行失败时,它借助ClaimedReceiptPrinter的一组设备状态属性给出具体原因(Scenario2_PrintReceipt.xaml.cs):
| 状态属性 | 含义 | 提示信息 |
|---|---|---|
IsCartridgeEmpty | 色带/墨盒已空 | 请更换墨盒 |
IsCartridgeRemoved | 墨盒缺失 | 请安装墨盒 |
IsCoverOpen | 机盖打开 | 请关闭机盖 |
IsHeadCleaning | 打印头正在清洗 | 请等待清洗完成 |
IsPaperEmpty | 纸卷用尽 | 请装入新纸卷 |
| 以上皆否 | 未知原因 | 无法打印 |
这份诊断清单直接复用了 WinRT 设备状态 API,是生产级 POS 应用处理打印故障的常见做法。C++/CX 版本(Scenario2_PrintReceipt.xaml.cpp)使用create_task(...).then(...)链式写法实现同样的异步流程,逻辑完全一致。
场景三:管理同一台打印机的竞争认领
场景三验证的是"同一台打印机能否被第二个客户端认领"这一竞争语义(Scenario3_MultipleClaims.xaml.cs):
using (var printer = await PosPrinter.FromIdAsync(rootPage.Printer.DeviceId)) { using (var claimedPrinter = await printer.ClaimPrinterAsync()) { if (claimedPrinter != null) { ClaimResultText.Text = "Claimed the printer."; } else { ClaimResultText.Text = "Did not claim the printer."; } } }该场景通过PosPrinter.FromIdAsync基于场景一已认领打印机的DeviceId重新创建第二个PosPrinter实例,并尝试再次ClaimPrinterAsync()。认领的成败完全取决于场景一的决策:
- 若场景一勾选了Retain device(重要交易),场景一的持有者调用
RetainDeviceAsync()保留设备,场景三的第二次认领失败(返回null); - 若场景一未勾选,持有者主动释放认领,场景三的第二次认领成功。
两个using语句块(C++ 版通过 RAII 析构)保证即使认领失败,临时创建的PosPrinter与ClaimedPosPrinter对象也会被正确Dispose。这个场景直观演示了 PointOfService 的独占性设计:同一时刻只有一方能持有打印机的有效认领,且认领是否让渡由持有者的事件响应决定。
关键实现细节与底层机制
设备能力声明:pointOfService
C# 与 C++ 两份 Package.appxmanifest 都声明了访问 POS 设备所需的系统能力:
<Capabilities> <Capability Name="internetClient" /> <DeviceCapability Name="pointOfService" /> </Capabilities>pointOfService设备能力是使用Windows.Devices.PointOfService命名空间下各类设备(收据打印机、磁条阅读器、钱箱、条码扫描器等)的前提,缺失该声明会导致运行时无法访问设备。示例工程在MinVersion="10.0.15063.0"、MaxVersionTested="10.0.22621.0"的范围内测试通过。
认领生命周期管理
整个示例对设备对象的生命周期管理非常严格,总结为三条原则:
- 认领前校验:
FromIdAsync之后先检查Capabilities.Receipt.IsPrinterPresent,不符合即Dispose(); - 认领后订阅:
ClaimPrinterAsync成功后立即SubscribeToReleaseDeviceRequested()订阅竞争通知,确保任何时刻都能响应被抢占请求; - 释放时退订:
ReleaseClaimedPrinter()先退订事件、再Dispose()、再置空引用并通知 UI 刷新,杜绝悬挂事件与僵尸对象。
跨语言对照
示例的 C# 与 C++/CX 版本在 API 调用上完全同构,仅在异步写法上不同:C# 使用async/await,C++/CX 使用create_task(...).then(...)任务链;对象清理上 C# 用Dispose()(配合using),C++/CX 用delete。对照阅读两份源码(如 C# 场景二 与 C++ 场景二)是学习两种语言 WinRT 异步编程的良好教材。
相关资源与延伸阅读
- 历史版本:本仓库的 archived/PosPrinter 目录保留了该示例的JavaScript(HTML/JS)旧版实现,README 中将其列为相关示例,可作为对照参考;
- API 参考:示例围绕
Windows.Devices.PointOfService命名空间展开,核心类型包括PosPrinter、ClaimedPosPrinter、ClaimedReceiptPrinter、ReceiptPrintJob、PosPrinterCapabilities,以及本示例中实际用到的PosPrinterBarcodeTextPosition、PosPrinterAlignment、BarcodeSymbologies等枚举; - 构建环境:整个 Windows-universal-samples 集合要求 Visual Studio 与 Windows 10 环境,构建前需完整解压以获取共享依赖(本示例依赖
SharedContent目录下的公共模板与资源)。
小结
PosPrinter 示例以三个递进场景完整覆盖了 UWP 收据打印的实战路径:查找 → 认领 → 启用 → 打印(文本/位图/条码)→ 切纸保护 → 竞争仲裁。其中LinesToPaperCut驱动的切纸时序、ReleaseDeviceRequested+RetainDeviceAsync的让渡机制,以及失败时的设备状态诊断,都是真实收银系统中高频使用的工程模式。结合本仓库的 C# 与 C++ 双版本源码逐行研读,即可将这套模式迁移到自己的 UWP 收银、自助终端或票据打印应用中。
- 示例工程
【免费下载链接】Windows-universal-samples
API samples for the Universal Windows Platform.
相关推荐
Windows-universal-samples 之 Printing 示例:UWP 应用打印支持完整实战指南
Windows universal samples 之 Printing 示例:UWP 应用打印支持完整实战指南 导读 本文以 Windows universa
示例工程UWP 应用数据存储实战:Windows-universal-samples 之 ApplicationData 示例深度解析
UWP 应用数据存储实战:Windows universal samples 之 ApplicationData 示例深度解析 导读 本文以 Windows u
示例工程Windows-universal-samples 之 Compression 示例:UWP 数据压缩与解压实战
Windows universal samples 之 Compression 示例:UWP 数据压缩与解压实战 导读 本文围绕 Windows univers
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考