- 新增打印终端、配对码、打印任务相关表结构与接口\n- 增加后台打印终端/任务管理页和资产标签远程打印入口\n- 引入 .NET 8 Windows 打印助手源码及下载清单配置
366 lines
12 KiB
C#
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..]];
|
|
}
|
|
}
|