Intro
This time, I will try setting printer properties and printing PDF files what have multiple pages.
Environments
- .NET ver.5.0.103
- Microsoft.Extensions.DependencyInjection ver.5.0.1
- Microsoft.Xaml.Behaviors.Wpf ver.1.1.31
Data Bindings
I will implement these functions.
- Add one textbox, two buttons, and one combobox.
- The first button opens a dialog to select a PDF file for printing.
- After selecting a PDF file, the file path will be set into the textbox.
- The combobox has printer names what are installed in the PC.
- The second button starts printing.
- After the file and the printer selected, the second button will be enabled.
1. Add one textbox, two buttons, and one combobox
MainWindow.xaml
<Window x:Class="PrintSample.Main.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors"
xmlns:vm="clr-namespace:PrintSample.Main"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
xmlns:local="clr-namespace:PrintSample.Main"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Button x:Name="OpenFileButton" Content="Open" HorizontalAlignment="Left" Height="32"
Margin="586,48,0,0" VerticalAlignment="Top" Width="80" FontSize="18"
Command="{Binding Open}"/>
<ComboBox x:Name="PrinterSelector" HorizontalAlignment="Left" Margin="37,128,0,0"
VerticalAlignment="Top" Width="629" Height="32" FontSize="18" />
<TextBox x:Name="LoadFileInput" HorizontalAlignment="Left" Margin="220,48,0,0"
TextWrapping="NoWrap" VerticalAlignment="Top"
Width="320" Height="32" FontSize="18" IsReadOnly="True" IsUndoEnabled="False"/>
<Button x:Name="PrintButton" Content="Print" HorizontalAlignment="Left" Height="32"
Margin="688,368,0,0" VerticalAlignment="Top" Width="80" FontSize="18"
Command="{Binding Print}"/>
</Grid>
</Window>
OpenDialogCommand.cs
using System;
using System.Windows.Input;
namespace PrintSample.Main.Commands
{
public class OpenDialogCommand: ICommand
{
public event EventHandler? CanExecuteChanged;
public Action? Action;
public bool CanExecute(object? parameter)
{
return true;
}
public void Execute(object? parameter)
{
Action?.Invoke();
}
}
}
PrintCommand.cs
using System;
using System.Windows.Input;
namespace PrintSample.Main.Commands
{
public class PrintCommand: ICommand
{
public event EventHandler? CanExecuteChanged;
public Action? Action;
public bool CanExecute(object? parameter)
{
// TODO: Set enabled after selecting a file and a printer
return true;
}
public void Execute(object? parameter)
{
Action?.Invoke();
}
}
}
MainViewModel.cs
using System;
using PrintSample.Files;
using PrintSample.Main.Commands;
using PrintSample.Main.Properties;
using PrintSample.Pdf;
using PrintSample.Prints;
namespace PrintSample.Main
{
public class MainViewModel
{
public OpenDialogCommand Open { get; }
public PrintCommand Print { get; }
private readonly FileSelector fileSelector;
public MainViewModel()
{
this.Open = new OpenDialogCommand();
this.Print = new PrintCommand();
this.fileSelector = new FileSelector();
this.Print.Action += async () =>
{
// TODO: Print selected PDF
};
this.Open.Action += () =>
{
// Open file dialog
fileSelector.Open();
};
}
}
}
2. The first button opens a dialog to select a PDF file for printing
I used "OpenFileDialog" to show a file dialog.
FileSelector.cs
using System;
using Microsoft.Win32;
namespace PrintSample.Main
{
public class FileSelector
{
public Action<string>? FileSelected;
public void Open()
{
var dialog = new OpenFileDialog();
dialog.Title = "Open File";
dialog.Filter = "PDF(*.pdf)|*.pdf";
if (dialog.ShowDialog() == true)
{
// Send the selected file path.
FileSelected?.Invoke(dialog.FileName);
}
}
}
}
Result
3. After selecting a PDF file, the file path will be set into the textbox
I added a data binding into the textbox.
MainWindow.xaml
...
<TextBox x:Name="LoadFileInput" HorizontalAlignment="Left" Margin="220,48,0,0"
Text="{Binding Path=SelectedFile.FilePath, UpdateSourceTrigger=PropertyChanged}"
TextWrapping="NoWrap" VerticalAlignment="Top"
Width="320" Height="32" FontSize="18" IsReadOnly="True" IsUndoEnabled="False"/>
...
SelectedFile.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace PrintSample.Main.Properties
{
public class SelectedFile: INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string filePath = "";
public string FilePath
{
get
{
return this.filePath;
}
set
{
this.filePath = value;
OnPropertyChanged("FilePath");
}
}
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
MainViewModel.cs
...
public class MainViewModel
{
...
public SelectedFile SelectedFile { get; set; }
public MainViewModel()
{
...
this.SelectedFile = new SelectedFile();
this.fileSelector.FileSelected += (filePath) =>
{
this.SelectedFile.FilePath = filePath;
};
...
- How to: Control When the TextBox Text Updates the Source - WPF .NET Framework | Microsoft Docs
- How to: Implement Property Change Notification - WPF .NET Framework | Microsoft Docs
Property classes
The property name and the argument of "OnPropertyChanged" must be same.
And I can add two or more properties in a class.
I can change the data type from "string".
I can convert the type in the setter like below.
private double width = 0d;
public string Width
{
get
{
return this.width.ToString();
}
set
{
if(string.IsNullOrEmtpy(value) ||
double.TryParse(value, out var width) == false)
{
this.width = 0d;
}
else
{
this.width = width;
}
OnPropertyChanged("Width");
}
}
4. The combobox has printer names what are installed in the PC
MainWindow.xaml
...
<ComboBox x:Name="PrinterSelector" HorizontalAlignment="Left" Margin="37,128,0,0"
VerticalAlignment="Top" Width="629" Height="32" FontSize="18"
ItemsSource="{Binding SelectedPrinter.PrinterNames, Mode=OneWay}"
SelectedItem="{Binding SelectedPrinter.PrinterName}"/>
...
SelectedPrinter.cs
using System.ComponentModel;
using System.Linq;
using System.Printing;
using System.Runtime.CompilerServices;
namespace PrintSample.Main.Properties
{
public class SelectedPrinter: INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private readonly string[] printerNames;
public string[] PrinterNames => this.printerNames;
private string printerName = "";
public string PrinterName
{
get
{
return this.printerName;
}
set
{
this.printerName = value;
OnPropertyChanged("PrinterName");
}
}
public SelectedPrinter()
{
LocalPrintServer printServer = new LocalPrintServer();
printerNames = printServer.GetPrintQueues()
.Select(q => q.Name)
.ToArray();
}
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
For data binding, the "ItemsSource" must be a property.
If I change "PrinterNames" like below, the combobox will be empty.
public string[] PrinterNames()
{
return this.printerNames;
}
6. After the file and the printer selected, the second button will be enabled.
PrintCommand.cs
...
public class PrintCommand: ICommand
{
public event EventHandler? CanExecuteChanged;
public Action? Action;
private bool actionEnabled = false;
public bool CanExecute(object? parameter)
{
return this.actionEnabled;
}
...
public void CheckUpdatingInput(string? filePath, string? printer)
{
var newValue = (string.IsNullOrEmpty(filePath) == false &&
File.Exists(filePath) &&
string.IsNullOrEmpty(printer) == false);
var statusChanged = (newValue != this.actionEnabled);
this.actionEnabled = newValue;
if(statusChanged)
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}
}
MainViewModel.cs
...
public MainViewModel()
{
...
this.SelectedFile.PropertyChanged += (_, _) => {
this.Print.CheckUpdatingInput(this.SelectedFile.FilePath, this.SelectedPrinter.PrinterName);
};
this.SelectedPrinter.PropertyChanged += (_, _) => {
this.Print.CheckUpdatingInput(this.SelectedFile.FilePath, this.SelectedPrinter.PrinterName);
};
}
...
Print PDF files what have multiple pages
Convert from PDF to list of "Image"
I write a method to convert from PDF to list of "Image".
PdfLoader.cs (Failed)
...
public async Task<List<Image>> LoadAsync(LoadPdfFileArgs args)
{
using(FileStream fileStream = new FileStream(args.FilePath, FileMode.Open))
using(IRandomAccessStream stream = fileStream.AsRandomAccessStream())
{
var results = new List<Image>();
PdfDocument document = await PdfDocument.LoadFromStreamAsync(stream);
for(uint i = 0; i < document.PageCount; i++)
{
using(PdfPage page = document.GetPage(i))
using(MemoryStream memoryStream = new MemoryStream())
using(IRandomAccessStream outputStream = memoryStream.AsRandomAccessStream())
{
await page.RenderToStreamAsync(outputStream);
BitmapFrame? bitmap = BitmapDecoder.Create(memoryStream, BitmapCreateOptions.None,
BitmapCacheOption.OnLoad).Frames[0];
results.Add(new Image
{
Source = bitmap,
});
}
}
return results;
}
}
...
When I used a single page file, I didn't get any problems.
But when the file had two or more pages, I got an exception.
Specified cast is not valid.
So I changed the methods to load the file.
PdfLoader.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using PrintSample.Files;
using Windows.Data.Pdf;
using Windows.Storage;
using Windows.Storage.Streams;
namespace PrintSample.Pdf
{
public class PdfLoader: IPdfLoader
{
public async Task<List<Image>> LoadAsync(LoadPdfFileArgs args)
{
StorageFile file = await StorageFile.GetFileFromPathAsync(args.FilePath);
using(IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read))
{
var results = new List<Image>();
PdfDocument document = await PdfDocument.LoadFromStreamAsync(stream);
for(uint i = 0; i < document.PageCount; i++)
{
using(PdfPage page = document.GetPage(i))
using(IRandomAccessStream outputStream = new InMemoryRandomAccessStream())
{
await page.RenderToStreamAsync(outputStream);
BitmapFrame? bitmap = BitmapDecoder.Create(outputStream.AsStream(),
BitmapCreateOptions.None,
BitmapCacheOption.OnLoad).Frames[0];
results.Add(new Image
{
Source = bitmap,
});
}
}
return results;
}
}
}
}
Print multiple pages
I can add created "Image" into "FixedDocument" to print multiple pages.
Printer.cs
using System.Collections.Generic;
using System.Printing;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Xps;
namespace PrintSample.Prints
{
public class Printer: IPrinter
{
public void Print(List<Image> targets, string printer)
{
LocalPrintServer printServer = new LocalPrintServer();
PrintQueue queue = printServer.GetPrintQueue(printer);
PrintTicket ticket = queue.DefaultPrintTicket;
ticket.PageMediaSize = new PageMediaSize(PageMediaSizeName.ISOA4);
ticket.CopyCount = 1;
ticket.PageResolution = new PageResolution(72, 72);
ticket.PageOrientation = PageOrientation.Landscape;
ticket.PageBorderless = PageBorderless.Borderless;
var document = GenerateDocument(targets);
XpsDocumentWriter writer = PrintQueue.CreateXpsDocumentWriter(queue);
writer.Write(document, ticket);
}
private FixedDocument GenerateDocument(List<Image> targets)
{
FixedDocument result = new FixedDocument();
// add list of "Image" as pages
foreach(var target in targets)
{
var page = new FixedPage();
page.Children.Add(target);
PageContent content = new PageContent();
content.Child = page;
result.Pages.Add(content);
}
return result;
}
}
}
MainViewModel.cs
...
private readonly IPdfLoader pdfLoader;
private readonly IPrinter printer;
public MainViewModel(IPdfLoader pdfLoader,
IPrinter printer)
{
...
this.pdfLoader = pdfLoader;
this.printer = printer;
this.Print.Action += async () =>
{
var loadedImages = await this.pdfLoader.LoadAsync(
new LoadPdfFileArgs(this.SelectedFile.FilePath));
this.printer.Print(loadedImages);
};
}
...
Print same images?
All pages has the same image.
I added outputting bitmap images to check.
But they didn't any problems.
PdfLoader.cs
...
public async Task<List<Image>> LoadAsync(LoadPdfFileArgs args)
{
StorageFile file = await StorageFile.GetFileFromPathAsync(args.FilePath);
using(IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read))
{
var results = new List<Image>();
PdfDocument document = await PdfDocument.LoadFromStreamAsync(stream);
for(uint i = 0; i < document.PageCount; i++)
{
using(PdfPage page = document.GetPage(i))
using(IRandomAccessStream outputStream = new InMemoryRandomAccessStream())
{
await page.RenderToStreamAsync(outputStream);
// Get bitmap from stream
BitmapFrame? bitmap = BitmapDecoder.Create(outputStream.AsStream(),
BitmapCreateOptions.None,
BitmapCacheOption.OnLoad).Frames[0];
// for debugging
SaveDebugImage(i, bitmap);
results.Add(new Image
{
Source = bitmap,
});
}
}
return results;
}
}
private void SaveDebugImage(uint index, BitmapSource bitmap)
{
byte[] imageData;
using(var bmpStream = new MemoryStream())
{
BitmapEncoder enc = new BmpBitmapEncoder();
enc.Frames.Add(BitmapFrame.Create(bitmap));
enc.Save(bmpStream);
imageData = bmpStream.ToArray();
}
using(var fileStream = new FileStream($"sample_{index}_{System.DateTime.Now:yyyyMMddHHmmssfff}.bmp", FileMode.Create))
{
fileStream.Write(imageData, 0, imageData.Length);
}
}
}
}
I don't know why.
But maybe because the references of images are shared, so I re-create "BitmapFrame" when I set them as source of "Image".
PdfLoader.cs
...
BitmapFrame? bitmap = BitmapDecoder.Create(outputStream.AsStream(),
BitmapCreateOptions.None,
BitmapCacheOption.OnLoad).Frames[0];
...
results.Add(new Image
{
Source = BitmapFrame.Create(bitmap),
});
...
Top comments (0)