自定义数据网格文档分页器






3.77/5 (16投票s)
本文介绍如何创建自己的自定义数据网格文档分页器。
引言
是否需要打印一个数据网格或一个列表,并在每页上重复表头?或者包含文档标题和页码的页眉和页脚?本文将介绍如何通过创建自己的自定义文档分页器来实现这些功能。
背景
最近,我需要扩展我们的自定义数据网格组件,以便用户可以将网格的内容打印为文档,其中包含重复的表头以及包含文档标题的页眉和包含页码和当前日期/时间的页脚。
以前没有在 WPF 中编写任何打印类型的功能,我认为这很容易,只需使用 DataTemplates 创建页面定义,然后将其设置为某种打印类型组件即可。我大错特错 - 现实情况是,您可以选择 FlowDocument
或 FixedDocument
,但它们都无法让您直接实现这些功能。
所以我开始进行一些调查,我发现为了解决这个问题,您必须创建自己的自定义文档分页器,最终我也这样做了。但是,我提出的解决方案与我在互联网上看到的其他解决方案之间的一个重要区别是,我没有使用 FlowDocument
或 FixedDocument
作为文档内容的基础。
相反,在发现可以将几乎任何包装在 DocumentPage
中的 WPF 视觉对象打印出来后,我决定使用标准 WPF 控件(例如 Grid
、TextBlock
和 Border
控件)手动生成我的文档。
那么它是如何工作的?
它的工作原理是继承 System.Windows.Documents.DocumentPaginator
类,这是一个抽象类,允许您从单个文档源创建多个页面元素,在我们的例子中是一个 DataGrid
。
在我们的自定义文档分页器中,我们计算出可用于显示网格内容的空间大小。我们通过测量已知的元素(即页眉、页脚和表头)来实现这一点。当我们把所有这些元素的高度加在一起,并减去任何边距时,我们得到总的分配空间;见下文
double allocatedSpace = 0;
//Measure the page header
ContentControl pageHeader = new ContentControl();
pageHeader.Content = pageHeader;
allocatedSpace = MeasureHeight(pageHeader);
//Measure the page footer
ContentControl pageFooter = new ContentControl();
pageFooter.Content = pageFooter;
allocatedSpace += MeasureHeight(pageFooter);
//Measure the table header
ContentControl tableHeader = new ContentControl();
tableHeader.Content = CreateTable(false);
allocatedSpace += MeasureHeight(tableHeader);
//Include any margins
allocatedSpace += this.PageMargin.Bottom + this.PageMargin.Top;
可用空间是通过从页面高度中减去分配空间得到的
//Work out how much space we need to display the grid
_availableHeight = this.PageSize.Height - allocatedSpace;
下一步是计算出在可用空间中每页可以容纳多少行。我们通过测量第一行的高度来实现这一点
//Calculate the height of the first row
_avgRowHeight = MeasureHeight(CreateTempRow());
//Calculate how many rows we can fit on each page
double rowsPerPage = Math.Floor(_availableHeight / _avgRowHeight);
if (!double.IsInfinity(rowsPerPage))
_rowsPerPage = Convert.ToInt32(rowsPerPage);
最后,我们通过将总行数除以每页可以容纳的行数来计算出需要多少页,从而完成测量过程。
//Count the rows in the document source
double rowCount = CountRows(_documentSource.ItemsSource);
//Calculate the nuber of pages that we will need
if (rowCount > 0)
_pageCount = Convert.ToInt32(Math.Ceiling(rowCount / rowsPerPage));
该过程的下一步是将我们的自定义文档分页器通过 PrintDocument
方法传递给 PrintDialog
。接下来发生的是 PrintDialog
将调用分页器上非常重要的 GetPage
方法。女士们先生们,这就是奇迹发生的地方!
GetPage
方法构造一个视觉对象,并在 DocumentPage
的新实例中返回它。使用传递到我们重写的 GetPage
方法中的页码参数,我们计算出哪些行应该进入所请求的页面。我们通过确定起始和结束位置来做到这一点,就像这样
int startPos = pageNumber * _rowsPerPage;
int endPos = startPos + _rowsPerPage;
一旦我们知道起始和结束位置,我们就可以通过迭代文档源来创建一个新的表格 (Grid
)
//Create a new grid
Grid tableGrid = CreateTable(true) as Grid;
for (int index = startPos; index < endPos &&
index < itemsSource.Count; index++)
{
Console.WriteLine("Adding: " + index);
if (rowIndex > 0)
{
object item = itemsSource[index];
int columnIndex = 0;
if (_documentSource.Columns != null)
{
foreach (DataGridColumn column in _documentSource.Columns)
{
if (column.Visibility == Visibility.Visible)
{
AddTableCell(tableGrid, column, item, columnIndex, rowIndex);
columnIndex++;
}
}
}
if (this.AlternatingRowBorderStyle != null && rowIndex % 2 == 0)
{
Border alernatingRowBorder = new Border();
alernatingRowBorder.Style = this.AlternatingRowBorderStyle;
alernatingRowBorder.SetValue(Grid.RowProperty, rowIndex);
alernatingRowBorder.SetValue(Grid.ColumnSpanProperty, columnIndex);
alernatingRowBorder.SetValue(Grid.ZIndexProperty, -1);
tableGrid.Children.Add(alernatingRowBorder);
}
}
rowIndex++;
}
现在我们有了内容,我们可以通过调用 ConstuctPage
方法,并将文档内容作为参数来生成 DocumentPage
。正是这个方法通过构建一个包含文档头、文档内容和文档尾部的容器网格来生成文档。
瞧!
最后说明:您会注意到在提供的示例中,我们的自定义分页器上存在几个 Style 属性。这些属性允许您设置元素样式,例如单个表格单元格、表格标题、页面标题和文档页脚。您甚至可以更进一步,添加自己的 ControlTemplate 属性,以便您准确地定义页面标题和页脚的外观。不幸的是,由于时间限制,我无法自己完成此操作,但将来可能会这样做。
结论
如果您认为 FixedDocument
和 FlowDocument
类在文档布局和样式方面没有给您足够的权限,那么可以选择创建自己的文档分页器,在其中使用标准 WPF 控件生成自己的内容。
祝您编码愉快!