WuYongJie c7a9324635 feat(打印): 新增远程标签打印与 .NET 打印助手
- 新增打印终端、配对码、打印任务相关表结构与接口\n- 增加后台打印终端/任务管理页和资产标签远程打印入口\n- 引入 .NET 8 Windows 打印助手源码及下载清单配置
2026-03-25 21:05:30 +08:00

366 lines
12 KiB
C#

using System.Drawing.Drawing2D;
using System.Drawing.Printing;
using System.Drawing.Text;
using QRCoder;
namespace FixedAssetPrintAgent;
internal sealed class LabelPrintService
{
private const string CompanyName = "常州简尚家具有限公司";
private const int CanvasWidth = 480;
private const int CanvasHeight = 320;
private const int PaperWidthHundredthsOfInch = 236;
private const int PaperHeightHundredthsOfInch = 157;
public List<PrinterInfo> ListPrinters()
{
var defaultPrinter = string.Empty;
try
{
var settings = new PrinterSettings();
defaultPrinter = settings.PrinterName;
}
catch
{
defaultPrinter = string.Empty;
}
return PrinterSettings.InstalledPrinters
.Cast<string>()
.Select(name => new PrinterInfo
{
Name = name,
IsDefault = string.Equals(name, defaultPrinter, StringComparison.OrdinalIgnoreCase),
IsAvailable = true
})
.OrderByDescending(item => item.IsDefault)
.ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
public Task<int> PrintAssetLabelsAsync(string printerName, IReadOnlyList<AssetLabelItem> items)
{
return Task.Run(() =>
{
var pages = items.Select(RenderAssetLabel).ToList();
try
{
PrintBitmaps(printerName, pages, "固定资产标签打印");
return pages.Count;
}
finally
{
foreach (var page in pages)
{
page.Dispose();
}
}
});
}
public Task PrintTestLabelAsync(string printerName)
{
return Task.Run(() =>
{
using var bitmap = RenderAssetLabel(new AssetLabelItem
{
EquipmentCode = "TEST-60X40",
EquipmentName = "测试标签",
EquipmentType = "测试标签",
PurchaseDate = DateTimeOffset.Now.ToString("yyyy-MM-dd"),
Department = new DepartmentInfo
{
Name = $"测试打印 · {printerName}"
}
});
PrintBitmaps(printerName, [bitmap], "固定资产测试标签");
});
}
private static Bitmap RenderAssetLabel(AssetLabelItem item)
{
var bitmap = new Bitmap(CanvasWidth, CanvasHeight);
bitmap.SetResolution(203, 203);
using var graphics = Graphics.FromImage(bitmap);
graphics.Clear(Color.White);
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;
using var outerPen = new Pen(Color.Black, 4);
graphics.DrawRectangle(outerPen, 2, 2, CanvasWidth - 4, CanvasHeight - 4);
DrawHeader(graphics);
DrawQrSection(graphics, item);
DrawInfoSection(graphics, item);
return bitmap;
}
private static void DrawHeader(Graphics graphics)
{
var headerRect = new Rectangle(0, 0, CanvasWidth, 54);
using var fillBrush = new SolidBrush(Color.Black);
graphics.FillRectangle(fillBrush, headerRect);
using var companyFont = new Font("Microsoft YaHei", 22, FontStyle.Bold, GraphicsUnit.Pixel);
using var whiteBrush = new SolidBrush(Color.White);
using var companyFormat = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter,
FormatFlags = StringFormatFlags.NoWrap
};
graphics.DrawString(CompanyName, companyFont, whiteBrush, new RectangleF(18, 0, 350, 54), companyFormat);
graphics.FillRectangle(whiteBrush, new Rectangle(414, 16, 18, 18));
graphics.FillRectangle(whiteBrush, new Rectangle(440, 16, 5, 18));
}
private static void DrawQrSection(Graphics graphics, AssetLabelItem item)
{
var panelRect = new Rectangle(14, 66, 184, 184);
using var framePen = new Pen(Color.Black, 2);
graphics.DrawRectangle(framePen, panelRect);
var innerRect = Rectangle.Inflate(panelRect, -10, -10);
graphics.DrawRectangle(framePen, innerRect);
using var qrBitmap = BuildQrBitmap(item.EquipmentCode);
var qrRect = Rectangle.Inflate(innerRect, -10, -10);
graphics.DrawImage(qrBitmap, qrRect);
}
private static void DrawInfoSection(Graphics graphics, AssetLabelItem item)
{
var startX = 212;
var contentWidth = 252;
var typeText = NormalizeText(item.EquipmentType, "未分类资产");
var codeLines = SplitCodeForDisplay(item.EquipmentCode);
var purchaseDate = FormatLabelDate(item.PurchaseDate);
var organization = NormalizeText(item.Department?.Name, "未分配组织");
using var blackBrush = new SolidBrush(Color.Black);
using var whiteBrush = new SolidBrush(Color.White);
using var typeFont = new Font("Bahnschrift", 18, FontStyle.Bold, GraphicsUnit.Pixel);
using var codeFont = new Font("Bahnschrift", 34, FontStyle.Bold, GraphicsUnit.Pixel);
using var captionFont = new Font("Bahnschrift", 13, FontStyle.Bold, GraphicsUnit.Pixel);
using var valueFont = new Font("Microsoft YaHei", 18, FontStyle.Bold, GraphicsUnit.Pixel);
using var orgFont = new Font("Microsoft YaHei", 17, FontStyle.Bold, GraphicsUnit.Pixel);
using var noWrapFormat = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter,
FormatFlags = StringFormatFlags.NoWrap
};
var typeWidth = Math.Min(contentWidth, Math.Max(92, (int)Math.Ceiling(graphics.MeasureString(typeText, typeFont).Width) + 24));
var typeRect = new Rectangle(startX, 70, typeWidth, 34);
graphics.FillRectangle(blackBrush, typeRect);
graphics.DrawString(typeText, typeFont, whiteBrush, typeRect, noWrapFormat);
var codeY = 114;
foreach (var line in codeLines.Take(2))
{
graphics.DrawString(line, codeFont, blackBrush, new RectangleF(startX, codeY, contentWidth, 38), noWrapFormat);
codeY += 34;
}
graphics.FillRectangle(blackBrush, new Rectangle(startX, 184, contentWidth - 4, 3));
DrawCaptionWithValue(
graphics,
startX,
198,
"采购日期",
string.IsNullOrWhiteSpace(purchaseDate) ? "-" : purchaseDate,
captionFont,
valueFont);
DrawCaptionWithMultilineValue(
graphics,
startX,
242,
"所属组织",
organization,
captionFont,
orgFont,
contentWidth,
44);
graphics.FillRectangle(blackBrush, new Rectangle(startX, 296, 180, 3));
graphics.FillRectangle(blackBrush, new Rectangle(startX + 188, 287, 26, 12));
}
private static void DrawCaptionWithValue(
Graphics graphics,
int x,
int y,
string caption,
string value,
Font captionFont,
Font valueFont)
{
using var blackBrush = new SolidBrush(Color.Black);
using var whiteBrush = new SolidBrush(Color.White);
using var captionFormat = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
FormatFlags = StringFormatFlags.NoWrap
};
using var valueFormat = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter,
FormatFlags = StringFormatFlags.NoWrap
};
var captionRect = new Rectangle(x, y, 82, 24);
graphics.FillRectangle(blackBrush, captionRect);
graphics.DrawString(caption, captionFont, whiteBrush, captionRect, captionFormat);
graphics.DrawString(value, valueFont, blackBrush, new RectangleF(x, y + 24, 210, 28), valueFormat);
}
private static void DrawCaptionWithMultilineValue(
Graphics graphics,
int x,
int y,
string caption,
string value,
Font captionFont,
Font valueFont,
int maxWidth,
int maxHeight)
{
using var blackBrush = new SolidBrush(Color.Black);
using var whiteBrush = new SolidBrush(Color.White);
using var captionFormat = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
FormatFlags = StringFormatFlags.NoWrap
};
using var valueFormat = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Near,
Trimming = StringTrimming.EllipsisCharacter
};
var captionRect = new Rectangle(x, y, 82, 24);
graphics.FillRectangle(blackBrush, captionRect);
graphics.DrawString(caption, captionFont, whiteBrush, captionRect, captionFormat);
graphics.DrawString(value, valueFont, blackBrush, new RectangleF(x, y + 24, maxWidth, maxHeight), valueFormat);
}
private static Bitmap BuildQrBitmap(string text)
{
using var generator = new QRCodeGenerator();
using var data = generator.CreateQrCode(NormalizeText(text, "UNKNOWN"), QRCodeGenerator.ECCLevel.Q);
using var qrCode = new QRCode(data);
return qrCode.GetGraphic(20, Color.Black, Color.White, true);
}
private static void PrintBitmaps(string printerName, IReadOnlyList<Bitmap> pages, string documentName)
{
if (string.IsNullOrWhiteSpace(printerName))
{
throw new InvalidOperationException("尚未选择标签打印机,请先在助手里保存打印机。");
}
using var document = new PrintDocument();
document.DocumentName = documentName;
document.PrinterSettings.PrinterName = printerName;
if (!document.PrinterSettings.IsValid)
{
throw new InvalidOperationException($"找不到打印机:{printerName}");
}
document.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0);
document.DefaultPageSettings.PaperSize = new PaperSize("AssetLabel60x40", PaperWidthHundredthsOfInch, PaperHeightHundredthsOfInch);
document.DefaultPageSettings.Landscape = false;
document.OriginAtMargins = false;
document.PrintController = new StandardPrintController();
var pageIndex = 0;
document.PrintPage += (_, eventArgs) =>
{
var page = pages[pageIndex];
var graphics = eventArgs.Graphics;
if (graphics is null)
{
throw new InvalidOperationException("当前打印驱动没有返回可用的绘图上下文。");
}
graphics.PageUnit = GraphicsUnit.Display;
graphics.Clear(Color.White);
graphics.DrawImage(page, eventArgs.PageBounds);
pageIndex += 1;
eventArgs.HasMorePages = pageIndex < pages.Count;
};
document.Print();
}
private static string NormalizeText(string? value, string fallback)
{
var normalized = string.IsNullOrWhiteSpace(value)
? string.Empty
: string.Join(" ", value.Split(['\r', '\n', '\t', ' '], StringSplitOptions.RemoveEmptyEntries));
return string.IsNullOrWhiteSpace(normalized) ? fallback : normalized;
}
private static string FormatLabelDate(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var normalized = value.Trim().Replace('/', '-');
return normalized.Length >= 10 ? normalized[..10] : normalized;
}
private static List<string> SplitCodeForDisplay(string? value)
{
var normalized = NormalizeText(value, "-");
if (normalized.Length <= 14)
{
return [normalized];
}
var separatorIndexes = normalized
.Select((character, index) => (character, index))
.Where(item => item.character is '-' or '/')
.Select(item => item.index)
.ToList();
if (separatorIndexes.Count > 0)
{
var middleIndex = normalized.Length / 2.0;
var bestIndex = separatorIndexes
.OrderBy(index => Math.Abs(index - middleIndex))
.First();
var first = normalized[..(bestIndex + 1)].Trim();
var second = normalized[(bestIndex + 1)..].Trim();
if (!string.IsNullOrWhiteSpace(first) && !string.IsNullOrWhiteSpace(second))
{
return [first, second];
}
}
var midpoint = (int)Math.Ceiling(normalized.Length / 2.0);
return [normalized[..midpoint], normalized[midpoint..]];
}
}