DEV Community

Masui Masanori
Masui Masanori

Posted on

【.NET 5】【WPF】Edit and print PDF files 2

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.

  1. Add one textbox, two buttons, and one combobox.
  2. The first button opens a dialog to select a PDF file for printing.
  3. After selecting a PDF file, the file path will be set into the textbox.
  4. The combobox has printer names what are installed in the PC.
  5. The second button starts printing.
  6. 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>
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
            };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Result

Alt Text

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"/>
...
Enter fullscreen mode Exit fullscreen mode

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));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

MainViewModel.cs

...
    public class MainViewModel
    {
...
        public SelectedFile SelectedFile { get; set; }
        public MainViewModel()
        {
...
            this.SelectedFile = new SelectedFile();
            this.fileSelector.FileSelected += (filePath) =>
            {
                this.SelectedFile.FilePath = filePath;
            };
...
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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}"/>
...
Enter fullscreen mode Exit fullscreen mode

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));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
            };
        }
...
Enter fullscreen mode Exit fullscreen mode

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;
            }            
        }
...
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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;
            }            
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
            };
        }
...
Enter fullscreen mode Exit fullscreen mode

Print same images?

The result like below.
Alt Text

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);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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),
    });
...
Enter fullscreen mode Exit fullscreen mode

Result

Alt Text

Discussion (0)