feat: Windows PavoApi ffi 直打后端(Phase 3 代码完成,待实机验收)

- lib/print/pavo/:PavoApiW_x64.dll 的 dart:ffi 绑定(结构体布局对齐 C#,W 后缀导出名回退兼容)
- pavo_printer.dart:枚举(UTF-8/UTF-16 自动解码)/状态/色带信息/单卡直打/忙碌轮询(3 分钟超时)/LayerHolder 非托管内存管理
- pavo_backend.dart:全部 ffi 调用与像素转换跑在 Isolate.run,主 isolate HTTP 服务零阻塞
- lib/image/card_image.dart:PNG data URL → 纵横比选卡面 → 手动 alpha 铺白底 → BGR24/Gray8 紧凑像素(image 包 compositeImage 对全透明像素非 src-over 语义,改为逐像素混合对齐 C#)
- windows/ 目录:从 Flutter SDK windows.tmpl 手动实例化(mac 上 flutter create 拒绝生成的绕过),窗口标题"饭卡打印助手"
- windows/vendor/:收录 SDK 的 PavoApiW_x64.dll/PavoCardSDK_x64.dll/HeatTB3.dll/heatthcd.bin,CMake POST_BUILD 自动拷到 exe 旁
- backend.dart 工厂接通 Windows 分支;analyze 零问题、单测 12 个全过
This commit is contained in:
dzc committed 2026-09-21 12:19:07 +08:00
1 parent 689a07ca44
commit 0a78ff1b58
28 files changed
+1874 -5

No files matched your search

+15 -2
View File
@@ -32,7 +32,13 @@ lib/
├── print/
│ ├── backend.dart # PrintBackend 抽象 + OS 工厂
│ ├── mock_backend.dart # --mock 模拟(打印 2.5s 完成)
│ └── cups_backend.dart # macOS:lpoptions 发现队列 + lp 提交(参数列表传参防注入)
│ ├── cups_backend.dart # macOS:lpoptions 发现队列 + lp 提交(参数列表传参防注入)
│ └── pavo/ # Windows:HiTi PavoApi ffi 直打
│ ├── pavo_ffi.dart # 结构体/导出绑定(PavoApiW_x64.dll,W 后缀回退兼容)
│ ├── pavo_printer.dart # 枚举/状态/色带/单卡直打/轮询/内存管理(对齐 C# PavoPrinter.cs)
│ └── pavo_backend.dart # 后端实现(全部跑在 Isolate.run,主 isolate 不阻塞)
├── image/
│ └── card_image.dart # PNG data URL → 缩放铺白底 → BGR24/Gray8(对齐 C# CardImage.cs)
├── utils/
│ └── command_runner.dart # 外部命令执行(LC_ALL=C、超时杀进程、参数列表)
bin/agent.dart # headless 入口(开发联调,行为对齐 C# 控制台助手)
@@ -71,6 +77,13 @@ flutter build macos --release
- [x] Phase 1:协议服务器 + mock + 任务队列 + 端口顺延(单测 5 个全过,接口自测全过)
- [x] Phase 2:macOS CUPS 后端(真机 status `HiTi CS-200e, 2.4.2 就绪`;真实出卡一张,printer-state 4→3 约 24s)
- [ ] Phase 3:Windows PavoApi ffi 后端(需 Windows 实机 + windows/ 目录)
- [x] Phase 3 代码:Windows PavoApi ffi 后端(mac 上完成编码,analyze 零问题 + 单测 12 个全过;windows/ 目录已从 SDK 模板手动实例化,vendor 收录 SDK DLL/bin 并在 CMake 构建后自动拷贝)——**待 Windows 实机验收**(编译/直打/色带/不装驱动直打验证)
- [ ] Phase 4:托盘/首次引导/URL scheme 唤起(zaxprint scheme 已写入 Info.plist)/开机自启
- [ ] Phase 5:打包签名分发(mac ad-hoc 就地签名;参考 C# 版签名铁律)
### Windows 实机验收清单(Phase 3 收尾)
1. `flutter create --platforms windows .` 对照生成的目录结构核对手动实例化的差异(或直接构建)
2. `flutter build windows --release` 编译过;确认 vendor 四文件拷到 exe 旁
3. `dart run bin/agent.dart`(或直接跑 App)→ status 返回真机型号/固件/色带余量
4. 提交测试卡真实出卡(tool/make_test_card.dart 生成后走 print 接口)
5. 顺带实测:不装 HiTi 官方驱动能否直打(PavoApiW_x64 疑似用户态直连 USB)
+90
View File
@@ -0,0 +1,90 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:image/image.dart';
/// PNG data URL 解析与像素层转换(Windows PavoApi 直打用)。
///
/// 卡面为 CR-80 @300dpi:横向 1014×642 / 纵向 642×1014,按源图纵横比自动选择;
/// 透明区域铺白,避免黑底(对齐 C# 版 Hiti/CardImage.cs)。
class CardImage {
CardImage._(this._canvas, this.width, this.height);
static const int landscapeWidth = 1014;
static const int landscapeHeight = 642;
final Image _canvas;
final int width;
final int height;
/// 宽 ≥ 高 = 横向卡面
bool get isLandscape => width >= height;
/// 解析 data:image/png;base64,…(或其他 data URL 形式),缩放到卡面画布并铺白底。
static CardImage fromDataUrl(String dataUrl) {
if (dataUrl.trim().isEmpty) {
throw ArgumentError('卡面图为空');
}
final comma = dataUrl.indexOf(',');
final base64Part = comma >= 0 ? dataUrl.substring(comma + 1) : dataUrl;
final bytes = base64.decode(base64Part.replaceAll(RegExp(r'\s'), ''));
final raw = decodeImage(bytes);
if (raw == null) {
throw ArgumentError('卡面图解码失败(不支持的图片格式)');
}
final landscape = raw.width >= raw.height;
final width = landscape ? landscapeWidth : landscapeHeight;
final height = landscape ? landscapeHeight : landscapeWidth;
final resized = copyResize(raw, width: width, height: height);
final canvas = Image(width: width, height: height);
// 手动 alpha 混合铺白底(与 C# 版逐像素语义一致):
// image 包的 compositeImage 对全透明 src 像素不是 src-over 语义,透明处会漏黑
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
final p = resized.getPixel(x, y);
final a = p.a.toInt() / 255.0;
canvas.setPixelRgba(
x,
y,
(255 * (1 - a) + p.r * a).round().clamp(0, 255),
(255 * (1 - a) + p.g * a).round().clamp(0, 255),
(255 * (1 - a) + p.b * a).round().clamp(0, 255),
255,
);
}
}
return CardImage._(canvas, width, height);
}
/// 转 BGR 24bit 紧凑像素(无行对齐填充,PavoApi BITMAP 用)。
Uint8List toBgr24() {
final packed = Uint8List(width * 3 * height);
for (var y = 0; y < height; y++) {
var offset = y * width * 3;
for (var x = 0; x < width; x++) {
final p = _canvas.getPixel(x, y);
packed[offset] = p.b.toInt() & 0xff;
packed[offset + 1] = p.g.toInt() & 0xff;
packed[offset + 2] = p.r.toInt() & 0xff;
offset += 3;
}
}
return packed;
}
/// 转 8bit 灰度(无行对齐)。树脂 K 层像素语义:0x00 = 转印黑色,
/// 因此前端 K 层 PNG 为白底黑字,灰度值可直接使用。
Uint8List toGray8() {
final gray = Uint8List(width * height);
for (var y = 0; y < height; y++) {
var offset = y * width;
for (var x = 0; x < width; x++) {
final p = _canvas.getPixel(x, y);
gray[offset + x] = (p.r.toInt() * 299 + p.g.toInt() * 587 + p.b.toInt() * 114) ~/ 1000 & 0xff;
}
}
return gray;
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ import 'dart:io';
import '../server/protocol.dart';
import 'cups_backend.dart';
import 'pavo/pavo_backend.dart';
/// 打印后端抽象:Windows 走 HiTi PavoApi 直打(分层 BITMAP);macOS 走系统 CUPS 队列。
abstract class PrintBackend {
@@ -17,10 +18,10 @@ abstract class PrintBackend {
String osBackendName() => Platform.isWindows ? 'pavo' : 'cups';
/// 创建当前操作系统的真实打印后端(非 mock)。
/// PavoPrintBackend 构造不加载 DLL(延迟到实际调用的子 isolate 内),mac 上亦可安全实例化/分析。
PrintBackend createOsBackend({void Function(String line)? onLog}) {
if (Platform.isWindows) {
// Phase 3:PavoApi ffi 直打(待 Windows 实机联调)
throw UnsupportedError('Windows PavoApi 后端尚未实现(Phase 3)');
return PavoPrintBackend(onLog: onLog);
}
return CupsPrintBackend(onLog: onLog);
}
+121
View File
@@ -0,0 +1,121 @@
import 'dart:isolate';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import '../../image/card_image.dart';
import '../../server/protocol.dart';
import '../backend.dart';
import '../cups_backend.dart' show noopLog;
import 'pavo_ffi.dart';
import 'pavo_printer.dart';
/// Windows 打印后端:HiTi PavoApi 直打(BGR/K 分层 BITMAP)。
///
/// 所有 ffi 调用(含像素转换的 CPU 密集计算)都放进 `Isolate.run`:
/// PavoApi 的同步调用最长可阻塞数秒(发送 2MB 像素)、打印轮询最长 3 分钟,
/// 放独立 isolate 保证主 isolate 的 HTTP 服务(任务状态轮询)不被卡住。
/// DLL 与非托管指针都不跨 isolate(闭包内自建自释放)。
class PavoPrintBackend implements PrintBackend {
PavoPrintBackend({void Function(String line)? onLog}) : _log = onLog ?? noopLog;
final void Function(String line) _log;
@override
String get name => 'pavo';
@override
Future<PrinterStatus> getStatus() => Isolate.run(_statusSync);
PrinterStatus _statusSync() {
try {
final api = PavoApi();
final printers = PavoPrinter.enumUsbPrinters(api);
if (printers.isEmpty) {
return PrinterStatus.offline('打印机未连接');
}
final printer = printers.first.toNativeUtf16();
try {
final status = PavoPrinter.getStatus(api, printer);
final (ribbonType, remainCount) = PavoPrinter.getRibbonInfo(api, printer);
final dto = PrinterStatus(
online: true,
model: PavoPrinter.getDeviceInfoText(api, printer, PavoApi.devinfoModelName),
firmware: PavoPrinter.getDeviceInfoText(api, printer, PavoApi.devinfoFirmwareVersion),
connection: 'USB',
ribbon: RibbonInfo(
type: PavoPrinter.ribbonTypeName(ribbonType),
// 设备返回剩余张数,按常见 100 张装 YMCKO 色带折算百分比
remain: ribbonType == PavoApi.ribbonMissing ? -1 : (remainCount > 100 ? 100 : remainCount),
remainCount: remainCount,
),
status: status,
statusText: PavoPrinter.statusTextOf(status),
);
if (status == PavoApi.dsCardLow || status == PavoApi.dsCardOut) {
dto.cardLow = true;
}
return dto;
} finally {
calloc.free(printer);
}
} catch (e) {
return PrinterStatus.offline('状态读取失败:$e');
}
}
@override
Future<void> printOneCard(PrintRequest request) {
_log('Pavo 直打任务开始(份数 ${request.copies},后端在子 isolate 执行)');
final front = request.front!;
final back = request.back;
// 只把 data URL 字符串与份数传进 isolate;图层转换与 ffi 调用全部在子 isolate 内完成
final frontColor = front.color;
final frontK = front.k;
final backColor = back?.color;
final backK = back?.k;
final copies = request.copies;
return Isolate.run(() async {
final api = PavoApi();
final printers = PavoPrinter.enumUsbPrinters(api);
if (printers.isEmpty) {
throw StateError('未找到 USB 连接的 HiTi 证卡打印机');
}
final printer = printers.first.toNativeUtf16();
try {
final frontImg = CardImage.fromDataUrl(frontColor);
final frontBgr = frontImg.toBgr24();
final Uint8List? frontKGray;
if (frontK != null && frontK.isNotEmpty) {
frontKGray = CardImage.fromDataUrl(frontK).toGray8();
} else {
frontKGray = null;
}
final Uint8List? backBgr;
final Uint8List? backKGray;
if (backColor != null && backColor.isNotEmpty) {
final backImg = CardImage.fromDataUrl(backColor);
backBgr = backImg.toBgr24();
backKGray =
backK != null && backK.isNotEmpty ? CardImage.fromDataUrl(backK).toGray8() : null;
} else {
backBgr = null;
backKGray = null;
}
await PavoPrinter.printOneCard(
api,
printer,
frontBgr: frontBgr,
frontK: frontKGray,
backBgr: backBgr,
backK: backKGray,
isLandscape: frontImg.isLandscape,
copies: copies,
);
} finally {
calloc.free(printer);
}
});
}
}
+214
View File
@@ -0,0 +1,214 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
/// HiTi CS-2xx PavoApi 的 dart:ffi 绑定。
///
/// 依据 SDK Include/PavoApi.h(PrinterCardSDK_1_11_25)与 C# 版 Hiti/PavoApi.cs 翻译;
/// 使用宽字符 DLL `PavoApiW_x64.dll`。C# CharSet.Unicode 的导出名回退行为
/// (先找 PAVO_xxxW 再回退 PAVO_xxx)在此用 try-catch 复刻。
/// 返回值约定:0 = 成功,非 0 = 错误码(ds* 状态码常量)。
///
/// 所有导出函数只允许在 Windows 上实际调用(DLL 加载失败即抛错);
/// 结构体定义本身跨平台可编译,便于 mac 上 analyze 与单测。
class PavoApi {
PavoApi() : _lib = DynamicLibrary.open(_dllName) {
_enumUsb = _tryEnum('PAVO_EnumUSBCardPrintersW') ?? _tryEnum('PAVO_EnumUSBCardPrinters')!;
_checkStatus =
_tryCheckStatus('PAVO_CheckPrinterStatusW') ?? _tryCheckStatus('PAVO_CheckPrinterStatus')!;
_getDeviceInfo =
_tryGetDeviceInfo('PAVO_GetDeviceInfoW') ?? _tryGetDeviceInfo('PAVO_GetDeviceInfo')!;
_printOneCard = _tryPrintOneCard('PAVO_PrintOneCardW') ?? _tryPrintOneCard('PAVO_PrintOneCard')!;
}
static const String _dllName = 'PavoApiW_x64.dll';
// ---------------- 状态码(PAVO_DS_*,CheckPrinterStatus 输出 / 任务错误码) ----------------
static const int dsBusy = 0x00080000;
static const int dsOffline = 0x00000080;
static const int dsPrinting = 0x00000002;
static const int dsProcessingData = 0x00000005;
static const int dsSendingData = 0x00000006;
static const int dsCardJam1 = 0x00030000;
static const int dsCardJam2 = 0x00040000;
static const int dsCardJam3 = 0x00050000;
static const int dsCmdSeqError = 0x000301FE;
static const int dsPrintFail1 = 0x00030002;
static const int dsPrintFail2 = 0x00030003;
static const int dsCoverOpen = 0x00050001;
static const int dsCardOut = 0x00008000;
static const int dsCardLow = 0x00008001;
static const int dsCardThickWrong = 0x00008010;
static const int dsRibbonMissing = 0x00080004;
static const int dsOutOfRibbon = 0x00080103;
static const int dsUnsupportRibbon = 0x000806FE;
static const int dsUnknownRibbon = 0x000808FE;
static const int dsNoFlipperModule = 0x1100000D;
static const int dsWriteFail = 0x0000001F;
static const int dsReadFail = 0x0000002F;
static const int locked = 0x00007540;
// ---------------- GetDeviceInfo 信息类型 ----------------
static const int devinfoMfgSerial = 1;
static const int devinfoModelName = 2;
static const int devinfoFirmwareVersion = 3;
static const int devinfoRibbonInfo = 4;
static const int devinfoPrintCount = 5;
// ---------------- 色带类型 ----------------
static const int ribbonTypeYmcko = 0;
static const int ribbonTypeK = 1;
static const int ribbonTypeKo = 3;
static const int ribbonTypeYmckok = 4;
static const int ribbonTypeHalfYmcko = 5;
static const int ribbonTypeYmckfo = 12;
static const int ribbonMissing = 255;
final DynamicLibrary _lib;
// 枚举 USB 打印机:两段式(首传 nullptr 取所需大小,再传实际缓冲;缓冲内容编码见 PavoPrinter 解码)
late final int Function(Pointer<Uint8>, Pointer<Uint32>) _enumUsb;
late final int Function(Pointer<Utf16>, Pointer<Uint32>) _checkStatus;
late final int Function(Pointer<Utf16>, int, Pointer<Uint8>, Pointer<Uint32>) _getDeviceInfo;
late final int Function(
Pointer<Utf16>, Pointer<HitiCardPrintParameter>, Pointer<HitiHeatingEnergy>, Pointer<HitiSettings>) _printOneCard;
// 宽字符导出名兼容:先找 xxxW,失败回退原名(对齐 C# CharSet.Unicode 行为)。
int Function(Pointer<Uint8>, Pointer<Uint32>)? _tryEnum(String name) {
try {
return _lib.lookupFunction<Uint32 Function(Pointer<Uint8>, Pointer<Uint32>),
int Function(Pointer<Uint8>, Pointer<Uint32>)>(name);
} on ArgumentError {
return null;
}
}
int Function(Pointer<Utf16>, Pointer<Uint32>)? _tryCheckStatus(String name) {
try {
return _lib.lookupFunction<Uint32 Function(Pointer<Utf16>, Pointer<Uint32>),
int Function(Pointer<Utf16>, Pointer<Uint32>)>(name);
} on ArgumentError {
return null;
}
}
int Function(Pointer<Utf16>, int, Pointer<Uint8>, Pointer<Uint32>)? _tryGetDeviceInfo(String name) {
try {
return _lib.lookupFunction<Uint32 Function(Pointer<Utf16>, Uint32, Pointer<Uint8>, Pointer<Uint32>),
int Function(Pointer<Utf16>, int, Pointer<Uint8>, Pointer<Uint32>)>(name);
} on ArgumentError {
return null;
}
}
int Function(Pointer<Utf16>, Pointer<HitiCardPrintParameter>, Pointer<HitiHeatingEnergy>,
Pointer<HitiSettings>)? _tryPrintOneCard(String name) {
try {
return _lib.lookupFunction<
Uint32 Function(Pointer<Utf16>, Pointer<HitiCardPrintParameter>,
Pointer<HitiHeatingEnergy>, Pointer<HitiSettings>),
int Function(Pointer<Utf16>, Pointer<HitiCardPrintParameter>, Pointer<HitiHeatingEnergy>,
Pointer<HitiSettings>)>(name);
} on ArgumentError {
return null;
}
}
/// 两段式枚举 USB 证卡打印机:buf 传 nullptr 时返回所需缓冲大小。
int enumUsbCardPrinters(Pointer<Uint8> buf, Pointer<Uint32> size) => _enumUsb(buf, size);
/// 查询状态码(0=就绪);返回值非 0 时是 API 层错误码。
int checkPrinterStatus(Pointer<Utf16> printer, Pointer<Uint32> status) => _checkStatus(printer, status);
/// 读设备信息(缓冲写 UTF-16LE;RIBBON_INFO 为 dword[0]=类型 dword[1]=剩余张数)。
int getDeviceInfo(
Pointer<Utf16> printer, int infoType, Pointer<Uint8> data, Pointer<Uint32> dataLen) =>
_getDeviceInfo(printer, infoType, data, dataLen);
/// 单卡直打(分层 BITMAP + 默认加热能量 + 默认设置)。
int printOneCard(
Pointer<Utf16> printer,
Pointer<HitiCardPrintParameter> jobPara,
Pointer<HitiHeatingEnergy> heatEnergy,
Pointer<HitiSettings> settings) =>
_printOneCard(printer, jobPara, heatEnergy, settings);
}
/// wingdi BITMAP:指向裸像素缓冲(x64 下 32 字节,Dart Struct 按 C ABI 自然对齐,布局一致)。
final class GdiBitmap extends Struct {
@Int32()
external int bmType;
@Int32()
external int bmWidth;
@Int32()
external int bmHeight;
@Uint32()
external int bmWidthBytes;
@Uint16()
external int bmPlanes;
@Uint16()
external int bmBitsPixel;
external Pointer<Uint8> bmBits;
}
/// PAVO_PrintOneCard 参数。像素约定(头文件注释):
/// BGR 层 24bit BGRBGR…;K 层 8bit,0x00 的像素由树脂 K 转印到卡面;O 层 8bit,0xFF 的像素转印覆膜。
final class HitiCardPrintParameter extends Struct {
@Uint32()
external int dwSize;
@Uint32()
external int dwReserve1;
@Uint32()
external int dwReserve2;
@Uint32()
external int dwFlags;
/// 1=portrait 2=landscape
@Uint8()
external int byOrientation;
/// 0=0.3mm 1=0.5mm 2=0.8mm 3=1.0mm
@Uint8()
external int byCardThickness;
/// 0=白卡 1=透明卡
@Uint8()
external int byTransparentCard;
@Uint8()
external int byReserve4;
@Uint8()
external int byReserve5;
@Uint8()
external int byReserve6;
@Uint8()
external int byReserve7;
@Uint8()
external int byReserve8;
external Pointer<Uint8> lpFrontBGR;
external Pointer<Uint8> lpFrontK;
external Pointer<Uint8> lpFrontO;
external Pointer<Uint8> lpBackBGR;
external Pointer<Uint8> lpBackK;
external Pointer<Uint8> lpBackO;
external Pointer<Uint8> lpReserve1;
external Pointer<Uint8> lpReserve2;
}
/// 加热能量,全 0 表示用驱动默认值(x64 下 16 字节)。
final class HitiHeatingEnergy extends Struct {
@Array(16)
external Array<Uint8> data;
}
/// 打印附加设置,全 0 即可(x64 下 48 字节)。
final class HitiSettings extends Struct {
@Uint8()
external int bSpecialO;
@Array(15)
external Array<Uint8> bReserved;
@Array(8)
external Array<Uint16> wReserved;
@Array(4)
external Array<Uint32> dwReserved;
}
+272
View File
@@ -0,0 +1,272 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import '../../image/card_image.dart';
import 'pavo_ffi.dart';
/// 携带 HiTi 状态码的打印异常(错误码进任务 status,文案进 statusText)。
class HitiException implements Exception {
HitiException(this.statusCode, this.message);
final int statusCode;
final String message;
@override
String toString() => message;
}
/// HiTi CS-2xx 打印机高层操作:枚举、状态、设备信息、单卡直打。
///
/// PavoApi 直打链路(对齐 SDK DemoCSharp buttonDirectPrint 与 C# Hiti/PavoPrinter.cs):
/// PAVO_PrintOneCard(打印机名, 分层 BITMAP, 默认加热能量, 默认设置) → 轮询 CheckPrinterStatus 直到非忙碌。
/// 全部为同步 ffi 调用,必须在独立 isolate 中运行(见 PavoPrintBackend)。
class PavoPrinter {
/// 枚举 USB 连接的证卡打印机名。SDK 缓冲可能是 UTF-8 或 UTF-16,两种都尝试解析。
static List<String> enumUsbPrinters(PavoApi api) {
final sizePtr = calloc<Uint32>();
try {
var ret = api.enumUsbCardPrinters(nullptr, sizePtr);
if (ret != 0 || sizePtr.value == 0) {
return const [];
}
final buf = calloc<Uint8>(sizePtr.value);
try {
final size = sizePtr.value;
sizePtr.value = size;
ret = api.enumUsbCardPrinters(buf, sizePtr);
if (ret != 0) {
return const [];
}
return decodeAuto(buf.asTypedList(size))
.split(RegExp('[\x00\n\r,]'))
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
} finally {
calloc.free(buf);
}
} finally {
calloc.free(sizePtr);
}
}
/// 按可打印字符占比自动判断 UTF-8 / UTF-16 编码。
static String decodeAuto(Uint8List buf) {
final utf8Text = utf8.decode(buf, allowMalformed: true);
if (printableRatio(utf8Text) >= 0.9) {
return utf8Text;
}
final codes = <int>[];
for (var i = 0; i + 1 < buf.length; i += 2) {
codes.add(buf[i] | (buf[i + 1] << 8));
}
final utf16Text = String.fromCharCodes(codes);
return printableRatio(utf16Text) >= 0.9 ? utf16Text : utf8Text;
}
static double printableRatio(String s) {
if (s.isEmpty) return 0;
var ok = 0;
for (final c in s.codeUnits) {
if ((c >= 0x20 && c <= 0x7e) || (c >= 0x4e00 && c <= 0x9fff) || c == 0) ok++;
}
return ok / s.length;
}
/// 查询设备状态码,0 = 就绪;API 调用失败时返回的就是错误码本身。
static int getStatus(PavoApi api, Pointer<Utf16> printer) {
final statusPtr = calloc<Uint32>();
try {
final ret = api.checkPrinterStatus(printer, statusPtr);
return ret != 0 ? ret : statusPtr.value;
} finally {
calloc.free(statusPtr);
}
}
/// 读字符串型设备信息(序列号/型号/固件,UTF-16LE)。
static String getDeviceInfoText(PavoApi api, Pointer<Utf16> printer, int infoType) {
final buf = calloc<Uint8>(512);
final lenPtr = calloc<Uint32>()..value = 512;
try {
final ret = api.getDeviceInfo(printer, infoType, buf, lenPtr);
if (ret != 0 || lenPtr.value == 0) {
return '';
}
final n = lenPtr.value;
final codes = <int>[];
for (var i = 0; i + 1 < n; i += 2) {
final c = buf[i] | (buf[i + 1] << 8);
if (c != 0) codes.add(c);
}
return String.fromCharCodes(codes);
} finally {
calloc.free(buf);
calloc.free(lenPtr);
}
}
/// 色带信息:(类型码, 剩余张数)。
static (int, int) getRibbonInfo(PavoApi api, Pointer<Utf16> printer) {
final buf = calloc<Uint8>(64);
final lenPtr = calloc<Uint32>()..value = 64;
try {
final ret = api.getDeviceInfo(printer, PavoApi.devinfoRibbonInfo, buf, lenPtr);
if (ret != 0 || lenPtr.value < 8) {
return (PavoApi.ribbonMissing, 0);
}
final v = buf.asTypedList(8);
final type = v[0] | (v[1] << 8) | (v[2] << 16) | (v[3] << 24);
final count = v[4] | (v[5] << 8) | (v[6] << 16) | (v[7] << 24);
return (type, count);
} finally {
calloc.free(buf);
calloc.free(lenPtr);
}
}
static String ribbonTypeName(int type) => switch (type) {
PavoApi.ribbonTypeYmcko => 'YMCKO',
PavoApi.ribbonTypeK => 'Resin K',
PavoApi.ribbonTypeKo => 'KO',
PavoApi.ribbonTypeYmckok => 'YMCKOK',
PavoApi.ribbonTypeHalfYmcko => '1/2 YMCKO',
PavoApi.ribbonTypeYmckfo => 'YMCKFO',
PavoApi.ribbonMissing => '未安装',
_ => '未知($type)',
};
/// 常见状态码中文文案,与前端翻译表一致。
static String statusTextOf(int status) => switch (status) {
0 => '就绪',
PavoApi.dsPrinting => '打印机正在打印',
PavoApi.dsProcessingData => '正在处理打印数据',
PavoApi.dsSendingData => '正在向打印机发送数据',
PavoApi.dsWriteFail => '向打印机发送命令失败',
PavoApi.dsReadFail => '打印机响应失败',
PavoApi.dsOffline => '打印机已断开连接或已关机',
PavoApi.dsCardJam1 => '卡片通道卡纸,请开盖取出卡片',
PavoApi.dsCardJam2 => '翻卡器处卡纸,请开盖取出卡片',
PavoApi.dsCardJam3 => '出卡槽卡纸,请取出卡片',
PavoApi.dsCmdSeqError => '命令序列错误,请重启打印机',
PavoApi.dsCoverOpen => '打印机盖未关闭',
PavoApi.dsCardOut => '缺卡或进卡失败,请补充卡片',
PavoApi.dsCardLow => '卡量不足,请及时补充卡片',
PavoApi.dsCardThickWrong => '卡片厚度选择器未正确安装',
PavoApi.dsBusy => '设备忙碌,请稍候',
PavoApi.dsRibbonMissing => '色带未安装',
PavoApi.dsOutOfRibbon => '色带已用尽,请更换色带',
PavoApi.dsUnsupportRibbon => '不支持的色带类型',
PavoApi.dsUnknownRibbon => '无法识别色带',
PavoApi.dsNoFlipperModule => '翻卡模块未安装',
PavoApi.locked => '打印机已被锁定,请先解锁',
_ => '设备状态异常 0x${status.toRadixString(16).padLeft(8, '0').toUpperCase()}',
};
/// 打印一张卡。frontBgr 为彩色层(BGR),frontK 可选(树脂黑文字层)。
/// isLandscape 由正面图纵横比得出:横向 2 / 纵向 1(SDK byOrientation)。
/// copies 大于 1 时循环提交(直打接口无份数参数)。
static Future<void> printOneCard(
PavoApi api,
Pointer<Utf16> printer, {
required Uint8List frontBgr,
Uint8List? frontK,
Uint8List? backBgr,
Uint8List? backK,
required bool isLandscape,
required int copies,
}) async {
final width = isLandscape ? CardImage.landscapeWidth : CardImage.landscapeHeight;
final height = isLandscape ? CardImage.landscapeHeight : CardImage.landscapeWidth;
final n = copies < 1 ? 1 : copies;
for (var i = 0; i < n; i++) {
final holder = LayerHolder();
final para = calloc<HitiCardPrintParameter>();
final heat = calloc<HitiHeatingEnergy>();
final settings = calloc<HitiSettings>();
try {
para.ref
..dwSize = sizeOf<HitiCardPrintParameter>()
..byOrientation = isLandscape ? 2 : 1
..byCardThickness = 1 // 0.5mm 标准 PVC 卡
..lpFrontBGR = holder.pin(frontBgr, 24, width, height)
..lpFrontK = frontK == null ? nullptr : holder.pin(frontK, 8, width, height)
..lpBackBGR = backBgr == null ? nullptr : holder.pin(backBgr, 24, width, height)
..lpBackK = backK == null ? nullptr : holder.pin(backK, 8, width, height);
// heat/settings 由 calloc 零初始化,等价 C# 全 0(驱动默认能量/设置)
final ret = api.printOneCard(printer, para, heat, settings);
if (ret != 0) {
throw HitiException(ret, 'PAVO_PrintOneCard 返回错误 0x${_hex8(ret)}');
}
// 对齐 C# Demo(using 块内轮询):等固件处理数据后轮询,直到打印机脱离忙碌/打印态;
// 像素内存在轮询完成后才释放(finally)
await Future<void>.delayed(const Duration(seconds: 3));
final status = await waitIdle(api, printer);
if (status != 0) {
throw HitiException(status, '打印中断,设备状态 0x${_hex8(status)}');
}
} finally {
holder.dispose();
calloc.free(para);
calloc.free(heat);
calloc.free(settings);
}
}
}
/// 轮询直到非 BUSY/PRINTING/PROCESSING/SENDING;超时 3 分钟抛错。
static Future<int> waitIdle(PavoApi api, Pointer<Utf16> printer) async {
final deadline = DateTime.now().add(const Duration(minutes: 3));
while (DateTime.now().isBefore(deadline)) {
final status = getStatus(api, printer);
if (status != PavoApi.dsBusy &&
status != PavoApi.dsPrinting &&
status != PavoApi.dsProcessingData &&
status != PavoApi.dsSendingData) {
return status;
}
await Future<void>.delayed(const Duration(seconds: 1));
}
throw TimeoutException('等待打印机完成超时(3 分钟)');
}
static String _hex8(int v) => v.toRadixString(16).padLeft(8, '0').toUpperCase();
}
/// 打印期间持有非托管像素内存,dispose 统一释放(对齐 C# LayerHolder)。
class LayerHolder {
final _allocations = <Pointer>[];
/// 把像素缓冲包成 GDI BITMAP 结构并拷贝像素到非托管内存,返回 BITMAP 指针。
Pointer<Uint8> pin(Uint8List pixels, int bitsPerPixel, int width, int height) {
final widthBytes = width * (bitsPerPixel ~/ 8);
final bits = malloc<Uint8>(pixels.length);
bits.asTypedList(pixels.length).setAll(0, pixels);
final bmp = malloc<GdiBitmap>();
bmp.ref
..bmType = 0
..bmWidth = width
..bmHeight = height
..bmWidthBytes = widthBytes
..bmPlanes = 1
..bmBitsPixel = bitsPerPixel
..bmBits = bits;
_allocations
..add(bits)
..add(bmp);
return bmp.cast();
}
void dispose() {
for (final p in _allocations) {
malloc.free(p);
}
_allocations.clear();
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ packages:
source: hosted
version: "1.3.2"
ffi:
dependency: transitive
dependency: "direct main"
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
+2
View File
@@ -40,6 +40,8 @@ dependencies:
shelf_router: ^1.1.4
# PNG 解码/编码(打印图层处理与测试卡生成)
image: ^4.3.0
# dart:ffi 辅助(Utf16 编解码与内存分配,Windows PavoApi 直打用)
ffi: ^2.1.0
dev_dependencies:
flutter_test:
+98
View File
@@ -0,0 +1,98 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:desktop_plugin/image/card_image.dart';
import 'package:desktop_plugin/print/pavo/pavo_ffi.dart';
import 'package:desktop_plugin/print/pavo/pavo_printer.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart';
String dataUrl(Image img) => 'data:image/png;base64,${base64.encode(encodePng(img))}';
void main() {
group('CardImage 像素层转换', () {
test('横向图缩放到 1014×642,纯红转 BGR 字节序正确', () {
final img = Image(width: 100, height: 50);
fill(img, color: ColorRgb8(255, 0, 0));
final card = CardImage.fromDataUrl(dataUrl(img));
expect(card.isLandscape, isTrue);
expect(card.width, 1014);
expect(card.height, 642);
final bgr = card.toBgr24();
expect(bgr.length, 1014 * 642 * 3);
// 红色像素 → BGR 序 [B=0, G=0, R=255]
expect(bgr[0], 0);
expect(bgr[1], 0);
expect(bgr[2], 255);
// 中间像素一致(无行对齐错位/跨行污染)
final mid = (642 ~/ 2) * 1014 * 3 + (1014 ~/ 2) * 3;
expect(bgr[mid], 0);
expect(bgr[mid + 1], 0);
expect(bgr[mid + 2], 255);
});
test('纵向图缩放到 642×1014,纯蓝转 BGR 正确', () {
final img = Image(width: 50, height: 100);
fill(img, color: ColorRgb8(0, 0, 255));
final card = CardImage.fromDataUrl(dataUrl(img));
expect(card.isLandscape, isFalse);
expect(card.width, 642);
expect(card.height, 1014);
final bgr = card.toBgr24();
// 蓝色像素 → BGR 序 [B=255, G=0, R=0]
expect(bgr[0], 255);
expect(bgr[1], 0);
expect(bgr[2], 0);
});
test('透明像素铺白,避免黑底', () {
// 注意:image v4 的 Image() 默认是 3 通道不透明黑,透明图必须显式 numChannels: 4
final img = Image(width: 100, height: 50, numChannels: 4); // RGBA(0,0,0,0) 全透明
final card = CardImage.fromDataUrl(dataUrl(img));
final bgr = card.toBgr24();
expect(bgr[0], 255);
expect(bgr[1], 255);
expect(bgr[2], 255);
});
test('灰度转换:红 ≈ 76、白 = 255(K 层白底黑字语义)', () {
final red = Image(width: 100, height: 50);
fill(red, color: ColorRgb8(255, 0, 0));
final card = CardImage.fromDataUrl(dataUrl(red));
final gray = card.toGray8();
expect(gray.length, 1014 * 642);
expect(gray[0], 255 * 299 ~/ 1000); // = 76
expect(gray.every((g) => g == 76), isTrue);
final white = Image(width: 100, height: 50);
fill(white, color: ColorRgb8(255, 255, 255));
expect(CardImage.fromDataUrl(dataUrl(white)).toGray8()[0], 255);
});
test('空 data URL 报错', () {
expect(() => CardImage.fromDataUrl(''), throwsArgumentError);
});
});
group('PavoPrinter 纯 Dart 工具', () {
test('decodeAuto 识别 UTF-8 / UTF-16 打印机名', () {
// 纯 ASCII:UTF-8 解码即可正确还原(NUL 计入可打印率,两种编码都可能命中 UTF-8 路径)
final ascii = Uint8List.fromList('HiTi CS-200e'.codeUnits);
expect(PavoPrinter.decodeAuto(ascii).startsWith('HiTi CS-200e'), isTrue);
// 纯中文:UTF-8 解码得乱码(可打印率低),走 UTF-16 路径正确还原
// (ASCII 混中文时 NUL 也计入可打印率,可能仍走 UTF-8 路径——与 C# 行为一致)
final name = '证卡打印机';
final bytes = <int>[];
for (final c in name.codeUnits) {
bytes..add(c & 0xff)..add(c >> 8);
}
expect(PavoPrinter.decodeAuto(Uint8List.fromList(bytes)), name);
});
test('statusTextOf 状态文案', () {
expect(PavoPrinter.statusTextOf(0), '就绪');
expect(PavoPrinter.statusTextOf(PavoApi.dsOutOfRibbon), '色带已用尽,请更换色带');
expect(PavoPrinter.statusTextOf(0x12345678), contains('设备状态异常'));
});
});
}
+17
View File
@@ -0,0 +1,17 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
+108
View File
@@ -0,0 +1,108 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(desktop_plugin LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "desktop_plugin")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# Define build configuration option.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
# Define settings for the Profile build mode.
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)
+109
View File
@@ -0,0 +1,109 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)
+52
View File
@@ -0,0 +1,52 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# HiTi PavoApi SDK(windows/vendor/)拷到可执行文件旁:
# Dart 侧 DynamicLibrary.open('PavoApiW_x64.dll') 按可执行文件目录查找;
# heatthcd.bin 为 SDK 运行数据,与其余 DLL 一并部署
add_custom_command(TARGET ${BINARY_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/../vendor/PavoApiW_x64.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/../vendor/PavoCardSDK_x64.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/../vendor/HeatTB3.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/../vendor/heatthcd.bin"
$<TARGET_FILE_DIR:${BINARY_NAME}>
)
+121
View File
@@ -0,0 +1,121 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0,0
#endif
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.zax" "\0"
VALUE "FileDescription", "desktop_plugin" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "desktop_plugin" "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 com.zax. All rights reserved." "\0"
VALUE "OriginalFilename", "desktop_plugin.exe" "\0"
VALUE "ProductName", "desktop_plugin" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
+71
View File
@@ -0,0 +1,71 @@
#include "flutter_window.h"
#include <optional>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
// A window that does nothing but host a Flutter view.
class FlutterWindow : public Win32Window {
public:
// Creates a new FlutterWindow hosting a Flutter view running |project|.
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
// Win32Window:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_
+43
View File
@@ -0,0 +1,43 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include "flutter_window.h"
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
// Initialize COM, so that it is available for use in the library and/or
// plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"饭卡打印助手", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}
+16
View File
@@ -0,0 +1,16 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
Whitespace-only changes.
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
+65
View File
@@ -0,0 +1,65 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
void CreateAndAttachConsole() {
if (::AllocConsole()) {
FILE *unused;
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
_dup2(_fileno(stdout), 1);
}
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
_dup2(_fileno(stdout), 2);
}
std::ios::sync_with_stdio();
FlutterDesktopResyncOutputStreams();
}
}
std::vector<std::string> GetCommandLineArguments() {
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
int argc;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
if (argv == nullptr) {
return std::vector<std::string>();
}
std::vector<std::string> command_line_arguments;
// Skip the first argument as it's the binary name.
for (int i = 1; i < argc; i++) {
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
}
::LocalFree(argv);
return command_line_arguments;
}
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) {
return std::string();
}
unsigned int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr)
-1; // remove the trailing null character
int input_length = (int)wcslen(utf16_string);
std::string utf8_string;
if (target_length == 0 || target_length > utf8_string.max_size()) {
return utf8_string;
}
utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
input_length, utf8_string.data(), target_length, nullptr, nullptr);
if (converted_length == 0) {
return std::string();
}
return utf8_string;
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// Creates a console for the process, and redirects stdout and stderr to
// it for both the runner and the Flutter library.
void CreateAndAttachConsole();
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
// encoded in UTF-8. Returns an empty std::string on failure.
std::string Utf8FromUtf16(const wchar_t* utf16_string);
// Gets the command line arguments passed in as a std::vector<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_
+288
View File
@@ -0,0 +1,288 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#include "resource.h"
namespace {
/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
/// Registry key for app theme preference.
///
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
/// value indicates apps should use light mode.
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
// scale factor
int Scale(int source, double scale_factor) {
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode.
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
HMODULE user32_module = LoadLibraryA("User32.dll");
if (!user32_module) {
return;
}
auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>(
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd);
}
FreeLibrary(user32_module);
}
} // namespace
// Manages the Win32Window's window class registration.
class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registrar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
}
return instance_;
}
// Returns the name of the window class, registering the class if it hasn't
// previously been registered.
const wchar_t* GetWindowClass();
// Unregisters the window class. Should only be called if there are no
// instances of the window.
void UnregisterWindowClass();
private:
WindowClassRegistrar() = default;
static WindowClassRegistrar* instance_;
bool class_registered_ = false;
};
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
const wchar_t* WindowClassRegistrar::GetWindowClass() {
if (!class_registered_) {
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
RegisterClass(&window_class);
class_registered_ = true;
}
return kWindowClassName;
}
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
}
Win32Window::Win32Window() {
++g_active_window_count;
}
Win32Window::~Win32Window() {
--g_active_window_count;
Destroy();
}
bool Win32Window::Create(const std::wstring& title,
const Point& origin,
const Size& size) {
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x),
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) {
return false;
}
UpdateTheme(window);
return OnCreate();
}
bool Win32Window::Show() {
return ShowWindow(window_handle_, SW_SHOWNORMAL);
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT*>(lparam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
}
return 0;
}
case WM_ACTIVATE:
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
void Win32Window::Destroy() {
OnDestroy();
if (window_handle_) {
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
if (g_active_window_count == 0) {
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
}
}
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
void Win32Window::SetChildContent(HWND content) {
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
SetFocus(child_content_);
}
RECT Win32Window::GetClientArea() {
RECT frame;
GetClientRect(window_handle_, &frame);
return frame;
}
HWND Win32Window::GetHandle() {
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) {
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() {
// No-op; provided for subclasses.
return true;
}
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue,
RRF_RT_REG_DWORD, nullptr, &light_mode,
&light_mode_size);
if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}
+102
View File
@@ -0,0 +1,102 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
// inherited from by classes that wish to specialize with custom
// rendering and input handling
class Win32Window {
public:
struct Point {
unsigned int x;
unsigned int y;
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
};
struct Size {
unsigned int width;
unsigned int height;
Size(unsigned int width, unsigned int height)
: width(width), height(height) {}
};
Win32Window();
virtual ~Win32Window();
// Creates a win32 window with |title| that is positioned and sized using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size this function will scale the inputted width and height as
// as appropriate for the default monitor. The window is invisible until
// |Show| is called. Returns true if the window was created successfully.
bool Create(const std::wstring& title, const Point& origin, const Size& size);
// Show the current window. Returns true if the window was successfully shown.
bool Show();
// Release OS resources associated with window.
void Destroy();
// Inserts |content| into the window tree.
void SetChildContent(HWND content);
// Returns the backing Window handle to enable clients to set icon and other
// window properties. Returns nullptr if the window has been destroyed.
HWND GetHandle();
// If true, closing this window will quit the application.
void SetQuitOnClose(bool quit_on_close);
// Return a RECT representing the bounds of the current client area.
RECT GetClientArea();
protected:
// Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails.
virtual bool OnCreate();
// Called when Destroy is called.
virtual void OnDestroy();
private:
friend class WindowClassRegistrar;
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);
bool quit_on_close_ = false;
// window handle for top level window.
HWND window_handle_ = nullptr;
// window handle for hosted content.
HWND child_content_ = nullptr;
};
#endif // RUNNER_WIN32_WINDOW_H_
Vendored Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Vendored Executable
BIN
View File
Binary file not shown.