WPF 打印与报表
大约 10 分钟约 3033 字
WPF 打印与报表
简介
WPF 提供了内置的打印支持,可以直接打印 Visual 元素、FlowDocument 和 FixedDocument。结合数据绑定和模板,可以生成丰富的报表。本文介绍 WPF 中打印文档、打印预览和报表生成的常用方式。
特点
基本打印
打印 Visual 元素
/// <summary>
/// 打印任意 UI 元素
/// </summary>
public class PrintService
{
// 打印 Visual
public void PrintVisual(Visual visual, string description = "WPF打印")
{
var printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
printDialog.PrintVisual(visual, description);
}
}
// 打印指定尺寸
public void PrintVisualSized(Visual visual, Size pageSize)
{
var printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
// 调整 Visual 大小以匹配打印页
if (visual is FrameworkElement element)
{
element.Width = pageSize.Width;
element.Height = pageSize.Height;
element.Measure(pageSize);
element.Arrange(new Rect(pageSize));
element.UpdateLayout();
}
printDialog.PrintVisual(visual, "WPF打印");
}
}
}
// 使用
private void PrintButton_Click(object sender, RoutedEventArgs e)
{
var printService = new PrintService();
printService.PrintVisual(PrintArea, "销售报表");
}打印 FlowDocument
/// <summary>
/// 打印 FlowDocument — 流式文档
/// </summary>
public void PrintFlowDocument(FlowDocument document)
{
var printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
// 设置文档页面大小
document.PageHeight = printDialog.PrintableAreaHeight;
document.PageWidth = printDialog.PrintableAreaWidth;
document.PagePadding = new Thickness(50);
document.ColumnGap = 0;
document.ColumnWidth = printDialog.PrintableAreaWidth;
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
printDialog.PrintDocument(paginator, "FlowDocument打印");
}
}FlowDocument 报表
XAML 定义报表
<!-- ReportTemplate.xaml — 报表模板 -->
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
FontFamily="Microsoft YaHei" FontSize="12"
PagePadding="40">
<!-- 报表标题 -->
<Paragraph FontSize="24" FontWeight="Bold" TextAlignment="Center">
<Run Text="{Binding ReportTitle}"/>
</Paragraph>
<Paragraph FontSize="14" TextAlignment="Center" Foreground="Gray">
<Run Text="报表日期:"/>
<Run Text="{Binding ReportDate, StringFormat='{}{0:yyyy-MM-dd}'}"/>
</Paragraph>
<LineBreak/>
<!-- 基本信息表格 -->
<Table CellSpacing="0" BorderBrush="Black" BorderThickness="1">
<Table.Columns>
<TableColumn Width="150"/>
<TableColumn Width="200"/>
<TableColumn Width="150"/>
<TableColumn Width="200"/>
</Table.Columns>
<TableRowGroup>
<TableRow Background="#3498DB">
<TableCell>
<Paragraph FontWeight="Bold" Foreground="White">客户名称</Paragraph>
</TableCell>
<TableCell>
<Paragraph><Run Text="{Binding CustomerName}"/></Paragraph>
</TableCell>
<TableCell>
<Paragraph FontWeight="Bold" Foreground="White">订单编号</Paragraph>
</TableCell>
<TableCell>
<Paragraph><Run Text="{Binding OrderNo}"/></Paragraph>
</TableCell>
</TableRow>
<TableRow Background="#ECF0F1">
<TableCell>
<Paragraph FontWeight="Bold">联系人</Paragraph>
</TableCell>
<TableCell>
<Paragraph><Run Text="{Binding ContactPerson}"/></Paragraph>
</TableCell>
<TableCell>
<Paragraph FontWeight="Bold">联系电话</Paragraph>
</TableCell>
<TableCell>
<Paragraph><Run Text="{Binding Phone}"/></Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
<LineBreak/>
<!-- 明细表 -->
<Paragraph FontSize="16" FontWeight="Bold">订单明细</Paragraph>
<Table CellSpacing="0" BorderBrush="Black" BorderThickness="1">
<Table.Columns>
<TableColumn Width="50"/>
<TableColumn Width="*"/>
<TableColumn Width="80"/>
<TableColumn Width="100"/>
<TableColumn Width="120"/>
</Table.Columns>
<TableRowGroup>
<!-- 表头 -->
<TableRow Background="#2C3E50">
<TableCell><Paragraph Foreground="White" FontWeight="Bold">序号</Paragraph></TableCell>
<TableCell><Paragraph Foreground="White" FontWeight="Bold">商品名称</Paragraph></TableCell>
<TableCell><Paragraph Foreground="White" FontWeight="Bold">数量</Paragraph></TableCell>
<TableCell><Paragraph Foreground="White" FontWeight="Bold">单价</Paragraph></TableCell>
<TableCell><Paragraph Foreground="White" FontWeight="Bold">小计</Paragraph></TableCell>
</TableRow>
<!-- 数据行(通过代码动态生成) -->
</TableRowGroup>
</Table>
<Paragraph TextAlignment="Right" FontSize="16" FontWeight="Bold">
<Run Text="合计:"/>
<Run Text="{Binding TotalAmount, StringFormat='¥{0:N2}'}"/>
</Paragraph>
</FlowDocument>动态生成报表
/// <summary>
/// 动态生成 FlowDocument 报表
/// </summary>
public class ReportGenerator
{
public FlowDocument GenerateOrderReport(OrderReportData data)
{
var doc = new FlowDocument
{
FontFamily = new FontFamily("Microsoft YaHei"),
FontSize = 12,
PagePadding = new Thickness(40)
};
// 标题
doc.Blocks.Add(new Paragraph(new Run("销售订单报表"))
{
FontSize = 24,
FontWeight = FontWeights.Bold,
TextAlignment = TextAlignment.Center
});
// 副标题
doc.Blocks.Add(new Paragraph(new Run($"报表日期:{DateTime.Now:yyyy-MM-dd}"))
{
FontSize = 14,
TextAlignment = TextAlignment.Center,
Foreground = Brushes.Gray
});
// 明细表格
var table = new Table { CellSpacing = 0, BorderBrush = Brushes.Black, BorderThickness = new Thickness(1) };
table.Columns.Add(new TableColumn { Width = new GridLength(50) });
table.Columns.Add(new TableColumn { Width = new GridLength(GridUnitType.Star) });
table.Columns.Add(new TableColumn { Width = new GridLength(80) });
table.Columns.Add(new TableColumn { Width = new GridLength(100) });
table.Columns.Add(new TableColumn { Width = new GridLength(120) });
var rowGroup = new TableRowGroup();
// 表头
var headerRow = new TableRow { Background = new SolidColorBrush(Color.FromRgb(44, 62, 80)) };
headerRow.Cells.Add(CreateHeaderCell("序号"));
headerRow.Cells.Add(CreateHeaderCell("商品名称"));
headerRow.Cells.Add(CreateHeaderCell("数量"));
headerRow.Cells.Add(CreateHeaderCell("单价"));
headerRow.Cells.Add(CreateHeaderCell("小计"));
rowGroup.Rows.Add(headerRow);
// 数据行
for (int i = 0; i < data.Items.Count; i++)
{
var item = data.Items[i];
var row = new TableRow { Background = i % 2 == 0 ? Brushes.White : new SolidColorBrush(Color.FromRgb(236, 240, 241)) };
row.Cells.Add(CreateCell((i + 1).ToString()));
row.Cells.Add(CreateCell(item.ProductName));
row.Cells.Add(CreateCell(item.Quantity.ToString()));
row.Cells.Add(CreateCell($"¥{item.Price:N2}"));
row.Cells.Add(CreateCell($"¥{item.Subtotal:N2}"));
rowGroup.Rows.Add(row);
}
table.RowGroups.Add(rowGroup);
doc.Blocks.Add(table);
// 合计
doc.Blocks.Add(new Paragraph(new Run($"合计:¥{data.TotalAmount:N2}"))
{
FontSize = 16,
FontWeight = FontWeights.Bold,
TextAlignment = TextAlignment.Right
});
return doc;
}
private static TableCell CreateHeaderCell(string text)
{
return new TableCell(new Paragraph(new Run(text))
{
FontWeight = FontWeights.Bold,
Foreground = Brushes.White
})
{
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(0.5)
};
}
private static TableCell CreateCell(string text)
{
return new TableCell(new Paragraph(new Run(text)))
{
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(0.5)
};
}
}打印预览
打印预览窗口
/// <summary>
/// 打印预览窗口
/// </summary>
public partial class PrintPreviewWindow : Window
{
public PrintPreviewWindow(FlowDocument document)
{
InitializeComponent();
var viewer = new FlowDocumentScrollViewer
{
Document = document,
Zoom = 100,
IsToolBarVisible = true
};
Content = viewer;
}
public PrintPreviewWindow(FixedDocument document)
{
InitializeComponent();
var viewer = new DocumentViewer
{
Document = document
};
Content = viewer;
}
}
// 使用
private void PreviewButton_Click(object sender, RoutedEventArgs e)
{
var reportData = GetReportData();
var generator = new ReportGenerator();
var document = generator.GenerateOrderReport(reportData);
var previewWindow = new PrintPreviewWindow(document)
{
Title = "打印预览",
Width = 800,
Height = 600
};
previewWindow.ShowDialog();
}直接打印(无需对话框)
静默打印
/// <summary>
/// 直接打印到默认打印机
/// </summary>
public class AutoPrinter
{
// 使用默认打印机
public void PrintToDefault(FlowDocument document, string description = "自动打印")
{
var printServer = new LocalPrintServer();
var queue = printServer.DefaultPrintQueue;
if (queue == null)
{
throw new InvalidOperationException("未找到默认打印机");
}
var ticket = queue.DefaultPrintTicket;
document.PageHeight = ticket.PageMediaSize.Height ?? 1122;
document.PageWidth = ticket.PageMediaSize.Width ?? 793;
document.PagePadding = new Thickness(40);
var writer = PrintQueue.CreateXpsDocumentWriter(queue);
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
writer.Write(paginator);
}
// 指定打印机
public void PrintToPrinter(string printerName, FlowDocument document)
{
var printServer = new LocalPrintServer();
var queue = printServer.GetPrintQueue(printerName);
if (queue == null)
{
throw new InvalidOperationException($"未找到打印机:{printerName}");
}
var writer = PrintQueue.CreateXpsDocumentWriter(queue);
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
writer.Write(paginator);
}
}第三方报表工具
常用报表库
| 工具 | 特点 | 适用 |
|---|---|---|
| FlowDocument | WPF 内置 | 简单报表 |
| RDLC Report | 微软官方 | 复杂报表 |
| FastReport | 第三方商业 | 企业报表 |
| Stimulsoft | 第三方商业 | 交互式报表 |
| iTextSharp | PDF 生成 | PDF 报表 |
优点
缺点
总结
WPF 打印功能覆盖基本需求:简单打印用 PrintVisual,报表用 FlowDocument,复杂报表推荐 RDLC 或 FastReport。打印前提供预览功能是良好的用户体验。核心原则:UI 元素可以直接打印,复杂报表用专业工具。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《WPF 打印与报表》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《WPF 打印与报表》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《WPF 打印与报表》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《WPF 打印与报表》最大的收益和代价分别是什么?
FixedDocument 精确排版
FixedDocument 适合需要像素级精确定位的场景,如发票、标签打印。
/// <summary>
/// 使用 FixedDocument 生成精确排版的打印文档
/// </summary>
public class FixedDocumentGenerator
{
public FixedDocument GenerateInvoice(InvoiceData data, Size pageSize)
{
var doc = new FixedDocument();
doc.DocumentPaginator.PageSize = pageSize;
var page = new FixedPage
{
Width = pageSize.Width,
Height = pageSize.Height
};
var canvas = new Canvas { Width = pageSize.Width, Height = pageSize.Height };
// 公司名称
var companyTitle = new TextBlock
{
Text = data.CompanyName,
FontSize = 20,
FontWeight = FontWeights.Bold
};
Canvas.SetLeft(companyTitle, 40);
Canvas.SetTop(companyTitle, 30);
canvas.Children.Add(companyTitle);
// 发票编号
var invoiceNo = new TextBlock
{
Text = $"发票编号: {data.InvoiceNo}",
FontSize = 12
};
Canvas.SetLeft(invoiceNo, 40);
Canvas.SetTop(invoiceNo, 65);
canvas.Children.Add(invoiceNo);
// 日期
var dateText = new TextBlock
{
Text = $"日期: {data.Date:yyyy-MM-dd}",
FontSize = 12
};
Canvas.SetLeft(dateText, 500);
Canvas.SetTop(dateText, 65);
canvas.Children.Add(dateText);
// 使用 Grid 显示明细表格
var grid = new Grid();
Canvas.SetLeft(grid, 40);
Canvas.SetTop(grid, 120);
grid.Width = pageSize.Width - 80;
// 定义列
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(40) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(100) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(120) });
// 表头
var headers = new[] { "序号", "商品名称", "数量", "单价", "小计" };
for (int col = 0; col < headers.Length; col++)
{
var headerBorder = new Border
{
Background = Brushes.DarkSlateGray,
Padding = new Thickness(5),
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(0.5)
};
var headerText = new TextBlock
{
Text = headers[col],
Foreground = Brushes.White,
FontWeight = FontWeights.Bold
};
headerBorder.Child = headerText;
Grid.SetColumn(headerBorder, col);
grid.Children.Add(headerBorder);
}
page.Children.Add(canvas);
page.Children.Add(grid);
var pageContent = new PageContent { Child = page };
doc.Pages.Add(pageContent);
return doc;
}
}
public class InvoiceData
{
public string CompanyName { get; set; } = "";
public string InvoiceNo { get; set; } = "";
public DateTime Date { get; set; } = DateTime.Now;
public List<InvoiceItem> Items { get; set; } = new();
public decimal TotalAmount => Items.Sum(i => i.Subtotal);
}
public class InvoiceItem
{
public string ProductName { get; set; } = "";
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Subtotal => Quantity * UnitPrice;
}RDLC 报表集成
对于复杂报表(分组、交叉表、图表),推荐使用 RDLC(Microsoft Report Viewer)。
/// <summary>
/// RDLC 报表生成示例
/// 需要 NuGet: Microsoft.ReportingServices.ReportViewerControl.Winforms
/// </summary>
public class RdlcReportService
{
// 准备数据源
public void GenerateReport(string reportPath, OrderReportData data, string outputPath)
{
// 1. 创建数据源
var dataSource = new Microsoft.Reporting.WinForms.ReportDataSource();
dataSource.Name = "OrderDataSet";
dataSource.Value = ConvertToDataTable(data.Items);
// 2. 配置 ReportViewer
using var viewer = new Microsoft.Reporting.WinForms.ReportViewer();
viewer.LocalReport.ReportPath = reportPath;
viewer.LocalReport.DataSources.Clear();
viewer.LocalReport.DataSources.Add(dataSource);
// 3. 设置参数
viewer.LocalReport.SetParameters(new[]
{
new Microsoft.Reporting.WinForms.ReportParameter("CompanyName", data.CompanyName),
new Microsoft.Reporting.WinForms.ReportParameter("ReportDate", DateTime.Now.ToString("yyyy-MM-dd")),
new Microsoft.Reporting.WinForms.ReportParameter("TotalAmount", data.TotalAmount.ToString("N2"))
});
// 4. 导出为 PDF
byte[] pdfBytes = viewer.LocalReport.Render("PDF");
File.WriteAllBytes(outputPath, pdfBytes);
}
private DataTable ConvertToDataTable(List<OrderItem> items)
{
var table = new DataTable();
table.Columns.Add("ProductName", typeof(string));
table.Columns.Add("Quantity", typeof(int));
table.Columns.Add("UnitPrice", typeof(decimal));
table.Columns.Add("Subtotal", typeof(decimal));
foreach (var item in items)
{
table.Rows.Add(item.ProductName, item.Quantity, item.UnitPrice, item.Subtotal);
}
return table;
}
}PDF 导出(不依赖第三方报表工具)
/// <summary>
/// 使用 iTextSharp 生成 PDF 报表
/// NuGet: itext7
/// </summary>
using iText.Kernel.Pdf;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Properties;
public class PdfReportGenerator
{
public byte[] GenerateOrderReport(OrderReportData data)
{
using var ms = new MemoryStream();
using var writer = new PdfWriter(ms);
using var pdf = new PdfDocument(writer);
var document = new Document(pdf);
// 标题
document.Add(new Paragraph("销售订单报表")
.SetFontSize(24)
.SetBold()
.SetTextAlignment(TextAlignment.CENTER));
document.Add(new Paragraph($"报表日期:{DateTime.Now:yyyy-MM-dd}")
.SetFontSize(12)
.SetTextAlignment(TextAlignment.CENTER)
.SetFontColor(iText.Kernel.Colors.ColorConstants.GRAY));
document.Add(new Paragraph("\n"));
// 表格
var table = new Table(5, true);
table.SetWidth(UnitValue.CreatePercentValue(100));
// 表头
foreach (var header in new[] { "序号", "商品名称", "数量", "单价", "小计" })
{
table.AddHeaderCell(new Cell().Add(
new Paragraph(header).SetBold().SetFontSize(10)));
}
// 数据行
for (int i = 0; i < data.Items.Count; i++)
{
var item = data.Items[i];
table.AddCell(new Cell().Add(new Paragraph((i + 1).ToString())));
table.AddCell(new Cell().Add(new Paragraph(item.ProductName)));
table.AddCell(new Cell().Add(new Paragraph(item.Quantity.ToString())));
table.AddCell(new Cell().Add(new Paragraph($"¥{item.UnitPrice:N2}")));
table.AddCell(new Cell().Add(new Paragraph($"¥{item.Subtotal:N2}")));
}
document.Add(table);
// 合计
document.Add(new Paragraph($"\n合计:¥{data.TotalAmount:N2}")
.SetFontSize(16)
.SetBold()
.SetTextAlignment(TextAlignment.RIGHT));
document.Close();
return ms.ToArray();
}
}
// 使用
var generator = new PdfReportGenerator();
var pdfBytes = generator.GenerateOrderReport(reportData);
File.WriteAllBytes("order_report.pdf", pdfBytes);打印多页文档
/// <summary>
/// 多页文档打印 — 自动分页
/// </summary>
public class MultiPagePrinter
{
public void PrintMultiPage(FlowDocument document)
{
var printDialog = new PrintDialog();
if (printDialog.ShowDialog() != true) return;
// 设置页面大小
document.PageHeight = printDialog.PrintableAreaHeight;
document.PageWidth = printDialog.PrintableAreaWidth;
document.PagePadding = new Thickness(40);
document.ColumnGap = 0;
document.ColumnWidth = printDialog.PrintableAreaWidth;
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
// 自动分页打印
printDialog.PrintDocument(paginator, "多页文档打印");
}
// 大数据量报表的分页策略
public List<FlowDocument> CreatePagedReports<T>(
List<T> data, int itemsPerPage, Func<List<T>, FlowDocument> pageGenerator)
{
var documents = new List<FlowDocument>();
for (int i = 0; i < data.Count; i += itemsPerPage)
{
var pageData = data.Skip(i).Take(itemsPerPage).ToList();
documents.Add(pageGenerator(pageData));
}
return documents;
}
}