C#でPDFを表示する(WPF)
C#のWPFアプリで、PDFファイルを表示します。
目次
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に変換する必要があります。
おおよその手順は下記になります。
GetFileFromPathAsyncメソッドで、PDFファイルのインスタンスを作る。
LoadFromFileAsyncメソッドで、ファイルインスタンスからPdfDocumentクラスのインスタンスを作る。
GetPageメソッドで、PdfDocumentからPdfPageインスタンスを作る。
InMemoryRandomAccessStreamコンストラクタで、メモリストリームを作る。
RenderToStreamAsyncメソッドで、PdfPageインスタンスからメモリストリームにレンダリングする。
PngBitmapDecoderでメモリストリームのデータをBitmapFrameに変換し、さらにBitmapSourceに変換する。
各ページに対して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でなければなりません。
公開日
広告
C#カテゴリの投稿
- C#でMVVMって何でしょう
- C#でPDFを表示する(WPF)
- C#でアプリのログを記録してみる
- C#でアプリの設定を保存する
- C#でインスタンスをプログラムで作ってみた(Activator.CreateInstance編)
- C#でインスタンスをプログラムで作ってみた(Type.InvokeMember編)
- C#でインスタンス間のデータの受け渡しをしてみた
- C#でウェブサイトのソースを取得してみた
- C#でエラーの処理をする
- C#でクラスのフィールド宣言とコンストラクターでの初期化はどっちが優先する?
- C#でスタックを使って逆ポーランド記法の計算をしてみた
- C#で数式を中置記法から後置記法(逆ポーランド記法)に変換してみた(三角関数編)
- C#で選択(switch-case編)
- C#のWPFでデータバインディング
- C#のXAMLでメニューとステータスバーのレイアウトをしてみた
- C#のアプリの情報を表示してみた
- C#のクラスとインスタンスとオブジェクト
- C#の反復処理(foreach編)
- C#の命名規則
- C#へのMicrosoft.TeamFoundation.Controlsの参照の追加について