LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

C#打印神器:一个类库配置JSON搞定所有复杂表格打印需求

admin
2025年11月26日 22:38 本文热度 788

还在为复杂的报表打印而头疼吗?这个有周末闲着把老早写的一个打印类重写了,好处就是可以用json定义结构了,不过发现写到最后还是有些小麻烦,人也懒了,等有时间再优化吧。Excel导出太慢,Crystal Reports太重,自己画Graphics太复杂?今天分享一个轻量级的C# WinForms表格打印解决方案,让你用JSON配置就能搞定各种复杂的表格打印需求!

这套方案不仅支持动态数据绑定、单元格合并,还能轻松添加二维码和条形码,最关键的是配置简单、性能优秀。无论是物料标签、采购单据还是各种业务报表,都能快速搞定。

🎯 传统打印方案的痛点分析

常见问题汇总

  • • Graphics绘制繁琐:每个表格都要写一堆坐标计算代码
  • • Excel导出缓慢:大量数据处理时性能瓶颈明显
  • • 报表工具笨重:Crystal Reports等需要额外许可费用
  • • 维护成本高:表格样式调整需要重新编译发布

业务场景需求

实际开发中,我们经常需要打印:

  • • 物料标签(带条码/二维码)
  • • 采购订单(动态行数)
  • • 出库单据(复杂表头)
  • • 质检报告(数据格式化)

🚩 基本流程

💡 JSON配置驱动的解决方案

🔧 核心设计理念

这套方案采用配置与逻辑分离的设计思路:

  • • JSON配置文件:定义表格结构、样式、数据绑定
  • • 渲染引擎:负责解析配置并绘制表格
  • • 数据适配器:处理动态数据和占位符替换

📊 架构组件说明

// 核心类结构
public class TableDocument        // 主渲染引擎
public class TableConfig         // 配置数据模型  
public class Position           // 单元格位置管理
public class Row/Column         // 行列定义

🔥 实战代码解析

1️⃣ 基础配置结构

{
  "Printer":{
    "Name":"Microsoft Print to PDF",
    "PaperSize":"A4"
},
"StartPosition":{"X":10,"Y":10},
"RowsCount":7,
"ColumnsCount":7,
"ColumnsWidths":[80,200,200,80,80,80,80]
}

关键特性:

  • • 支持自定义纸张大小
  • • 灵活的起始位置设置
  • • 动态列宽配置

2️⃣ 动态数据绑定

{
  "RowIndex":"6",
"Is_Items":true,// 标记为动态数据行
"Cells":[
    {
      "Title":"$seq_no$",        // 序号占位符
      "ColumnIndex":"1"
    },
    {
      "Title":"$part_no$",       // 物料编号
      "ColumnIndex":"2"
    }
]
}

占位符语法:

  • • $property$:简单属性绑定
  • • $date_field,date,yyyy-MM-dd$:日期格式化
  • • $seq_no$:自动序号生成

3️⃣ 核心渲染逻辑

public async Task DrawTableAsync(Graphics graphics, object headerData, 
    IEnumerable<object> items, TableConfig config
)

{
    // 1. 清理状态并重新计算布局
    ClearState();
    await OrganizeConfigAsync(config, items.Count());
    
    // 2. 构建表格结构
    DrawStructure(config, items.Count());
    
    // 3. 绘制表格边框和网格线
    Draw(graphics, config);
    
    // 4. 填充数据内容
    await RenderCellData(graphics, headerData, items, config);
}

4️⃣ 智能单元格合并

private (int rowSpan, int colSpan) GetMergeSpan(string[]? merge, int? seqNo)
{
    if (merge == null || merge.Length < 4return (00);
    
    // 支持动态计算合并范围
    string startRow = merge[0];
    if (seqNo.HasValue && startRow.Contains("+"))
    {
        startRow = Calculate(startRow.Replace("seq_no", seqNo.Value.ToString())).ToString();
    }
    
    return (rowSpan, colSpan);
}

5️⃣ 条码/二维码集成

public void DrawQRCode(Graphics graphics, string code, Position position, 
    Point offset, int width = 64int height = 64
)

{
    var writer = new BarcodeWriter
    {
        Options = new EncodingOptions { 
            Width = width, Height = height, 
            Margin = 0, PureBarcode = true 
        },
        Format = BarcodeFormat.QR_CODE
    };
    
    using var bitmap = writer.Write(code);
    DrawImageInCell(graphics, position, bitmap, offset);
}

🎨 高级功能特性

💪 动态行数计算

{
  "RowIndex":"5+Items.Count+1",// 支持表达式计算
"Cells":[
    {
      "Title":"总计行内容",
      "Merge":["5+Items.Count+1","1","5+Items.Count+1","7"]
    }
]
}

🎯 灵活的边框控制

{
  "Borders":[
    {
      "Direction":"Bottom",
      "Width":1,
      "Style":"Dot",      // 支持虚线样式
      "Color":"#000000"
    }
]
}

📝 字体样式配置

{
  "Font":{
    "Name":"SimHei",
    "Size":20,
    "Bold":true,
    "Italic":false
},
"Alignment":"Center",        // 水平对齐
"LineAlignment":"Center"     // 垂直对齐
}

⚡ 性能优化亮点

🚀 缓存机制

private readonly Dictionary<(string? h, string? v), StringFormat> _stringFormatCache = new();

private StringFormat GetCachedStringFormat(string? alignment, string? lineAlignment)
{
    var key = (alignment, lineAlignment);
    if (_stringFormatCache.TryGetValue(key, out var sf)) return sf;
    
    // 创建并缓存StringFormat对象
    sf = new StringFormat { /* 配置对象 */ };
    _stringFormatCache[key] = sf;
    return sf;
}

💡 智能位置索引

private readonly Dictionary<(int Row, int Col), Position> _positionMap = new();

public Position? GetPosition(int rowIndex, int columnIndex)
{
    _positionMap.TryGetValue((rowIndex, columnIndex), out var p);
    return p;
}

🏳️‍🌈 完整核心类

using Microsoft.CodeAnalysis.CSharp.Scripting;
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using ZXing;
using ZXing.Windows.Compatibility;

namespace PrinterLibrary
{
    public class TableDocument
    {
        public List<Column> Columns { get; } = new();
        public List<Row> Rows { get; } = new();
        public List<Position> Positions { get; } = new(); // 单元格位置索引
        public Dictionary<Position, Position> MergeCell { get; } = new(); // 合并单元格信息

        private readonly Dictionary<(int Row, int Col), Position> _positionMap = new();
        private readonly Dictionary<(string? h, string? v), StringFormat> _stringFormatCache = new();

        public void ClearState()
        {
            Columns.Clear();
            Rows.Clear();
            Positions.Clear();
            MergeCell.Clear();
            _positionMap.Clear();
            _stringFormatCache.Clear();
        }

        public Position? GetPosition(int rowIndex, int columnIndex)
        {
            _positionMap.TryGetValue((rowIndex, columnIndex), outvar p);
            return p;
        }

        public int GetColumnWidth(int columnIndex) => Columns[columnIndex].Width;
        public int GetRowHeight(int rowIndex) => Rows[rowIndex].Height;

        public void DrawImageInCell(Graphics graphics, Position position, Image image, Point offset)
        {
            var rect = new Rectangle(position.Point.X + offset.X, position.Point.Y + offset.Y, image.Width, image.Height);
            graphics.DrawImage(image, rect);
        }

        public Image ResizeImage(Image image, int width, int height)
        {
            var resized = new Bitmap(width, height);
            usingvar g = Graphics.FromImage(resized);
            g.InterpolationMode = InterpolationMode.HighQualityBicubic;
            g.SmoothingMode = SmoothingMode.HighQuality;
            g.PixelOffsetMode = PixelOffsetMode.HighQuality;
            g.DrawImage(image, new Rectangle(00, width, height));
            return resized;
        }

        public void DrawQRCode(Graphics graphics, string code, Position position, Point offset, int width = 64int height = 64)
        {
            var writer = new BarcodeWriter
            {
                Options = new ZXing.Common.EncodingOptions { Width = width, Height = height, Margin = 0, PureBarcode = true },
                Format = ZXing.BarcodeFormat.QR_CODE
            };
            usingvar bitmap = writer.Write(code);
            DrawImageInCell(graphics, position, bitmap, offset);
        }

        public void Draw128Code(Graphics graphics, string code, Position position, int width, int height, Point offset, bool pureBarcode = false)
        {
            var writer = new BarcodeWriter
            {
                Options = new ZXing.Common.EncodingOptions { Width = width, Height = height, Margin = 0, PureBarcode = pureBarcode },
                Format = ZXing.BarcodeFormat.CODE_128
            };
            usingvar bitmap = writer.Write(code);
            DrawImageInCell(graphics, position, bitmap, offset);
        }

        private void DrawCellText(Graphics graphics, Position pos, string text, Font font, Brush brush,
            int mergeRowSpan, int mergeColSpan, StringFormat stringFormat
)

        {
            int rectHeight = GetRowHeight(pos.RowIndex);
            for (int i = 0; i < mergeRowSpan; i++) rectHeight += GetRowHeight(pos.RowIndex + i);

            int rectWidth = GetColumnWidth(pos.ColumnIndex);
            for (int i = 1; i < mergeColSpan; i++) rectWidth += GetColumnWidth(pos.ColumnIndex + i);

            var layout = new RectangleF(pos.Point.X, pos.Point.Y, rectWidth, rectHeight);
            graphics.DrawString(text, font, brush, layout, stringFormat);
        }

        public static SizeF MeasureTextSize(Graphics graphics, string text, Font font) => graphics.MeasureString(text, font);

        public void Draw(Graphics graphics, TableConfig config)
        {
            graphics.SmoothingMode = SmoothingMode.None;
            int startX = config.StartPosition.X;
            int startY = config.StartPosition.Y;

            usingvar pen = new Pen(Color.Black, 1);

            // 构建坐标(Position)而不立即绘制格子
            int currentY = startY;
            for (int r = 0; r < Rows.Count; r++)
            {
                int currentX = startX;
                for (int c = 0; c < Columns.Count; c++)
                {
                    var pos = new Position(r, c, new Point(currentX, currentY));
                    Positions.Add(pos);
                    _positionMap[(r, c)] = pos;
                    currentX += Columns[c].Width;
                }
                currentY += Rows[r].Height;
            }

            // 计算合并区域矩形(用于跳过内部边界线)
            var mergedRects = new List<Rectangle>();
            foreach (var kv in MergeCell)
            {
                int startRowZero = kv.Key.RowIndex - 1;
                int startColZero = kv.Key.ColumnIndex - 1;
                int endRow = kv.Value.RowIndex - 1;     // inclusive zero-based
                int endCol = kv.Value.ColumnIndex - 1;  // inclusive zero-based
                var startPos = GetPosition(startRowZero, startColZero);
                if (startPos == nullcontinue;
                int w = 0int h = 0;
                for (int c = startColZero; c <= endCol; c++) w += GetColumnWidth(c);
                for (int r = startRowZero; r <= endRow; r++) h += GetRowHeight(r);
                mergedRects.Add(new Rectangle(startPos.Point.X, startPos.Point.Y, w, h));
            }

            // 列边界坐标
            var colBoundaries = newint[Columns.Count + 1];
            colBoundaries[0] = startX;
            for (int i = 0; i < Columns.Count; i++) colBoundaries[i + 1] = colBoundaries[i] + Columns[i].Width;
            // 行边界坐标
            var rowBoundaries = newint[Rows.Count + 1];
            rowBoundaries[0] = startY;
            for (int i = 0; i < Rows.Count; i++) rowBoundaries[i + 1] = rowBoundaries[i] + Rows[i].Height;

            int totalWidth = colBoundaries[^1] - startX;
            int totalHeight = rowBoundaries[^1] - startY;

            // 绘制外框
            graphics.DrawRectangle(pen, startX, startY, totalWidth, totalHeight);

            // 垂直线(内部列分隔)
            for (int ci = 1; ci < colBoundaries.Length - 1; ci++)
            {
                int x = colBoundaries[ci];
                // 分段绘制,遇到合并区域内部则跳过
                int segmentStartY = startY;
                for (int ri = 0; ri < rowBoundaries.Length - 1; ri++)
                {
                    int yTop = rowBoundaries[ri];
                    int yBottom = rowBoundaries[ri + 1];

                    if (IsVerticalLineInsideMerged(x, yTop, yBottom, mergedRects))
                    {
                        // 跳过该段
                        continue;
                    }
                    graphics.DrawLine(pen, x, yTop, x, yBottom);
                }
            }

            // 水平线(内部行分隔)
            for (int ri = 1; ri < rowBoundaries.Length - 1; ri++)
            {
                int y = rowBoundaries[ri];
                for (int ci = 0; ci < colBoundaries.Length - 1; ci++)
                {
                    int xLeft = colBoundaries[ci];
                    int xRight = colBoundaries[ci + 1];
                    if (IsHorizontalLineInsideMerged(y, xLeft, xRight, mergedRects))
                    {
                        continue;
                    }
                    graphics.DrawLine(pen, xLeft, y, xRight, y);
                }
            }

            // 行与单元格边框
            foreach (var row in config.Rows)
            {
                if (!int.TryParse(row.RowIndex, outint intRowIndex)) continue;
                if (row.Borders != null)
                {
                    foreach (var br in row.Borders)
                    {
                        usingvar penBorder = CreatePen(br);
                        switch (br.Direction)
                        {
                            case"Top":
                                {
                                    var leftX = startX;
                                    var rightX = colBoundaries[^1];
                                    int y = rowBoundaries[intRowIndex - 1];
                                    graphics.DrawLine(penBorder, leftX, y, rightX, y);
                                    break;
                                }
                            case"Bottom":
                                {
                                    var leftX = startX;
                                    var rightX = colBoundaries[^1];
                                    int y = rowBoundaries[intRowIndex];
                                    graphics.DrawLine(penBorder, leftX, y, rightX, y);
                                    break;
                                }
                            case"Left":
                                {
                                    int x = startX;
                                    int y1 = rowBoundaries[intRowIndex - 1];
                                    int y2 = rowBoundaries[intRowIndex];
                                    graphics.DrawLine(penBorder, x, y1, x, y2);
                                    break;
                                }
                            case"Right":
                                {
                                    int x = colBoundaries[^1];
                                    int y1 = rowBoundaries[intRowIndex - 1];
                                    int y2 = rowBoundaries[intRowIndex];
                                    graphics.DrawLine(penBorder, x, y1, x, y2);
                                    break;
                                }
                        }
                    }
                }

                foreach (var cell in row.Cells)
                {
                    if (cell.Borders == null || cell.Borders.Count == 0continue;
                    if (!int.TryParse(cell.ColumnIndex, outint colIndex)) continue;
                    int cellLeft = colBoundaries[colIndex - 1];
                    int cellRight = colBoundaries[colIndex];
                    int cellTop = rowBoundaries[intRowIndex - 1];
                    int cellBottom = rowBoundaries[intRowIndex];
                    foreach (var br in cell.Borders)
                    {
                        if (br.Direction != "Left" && br.Direction != "Right"continue;
                        usingvar penBorder = CreatePen(br);
                        if (br.Direction == "Left")
                        {
                            graphics.DrawLine(penBorder, cellLeft, cellTop, cellLeft, cellBottom);
                        }
                        else
                        {
                            graphics.DrawLine(penBorder, cellRight, cellTop, cellRight, cellBottom);
                        }
                    }
                }
            }
        }

        private bool IsVerticalLineInsideMerged(int x, int yTop, int yBottom, List<Rectangle> mergedRects)
        {
            foreach (var rect in mergedRects)
            {
                if (x > rect.Left && x < rect.Right && yTop >= rect.Top && yBottom <= rect.Bottom)
                {
                    returntrue// 该垂直线段位于合并区域内部
                }
            }
            returnfalse;
        }

        private bool IsHorizontalLineInsideMerged(int y, int xLeft, int xRight, List<Rectangle> mergedRects)
        {
            foreach (var rect in mergedRects)
            {
                if (y > rect.Top && y < rect.Bottom && xLeft >= rect.Left && xRight <= rect.Right)
                {
                    returntrue// 该水平线段位于合并区域内部
                }
            }
            returnfalse;
        }

        private static Pen CreatePen(TableConfig.Border br)
        {
            Brush brush = br.Style switch
            {
                "Dot" => new HatchBrush(HatchStyle.DashedHorizontal, ColorTranslator.FromHtml(br.Color), Color.White),
                _ => new SolidBrush(ColorTranslator.FromHtml(br.Color))
            };
            if (br.Width == 0)
            {
                brush.Dispose();
                returnnew Pen(Color.White, 2);
            }
            returnnew Pen(brush, br.Width);
        }

        public async Task DrawTableAsync(Graphics graphics, object dy, IEnumerable<object> items, TableConfig config)
        {
            dy ??= newobject();
            items ??= Enumerable.Empty<object>();
            int itemCount = items.Count();
            ClearState();
            await OrganizeConfigAsync(config, itemCount);
            DrawStructure(config, itemCount);
            Draw(graphics, config);

            var headerType = dy.GetType();
            var headerProps = headerType.GetProperties().ToDictionary(p => p.Name, p => p);
            var itemPropCache = new Dictionary<Type, Dictionary<string, System.Reflection.PropertyInfo>>();

            int currentDynamicCursor = 0;
            foreach (var row in config.Rows.OrderBy(r => int.Parse(r.RowIndex)))
            {
                int rowIndex = int.Parse(row.RowIndex);
                if (row.Is_Items)
                {
                    currentDynamicCursor = rowIndex;
                    int seq = 1;
                    foreach (var it in items)
                    {
                        var t = it.GetType();
                        if (!itemPropCache.TryGetValue(t, outvar dict))
                        {
                            dict = t.GetProperties().ToDictionary(p => p.Name, p => p);
                            itemPropCache[t] = dict;
                        }
                        foreach (var cell in row.Cells)
                        {
                            if (!int.TryParse(cell.ColumnIndex, outint cIndex)) continue;
                            var pos = GetPosition(currentDynamicCursor - 1, cIndex - 1);
                            if (pos == nullcontinue;
                            string title = cell.Title ?? string.Empty;
                            var fmt = GetCachedStringFormat(cell.Alignment, cell.LineAlignment);
                            title = ReplacePlaceHolderForItem(title, it, dict, seq);
                            var (mergeRowSpan, mergeColSpan) = GetMergeSpan(cell.Merge, seq);
                            DrawCellText(graphics, pos, title, CellFont(cell), Brushes.Black, mergeRowSpan, mergeColSpan, fmt);
                        }
                        currentDynamicCursor++;
                        seq++;
                    }
                }
                else
                {
                    foreach (var cell in row.Cells)
                    {
                        if (!int.TryParse(cell.ColumnIndex, outint cIndex)) continue;
                        var pos = GetPosition(rowIndex - 1, cIndex - 1);
                        if (pos == nullcontinue;
                        string title = cell.Title ?? string.Empty;
                        var fmt = GetCachedStringFormat(cell.Alignment, cell.LineAlignment);
                        title = ReplacePlaceHolderForHeader(title, dy, headerProps);
                        var (mergeRowSpan, mergeColSpan) = GetMergeSpan(cell.Merge, null);
                        DrawCellText(graphics, pos, title, CellFont(cell), Brushes.Black, mergeRowSpan, mergeColSpan, fmt);
                    }
                }
            }

            foreach (var barcode in config.BarCodes ?? Enumerable.Empty<TableConfig.BarCode>())
            {
                if (!int.TryParse(barcode.RowIndex, outint rIdx) || !int.TryParse(barcode.ColumnIndex, outint cIdx)) continue;
                var pos = GetPosition(rIdx - 1, cIdx - 1);
                if (pos == nullcontinue;
                string raw = barcode.Tilte ?? string.Empty;
                string replaced = ReplacePlaceHolderForHeader(raw, dy, headerProps);
                barcode.Sizes ??= new Size(6464);
                if (barcode.BarcodeType == "QRCODE")
                {
                    DrawQRCode(graphics, replaced, pos, new Point(155), barcode.Sizes.Value.Width, barcode.Sizes.Value.Height);
                }
                elseif (barcode.BarcodeType == "CODE128")
                {
                    Draw128Code(graphics, replaced, pos, barcode.Sizes.Value.Width, barcode.Sizes.Value.Height, new Point(22));
                }
            }
        }

        private (int rowSpan, int colSpan) GetMergeSpan(string[]? merge, int? seqNo)
        {
            if (merge == null || merge.Length < 4return (00);
            string startRow = merge[0];
            string startCol = merge[1];
            string endRow = merge[2];
            string endCol = merge[3];
            if (seqNo.HasValue)
            {
                if (startRow.Contains("+")) startRow = Calculate(startRow.Replace("seq_no", seqNo.Value.ToString())).ToString();
                if (endRow.Contains("+")) endRow = Calculate(endRow.Replace("seq_no", seqNo.Value.ToString())).ToString();
            }
            int sr = int.Parse(startRow);
            int er = int.Parse(endRow);
            int sc = int.Parse(startCol);
            int ec = int.Parse(endCol);
            int rowSpan = er - sr;
            int colSpan = ec - sc + 1;
            return (rowSpan, colSpan);
        }

        private string ReplacePlaceHolderForItem(string title, object item, Dictionary<string, System.Reflection.PropertyInfo> props, int seq)
        {
            var matches = Regex.Matches(title, @"\$(.*?)\$");
            foreach (Match m in matches.Cast<Match>())
            {
                string expr = m.Groups[1].Value;
                if (expr == "seq_no")
                {
                    title = title.Replace($"${expr}$", seq.ToString());
                    continue;
                }
                var parts = expr.Split(',');
                string propName = parts[0];
                if (!props.TryGetValue(propName, outvar pi)) continue;
                var valueObj = pi.GetValue(item);
                stringvalue = valueObj?.ToString() ?? string.Empty;
                if (parts.Length > 1 && parts[1] == "date" && parts.Length > 2)
                {
                    if (DateTime.TryParse(valueoutvar dt)) value = dt.ToString(parts[2]);
                }
                title = title.Replace($"${expr}$"value);
            }
            return title;
        }

        private string ReplacePlaceHolderForHeader(string title, object header, Dictionary<string, System.Reflection.PropertyInfo> props)
        {
            var matches = Regex.Matches(title, @"\$(.*?)\$");
            foreach (Match m in matches.Cast<Match>())
            {
                string expr = m.Groups[1].Value;
                var parts = expr.Split(',');
                string propName = parts[0];
                if (!props.TryGetValue(propName, outvar pi)) continue;
                var valueObj = pi.GetValue(header);
                stringvalue = valueObj?.ToString() ?? string.Empty;
                if (parts.Length > 1 && parts[1] == "date" && parts.Length > 2)
                {
                    if (DateTime.TryParse(valueoutvar dt)) value = dt.ToString(parts[2]);
                }
                title = title.Replace($"${expr}$"value);
            }
            return title;
        }

        private Font CellFont(TableConfig.Cell cell)
        {
            if (cell.Font == nullreturnnew Font("SimHei"12, FontStyle.Regular);
            FontStyle style = FontStyle.Regular;
            if (cell.Font.Bold) style |= FontStyle.Bold;
            if (cell.Font.Italic) style |= FontStyle.Italic;
            returnnew Font(cell.Font.Name, cell.Font.Size, style);
        }

        public void DrawStructure(TableConfig config, int itemCount)
        {
            // 行构建
            for (int i = 0; i < config.RowsCount + itemCount; i++)
            {
                var found = config.Rows.FirstOrDefault(x => x.RowIndex == (i + 1).ToString());
                if (found != null)
                {
                    int h = 35;
                    if (!string.IsNullOrEmpty(found.Height) && int.TryParse(found.Height, outint parsed)) h = parsed;
                    Rows.Add(new Row(h));
                }
                else
                {
                    Rows.Add(new Row(35));
                }
            }
            // 列构建
            foreach (var w in config.ColumnsWidths) Columns.Add(new Column(w));

            // 追加动态行
            var dynamicTemplate = config.Rows.FirstOrDefault(x => x.Is_Items);
            if (dynamicTemplate != null)
            {
                for (int i = 0; i < itemCount - 1; i++)
                {
                    var clone = JsonSerializer.Deserialize<TableConfig.Row>(JsonSerializer.Serialize(dynamicTemplate));
                    if (clone != null) config.Rows.Add(clone);
                }
            }

            int dynSeq = 0;
            foreach (var row in config.Rows)
            {
                var merges = row.Cells.Where(c => c.Merge != null).ToList();
                foreach (var merge in merges)
                {
                    if (merge.Merge.Length < 4continue;
                    string startRowStr = merge.Merge[0];
                    string startColStr = merge.Merge[1];
                    string endRowStr = merge.Merge[2];
                    string endColStr = merge.Merge[3];
                    if (row.Is_Items)
                    {
                        if (startRowStr.Contains("+")) startRowStr = Calculate(startRowStr.Replace("seq_no", dynSeq.ToString())).ToString();
                        if (endRowStr.Contains("+")) endRowStr = Calculate(endRowStr.Replace("seq_no", dynSeq.ToString())).ToString();
                    }
                    int startRowIndex = int.Parse(startRowStr);
                    int startColumnIndex = int.Parse(startColStr);
                    int endRowIndex = int.Parse(endRowStr);
                    int endColumnIndex = int.Parse(endColStr);
                    MergeCell[new Position(startRowIndex, startColumnIndex)] = new Position(endRowIndex, endColumnIndex);
                }
                if (row.Is_Items) dynSeq++;
            }
        }

        public async Task OrganizeConfigAsync(TableConfig config, int itemCount)
        {
            foreach (var row in config.Rows)
            {
                if (row.RowIndex.Contains("Items.Count"))
                {
                    string expr = row.RowIndex.Replace("Items.Count", itemCount.ToString());
                    row.RowIndex = (await CSharpScript.EvaluateAsync(expr)).ToString();
                }
            }
            foreach (var row in config.Rows)
            {
                foreach (var cell in row.Cells.Where(c => c.Merge != null))
                {
                    if (cell.Merge[0].Contains("Items.Count"))
                    {
                        string expr = cell.Merge[0].Replace("Items.Count", itemCount.ToString());
                        cell.Merge[0] = (await CSharpScript.EvaluateAsync(expr)).ToString();
                    }
                    if (cell.Merge[2].Contains("Items.Count"))
                    {
                        string expr = cell.Merge[2].Replace("Items.Count", itemCount.ToString());
                        cell.Merge[2] = (await CSharpScript.EvaluateAsync(expr)).ToString();
                    }
                }
            }
            foreach (var bc in config.BarCodes ?? Enumerable.Empty<TableConfig.BarCode>())
            {
                if (bc.RowIndex != null && bc.RowIndex.Contains("Items.Count"))
                {
                    string expr = bc.RowIndex.Replace("Items.Count", itemCount.ToString());
                    bc.RowIndex = (await CSharpScript.EvaluateAsync(expr)).ToString();
                }
            }
        }

        private StringFormat GetCachedStringFormat(string? alignment, string? lineAlignment)
        {
            var key = (alignment, lineAlignment);
            if (_stringFormatCache.TryGetValue(key, outvar sf)) return sf;
            sf = new StringFormat
            {
                Alignment = alignment switch { "Left" => StringAlignment.Near, "Right" => StringAlignment.Far, _ => StringAlignment.Center },
                LineAlignment = lineAlignment switch { "Top" => StringAlignment.Near, "Bottom" => StringAlignment.Far, _ => StringAlignment.Center }
            };
            _stringFormatCache[key] = sf;
            return sf;
        }

        public string RemoveCommentsFromJson(string json)
        {
            string singleLine = @"(//.*?$)";
            string multiLine = @"/\*[\s\S]*?\*/";
            return Regex.Replace(json, $"{singleLine}|{multiLine}"string.Empty, RegexOptions.Multiline);
        }

        private int Calculate(string formula)
        {
            try
            {
                var table = new DataTable();
                table.Columns.Add("expression"typeof(string), formula);
                var row = table.NewRow();
                table.Rows.Add(row);
                return Convert.ToInt32(row["expression"]);
            }
            catch
            {
                return0;
            }
        }
    }
}

📋 实际使用示例

物料标签打印

// 定义数据对象
var materialData = new
{
    barcode = "A0001",
    supplier = "中国制造",
    part_no = "A000001"
    part_description = "精密螺丝M6x20",
    unit = "EA",
    qty = 100,
    batch_no = "A01"
};

// 加载配置并打印
var config = JsonSerializer.Deserialize<TableConfig>(
    File.ReadAllText("templates/物料标签.json"));
    
await tableDocument.DrawTableAsync(graphics, materialData, null, config);

采购单列表打印

var purchaseOrder = new
{
    main = new { 
        purchase_no = "P0001"
        supplier = "供应商A",
        requested_arrival_time = DateTime.Now 
    },
    items = new List<dynamic> {
        new { part_no = "PN001", description = "螺丝", qty = "100" },
        new { part_no = "PN002", description = "垫片", qty = "200" }
    }
};

await tableDocument.DrawTableAsync(graphics, purchaseOrder.main, 
    purchaseOrder.items, config);



🔧 最佳实践建议

✅ 配置文件管理

  • • 模板化设计:为不同业务场景创建标准模板
  • • 版本控制:配置文件纳入Git管理,方便回滚
  • • 环境隔离:开发/测试/生产环境使用不同配置

⚠️ 常见陷阱避免

  1. 1. 内存泄漏:及时释放Graphics和Bitmap资源
  2. 2. 字体缺失:生产环境确保字体文件存在
  3. 3. 打印机兼容:测试不同品牌打印机的效果差异
  4. 4. 数据格式化:日期、数字格式要考虑本地化需求

🎯 扩展方向

  • • 导出PDF:集成iTextSharp实现PDF生成
  • • 批量打印:支持多个数据源批量处理
  • • 模板编辑器:开发可视化配置工具
  • • 云端配置:支持从API获取打印模板

🌟 总结与思考

这套基于JSON配置的表格打印方案具有三个核心优势:配置灵活(JSON驱动,无需编译)、功能完整(支持合并、条码、样式)、性能优秀(缓存机制,直接绘制)。

相比传统方案,它既避免了Excel的性能问题,又比Crystal Reports更轻量,最重要的是维护成本大幅降低。当业务需求变更时,只需修改JSON配置文件即可,无需重新发布程序。

在实际项目中,建议将不同的打印模板做成标准化组件,这样不仅提高了代码复用率,还让新同事能够快速上手。


阅读原文:原文链接


该文章在 2025/11/27 16:23:29 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved