☰
Windows-universal-samples 之 PosPrinter 示例深度解析:UWP 收据打印机的查找、认领、打印与多客户端竞争管理实战
2026/9/25 12:28:48 网站建设 项目流程
  • 示例工程

【免费下载链接】Windows-universal-samples

API samples for the Universal Windows Platform.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载

本文围绕 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.csScenario1_FindClaimEnable.xaml.cppScenario1_FindClaimEnable.xaml查找、认领、启用收据打印机
场景二Scenario2_PrintReceipt.xaml.csScenario2_PrintReceipt.xaml.cppScenario2_PrintReceipt.xaml打印文本、整张小票、位图与条码
场景三Scenario3_MultipleClaims.xaml.csScenario3_MultipleClaims.xaml.cppScenario3_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 示例一致):

  1. 如果以 ZIP 方式下载了整个示例集合,务必解压整个压缩包,而不是只解压目标示例所在的文件夹——示例之间共享SharedContent等公共依赖,解压不完整会导致编译失败;
  2. 启动 Visual Studio,选择File → Open → Project/Solution;
  3. 进入示例的Samples\PosPrinter子目录,再进入你偏好的语言子目录(cs或cpp),双击其中的解决方案文件(PosPrinter.sln)打开工程;
  4. 按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++ 实现一致)根据"是否忙碌、是否已找到打印机、是否已认领"三组状态,动态启用/禁用五个按钮,直观展示了认领状态流转:

状态FindClaim and enableRelease claimRelease 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(); } }

要点有三:

  1. LinesToPaperCut:ClaimedReceiptPrinter暴露的、从最后一打印行到切纸器之间所需的空行数。示例按该值生成等量换行符,把内容"送"过切纸器刀口后再执行切割;
  2. 多行字符串用Print()而非多次PrintLine():源码注释明确说明,这样走纸更平滑;
  3. 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"的范围内测试通过。

认领生命周期管理

整个示例对设备对象的生命周期管理非常严格,总结为三条原则:

  1. 认领前校验:FromIdAsync之后先检查Capabilities.Receipt.IsPrinterPresent,不符合即Dispose();
  2. 认领后订阅:ClaimPrinterAsync成功后立即SubscribeToReleaseDeviceRequested()订阅竞争通知,确保任何时刻都能响应被抢占请求;
  3. 释放时退订: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.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载
上一篇:Wazuh 漏洞扫描器 TC-005 测试用例深度解析:软件包扫描、数据清理与全量重扫机制
下一篇:京东抢购助手终极指南:免费开源工具实现自动化抢单

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询