C#(一)—— WPF实现自定义打印、预览、分页以及生成PDF

caroly 2020年04月25日 275次浏览

WPF实现自定义打印、预览、分页以及生成PDF

PS:本文可以解决绝大多数WPF的打印问题。如果您看过之后(动手尝试)依旧不能解决您的问题,欢迎来骚扰。


WPF中打印用到的类是『PrintDialog』,微软官方文献宣称:Invokes a standard Microsoft Windows print dialog box that configures a PrintTicket and PrintQueue according to user input and then prints a document. 大体意思是调用标准的Microsoft Windows 打印对话框,该对话框会根据用户输入配置『PrintTicket』和『PrintQueue』,然后打印文档。

以下打印内容为两个控件

//创建打印对话框对象并设置选项,包含PrintTicket、包含PrintQueue
PrintDialog pDialog = new PrintDialog();  
//使用XamlWriter类将第一个要打印的控件序列化为字符串
//然后使用XamlReader类上的静态Load方法将该字符串反序列化为基类
string childXaml = XamlWriter.Save(controls1);
StringReader sReader = new StringReader(childXaml);
XmlReader xmlReader = XmlReader.Create(sReader);
UIElement clonedChile = (UIElement)XamlReader.Load(xmlReader);

其中,『controls1』为第一个要打印的控件『name』。

//创建两个面板,用来自定义要打印的两个控件的位置
//创建的时候可以根据自己所想要的布局来添加属性,比如背景色之类...
var sPanel1 = new StackPanel() { };
var sPanel2 = new StackPanel() { 
	HorizontalAlignment = HorizontalAlignment.Center, 
	Margin = new Thickness(0, 0, 0, 0),
	... ...
};

如果要打印的控件可以反序列化为『UIElement』,则可以继续使用此方法将第二个控件反序列化为『UIElement』,最后将控件都放到第一个『StackPanel』中。

//controls2:第二个控件name
string childXaml1 = XamlWriter.Save(controls2);
StringReader stringReader1 = new StringReader(childXaml1);
XmlReader xmlReader1 = XmlReader.Create(stringReader1);
UIElement clonedChile1 = (UIElement)XamlReader.Load(xmlReader1);
sPanel2.Children.Add(clonedChile1);
sPanel1.Children.Add(clonedChile);
sPanel1.Children.Add(sPanel2);

如果要打印的控件不能反序列化为『UIElement』,需要使用以下方法:

var p2 = controls2 as StackPanel;
UIElement el = p2.Children[0];
//先清除与父控件的关系
p2.Children.Remove(el);          
sPanel2.Children.Add(el);
sPanel1.Children.Add(clonedChile);
sPanel1.Children.Add(sPanel2);

/*此处可以放置打印、预览、分页等方法*/
... ...

sPanel2.Children.Remove(el);
p2.Children.Add(el);  //最后需要将移除的元件再添加进去。
//至于为什么不能直接使用,是因为该元素是另一个元素的逻辑子元素,如果不清除它与父控件的关系,会报错误。

最后,集成的『sPanel1』需要转化为『FixedDocument』对象。若是需要打印的内容过多,超过一页长度,比如『Grid』、『DataGrid』等数据内容较多情况下,可以进行分页处理。

PrintCapabilities capabilities = pDialog.PrintQueue.GetPrintCapabilities(pDialog.PrintTicket);

Size pageSize = new Size(pDialog.PrintableAreaWidth, pDialog.PrintableAreaHeight);

Size visibleSize = new Size(capabilities.PageImageableArea.ExtentWidth, capabilities.PageImageableArea.ExtentHeight);

FixedDocument fixedDoc = new FixedDocument();

sPanel1.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
sPanel1.Arrange(new Rect(new System.Windows.Point(0, 0), sPanel1.DesiredSize));

Size size = sPanel1.DesiredSize;
double yOffset = 0;
while (yOffset < size.Height)   //分页处理
{
	VisualBrush vb = new VisualBrush(toPrint)
	{
		Stretch = Stretch.None,
		AlignmentX = AlignmentX.Left,
		AlignmentY = AlignmentY.Top,
		ViewboxUnits = BrushMappingMode.Absolute,
		TileMode = TileMode.None,
		Viewbox = new Rect(0, yOffset, sizeWidths, sizeHeights)
        //sizeWidths、sizeHeights根据自己所需调节宽高
	};

	PageContent pageContent = new PageContent();
	FixedPage page = new FixedPage();
	((IAddChild)pageContent).AddChild(page);
	fixedDoc.Pages.Add(pageContent);
	page.Width = pageSize.Width;
	page.Height = pageSize.Height;

	var stack = new StackPanel()
	{
		Width = sizeWidths,
		Height = sizeHeights,
		Background = vb,
        ... ...
	};
	page.Children.Add(stack);

	yOffset += sizeOffset;
    //sizeOffset偏移的像素值,下一页开始分页的地方,需要用户自己调节。例如:1000
}

至此,就可以根据返回的『fixedDoc』进行预览以及打印。

//预览
new Window() { Content = new DocumentViewer() { Document = fixedDoc } }.ShowDialog();
//打印
if (pDialog.ShowDialog() != true)
	return;
pDialog.PrintDocument(fixedDoc.DocumentPaginator, "描述");

有的项目可能会有生成『PDF』文件的需求,在此也贴一下方法。

MemoryStream mStream = new MemoryStream();
Package package = Package.Open(mStream, FileMode.Create);
XpsDocument doc = new XpsDocument(package);
var writer = XpsDocument.CreateXpsDocumentWriter(doc);     
writer.Write(fixedDoc.DocumentPaginator);
doc.Close();
package.Close();

var bytes = mStream.ToArray();
mStream.Dispose();

string strDesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);   //桌面路径
string aimPath = strDesktopPath+ "\\" + DateTime.Now.ToString("yyyyMMdd");
// 判断目标目录是否存在如果不存在则新建
if (!System.IO.Directory.Exists(aimPath))
{
	System.IO.Directory.CreateDirectory(aimPath);
}
// Print to PDF
//fileName: pdf名字
var outputFilePath = aimPath + "\\" + fileName + ".pdf";
PdfFilePrinter.PrintXpsToPdf(bytes, outputFilePath, fileName);

以上就是自定义打印的大体过程,若要打印出符合自己需求的文件,需要对上述代码进行微调。