C#でPDFを表示する(WPF)

C#のWPFアプリで、PDFファイルを表示します。

目次

  1. PDFを画像に変換する方法
  2. WPFアプリからWindows Runtime APIを呼び出す方法
    1. パッケージ管理方法の変更
    2. パッケージ参照の追加
  3. PDFをウィンドウに表示するアプリを作る
    1. 作るアプリの概要
    2. PDFをBitmapSourceに変換する手順
    3. 画面構成
    4. コード
    5. コードの説明
    6. 動作例
  4. 注意点
    1. プロセスがバックグラウンドに残った環境
    2. 問題なく動作した環境
  5. 主なメソッド
    1. PdfDocument.LoadFromFileAsync
    2. PdfDocument.PageCountプロパティ
    3. PdfDocument.GetPage
    4. RenderToStreamAsync

PDFを画像に変換する方法

WPFにはXPSドキュメントを表示するコントロールはありますが、PDFドキュメントを表示する標準のコントロールはありません。

そこで、PDFを画像に変換して、変換した画像をImageコントロールに表示します。

PDFを画像に変換する方法としては、画像変換(レンダリング)に対応したライブラリを使用するとか、ImageMagickやGhostScriptなどの外部プログラムを利用する方法があります。ただし、外部ライブラリにはライセンスへの注意が必要ですし、外部プログラムを利用するとなるとアプリの配布が難しくなります。

ということで、ここではWindows Runtimeの機能を利用してPDFを画像に変換します。

具体的には、UWPアプリ用のAPIであるWindows.Data.PdfをWPFアプリから呼び出して変換を行います。

WPFアプリからWindows Runtime APIを呼び出す方法

Microsoftの「デスクトップ アプリで Windows ランタイム API を呼び出す」という記事に手順が書かれています。

以下で実際に設定をしてみます。

パッケージ管理方法の変更

まず、Visual Studioのパッケージ参照の方法をPackageReferenceに変更します。

Visual Studioのメニューから、「ツール」→「NuGetパッケージマネージャー」→「パッケージマネージャー設定」の順に選択します。

パッケージマネジャーの設定

「オプション」ダイアログが開きますので、「NuGetパッケージマネージャー」の「全般」項目を選択して、「規定のパッケージ管理形式」を「PackageReference」に変更します。

オプションダイアログ

パッケージ参照の追加

パッケージ管理形式を変更したら、Visual Studioのメニューから「プロジェクト」→「NuGetパッケージの管理」を選択します。

パッケージマネージャー

NuGetパッケージ管理用のウィンドウが開きます。「参照」ページの検索欄にmicrosoft windows sdk contractsと入力して検索すると、Microsoft.Windows.SDK.Contractsというパッケージが見つかると思います。そうしたら「インストール」をクリックしてこのパッケージをインストールします。(下図はインストール済みの状態でのスクリーンショットなので、「インストール」のボタンに「アンインストール」と表示されてます。)

パッケージのインストール

依存関係のある、System.Runtime.WindowsRuntimeとSystem.Runtime.WindowsRuntime.UI.Xamlも一緒にインストールされるはずです。もし自動的にインストールされない場合は、手動でインストールします。

以上で、Windows.Data.Pdfライブラリを使用する準備ができました。

PDFをウィンドウに表示するアプリを作る

作るアプリの概要

PDFファイルのサンプルは厚生労働省配布しているものにしました。A4サイズのカラーのもので、24ページあります。

このPDFファイルをローカルドライブにtest.pdfというファイル名で保存しておきます。

このtest.pdfを読み込んで、ページ毎にBitmapSourceに変換し、Imageコントロールに画像として1ページずつ表示するWPFアプリを作ります。

PDFをBitmapSourceに変換する手順

Imageコントロールに画像として表示しますので、PDFの各ページをBitmapSourceに変換する必要があります。

おおよその手順は下記になります。

  1. GetFileFromPathAsyncメソッドで、PDFファイルのインスタンスを作る。

  2. LoadFromFileAsyncメソッドで、ファイルインスタンスからPdfDocumentクラスのインスタンスを作る。

  3. GetPageメソッドで、PdfDocumentからPdfPageインスタンスを作る。

  4. InMemoryRandomAccessStreamコンストラクタで、メモリストリームを作る。

  5. RenderToStreamAsyncメソッドで、PdfPageインスタンスからメモリストリームにレンダリングする。

  6. PngBitmapDecoderでメモリストリームのデータをBitmapFrameに変換し、さらにBitmapSourceに変換する。

  7. 各ページに対して3~6を繰り返す。

レンダリングはページ毎に行うわけですね。

画面構成

XAMLは下記のようにします。

<Window x:Class="myPdfViewer2.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:local="clr-namespace:myPdfViewer2"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel>
            <Menu>
                <MenuItem Header="File">
                    <MenuItem Header="Open" Name="menuOpen" Click="menuOpen_Click" />
                </MenuItem>
                <MenuItem Header="Prev" Name="menuPrev" Click="menuPrev_Click" />
                <MenuItem Header="Next" Name="menuNext" Click="menuNext_Click" />
                <TextBlock Name="txtPage" Text="Page" />
            </Menu>
            <Image Name="imgMain" />
        </StackPanel>
    </Grid>
</Window>

ウィンドウにMenuとImageを並べただけですね。Menuにはファイルを開くメニューとページ送りと現在のページの表示をします。

コード

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using Windows.Data.Pdf;

namespace myPdfViewer2
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private Windows.Data.Pdf.PdfDocument pdfDocument;
        private List<BitmapSource> pdfPages = new List<BitmapSource>();
        private int DisplayPageNumber = 1;

        private async Task ReadPDFtoImage()
        {
            var file = await Windows.Storage.StorageFile.GetFileFromPathAsync(System.IO.Path.GetFullPath("test.pdf"));

            try
            {
                pdfDocument = await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(file);
            }
            catch
            {
            }

            if (pdfDocument != null)
            {
                for (uint i = 0; i < pdfDocument.PageCount; i++)
                {
                    using (Windows.Data.Pdf.PdfPage page = pdfDocument.GetPage(i))
                    {
                        using (var stream = new Windows.Storage.Streams.InMemoryRandomAccessStream())
                        {
                            PdfPageRenderOptions renderOptions = new PdfPageRenderOptions();
                            renderOptions.DestinationWidth = (uint)Math.Round(page.Dimensions.ArtBox.Width / 96.0 * 200.0);
                            await page.RenderToStreamAsync(stream, renderOptions);
                            PngBitmapDecoder decoder = new PngBitmapDecoder(stream.AsStream(), BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
                            BitmapSource d = (BitmapSource)decoder.Frames[0];
                            pdfPages.Add(d);
                        }
                    }
                }
            }
        }
        private void ShowPage(int p)
        {
            if (p < 1) return;
            if (pdfPages.Count == 0) return;
            if (p > pdfPages.Count) return;
            imgMain.Source = pdfPages[p - 1];
        }

        private async void menuOpen_Click(object sender, RoutedEventArgs e)
        {
            DisplayPageNumber = 1;
            await ReadPDFtoImage();
            ShowPage(DisplayPageNumber);
            txtPage.Text = "Page " + DisplayPageNumber.ToString();
        }

        private void menuNext_Click(object sender, RoutedEventArgs e)
        {
            if (DisplayPageNumber < pdfPages.Count)
            {
                DisplayPageNumber++;
                ShowPage(DisplayPageNumber);
                txtPage.Text = "Page " + DisplayPageNumber.ToString();
            }
            else
            {
                return;
            }
        }

        private void menuPrev_Click(object sender, RoutedEventArgs e)
        {
            if (DisplayPageNumber > 1)
            {
                DisplayPageNumber--;
                ShowPage(DisplayPageNumber);
                txtPage.Text = "Page " + DisplayPageNumber.ToString();
            }
            else
            {
                return;
            }
        }
    }
}

コードの説明

PDFファイルの読み込みに使うメソッドが非同期なので、非同期のメソッドを含む関数にはasyncを付ける必要があります。クリックなどのイベントハンドラでも同様ですので、そこに注意が必要です。

ReadPDFtoImage()という関数で、PDFをBitmapSourceに変換しています。変換の手順は前述の通りです。

RenderToStreamAsyncメソッドを実行する際にPdfPageRenderOptionsを指定することで、レンダリングのオプションを指定できます。上記のコードでは、レンダリング後の画像の解像度を200DPIにしています。(無指定の場合は96DPIでレンダリングされたので逆算して200DPIにしていますが、もしかしたら環境に依存するかもしれません。)

メモリストリームにレンダリングした結果をPngBitmapDecoderでBitmapFrameにしていますが、JPEGデコーダーを使えばJpegにすることもできます。

BitmapFrameはBitmapSourceを継承していますので、BitmapSourceにキャストできます。(上記コードのように明示しなくても良いようです。)

ページ毎にレンダリングしたBitmapSourceをpdfPagesというBitmapSouce型のリストに登録して、あとはメニューでの操作に合わせてImageコントロールに指定するBitmapSourceを切り替えています。操作部分は特に難しいことはしていないと思います。

動作例

動かしてみるとこんな感じです。

起動した状態はこんな感じです。

起動状態

FileのOpenを選択すると、少し間を開けて下図のように表示されます。少し間が開くのは、レンダリングに時間がかかるからだと思います。レンダリング解像度を低くすると素早く表示されるようになります。

読み込み状態

メニューのNextを選択していって最終ページに達すると、下図のような表示になります。

最終ページ

注意点

私のメインの環境では、Visual Studioでのデバッグでの実行時にアプリのウィンドウを閉じてもデバッグが終了しない状態になりました。ビルドしたアプリを実行すると、ウィンドウを閉じてもアプリがバックグラウンドに残ってしまって、タスクマネージャーで強制終了しないといけない状態になりました。

どうもRenderToStreamAsyncメソッドが、環境によってうまく動作しないようです。

stack overflowの投稿にあったのですが、ビデオカードのドライバに関係する問題のようです。

いくつかのPCで試してみました。うまく動作する場合としない場合があるので、うまく動作しない場合はビデオカードのドライバを最新のものにしてみるのが良いのかもしれません。(とは言っても、私の環境は最新のドライバなのですが。)

プロセスがバックグラウンドに残った環境

  • Windows10 Home 20H2

  • Core i7 1185G7

  • Intel Iris Xe Graphics

  • ビデオのドライババージョン 27.20.100.9565

問題なく動作した環境

環境1

  • Windows10 Home 2004

  • Core i5 8250U

  • Intel UHD Graphics 620

  • ビデオのドライババージョン 26.20.100.7637

環境2

  • Windows10 Pro 20H2

  • Ryzen 2700X

  • TITAN RTX

  • ビデオのドライババージョン 27.21.14.5671

主なメソッド

PngBitmapDecoderについては、PNG形式の読み込み書き出しについての投稿を参照してください。

PdfDocument.LoadFromFileAsync

doc = Windows.Data.Pdf.LoadFromFileAsync(file);

変数

内容

file

IStorageFile

PDFファイル

doc

IAsyncOperation<PdfDocument>

PdfDocumentオブジェクト

非同期のメソッドです。指定されたPDFファイルを読み込みます。動作が完了すると、PdfDocumentオブジェクトを返します。

PdfDocument.PageCountプロパティ

p = doc.PageCount;

変数

内容

doc

PdfDocument

PdfDocumentオブジェクト

p

uint32

PdfDocumentのページ数

PdfDocumentオブジェクトのページ数を表すプロパティです。読み込み専用です。

PdfDocument.GetPage

page = doc.GetPage(p);

変数

内容

doc

PdfDocument

PdfDocumentオブジェクト

p

uint32

取得するページ番号

page

PdfPage

取得したページオブジェクト

PdfDocumentオブジェクトから、指定したページを取得します。ページ番号は0から始まります。PDFの最初のページの番号が0であることに注意してください。

RenderToStreamAsync

t = page.RenderToStreamAsync(stream, options);

変数

内容

page

PdfPage

レンダリングするページ

stream

IRandomAccessStream

出力先のストリーム

options

PdfPageRenderOptions

出力オプション

t

IAsyncAction

非同期のメソッドです。PdfPageオブジェクトを、指定のストリームに描画します。

ストリームは、System.IO.Streamではなくて、IRandomAccessStreamでなければなりません。

公開日

広告