×

.NET 8 WinForm 自定义报表控件与打印实战

独孤求败 独孤求败 发表于2026-06-08 13:02:46 浏览5 评论0

抢沙发发表评论

一、为什么选择自建报表控件?

工业上位机场景下,报表需求往往具备以下特征:

  • • 格式多变:不同客户对表头、Logo、签章位置要求各异,第三方控件改起来束手束脚
  • • 数据源复杂:PLC 寄存器、数据库、MQTT 消息等多种数据需要汇聚到一张报表
  • • 打印要求严格:套打、连续纸、针式打印机等场景,对分页和定位精度要求极高
  • • 部署要轻:上位机环境资源有限,不希望引入几十 MB 的第三方依赖

自建方案的核心优势:零授权费、体积可控、打印逻辑完全可定制。

图片


二、报表控件核心设计

2.1 三层架构



1
2

数据层(IDataSource)  →  模板层(ITemplate)  →  渲染层(IRenderer)
   多源适配                JSON/XML 定义            GDI+ 绘制



  • • 数据层:抽象出统一的数据源接口,支持 DataTable、实体列表、字典列表等
  • • 模板层:用 JSON 或 XML 描述报表结构(带区、列定义、样式)
  • • 渲染层:基于 GDI+ 将模板 + 数据绘制到 Graphics 画布

2.2 带区(Band)模型

将报表纵向划分为多个带区,每个带区独立渲染:

带区
职责
触发时机
ReportHeader
标题、Logo、日期范围
仅报表开头
PageHeader
列标题、页眉信息
每页顶部
Detail
明细数据行
每条记录
GroupHeader/Footer
分组标题与汇总
分组变化时
PageFooter
页码、打印时间
每页底部
ReportFooter
总计、签章区
仅报表末尾

2.3 核心接口定义



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public interface IReportDataSource
{
    IEnumerable<Dictionary<string, object>> GetRows();
}

public interface IReportTemplate
{
    PageSettings PageSettings { get; }
    Band ReportHeader { get; }
    Band PageHeader { get; }
    Band Detail { get; }
    Band PageFooter { get; }
    Band ReportFooter { get; }
}

public interface IReportRenderer
{
    void Render(IReportTemplate tpl, IReportDataSource data,
                Graphics g, Rectangle bounds);
}




三、打印实战:从渲染到纸张

打印的本质就是把 GDI+ 的画布换成打印机。.NET 提供的 PrintDocument 让这个过程非常自然。

3.1 PrintDocument 基础用法



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

var printDoc = new PrintDocument();
printDoc.PrinterSettings.PrinterName = "EPSON LQ-680K"; // 针式打印机
printDoc.DefaultPageSettings.PaperSize =
    new PaperSize("Custom", 800, 600); // 自定义纸张(单位:百分之一英寸)
printDoc.DefaultPageSettings.Landscape = false;
printDoc.DefaultPageSettings.Margins =
    new Margins(50, 50, 50, 50); // 边距

printDoc.PrintPage += (s, e) =>
{
    var g = e.Graphics;
    var bounds = e.MarginBounds;
    // 调用报表渲染器,与屏幕预览完全一致
    renderer.Render(template, dataSource, g, bounds);

    // 标记是否还有后续页
    e.HasMorePages = renderer.HasMorePages;
};

// 打印预览
var preview = new PrintPreviewDialog();
preview.Document = printDoc;
preview.ShowDialog();



关键点PrintPage 事件中的 Graphics 对象就是打印机画布,渲染逻辑与屏幕显示完全复用HasMorePages = true 时会再次触发 PrintPage,实现自动分页。

3.2 分页策略

分页是打印的核心难点。推荐预计算分页方案:



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

public class PaginationEngine
{
    // 预计算:将数据行分配到各页
    public List<Page> CalculatePages(IReportTemplate tpl,
        IReportDataSource data, float pageHeight)
    {
        var pages = new List<Page>();
        float y = 0;
        var headerH = EstimateHeight(tpl.PageHeader);
        var footerH = EstimateHeight(tpl.PageFooter);
        var detailH = EstimateHeight(tpl.Detail);
        float usableHeight = pageHeight - headerH - footerH;

        var currentPage = new Page();
        currentPage.Bands.Add(tpl.PageHeader);

        foreach (var row in data.GetRows())
        {
            if (y + detailH > usableHeight)
            {
                currentPage.Bands.Add(tpl.PageFooter);
                pages.Add(currentPage);
                currentPage = new Page();
                currentPage.Bands.Add(tpl.PageHeader);
                y = 0;
            }
            currentPage.DetailRows.Add(row);
            y += detailH;
        }

        currentPage.Bands.Add(tpl.ReportFooter);
        pages.Add(currentPage);
        return pages;
    }
}



预计算的好处

  • • 打印前就知道总页数,页码显示准确
  • • 可以精确控制分组不被跨页拆分(KeepTogether
  • • 支持打印进度条

3.3 套打与精确定位

上位机常见套打场景(如发票、标签纸),需要绝对坐标定位



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

// 套打模板:用毫米定义坐标
public class OverprintTemplate
{
    public List<OverprintField> Fields { get; set; }
}

public class OverprintField
{
    public string FieldName { get; set; }    // 数据字段名
    public float X { get; set; }             // 毫米
    public float Y { get; set; }             // 毫米
    public float Width { get; set; }         // 毫米
    public string Format { get; set; }       // 格式化字符串
}

// 渲染时将毫米转为 Graphics 坐标
float mmToPixel = g.DpiX / 25.4f;
var xPx = field.X * mmToPixel;
var yPx = field.Y * mmToPixel;



提示:套打调试时,建议先用 Bitmap 绘制一张 1:1 的图片,叠加在打印模板上对比偏移量,确认无误后再接打印机。

3.4 打印机设置与纸张管理



1
2
3
4
5
6
7
8
9
10
11

// 枚举已安装的打印机
foreach (string name in PrinterSettings.InstalledPrinters)
    comboBox1.Items.Add(name);
 
// 自定义纸张尺寸(连续纸、标签纸等)
var ps = new PaperSize("Continuous", 250, 140); // 80列连续纸
printDoc.DefaultPageSettings.PaperSize = ps;
 
// 打印份数与排序
printDoc.PrinterSettings.Copies = 3;
printDoc.PrinterSettings.Collate = true; // 逐份打印




四、导出能力扩展

打印之外,报表通常还需要导出能力:

导出格式
推荐方案
说明
PDF
QuestPDF / PdfSharp
MIT 协议,.NET 8 原生支持
Excel
EPPlus / MiniExcel
MiniExcel 更轻量,适合大数据量
图片
Bitmap + Graphics
直接从渲染器输出
打印
PrintDocument
本文重点,复用 GDI+ 渲染

由于渲染层统一基于 Graphics 抽象,切换输出目标只需替换画布来源,核心渲染逻辑零改动


五、总结

模块
核心要点
报表控件
三层架构 + Band 带区模型,数据/模板/渲染分离
打印引擎
PrintDocument + 预计算分页,渲染逻辑与显示复用
套打定位
毫米坐标 + DPI 转换,Bitmap 预调试
导出扩展
统一 Graphics 画布,切换输出零成本

自定义报表 + 打印方案的核心价值在于一套渲染逻辑,多种输出目标——屏幕预览、打印机输出、PDF 导出共享同一套代码。在 .NET 8 + WinForm 的组合下,这套方案足够轻量、足够灵活,完全能胜任上位机场景的报表需求。

图片

觉得有用?欢迎随手点赞、在看、转发,也可以给我个星标⭐。↓↓↓

精选

1.【2026年最新版】本地跑大模型必看:Ollama + 千问 Qwen2.5 完整部署教程

2.光电开关三种类型怎么选(漫反射 / 镜面 / 对射)

3.立体仓库物料存放形式:托盘式 vs 料箱式,一篇讲明白

4.能把 200 SMART 的所有功能块玩明白,你就已经掌握了 PLC 编程的 “半壁江山”

5.2026 年入门上位机,选 WinForm 还是 WPF?


群贤毕至

访客