问题
I have a huge data which I want to print using WPF. I found that WPF provides a PrintDialog.PrintVisual
method for printing any WPF control derived from the Visual
class.
PrintVisual
will only print a single page so I need to scale the control to fit on the page. Unfortunately this would not work for me since the report was sometimes long enough that it could not be read easily when scaled to fit on the page.
Another option for printing provided by WPF is to create a separate view in a FlowDocument
. This is probably the best way to print documents, but it was more work than I wished to put into it, not to mention the extra view that would have to be maintained for each control I wished to print.
I got another solution in this link, but it seems too complex for me.
Is there any better and simple solution for this ? Thanks for any help
回答1:
I'm assuming your report is displayed in a DataGrid
or something else that is scrollable?
I believe FlowDocument
is definitely your best choice here if you want to print something that looks, for lack of a better word, professional. But if you want something quick and dirty, you could use a series of operations using RenderTargetBitmap.Render
. The basic process would be:
- Create the
RenderTargetBitmap
- Scroll the view such that you have a region visible that you want to print on one page
- Call
RenderTargetBitmap.Render
on theDataGrid
or theScrollViewer
that's containing the "large" control - Print the resulting bitmap
- Repeat for the next "Page"
Again, don't call RenderTargetBitmap.Render
on the "large" control. Wrap the large control in a ScrollViewer
if it isn't already. That will essentially be your paginator.
I don't know if you'll be satisfied with the results, but this is the easiest method I can think of. It'll look like you manually hit PrintScreen
each time. Not sure if that's what you want, but if you want it to look nicer, I think you need to use FlowDocument
.
回答2:
I use PrintDialog and DocumentPaginator for printing.
What I do is:
- select printer (show print dialog or use the system default)
- create pages (wpf control in the size of paper)
Here is my test function:
public static void PrintTest1(Viewbox viewboxInWindowForRender)
{
FrameworkElement[] testContArr = PrepareTestContents();
//=========================
PrintManager man = new PrintManager();
// Show print dialog (or select default printer)
if (!man.SelectPrinter())
return;
man.SetPageMargins(new Thickness(PrintManager.Size1cm * 2));
//=========================
List<FrameworkElement> pagesForPrint = new List<FrameworkElement>();
for (int i = 0; i < testContArr.Length; i++)
{
// Put the page content into the control of the size of paper
FrameworkElement whitePage = man.CreatePageWithContentStretched(testContArr[i]);
// Temporary put the page into window (need for UpdateLayout)
viewboxInWindowForRender.Child = whitePage;
// Update and render whitePage.
// Measure and Arrange will be used properly.
viewboxInWindowForRender.UpdateLayout();
pagesForPrint.Add(whitePage);
}
viewboxInWindowForRender.Child = null;
//=========================
// Now you can show print preview to user.
// pagesForPrint has all pages.
// ...
//=========================
MyDocumentPaginator paginator = man.CreatePaginator();
paginator.AddPages(pagesForPrint);
// Start printing
man.Print(paginator, "Printing Test");
}
// For testing
public static FrameworkElement[] PrepareTestContents()
{
StackPanel sp1 = new StackPanel();
sp1.Width = PrintManager.PageSizeA4.Width - PrintManager.Size1cm * 2;
sp1.Children.Add(PrepareTestBorder("Alice has a cat."));
sp1.Children.Add(PrepareTestBorder("Page number one."));
StackPanel sp2 = new StackPanel();
sp2.Width = sp1.Width / 2;
sp2.Children.Add(PrepareTestBorder("Farmer has a dog."));
sp2.Children.Add(PrepareTestBorder("Page number two."));
return new FrameworkElement[] {sp1, sp2 };
}
// For testing
public static FrameworkElement PrepareTestBorder(string text)
{
Border b = new Border();
b.BorderBrush = Brushes.Black;
b.BorderThickness = new Thickness(1);
b.Margin = new Thickness(0, 0, 0, 5);
TextBlock t = new TextBlock();
t.Text = text;
b.Child = t;
return b;
}
Somewhere in the window you should have Viewbox for temporary layout update and render.
<Window ...>
<Grid>
<Viewbox x:Name="forRender" Visibility="Hidden" Width="100" Height="100"/>
...
</Grid>
</Window>
And than you can run test: PrintTest1(forRender);
Here's my PrintManager class:
public class PrintManager
{
public static readonly Size PageSizeA4 = new Size(21 * 96 / 2.54, 29.7 * 96 / 2.54); // (793.700787401575, 1122.51968503937)
public static readonly double Size1cm = 96 / 2.54; // 37.7952755905512
private PrintDialog _printDialog;
public PrintTicket PrintTicket { get; private set; }
public PrintCapabilities TicketCapabilities { get; private set; }
// Page size selected in print dialog (may not be exactly as paper size)
public Size PageSize { get; private set; }
public Thickness PageMargins { get; private set; }
public Rect PageContentRect {
get {
return new Rect(PageMargins.Left, PageMargins.Top,
PageSize.Width - PageMargins.Left - PageMargins.Right,
PageSize.Height - PageMargins.Top - PageMargins.Bottom);
}
}
public PrintManager()
{
}
/// <summary>
/// Show print dialog or try use default printer when useDefaultPrinter param set to true.
/// <para/>
/// Return false on error or when user pushed Cancel.
/// </summary>
public bool SelectPrinter(bool useDefaultPrinter = false)
{
if (_printDialog == null)
_printDialog = new PrintDialog();
try
{
if (useDefaultPrinter)
_printDialog.PrintQueue = LocalPrintServer.GetDefaultPrintQueue();
// pDialog.PrintQueue == null when default printer is not selected in system
if (_printDialog.PrintQueue == null || !useDefaultPrinter)
{
// Show print dialog
if (_printDialog.ShowDialog() != true)
return false;
}
if (_printDialog.PrintQueue == null)
throw new Exception("Printer error");
// Get default printer settings
//_printDialog.PrintTicket = _printDialog.PrintQueue.DefaultPrintTicket;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
PrintTicket = _printDialog.PrintTicket;
TicketCapabilities = _printDialog.PrintQueue.GetPrintCapabilities(PrintTicket);
PageSize = new Size((double)TicketCapabilities.OrientedPageMediaWidth,
(double)TicketCapabilities.OrientedPageMediaHeight);
SetPageMargins(PageMargins); // Update margins if too small
return true;
}
/// <summary>
/// Start printing pages from paginator.
/// </summary>
public void Print(MyDocumentPaginator paginator, string printTaskDescription)
{
if (_printDialog == null)
return;
// Start printing document
_printDialog.PrintDocument(paginator, printTaskDescription);
}
/// <summary>
/// Set page margins and return true.
/// <para/>
/// If new page margins are too small (unprinted area) then set minimum and return false.
/// </summary>
public bool SetPageMargins(Thickness margins)
{
PageImageableArea pia = TicketCapabilities.PageImageableArea;
PageMargins = new Thickness(Math.Max(margins.Left, pia.OriginWidth),
Math.Max(margins.Top, pia.OriginHeight),
Math.Max(margins.Right, PageSize.Width - pia.OriginWidth - pia.ExtentWidth),
Math.Max(margins.Bottom, PageSize.Height - pia.OriginHeight - pia.ExtentHeight));
return PageMargins == margins;
}
/// <summary>
/// Set pate margins with minimal
/// </summary>
public void SetMinimalPageMargins()
{
PageImageableArea pia = TicketCapabilities.PageImageableArea;
// Set minimal page margins to bypass the unprinted area.
PageMargins = new Thickness(pia.OriginWidth, pia.OriginHeight,
(double)TicketCapabilities.OrientedPageMediaWidth - - pia.OriginWidth - pia.ExtentWidth,
(double)TicketCapabilities.OrientedPageMediaHeight - pia.OriginHeight - pia.ExtentHeight);
}
/// <summary>
/// Create page control witch pageContent ready to print.
/// Content is stretched to the margins.
/// </summary>
public FrameworkElement CreatePageWithContentStretched(FrameworkElement pageContent)
{
// Place the content inside the page (without margins)
Viewbox pageInner = new Viewbox();
pageInner.VerticalAlignment = VerticalAlignment.Top; // From the upper edge
pageInner.Child = pageContent;
// Printed control - the page with content
Border whitePage = new Border();
whitePage.Width = PageSize.Width;
whitePage.Height = PageSize.Height;
whitePage.Padding = PageMargins;
whitePage.Child = pageInner;
return whitePage;
}
/// <summary>
/// Create page control witch pageContent ready to print.
/// <para/>
/// Content is aligned to the top-center and must have
/// a fixed size (max PageSize-PageMargins).
/// </summary>
public FrameworkElement CreatePageWithContentSpecSize(FrameworkElement contentSpecSize)
{
// Place the content inside the page
Decorator pageInner = new Decorator();
pageInner.HorizontalAlignment = HorizontalAlignment.Center;
pageInner.VerticalAlignment = VerticalAlignment.Top;
pageInner.Child = contentSpecSize;
// Printed control - the page with content
Border whitePage = new Border();
whitePage.Width = PageSize.Width;
whitePage.Height = PageSize.Height;
// We align to the top-center only, because padding will cut controls
whitePage.Padding = new Thickness(0, PageMargins.Top, 0, 0);
whitePage.Child = pageInner;
return whitePage;
}
/// <summary>
/// Create paginator for pages created by CreatePageWithContent().
/// </summary>
public MyDocumentPaginator CreatePaginator()
{
return new MyDocumentPaginator(PageSize);
}
}
And here's my MyDocumentPaginator class:
public class MyDocumentPaginator : DocumentPaginator
{
private List<FrameworkElement> _pages = new List<FrameworkElement>();
public override bool IsPageCountValid { get { return true; } }
public override int PageCount { get { return _pages.Count; } }
public override Size PageSize { get; set; }
public override IDocumentPaginatorSource Source { get { return null; } }
public MyDocumentPaginator(Size pageSize)
{
PageSize = pageSize;
}
public override DocumentPage GetPage(int pageNumber)
{
// Warning: DocumentPage remember only reference to Visual object.
// Visual object can not be changed until PrintDialog.PrintDocument() called
// or e.g. XpsDocumentWriter.Write().
// That's why I don't create DocumentPage in AddPage method.
return new DocumentPage(_pages[pageNumber], PageSize, new Rect(PageSize), new Rect(PageSize));
}
public void AddPage(FrameworkElement page)
{
_pages.Add(page);
}
public void AddPages(List<FrameworkElement> pages)
{
_pages.AddRange(pages);
}
}
You told me that you want to print control that you already have.
You can print this with my solution.
For example:
Suppose that you have a UserCtrl
, which is in ParentBorder
.
You need to removed it from the parent control, and then you can use it.
ParentBorder.Child = null;
// Or you can use my function
RemoveFromParent(UserCtrl);
Than you can prepare page:
FrameworkElement whitePage = man.CreatePageWithContentStretched(UserCtrl);
viewboxInWindowForRender.Child = whitePage;
viewboxInWindowForRender.UpdateLayout();
MyDocumentPaginator paginator = man.CreatePaginator();
paginator.AddPages(whitePage);
man.Print(paginator, "Printing UserControl");
// After print you can restore UserCtrl
RemoveFromParent(UserCtrl);
ParentBorder.Child = UserCtrl;
Here's is RemoveFromParent function:
public static void RemoveFromParent(FrameworkElement child)
{
DependencyObject parent = child.Parent;
if (parent == null)
return;
if (parent is Panel)
((Panel)parent).Children.Remove(child);
else if (parent is Decorator)
((Decorator)parent).Child = null;
else if (parent is ContentControl)
((ContentControl)parent).Content = null;
else if (parent is ContentPresenter)
((ContentPresenter)parent).Content = null;
else
throw new Exception("RemoveFromParent: Unsupported type " + parent.GetType().ToString());
}
Why I use UpdateLayout and Viewbox in window, instead of Measure and Arrange like people in other examples?
I try, but I had many problems with this. I use controls that I alredy have, I changing style for printing, and I also export to PDF. Measure and Arrange not working for me. Control have to be docked in window for properly layout update and render.
来源:https://stackoverflow.com/questions/39660350/printing-large-wpf-user-controls