Windows 8.1の新機能、PDFを表示するには?[Windows 8.1ストア・アプリ開発]WinRT/Metro TIPS

Windows 8.1用のWindowsストア・アプリでは、PDFファイルを表示できる新機能が追加された。その機能を使ってPDFファイルを画面に表示する方法を解説する。

» 2013年10月24日 13時25分 公開
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

「WinRT/Metro TIPS」のインデックス

連載目次

 待ち望まれた機能、といってよいだろう。Windows 8.1(以降、Win 8.1)用のWindowsストア・アプリ(以降、Win 8.1アプリ)では、PDFファイルを表示できるようになったのだ。本稿では、新設されたWindows.Data.Pdf名前空間の機能を使ってPDFファイルを画面に表示する方法を解説する。本稿のサンプルは「Windows Store app samples:MetroTips #55(Windows 8.1版)」からダウンロードできる。

事前準備

 Win 8.1アプリを開発するには、Win 8.1とVisual Studio 2013(以降、VS 2013)が必要である。本稿ではOracle VM VirtualBox上で64bit版Windows 8.1 Pro PreviewとVisual Studio Express 2013 Preview for Windowsを使用している。これらを準備する方法や注意事項は、「WinRT/Metro TIPS:Win8用のソース・コードをWin8.1用に変換するには?[Windows 8.1ストア・アプリ開発]」の記事をご参照いただきたい。

 なお、製品版のWin 8.1とVS 2013でも構わない。本稿で作成したコードは、Visual Studio Express 2013 for Windows*1でもビルドできて正常に動作することを確認している。ただし、操作の手順や自動生成されるコードなどで、Preview版と製品版では細部が異なる可能性がある。あらかじめご承知おきいただきたい。

*1マイクロソフト公式ダウンロード・センターの「Microsoft Visual Studio Express 2013 for Windows」から入手できる。


Windows.Data.Pdf名前空間

 Win 8.1アプリ用に新設されたWindows.Data.Pdf名前空間の機能は、さほど多くない。端的に言えば、PDFファイルをページ単位でビットマップに変換できるだけである(次の図)。そのビットマップを画面に表示したり印刷したりする機能は、アプリ側で個別に実装することになる。

Windows.Data.Pdf名前空間の機能 Windows.Data.Pdf名前空間の機能
Windows.Data.Pdf名前空間のPdfDocumentクラスがPDFファイルのストリームを解析しPdfPageクラスのオブジェクトを生成する。PdfPageオブジェクトがページごとのビットマップ描画を担当する。
アプリの動作のイメージとしては、まずアプリでPDFファイルを開き、それをPdfDocumentクラスに読み込ませて、PdfDocumentオブジェクトを作る。次に、PdfDocumentオブジェクトにページごとのPdfPageオブジェクトを生成させる。そして、用意しておいたビットマップ・ストリームに対して、PdfPageオブジェクトに描画させる。あとは、アプリ側でビットマップ・ストリームを表示する(あるいは、保存したり印刷したりする)。

ユーザー・コントロールを作る

 PDFを表示するために、PdfDocumentオブジェクトや表示しているページ番号などをメンバ変数として維持しなければならない。画面のソース・コードの中でそれをやると煩雑になってしまってよくないので、PDFを表示する部分だけを別のクラスに独立させよう。今回はそのためにユーザー・コントロールを使う。

 VS 2013のプロジェクトに新しい項目を追加する。ソリューション・エクスプローラでそのプロジェクトを右クリックし[追加]−[新しい項目]を選択すると表示される[新しい項目の追加]ダイアログで[ユーザー コントロール]を選び、「PdfView.xaml」という名前を付ける。

 「PdfView.xaml」ファイルを開いて、次のコードのようにScrollViewerコントロールとImageコントロールを配置する。

<UserControl
  x:Class="MetroTips055CS.PdfView"
  ……省略……
  d:DesignHeight="300"
  d:DesignWidth="400">

  <Grid>
    <ScrollViewer HorizontalScrollBarVisibility="Auto" >
      <Image x:Name="image1" />
    </ScrollViewer>
  </Grid>
</UserControl>

「PdfView.xaml」ファイル(XAML)
太字の部分を追加する。

 プロジェクトをいったんビルドしてから、アプリのメインとなる画面に今作ったユーザー・コントロールを配置する(次のコード)。ここでは、メイン画面にはGridコントロールがあって、それを3段(=3つのRowDefinitionオブジェクト)に分けてあるものとし、ユーザー・コントロールを3段目に置いた。

<Page
  x:Name="pageRoot"
  x:Class="MetroTips055CS.MainPage"
  ……省略……

    <Grid  Grid.Row="2" Background="LightGray">
        <local:PdfView x:Name="pdfView1" />
    </Grid>
  </Grid>
</Page>

メイン画面にPdfViewコントロールを配置する(XAML)
太字の部分を追加する。

 メイン画面にはこのほかに、ページを指定するUIなどを適宜構築してもらいたい。

PdfDocumentオブジェクトを作るには?

 アプリでPDFファイルを開いて、そのStorageFileオブジェクトをPdfDocumentクラスのLoadPdfDocumentAsyncメソッドに渡せばよい。

 まず、ユーザー・コントロールの側に次のコードのようなメソッドを作る。このメソッドは、渡されたStorageFileオブジェクトを使ってPdfDocumentオブジェクトを生成し、それをメンバ変数に保持するものだ。なお、PdfDocumentオブジェクトが読み込んだPDFファイルの総ページ数がPageCountプロパティで取得できる。

Windows.Data.Pdf.PdfDocument _pdfDoc;

public uint PageCount { get { return _pdfDoc.PageCount; } }

public async Task<uint> LoadPdfDocumentAsync(Windows.Storage.StorageFile pdfFile)
{
  _pdfDoc = await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(pdfFile);
  return this.PageCount;
}

Private _pdfDoc As Windows.Data.Pdf.PdfDocument

Public ReadOnly Property PageCount As UInteger
  Get
    Return _pdfDoc.PageCount
  End Get
End Property

Public Async Function LoadPdfDocumentAsync(pdfFile As Windows.Storage.StorageFile) As Task(Of UInteger)
  _pdfDoc = Await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(pdfFile)
  Return Me.PageCount
End Function

ユーザー・コントロール側でPdfDocumentオブジェクトを生成して保持する(上:C#、下:VB)
PdfDocumentクラスのLoadFromFileAsyncメソッドにPDFファイルを表すStorageFileオブジェクトを渡すと、PdfDocumentオブジェクトが生成される。もしもパスワードが掛かっているPDFファイルを開くなら、2番目の引数としてパスワードの文字列を渡す。
なお、総ページ数がPdfDocumentオブジェクトのPageCountプロパティで分かるので、それをメソッドの戻り値とするとともに、プロパティとして公開した。

 メイン画面の側には、ユーザー・コントロールのLoadPdfDocumentAsyncメソッド(=上記のメソッド)にPDFファイルを渡すメソッドを作る(次のコード)。ここでは、PDFファイルを「sample.pdf」という名前でプロジェクトのルート・フォルダに置いた*2

const string PdfFileName = "sample.pdf";

private async void LoadPdfDocument()
{
  Windows.Storage.StorageFile pdfFile
    = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync(PdfFileName);
  await this.pdfView1.LoadPdfDocumentAsync(pdfFile);
}

Const PdfFileName As String = "sample.pdf"

Private Async Sub LoadPdfDocument()
  Dim pdfFile As Windows.Storage.StorageFile _
      = Await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync(PdfFileName)
  Await Me.pdfView1.LoadPdfDocumentAsync(pdfFile)
End Sub

PDFファイルをユーザー・コントロールに渡すメソッド(上:C#、下:VB)
メイン画面のコンストラクタでこのメソッドを呼び出す。

*2PDFファイルをコピーした後、プロジェクトに追加し、そのプロパティの[ビルド アクション]を[コンテンツ]にしておく必要がある。このとき、ソリューション・エクスプローラでPDFファイルをクリックすると、テキスト・ファイルとして編集画面が開いてしまうので、うっかり上書き保存しないように注意してほしい。


 上記のメソッドをメイン画面の中で呼び出す。画面のインスタンスが作られて表示されるまでの間のどこかで呼び出せばよいが、ここではコンストラクタの最後に入れた。

public MainPage()
{
  this.InitializeComponent();
  ……省略……

  this.LoadPdfDocument();
}

Public Sub New()
  InitializeComponent()
  ……省略……

  Me.LoadPdfDocument()
End Sub

ここまでで作成したメソッドを呼び出す(上:C#、下:VB)
太字の部分をコンストラクタの末尾に追加する。VBのプロジェクトでコンストラクタがないときには新規に作成すること。

 以上で、PDFファイルからPdfDocumentオブジェクトを作成するコードが出来上がった。

PdfPageオブジェクトに描画させるには?

 PdfDocumentオブジェクトのGetPageメソッドで1つのPdfPageオブジェクトが得られる。別途、描画データを書き込むためのストリームを用意しておき、PdfPageオブジェクトのRenderToStreamAsyncメソッドでストリームに書き込ませればよい。

 書き込むためのストリームは、あらかじめ画像ファイルを作っておいてそれを開いてもよいし、InMemoryRandomAccessStreamオブジェクト(メモリ上のストリーム)を使ってもよい。今回は後者を使おう。

 ユーザー・コントロールに、表示するページのインデックスを表すメンバ変数と、上記の処理を行うRenderPageBitmapAsyncメソッドを追加する(次のコード)。このメソッドから返されたInMemoryRandomAccessStreamオブジェクトには、PDFファイルの1ページが描き込まれていることになる。

uint _currentPageIndex; // 0が1ページ目

private async Task<Windows.Storage.Streams.InMemoryRandomAccessStream> RenderPageBitmapAsync()
{
  using (Windows.Data.Pdf.PdfPage pdfPage = _pdfDoc.GetPage(_currentPageIndex))
  {
    var memStream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
    await pdfPage.RenderToStreamAsync(memStream);
    return memStream;
  }
}

Private _currentPageIndex As UInteger ' 0が1ページ目

Private Async Function RenderPageBitmapAsync() As Task(Of Windows.Storage.Streams.InMemoryRandomAccessStream)
  Using pdfPage As Windows.Data.Pdf.PdfPage = _pdfDoc.GetPage(_currentPageIndex)
    Dim memStream = New Windows.Storage.Streams.InMemoryRandomAccessStream()
    Await pdfPage.RenderToStreamAsync(memStream)
    Return memStream
  End Using
End Function

ユーザー・コントロール側でPdfPageオブジェクトを使ってメモリ上に描画する(上:C#、下:VB)
PdfDocumentオブジェクトのGetPageメソッドにページのインデックス(1ページ目が0)を渡すと、PdfPageオブジェクトが得られる。PdfPageオブジェクトのRenderToStreamAsyncメソッドを呼び出すと、その引数に渡したストリームに対して、PdfPageオブジェクトが持っているページの画像データが書き込まれる。
なお、PdfPageオブジェクトは保持せず、使い終わったら捨てることにした。
また、このメソッドから返されるInMemoryRandomAccessStreamオブジェクトは、呼び出した側で破棄しなければならない。

画面に表示するには?

 PDFファイルの1ページが描き込まれたInMemoryRandomAccessStreamオブジェクトを使ってBitmapImageオブジェクトを作り、それをImageコントロールに表示させればよい。

 ユーザー・コントロールに、上記の処理を行うShowImageAsyncメソッドを追加する(次のコード)。

private async Task ShowImageAsync(Windows.Storage.Streams.InMemoryRandomAccessStream bitmapStream)
{
  var bitmap = new Windows.UI.Xaml.Media.Imaging.BitmapImage();
  await bitmap.SetSourceAsync(bitmapStream);

  this.image1.Width = bitmap.PixelWidth;
  this.image1.Height = bitmap.PixelHeight;
  this.image1.Source = bitmap;
}

Private Async Function ShowImageAsync(bitmapStream As Windows.Storage.Streams.InMemoryRandomAccessStream) As Task
  Dim bitmap = New Windows.UI.Xaml.Media.Imaging.BitmapImage()
  Await bitmap.SetSourceAsync(bitmapStream)

  Me.image1.Width = bitmap.PixelWidth
  Me.image1.Height = bitmap.PixelHeight
  Me.image1.Source = bitmap
End Function

ユーザー・コントロール側でInMemoryRandomAccessStreamオブジェクトを画面に表示する(上:C#、下:VB)
このメソッドの引数には、PdfPageオブジェクトに1ページを描画してもらったInMemoryRandomAccessStreamオブジェクトを渡す。メソッド内部でBitmapImageオブジェクトを生成し、そのSetSourceAsyncメソッドにInMemoryRandomAccessStreamオブジェクトを渡すことで、表示可能なビットマップがストリームの内容から作成される。こうして作ったBitmapImageオブジェクトをImageコントロール(=image1オブジェクト。冒頭で示したPdfView.xamlファイルのリストを参照)のSourceプロパティにセットしてやると、画面に画像が表示される。
なお、画面上のImageコントロールはScrollViewerコントロールの中に配置されているので、画面からはみ出すサイズのビットマップを表示させてもスクロールできる。

 最後に、前述したRenderPageBitmapAsyncメソッドとこのShowImageAsyncメソッドを組み合わせて、指定したページを表示するためのRenderPageメソッドを仕立てる。このRenderPageメソッドは、メイン画面から呼び出せるようにpublicなメソッドにする。

public uint RenderPage(uint pageIndex)
{
  if (_currentPageIndex == pageIndex)
    return _currentPageIndex;

  if (_pdfDoc.PageCount <= pageIndex)
    return _currentPageIndex;

  _currentPageIndex = pageIndex;
  RenderPageAsync();

  return _currentPageIndex;
}

private async void RenderPageAsync()
{
  if (_pdfDoc == null || _pdfDoc.PageCount == 0)
    return;

  using (Windows.Storage.Streams.InMemoryRandomAccessStream bitmapStream
            = await RenderPageBitmapAsync())
  {
    await ShowImageAsync(bitmapStream);
  }
}

Public Function RenderPage(pageIndex As UInteger) As UInteger
  If (_currentPageIndex = pageIndex) Then
    Return _currentPageIndex
  End If

  If (_pdfDoc.PageCount <= pageIndex) Then
    Return _currentPageIndex
  End If

  _currentPageIndex = pageIndex
  RenderPageAsync()

  Return _currentPageIndex
End Function

Private Async Sub RenderPageAsync()
  If (_pdfDoc Is Nothing OrElse _pdfDoc.PageCount = 0) Then
    Return
  End If

  Using bitmapStream As Windows.Storage.Streams.InMemoryRandomAccessStream _
            = Await RenderPageBitmapAsync()
    Await ShowImageAsync(bitmapStream)
  End Using
End Sub

ユーザー・コントロール側で指定ページを画面に表示する(上:C#、下:VB)
ここまでのコードを組み合わせて、メイン画面から呼び出せるようにした。
RenderPage()メソッドでは、現在表示されているページを表示するように指定された場合と、存在しないページを表示するように指定された場合には現在のページを表示したままとして、それ以外の場合にRenderPageAsync()メソッドを呼び出して実際にページ表示を行っている。戻り値は現在表示されているページのインデックスだ。
RenderPageAsync()メソッドでは、using/Using構文を使ってInMemoryRandomAccessStreamオブジェクトを破棄するようにしている点に注意(RenderPageBitmapAsync()メソッドの説明を参照)。

 メイン画面に適当なUIを配置し、そのイベント・ハンドラで上記のRenderPageメソッドを呼び出せば、指定したページが画面に表示されるはずだ。ただし、このままではファイルをロードした直後に何も表示されないので、前述のLoadPdfDocumentAsyncメソッドの中にRenderPageAsyncメソッドの呼び出しを追加しておく(次のコード)。

public async Task<uint> LoadPdfDocumentAsync(Windows.Storage.StorageFile pdfFile)
{
  _pdfDoc = await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(pdfFile);

  _currentPageIndex = 0;
  RenderPageAsync();

  return this.PageCount;
}

Public Async Function LoadPdfDocumentAsync(pdfFile As Windows.Storage.StorageFile) As Task(Of UInteger)
  _pdfDoc = Await Windows.Data.Pdf.PdfDocument.LoadFromFileAsync(pdfFile)

  _currentPageIndex = 0
  RenderPageAsync()

  Return Me.PageCount
End Function

ユーザー・コントロール側のLoadPdfDocumentAsyncメソッド中でも画面に表示する(上:C#、下:VB)
太字の部分を追加する。
これでPDFファイルをロードしただけで最初のページが表示されるようになる。なお、別のファイルをロードしたときにはまた最初のページを表示すべきなので、currentPageIndex変数を0に初期化している(PDFの表示機能を再利用可能なユーザー・コントロールとしてまとめてあるため)。

描画させるビットマップのサイズを変えるには?

 PdfPageオブジェクトに描画してもらうときには、PdfPageRenderOptionsオブジェクトを渡せる。このPdfPageRenderOptionsオブジェクトで、描画される画像のサイズを指定すればよい。

 ほかにも描画するときの背景色などを指定できる。画面の回転はサポートされていないので、回転させる必要があるならそのような処理を別途記述しなければならない(本稿では説明しない)。

 画像の拡大率を表すメンバ変数を追加し、RenderPageBitmapAsyncメソッドを変更してみよう(次のコード)。

double _zoom = 1.0;

private async Task<Windows.Storage.Streams.InMemoryRandomAccessStream> RenderPageBitmapAsync()
{
  using (Windows.Data.Pdf.PdfPage pdfPage = _pdfDoc.GetPage(_currentPageIndex))
  {
    var options = new Windows.Data.Pdf.PdfPageRenderOptions();
    options.DestinationHeight = (uint)(pdfPage.Size.Height * _zoom);

    var memStream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
    await pdfPage.RenderToStreamAsync(memStream, options);

    return memStream;
  }
}

Private _zoom As Double = 1.0

Private Async Function RenderPageBitmapAsync() As Task(Of Windows.Storage.Streams.InMemoryRandomAccessStream)
  Using pdfPage As Windows.Data.Pdf.PdfPage = _pdfDoc.GetPage(_currentPageIndex)

    Dim options = New Windows.Data.Pdf.PdfPageRenderOptions()
    options.DestinationHeight = pdfPage.Size.Height * _zoom

    Dim memStream = New Windows.Storage.Streams.InMemoryRandomAccessStream()
    Await pdfPage.RenderToStreamAsync(memStream, options)
    Return memStream
  End Using
End Function

ユーザー・コントロール側のRenderPageBitmapAsyncメソッドで拡大できるようにする(上:C#、下:VB)
太字の部分を追加する。
これで、例えばメンバ変数_zoomに2.0が入っていると、縦横2倍になって画面に表示される。
DestinationHeightプロパティに設定するのは、倍率ではなくて、ピクセル単位の高さであることに注意。PdfPageオブジェクトのHeightプロパティで等倍時の画像サイズが得られるので、_zoom変数を掛けて必要な高さを算出している。

実行結果

 メイン画面にページ送りと倍率変更のUIを配置してアプリに仕立ててみた*3。実行すると次の画像のようになる。

実行結果
実行結果 実行結果
上は等倍、下は2倍の拡大表示。
なお、サンプルに使ったPDFは筆者の講演資料だ。

*3実際のコードは、別途公開のサンプル・コードを見てほしい。また、そちらには90°回転させる機能も実装してあるので参考にしてほしい。


まとめ

 Windows.Data.Pdf名前空間は画面への表示まではサポートしていないため、PDFを表示するにはストリームやBitmapImageオブジェクトを扱うスキルが必要だ。ちょっと面倒だが、自分が必要とする機能をまとめたユーザー・コントロールに仕立てておけば、アプリからは簡単に利用できるようになる。

「WinRT/Metro TIPS」のインデックス

WinRT/Metro TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。