简介:本章主要使用BinaryKits组件将zpl 指令远程连接打印机打印机内容,且通过解析zpl指令在本地软件预览,实时编辑,兼容Windows、Linux。支持中文、英文、特殊字符等,不依赖系统字体。开源、免费、国产化
1.引入核心组件
Install-Package BinaryKits.Zpl.Label 版本3.3.1
Install-Package BinaryKits.Zpl.Viewer 版本1.2.0
我测试1.2.0版本兼容,升级到3.0版本或其他版本,对ZplElementDrawer在进行绘制Draw时,DrawerOptions调用字体文件非public,无法指定字体文件。
各位道友不做文字处理,可以配置服务器字体文件,但是要担心SkiaSharp默认指向。
Install-Package SkiaSharp 版本2.88.8
Install-Package SkiaSharp.HarfBuzz 版本2.88.8
Install-Package SkiaSharp.NativeAssets.Linux.NoDependencies 版本2.88.8
该三个组件必须保持版本一致,不然会报错
错误:SkiaSharp Value cannot be null. (Parameter 'asset')或者依赖项指定错误、资源文件找不到等等
2.项目核心配置
<PublishSingleFile>false</PublishSingleFile>
禁用单文件发布,强制使用项目框架依赖
3.上传页面
Razor代码
html
@Html.AppendCssFile("/Areas/ResourceFile/UploadifyForm.css")
<div class="lr-form-layout lr-form-layout-top-bottom">
<div class="lr-form-layout-header">
<div id="lr_add_file_btn"><i class="fa fa-cloud-upload"></i>添加文件</div>
</div>
<div class="lr-form-layout-body">
<div class="lr-form-file-queue-wrap" id="lr_form_file_queue">
<div class="lr-form-file-queue" id="lr_form_file_queue_list">
</div>
<div class="lr-form-file-queue-bg">
<h1>试试将电脑里的文件拖拽到此上传</h1>
</div>
</div>
</div>
</div>
<script src="~/Content/webuploader/webuploader.nolog.min.js"></script>
@Html.AppendJsFile("/Areas/Views/UploadifyForm.js")
css代码
css
.lr-form-layout-body {
padding: 0 5px 5px 5px;
}
.lr-form-file-queue-wrap {
position: relative;
width: 100%;
height: 100%;
border: 1px solid #ddd;
border-radius: 4px;
}
.lr-form-file-queue {
position: relative;
}
.lr-form-file-queue-item {
position: relative;
width: 100%;
height: 50px;
border-bottom: 1px solid #ddd;
padding-left: 60px;
}
.lr-file-image {
position: absolute;
top: 5px;
left: 5px;
}
.lr-file-image img {
width: 40px;
height: 40px;
}
.lr-file-name {
line-height: 50px;
}
.lr-msg {
position: absolute;
top: 0;
right: 5px;
font-size: 25px;
line-height: 50px;
}
.lr-msg .fa-check-circle {
color: #5cb85c;
}
.lr-msg .fa-exclamation-circle {
color: #b94a48;
}
.lr-tool-bar {
position: absolute;
top: 0;
right: 35px;
font-size: 25px;
line-height: 50px;
color: #666;
cursor: pointer;
}
.lr-tool-bar > i {
margin-left: 8px;
}
.lr-tool-bar .fa-minus-circle {
color: #b94a48;
}
.lr-tool-bar .fa-cloud-download {
color: #3498db;
}
.lr-uploader-progress {
position: absolute;
bottom: 2px;
left: 60px;
height: 4px;
width: 500px;
width: calc(100% - 90px);
}
.lr-uploader-progress-bar {
position: relative;
height: 100%;
background-color: #039cfd;
border-radius: 4px;
}
.lr-form-file-queue-bg {
position: absolute;
top: 50%;
left: 50%;
width: 300px;
height: 30px;
line-height: 30px;
margin: -15px 0 0 -150px;
}
.lr-form-file-queue-bg h1 {
color: #666;
font-size: 20px;
font-family: Microsoft Yahei;
padding-bottom: 2px;
margin: 0;
}
/*对百度插件的样式重写*/
.webuploader-pick {
padding: 8px 15px;
background-color: #039cfd;
}
.webuploader-pick-hover {
background-color: #027dcb;
}
js代码
javascript
page.uploader = WebUploader.create({
auto: true,
swf: top.$.rootUrl + '/Content/webuploader/Uploader.swf',
// 文件接收服务端。
server: top.$.rootUrl + '/UploadifyFile',
// 选择文件的按钮。可选。
// 内部根据当前运行是创建,可能是input元素,也可能是flash.
pick: '#lr_add_file_btn',
dnd: '#lr_form_file_queue',
paste: 'document.body',
accept: {
extensions: "zpl"
},
multiple: true,
// 上传参数
formData: {
__RequestVerificationToken: $.lrToken
}
});
page.uploader.on('fileQueued', page.fileQueued);
page.uploader.on('uploadStart', page.uploadStart);
page.uploader.on('uploadBeforeSend', page.uploadBeforeSend);
page.uploader.on('uploadProgress', page.uploadProgress);
page.uploader.on('uploadSuccess', page.uploadSuccess);
page.uploader.on('uploadError', page.uploadError);
page.uploader.on('uploadComplete', page.uploadComplete);
page.uploader.on('error', page.error);
}
控制代码
cs
/// <summary>
/// 上传文件
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<ActionResult> UploadifyFile()
{
try
{
//没有文件上传,直接返回
var Filedata = Request.Form.Files["file"];
if (Filedata == null || string.IsNullOrEmpty(Filedata.FileName) || Filedata.Length == 0)
{
return NotFound();
}
//获取文件完整文件名(包含绝对路径)
//文件存放路径格式:/TagTemplate/{guid}.{后缀名}
string fileGuid = Guid.NewGuid().ToString();
long filesize = Filedata.Length;
// HttpContext.Request 里拿
var req = HttpContext.Request;
// 1. 协议:http / https
string scheme = req.Scheme;
// 2. Host(域名+端口) 例:172.16.42.206:8201
string host = req.Host.ToString();
// 3. 基础地址(协议+host)
string baseUrl = $"{req.Scheme}://{req.Host}";
string httpUrl = $"{baseUrl}/TagTemplate/{fileGuid}.zpl";
string fullFileName = Path.Combine(_hostEnvironment.WebRootPath, "TagTemplate", string.Format("{0}.zpl", fileGuid));
//创建文件夹
string path = Path.GetDirectoryName(fullFileName);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
if (!System.IO.File.Exists(fullFileName))
{
//保存文件
using (var stream = new FileStream(fullFileName, FileMode.Create))
{
await Filedata.CopyToAsync(stream);
}
//文件信息写入数据库
FileInfoEntity fileInfoEntity = new FileInfoEntity();
fileInfoEntity.Create();
fileInfoEntity.F_FileId = fileGuid;
if (!string.IsNullOrEmpty(folderId))
{
fileInfoEntity.F_FolderId = folderId;
}
else
{
fileInfoEntity.F_FolderId = "0";
}
fileInfoEntity.F_FileName = Filedata.FileName;
fileInfoEntity.F_EnabledMark = 0;
fileInfoEntity.F_FilePath = httpUrl;
fileInfoEntity.F_FileSize = filesize.ToString();
fileInfoEntity.F_FileExtensions = ".zpl";
fileInfoEntity.F_FileType = "zpl";
fileInfoBLL.SaveForm("", fileInfoEntity);
return Success("上传成功。");
}
else
{
return Fail("文件已经存在。");
}
}
catch (Exception ex)
{
return Fail(ex.Message);
}
}
4.预览页面
Razor代码
html
@Html.AppendCssFile("/Areas/Views/PreviewZpl.css")
<div class="container">
<!-- 顶部工具栏 -->
<div class="toolbar">
<span class="file-name" id="fileName"></span>
<a id="lr_refresh" class="btn btn-default"><i class="fa fa-refresh"></i> 刷新</a>
<a id="lr_print" class="btn btn-default"><i class="fa fa-print"></i> 打印</a>
<a id="lr_save" class="btn btn-default"><i class="fa fa-save"> 保存</i></a>
</div>
<!-- 主体:左编辑,右预览 -->
<div class="main-content">
<!-- 左侧 ZPL 编辑器 -->
<div class="panel">
<div class="panel-header">
<i class="layui-icon layui-icon-edit"></i> ZPL 源代码
</div>
<textarea id="zplEditor" placeholder="在此输入或编辑ZPL指令..."></textarea>
</div>
<!-- 右侧预览 -->
<div class="panel">
<div class="panel-header">
<i class="layui-icon layui-icon-picture"></i> 标签预览
</div>
<div class="preview-wrapper" id="previewWrapper">
<img id="previewImg" />
</div>
</div>
</div>
</div>
@Html.AppendJsFile("/Areas/Views/PreviewZpl.js")
css代码
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body, html {
width: 100%;
height: 100%;
font-family: "Microsoft YaHei", sans-serif;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
/* 顶部工具栏 */
.toolbar {
height: 50px;
background: #f8f8f8;
border-bottom: 1px solid #e6e6e6;
display: flex;
align-items: center;
padding: 0 20px;
gap: 10px;
}
.toolbar .file-name {
flex: 1;
font-size: 14px;
color: #333;
font-weight: 500;
}
.toolbar .btn {
padding: 6px 18px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.btn-save {
background: #1E9FFF;
color: #fff;
}
.btn-save:hover {
background: #0d8de6;
}
.btn-close {
background: #ff5722;
color: #fff;
}
.btn-close:hover {
background: #e64a19;
}
.btn-refresh {
background: #67c23a;
color: #fff;
}
.btn-refresh:hover {
background: #5aaf2f;
}
/* 主体区域 */
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
.panel {
width: 50%;
height: 100%;
display: flex;
flex-direction: column;
border-right: 1px solid #e6e6e6;
}
.panel:last-child {
border-right: none;
}
.panel-header {
height: 40px;
background: #f5f5f5;
display: flex;
align-items: center;
padding: 0 15px;
font-size: 13px;
color: #666;
border-bottom: 1px solid #e6e6e6;
font-weight: 500;
}
/* 左侧编辑器 */
#zplEditor {
flex: 1;
width: 100%;
padding: 12px;
border: none;
outline: none;
resize: none;
font-family: "Consolas", "Monaco", "Courier New", monospace;
font-size: 13px;
line-height: 1.6;
background: #1e1e1e;
color: #d4d4d4;
tab-size: 4;
}
#zplEditor::-webkit-scrollbar {
width: 10px;
height: 10px;
}
#zplEditor::-webkit-scrollbar-track {
background: #2d2d2d;
}
#zplEditor::-webkit-scrollbar-thumb {
background: #555;
border-radius: 5px;
}
#zplEditor::-webkit-scrollbar-thumb:hover {
background: #666;
}
/* 右侧预览区 */
.preview-wrapper {
flex: 1;
background: #e0e0e0;
overflow: auto;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 20px;
}
.preview-canvas {
background: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
}
.preview-empty {
color: #999;
font-size: 14px;
text-align: center;
margin-top: 50px;
}
js代码
javascript
// 刷新
$('#lr_refresh').on('click', function () {
var imgUrl = top.$.rootUrl + '/GetZplFileImage?zpl=' + $('#zplEditor').val();
$('#previewImg').attr("src", imgUrl);
});
// 打印
$('#lr_print').on('click', function () {
var postData = {};
postData.zpl = $('#zplEditor').val();
// 调用力软保存表单方法
$.lrSaveForm(top.$.rootUrl + '/PrintZpl', postData, function (res) {
}, function (errMsg) {
//保存失败回调:不关闭,只提示错误
learun.alert.error(errMsg);
});
});
// 保存
$('#lr_save').on('click', function () {
var postData = {};
postData.keyValue = keyValue;
postData.zpl = $('#zplEditor').val();
// 调用力软保存表单方法
$.lrSaveForm(top.$.rootUrl + '//SaveZpl', postData, function (res) {
// 刷新父页面表格
top.$("#gridtable").trigger("reloadGrid");
// 关闭当前弹窗
learun.layerClose(window.name);
}, function (errMsg) {
//保存失败回调:不关闭,只提示错误
learun.alert.error(errMsg);
});
});
InitData: function () {
if (!!keyValue) {
$.lrSetForm(top.$.rootUrl + '//GetZplFileContent?keyValue=' + keyValue, function (data) {
$('#zplEditor').val(data.content);
var imgUrl = top.$.rootUrl + '/GetZplFileImage?zpl=' + data.content;
$('#previewImg').attr("src", imgUrl);
});
}
},
控制器代码
cs
/// <summary>
/// 获取zpl指令内容
/// </summary>
/// <param name="keyValue">keyValue主键</param>
/// <returns></returns>
[HttpGet]
public ActionResult GetZplFileContent(string keyValue)
{
var data = fileInfoBLL.GetEntity(keyValue);
string filename = WebUtility.UrlDecode(data.F_FileName);//客户端保存的文件名
string filepath = Path.Combine(_hostEnvironment.WebRootPath, "TagTemplate", string.Format("{0}.zpl", data.F_FileId));
// 1. 读取文件内容
string content = System.IO.File.ReadAllText(filepath);
var jsonData = new
{
content = content
};
return Success(jsonData);
}
/// <summary>
/// ZPL预览接口
/// </summary>
/// <param name="zpl">ZPL字符串</param>
/// <returns></returns>
[HttpGet]
public ActionResult GetZplFileImage(string zpl)
{
try
{
if (string.IsNullOrWhiteSpace(zpl))
{
return Fail("ZPL指令不能为空");
}
var imgBytes = ZplPrintHelper.RenderZplToPng(zpl);
if (imgBytes == null || imgBytes.Length == 0)
{
return Fail("渲染失败,无标签数据");
}
return File(imgBytes, "image/png");
}
catch (Exception ex)
{
return Fail(ex.Message);
}
}
[HttpPost]
public ActionResult SaveZpl(string keyValue, string zpl)
{
var data = fileInfoBLL.GetEntity(keyValue);
string filename = WebUtility.UrlDecode(data.F_FileName);//客户端保存的文件名
string filepath = Path.Combine(_hostEnvironment.WebRootPath, "TagTemplate", string.Format("{0}.txt", data.F_FileId));
data.F_ModifyDate = DateTime.Now;
fileInfoBLL.SaveForm(keyValue, data);
System.IO.File.WriteAllText(filepath, zpl);
return Success("保存成功");
}
[HttpPost]
public async Task<ActionResult> PrintZpl(string zpl)
{
string Message = string.Empty;
if (!await ZplPrintHelper.IsPrinterOnlineAsync("192.168.123.100"))
{
Message = $"打印机不在线,请检查网络或电源";
return Fail(Message);
}
bool success = await ZplPrintHelper.SendZplAsync(zpl, "192.168.123.100");
Message = $"打印成功";
return Success(Message);
}
5.打印核心处理类
各位道友自己封装吧,我都写在一个类里面
cs
public class ZplPrintHelper
{
/// <summary>
/// 离线ZPL转PNG图片,内网离线,无外网,纯条码/二维码/英文数字
/// </summary>
/// <param name="zpl">ZPL指令文本</param>
/// <returns>png字节数组</returns>
public static byte[] RenderZplToPng(string zpl, IWebHostEnvironment _hostEnvironment)
{
// 正则读取 ^PW(宽度) ^LL(高度)
int pw = 400;
int ll = 300;
var matchPW = Regex.Match(zpl, @"\^PW(\d+)");
var matchLL = Regex.Match(zpl, @"\^LL(\d+)");
if (matchPW.Success) pw = int.Parse(matchPW.Groups[1].Value);
if (matchLL.Success) ll = int.Parse(matchLL.Groups[1].Value);
// 打印机存储,必须传入,存放图片、下载资源
IPrinterStorage printerStorage = new PrinterStorage();
var analyzer = new ZplAnalyzer(printerStorage);
var analyzeResult = analyzer.Analyze(zpl);
// 新版构造只需要 printerStorage
DrawerOptionsHelper.HostEnvironment = _hostEnvironment;
DrawerOptions opt = new DrawerOptions
{
FontLoader = DrawerOptionsHelper.CustomTtfFontLoader,
OpaqueBackground = true,
Antialias = true
};
var drawer = new ZplElementDrawer(printerStorage, opt);
foreach (var labelInfo in analyzeResult.LabelInfos)
{
byte[] pngBytes = drawer.Draw(labelInfo.ZplElements);
if (pngBytes == null || pngBytes.Length == 0)
{
LogHelper.Error("RenderZplToPng:drawer.Draw失败,渲染资源缺失", "ZPL");
return null;
}
else
{
// 裁剪到 ^PW x ^LL 画布
pngBytes = CropToLabelSize(pngBytes, pw, ll);
return pngBytes;
}
}
return null;
}
/// <summary>
/// 把渲染出来的大图,裁剪居中放到指定宽高的白底画布
/// </summary>
private static byte[] CropToLabelSize(byte[] sourcePng, int targetW, int targetH)
{
using var srcBmp = SKBitmap.Decode(sourcePng);
if (srcBmp == null)
{
LogHelper.Error("CropToLabelSize:SKBitmap.Decode解码失败,无效PNG", "ZPL");
return null;
}
// 目标画布:严格PW × LL,透明底
using var destBmp = new SKBitmap(targetW, targetH, SKColorType.Bgra8888, SKAlphaType.Premul);
using var canvas = new SKCanvas(destBmp);
canvas.Clear(SKColors.Transparent);
// =========重点:offsetX=0,offsetY=0!左上角对齐,不居中=========
// ZPL ^FO(x,y) 就是相对于标签左上角的坐标,居中一定会错位!
canvas.DrawBitmap(srcBmp, 0, 0);
using var img = SKImage.FromBitmap(destBmp);
using var data = img.Encode(SKEncodedImageFormat.Png, 100);
return data.ToArray();
}
/// <summary>
/// 检测打印机是否在线(能否建立 TCP 连接)
/// </summary>
/// <param name="_printerIp">打印机IP</param>
/// <param name="_port">打印机端口 默认9100</param>
/// <param name="_timeoutMs">超时</param>
/// <returns></returns>
public static async Task<bool> IsPrinterOnlineAsync(string _printerIp,int _port=9100,int _timeoutMs=5000)
{
try
{
using (var client = new TcpClient())
{
if (!IPAddress.TryParse(_printerIp, out var ipAddress))
return false;
// 使用 Task.Run 包装 BeginConnect,避免 FromAsync 的陷阱
var connectTask = Task.Run(() =>
{
IAsyncResult ar = client.BeginConnect(ipAddress, _port, null, null);
try
{
// 等待完成
ar.AsyncWaitHandle.WaitOne(_timeoutMs);
if (ar.IsCompleted)
{
client.EndConnect(ar); // 安全调用
return true;
}
else
{
return false; // 超时
}
}
catch (SocketException)
{
return false;
}
finally
{
ar.AsyncWaitHandle.Close();
}
});
var success = await connectTask;
return success;
}
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 异步发送 ZPL 指令到打印机
/// </summary>
/// <param name="zplCommand">完整的 ZPL 指令字符串</param>
/// <param name="_printerIp">打印机IP</param>
/// <param name="_port">打印机端口 默认9100</param>
/// <param name="_timeoutMs">超时</param>
/// <returns>成功返回 true</returns>
public static async Task<bool> SendZplAsync(string zplCommand, string _printerIp, int _port = 9100, int _timeoutMs = 5000)
{
if (string.IsNullOrWhiteSpace(zplCommand))
throw new ArgumentException("ZPL 指令不能为空", nameof(zplCommand));
using (var client = new TcpClient())
{
if (!IPAddress.TryParse(_printerIp, out var ipAddress))
throw new ArgumentException($"无效的 IP 地址: {_printerIp}", nameof(_printerIp));
// 使用 Task.Run 在后台线程执行同步阻塞的 WaitOne(避免死锁)
await Task.Run(() =>
{
IAsyncResult ar = null;
try
{
// 启动异步连接
ar = client.BeginConnect(ipAddress, _port, null, null);
// 等待连接完成或超时
bool completed = ar.AsyncWaitHandle.WaitOne(_timeoutMs);
if (!completed)
{
throw new TimeoutException($"连接打印机 {_printerIp}:{_port} 超时({_timeoutMs}ms)");
}
// 安全调用 EndConnect(此时 ar != null 且已完成)
client.EndConnect(ar);
}
catch (SocketException ex)
{
throw new Exception($"无法连接到打印机 {_printerIp}:{_port} - {ex.Message}", ex);
}
finally
{
ar?.AsyncWaitHandle?.Close(); // 释放等待句柄
}
});
// 连接成功,发送数据
using (var stream = client.GetStream())
{
byte[] data = Encoding.UTF8.GetBytes(zplCommand);
await stream.WriteAsync(data, 0, data.Length);
await stream.FlushAsync();
}
}
return true;
}
}
6.文字处理类
该类主要处理字体文件,不依赖系统字体文件。字体文件各位道友自己找镜像文件吧。
cs
public class DrawerOptionsHelper
{
/// <summary>
/// web下发路径
/// </summary>
public static IWebHostEnvironment HostEnvironment = null;
// 字体缓存,全局复用SKTypeface,防止内存泄漏
private static readonly ConcurrentDictionary<string, SKTypeface> _fontCache = new();
/// <summary>
/// 自定义字体加载:优先读取本地ttf,失败再回退系统字体
/// fontName:ZPL ^A0 / ^A1 传进来的标识
/// </summary>
public static Func<string, SKTypeface> CustomTtfFontLoader = fontName =>
{
try
{
// 命中缓存直接返回
if (_fontCache.TryGetValue(fontName, out var cacheTf))
{
return cacheTf;
}
SKTypeface typeface = null;
var fontMgr = SKFontManager.Default;
var families = fontMgr.FontFamilies.ToList();
// ========== 【重点】fontName=0 对应 ZPL ^A0,这里加载本地TTF ==========
if (fontName == "0" || fontName == "A" || !string.IsNullOrEmpty(fontName))
{
// 字体文件路径,可配置,建议绝对路径
string ttfPath = Path.Combine(HostEnvironment.WebRootPath, "ZplFonts", "LZHONGHEI.ttf");
if (File.Exists(ttfPath))
{
using var stream = File.OpenRead(ttfPath);
typeface = SKTypeface.FromStream(stream);
}
// TTF加载失败,回退原来系统字体逻辑
if (typeface == null)
{
if (families.Contains("Helvetica"))
{
typeface = SKTypeface.FromFamilyName("Helvetica", SKFontStyleWeight.Bold, SKFontStyleWidth.SemiCondensed, SKFontStyleSlant.Upright);
}
else if (families.Contains("Roboto Condensed"))
{
typeface = SKTypeface.FromFamilyName("Roboto Condensed", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
}
else if (families.Contains("Swis721 BT"))
{
typeface = SKTypeface.FromFamilyName("Swis721 BT");
}
else if (families.Contains("Arial"))
{
typeface = SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Bold, SKFontStyleWidth.Condensed, SKFontStyleSlant.Upright);
}
else
{
typeface = SKTypeface.FromFamilyName("Helvetica", SKFontStyleWeight.Bold, SKFontStyleWidth.SemiCondensed, SKFontStyleSlant.Upright);
}
}
}
else
{
// 其它字体标识(^A1、^A2等)沿用原有英文等宽逻辑
if (families.Contains("DejaVu Sans Mono"))
{
typeface = SKTypeface.FromFamilyName("DejaVu Sans Mono", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
}
else if (families.Contains("Courier New"))
{
typeface = SKTypeface.FromFamilyName("Courier New", SKFontStyleWeight.Medium, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
}
else
{
typeface = families.Contains("Courier")
? SKTypeface.FromFamilyName("Courier", SKFontStyleWeight.Medium, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright)
: SKTypeface.FromFamilyName("DejaVu Sans Mono", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
}
}
// 兜底,防止null
typeface ??= SKTypeface.Default;
_fontCache.TryAdd(fontName, typeface);
return typeface;
}
catch (Exception ex)
{
LogHelper.Error($"DrawerOptionsHelper:CustomTtfFontLoader失败,{ex.Message}", "ZPL");
return null;
}
};
}
结果展示

编辑模板完成,点击刷新,道友们也可以自行zpl文本改变实时刷新
