Reputation: 16121
Say I have the following small UserControl
:
<StackPanel>
<TextBlock Text="{Binding Title, StringFormat=Name: {0}}" FontWeight="Bold"/>
<Separator/>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=. ,StringFormat=Detail: {0}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Line Stroke="Black" HorizontalAlignment="Stretch" Margin="0,10"
X2="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
StrokeDashArray="2 2" StrokeThickness="1" />
</StackPanel>
My application will generate a collection of these controls and then they should be printed with automatic paginating. The number of items that is shown in the ItemsControl
is variable. I scourged the Net, read the chapter about printing in Pro WPF 4.5 Unleashed, but still I do not see how to accomplish this.
Any guidance is very welcome.
EDIT: any guidance is welcome. As I have the data available in a view model it will not be too difficult to redirect its data elsewhere. But where?
Upvotes: 5
Views: 2238
Reputation: 3352
Since you require automatic pagination, you'll need to create a DocumentPaginator
object.
The following is stolen from this example and modified for your case:
/// <summary>
/// Document paginator.
/// </summary>
public class UserControlDocumentPaginator : DocumentPaginator
{
private readonly UserControlItem[] userControlItems;
private Size pageSize;
private int pageCount;
private int maxRowsPerPage;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="userControlItems">The list of userControl items to display.</param>
/// <param name="pageSize">The size of the page in pixels.</param>
public UserControlDocumentPaginator
(
UserControlItem[] userControlItems,
Size pageSize
)
{
this.userControlItems = userControlItems;
this.pageSize = pageSize;
PaginateUserControlItems();
}
/// <summary>
/// Computes the page count based on the number of userControl items
/// and the page size.
/// </summary>
private void PaginateUserControlItems()
{
double actualHeight;
foreach (var uc in userControlItems)
{
actualHeight += uc.ActualHeight;
}
pageCount = (int)Math.Ceiling(actualHeight / pageSize.Height);
}
/// <summary>
/// Gets a range of userControl items from an array.
/// </summary>
/// <param name="array">The userControl items array.</param>
/// <param name="start">Start index.</param>
/// <param name="end">End index.</param>
/// <returns></returns>
private static UserControlItem[] GetRange(UserControlItem[] array, int start, int end)
{
List<UserControlItem> userControlItems = new List<UserControlItem>();
for (int i = start; i < end; i++)
{
if (i >= array.Count())
{
break;
}
userControlItems.Add(array[i]);
}
return userControlItems.ToArray();
}
#region DocumentPaginator Members
/// <summary>
/// When overridden in a derived class, gets the DocumentPage for the
/// specified page number.
/// </summary>
/// <param name="pageNumber">
/// The zero-based page number of the document page that is needed.
/// </param>
/// <returns>
/// The DocumentPage for the specified pageNumber, or DocumentPage.Missing
/// if the page does not exist.
/// </returns>
public override DocumentPage GetPage(int pageNumber)
{
// Compute the range of userControl items to display
int start = pageNumber * maxRowsPerPage;
int end = start + maxRowsPerPage;
UserControlListPage page = new UserControlListPage(GetRange(userControlItems, start, end), pageSize);
page.Measure(pageSize);
page.Arrange(new Rect(pageSize));
return new DocumentPage(page);
}
/// <summary>
/// When overridden in a derived class, gets a value indicating whether
/// PageCount is the total number of pages.
/// </summary>
public override bool IsPageCountValid
{
get { return true; }
}
/// <summary>
/// When overridden in a derived class, gets a count of the number of
/// pages currently formatted.
/// </summary>
public override int PageCount
{
get { return pageCount; }
}
/// <summary>
/// When overridden in a derived class, gets or sets the suggested width
/// and height of each page.
/// </summary>
public override System.Windows.Size PageSize
{
get
{
return pageSize;
}
set
{
if (pageSize.Equals(value) != true)
{
pageSize = value;
PaginateUserControlItems();
}
}
}
/// <summary>
/// When overridden in a derived class, returns the element being paginated.
/// </summary>
public override IDocumentPaginatorSource Source
{
get { return null; }
}
#endregion
}
Then in your code behind for your Window
, get a list of your user controls (replace YourUserControlContainer
with the name of the container in your Window
). Create a button named PrintButton
and attach the Click
event to the PrintButton_Click
method below. Put in code behind where appropriate:
List<UserControl> userControlItems = new List<UserControl>();
// 8.5 x 11 paper
Size pageSize = new Size(816, 1056);
private void PrintButton_Click(object sender, RoutedEventArgs e)
{
userControlItems = YourUserControlContainer.Children.ToList();
UserControlDocumentPaginator paginator = new UserControlDocumentPaginator
(
userControlItems.ToArray(),
pageSize
);
var dialog = new PrintDialog();
if (dialog.ShowDialog() != true) return;
dialog.PrintDocument(paginator, "Custom Paginator Print Job");
}
Edit You're right, I forgot a class. You'll need something like this:
public partial class UserControlListPage : UserControl
{
private readonly UserControlItem[] userControlItems;
private readonly Size pageSize;
public UserControlListPage
(
UserControlItem[] userControlItems,
Size pageSize
)
{
InitializeComponent();
this.userControlItems = userControlItems;
this.pageSize = pageSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Point point = new Point(0, 0);
foreach (UserControlItem item in userControlItems)
{
point.X = 0;
itemImageSource = CopyControlToImageSource(item);
drawingContext.DrawImage(itemImageSource, point);
point.Y += itemImageSource.Height;
}
}
}
Then somewhere put this:
/// <summary>
/// Gets an image "screenshot" of the specified UIElement
/// </summary>
/// <param name="source">UIElement to screenshot</param>
/// <param name="scale" value="1">Scale to render the screenshot</param>
/// <returns>Byte array of BMP data</returns>
private static byte[] GetUIElementSnapshot(UIElement source, double scale = 1)
{
double actualHeight = source.RenderSize.Height;
double actualWidth = source.RenderSize.Width;
double renderHeight = actualHeight * scale;
double renderWidth = actualWidth * scale;
RenderTargetBitmap renderTarget = new RenderTargetBitmap((int)renderWidth, (int)renderHeight, 96, 96, PixelFormats.Pbgra32);
VisualBrush sourceBrush = new VisualBrush(source);
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
using (drawingContext)
{
drawingContext.PushTransform(new ScaleTransform(scale, scale));
drawingContext.DrawRectangle(sourceBrush, null, new Rect(new System.Windows.Point(0, 0), new System.Windows.Point(actualWidth, actualHeight)));
}
renderTarget.Render(drawingVisual);
Byte[] _imageArray = null;
BmpBitmapEncoder bmpEncoder = new BmpBitmapEncoder();
bmpEncoder.Frames.Add(BitmapFrame.Create(renderTarget));
using (MemoryStream outputStream = new MemoryStream())
{
bmpEncoder.Save(outputStream);
_imageArray = outputStream.ToArray();
}
return _imageArray;
}
public static System.Windows.Media.Imaging.BitmapImage CopyControlToImageSource(UIElement UserControl)
{
ImageConverter ic = new ImageConverter();
System.Drawing.Image img = (System.Drawing.Image)ic.ConvertFrom(GetUIElementSnapshot(UserControl));
MemoryStream ms = new MemoryStream();
img.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
System.Windows.Media.Imaging.BitmapImage image = new BitmapImage();
image.BeginInit();
ms.Seek(0, SeekOrigin.Begin);
image.StreamSource = ms;
image.EndInit();
return image;
}
Upvotes: 3
Reputation: 8791
Its a bit difficult to understand where you stuck at exactly since we do not have you code and we do not know about your data, however here is an simple example:
<Window x:Class="WpfApplication1.PrintVisualDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Print Visual Demo" Height="350" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.9*" />
<RowDefinition Height="0.2*"/>
</Grid.RowDefinitions>
<StackPanel Name="printPanel"
Grid.Row="0"
Margin="5">
<Label Content="Sweet Baby"
HorizontalAlignment="Center"
FontSize="40"
FontFamily="Calibri">
<Label.Foreground>
<LinearGradientBrush Opacity="1"
StartPoint="0,0.5" EndPoint="1,0.5">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Blue" Offset="0" />
<GradientStop Color="Red" Offset="0.5" />
<GradientStop Color="Green" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Label.Foreground>
</Label>
<Image Source="D:\Temp\baby.jpg"
Height="150"
Width="200">
</Image>
</StackPanel>
<Button Content="Print"
Grid.Row="1" Margin="5"
Height="40" Width="150"
HorizontalAlignment="Center"
VerticalAlignment="Center" Click="Button_Click" />
</Grid>
</Window>
private void Button_Click(object sender, RoutedEventArgs e)
{
PrintDialog pd = new PrintDialog();
if (pd.ShowDialog() != true) return;
pd.PrintVisual(printPanel, "Printing StackPanel...");
}
PrintVisual method should do the job.
If you want to scale your visual to fit the page you could follow those steps:
PrintDialog printDlg = new System.Windows.Controls.PrintDialog();
if (printDlg.ShowDialog() == true)
{
//get selected printer capabilities
System.Printing.PrintCapabilities capabilities = printDlg.PrintQueue.GetPrintCapabilities(printDlg.PrintTicket);
//get scale of the print wrt to screen of WPF visual
double scale = Math.Min(capabilities.PageImageableArea.ExtentWidth / this.ActualWidth, capabilities.PageImageableArea.ExtentHeight /
this.ActualHeight);
//Transform the Visual to scale
this.LayoutTransform = new ScaleTransform(scale, scale);
//get the size of the printer page
Size sz = new Size(capabilities.PageImageableArea.ExtentWidth, capabilities.PageImageableArea.ExtentHeight);
//update the layout of the visual to the printer page size.
this.Measure(sz);
this.Arrange(new Rect(new Point(capabilities.PageImageableArea.OriginWidth, capabilities.PageImageableArea.OriginHeight), sz));
//now print the visual to printer to fit on the one page.
printDlg.PrintVisual(this, "First Fit to Page WPF Print");
}
As you can see all you need to do is remeasure and rearrange the specific element with new size and then you can call PrintVisual method.
I hope this helps you any futher. Consider post is more code or upload your project online somewhere.
Upvotes: 0