アプリからファイルを開くには?[ユニバーサルWindowsアプリ開発]WinRT/Metro TIPS

WindowsアプリとPhoneアプリでは多くの部分でコードを共有できるが、そうではない部分もある。今回はファイルピッカーを扱う際に両者の違いをどのように吸収すればよいか。その方法を説明する。

» 2014年07月03日 00時00分 公開
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

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

連載目次

 アプリから任意のファイルを開くには、ファイルピッカーを出してエンドユーザーに選択してもらう。Windowsストアアプリ(以降、Windowsアプリ)とWindows Phone 8.1のWindows Runtimeアプリ(以降、Phoneアプリ)では、ファイルピッカーを用意する部分は同じなのだが、ファイルピッカーを出してエンドユーザーの選択結果を受け取る部分が大きく異なる。それをできるだけ分かりやすい形にして、ユニバーサルプロジェクトの共有コードにまとめて記述するにはどうしたらよいだろうか? 本稿ではその方法を解説する。なお、本稿のサンプルは「Windows Store app samples:MetroTips #81」からダウンロードできる。

事前準備

 ユニバーサルプロジェクトを使ってユニバーサルWindowsアプリを開発するには、以下の開発環境が必要である。本稿では、無償のVisual Studio Express 2013 for Windowsを使っている。

  • SLAT対応のPC*1
  • 2014年4月のアップデート*2適用済みの64bit版Windows 8.1 Pro版以上*3
  • Visual Studio 2013 Update 2*4適用済みのVisual Studio 2013(以降、VS 2013)*5

*1 SLAT対応ハードウェアは、Windows Phone 8.1エミュレーターの実行に必要だ。ただし未対応でも、ソースコードのビルドと実機でのデバッグは可能だ。SLAT対応のチェック方法はMSDNブログの「Windows Phone SDK 8.0 ダウンロードポイント と Second Level Address Translation (SLAT) 対応PCかどうかを判定する方法」を参照。なお、SLAT対応ハードウェアであっても、VM上ではエミュレーターが動作しないことがあるのでご注意願いたい。

*2 事前には「Windows 8.1 Update 1」と呼ばれていたアップデート。スタート画面の右上に検索ボタンが(環境によっては電源ボタンも)表示されるようになるので、適用済みかどうかは簡単に見分けられる。ちなみに公式呼称は「the Windows RT 8.1, Windows 8.1, and Windows Server 2012 R2 update that is dated April, 2014」というようである。

*3 Windows Phone 8.1エミュレーターを使用しないのであれば、32bit版のWindows 8.1でもよい。

*4 マイクロソフトのダウンロードページから誰でも入手できる。

*5 本稿に掲載したコードを試すだけなら、無償のExpressエディションで構わない。Visual Studio Express 2013 Update 2 for Windows(製品版)はマイクロソフトのページから無償で入手できる。Expressエディションはターゲットプラットフォームごとに製品が分かれていて紛らわしいが、Windowsストアアプリの開発には「for Windows」を使う(「for Windows Desktop」はデスクトップで動作するアプリ用)。


用語

 本稿では、紛らわしくない限り次の略称を用いる。

  • Windows:Windows 8.1とWindows RT 8.1(2014年4月のアップデートを適用済みのもの)
  • Phone:Windows Phone 8.1

サンプルコードについて

 Visual Studio 2013 Update 2では、残念なことにVB用のユニバーサルプロジェクトのテンプレートは含まれていない*6。そのため、本稿で紹介するコードはC#のユニバーサルプロジェクトだけとさせていただく*7

*6 VB用のユニバーサルプロジェクトは、来年にリリースされるといわれているVisual Studio「14」からの提供となるようだ。「Visual Studio UserVoice」(英語)のリクエストに対する、6月18日付の「Visual Studio team (Product Team, Microsoft)」からの回答による。

*7 Visual Studio 2013 Update 2のVBでユニバーサルWindowsアプリを作る場合のお勧めは、「The Visual Basic Team」のブログ記事(英語)によれば、PCLを使う方法のようである。しかし、本稿で説明するパーシャルクラスによる方法は、PCLではうまく実装できない。


Windowsでファイルを開くには?

 例えば、画像ファイルを開いて画面に表示することを考えてみよう。

 画面には、ButtonコントロールとImageコントロールを配置する(次のコード)。

<StackPanel ……省略…… >
  <Button Click="Button1_Click" ……省略…… >画像ファイルを開く</Button>
  <Border Background="Gray" ……省略…… >
    <Image x:Name="Image1" ……省略…… />
  </Border>
</StackPanel>

ButtonコントロールとImageコントロールを画面に配置する(XML)
このコードは、Windowsプロジェクトの「MainPage.xaml」ファイルに追加する。

 あとはボタンのクリックイベントを処理するコードを、次のようにコードビハインドに書けばよい。

// ボタンのクリックイベントハンドラー
private async void Button1_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
  // ファイルオープンピッカーを準備する
  var picker = new Windows.Storage.Pickers.FileOpenPicker()
  {
    SettingsIdentifier = "FilePicker01",
    SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary,
  };
  picker.FileTypeFilter.Add(".png");
  picker.FileTypeFilter.Add(".jpeg");
  picker.FileTypeFilter.Add(".jpg");

  // ファイルオープンピッカーを出し、エンドユーザーの選択結果を受け取る
  var file = await picker.PickSingleFileAsync();

  // 画像ファイルの画像をImageコントロールに表示する
  await SetImageAsync(file);
}

// 画像ファイルの画像をImageコントロールに表示するメソッド
private async Task SetImageAsync(Windows.Storage.StorageFile file)
{
  if (file == null)
    return;

  Windows.Storage.Streams.IRandomAccessStream fileStream
    = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
  var bitmapImage = new Windows.UI.Xaml.Media.Imaging.BitmapImage();
  bitmapImage.SetSource(fileStream);
  this.Image1.Source = bitmapImage;
}

エンドユーザーにファイルオープンピッカーで選択してもらった画像を表示するコード(C#)
このコードは、Windowsプロジェクトの「MainPage.xaml.cs」ファイルに追加する。なお、Taskクラスを使用するには、MainPage.xaml.csファイルの先頭で「using System.Threading.Tasks;」を追加しておく必要がある。

 これで実行してみると、次の画像のようになる。

Windowsアプリでファイルオープンピッカーを使って画像ファイルを開く様子(Windows) Windowsアプリでファイルオープンピッカーを使って画像ファイルを開く様子(Windows)
ボタンをタップすると、ファイルオープンピッカーに切り替わる。
ファイルオープンピッカーで、画像ファイルを選択してから[開く]ボタンをタップすると、アプリの画面に戻って画像が表示される。

 なお、実行中に画面がファイルオープンピッカーに切り替わっているときでも、Windowsアプリは動作している。この点が、Phoneアプリと根本的に異なるのである(詳細は次で説明する)。

Phoneでファイルを開くには?

 Phoneでファイルオープンピッカーを出すところまでは、Windowsとほぼ同じだ。しかし、エンドユーザーが選択したファイルを受け取る方法がPhoneとWindowsでは大幅に異なる。Phoneでは、AppクラスのOnActivatedメソッドで受け取らねばならないのだ。

 そのようになっている理由は、ファイルピッカーに多くのメモリを割り当てるために、アプリが非アクティブ化されるためである。ファイルピッカーを出している間、アプリは中断され、それでもメモリが足りなければいったん終了させられてしまう。そして、ファイルピッカーから戻るときに、アプリは再びアクティベートされるのである。

 従って、Phoneでファイルピッカーを使うには、アクティベート時に前回の状態を復元する実装が必須となる。そして、復元後に、システムからアクティベート時の引数として渡されるファイル(=アプリが中断された後にファイルピッカーを使ってエンドユーザーが選択したファイル)を取り出して処理を行うことになる。以下、状態を復元する実装/ファイルピッカーを出す方法/エンドユーザーが選択したファイルを取り出して処理を行う方法に分けて、順に説明していく。

アクティベート時に前回の状態を復元する実装

 共有プロジェクトのAppクラス(「App.xaml.cs」ファイル)にあるOnLaunchedメソッドとOnSuspendingメソッドを、以下のように修正する。

 まず、OnLaunchedメソッドでは、画面のフレームをSuspensionManagerクラスに登録し、再開時に状態を復元するコードを追加する。なお、後で述べるが、OnLaunchedメソッドのほとんどの部分を再利用するので、その部分を「CreateRootFrameAsync」という名前のメソッドに切り出しておく(次のコード)。

protected override async void OnLaunched(LaunchActivatedEventArgs e)
{
  // 後で追加するコードと共通する部分を、CreateRootFrameAsyncメソッドとして切り出した
  await CreateRootFrameAsync(e);

  // 現在のウィンドウがアクティブであることを確認します
  Window.Current.Activate();
}

// OnLaunchedメソッドと後で追加するコードに共通するコードを切り出した。
// 引数はOnLaunchedメソッドではLaunchActivatedEventArgsクラスだが、
// 後で追加するコードからも使えるようにするためにIActivatedEventArgsインターフェースに変更している
// また、元のコメントや空行は、変更箇所の目印になるもの以外は削除した
private async System.Threading.Tasks.Task CreateRootFrameAsync(IActivatedEventArgs e)
{
#if DEBUG
  if (System.Diagnostics.Debugger.IsAttached)
  {
    this.DebugSettings.EnableFrameRateCounter = true;
  }
#endif
  Frame rootFrame = Window.Current.Content as Frame;
  if (rootFrame == null)
  {
    rootFrame = new Frame();

    // 画面のフレームをSuspensionManagerに関連付ける
    Common.SuspensionManager.RegisterFrame(rootFrame, "AppFrame");

    rootFrame.CacheSize = 1;
    if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
    {
      //TODO: 以前中断したアプリケーションから状態を読み込みます。

      // 前回の状態を復元する
      try
      {
        await Common.SuspensionManager.RestoreAsync();
      }
      catch (Common.SuspensionManagerException){}

    }
    Window.Current.Content = rootFrame;
  }
  if (rootFrame.Content == null)
  {
#if WINDOWS_PHONE_APP
    if (rootFrame.ContentTransitions != null)
    {
      this.transitions = new TransitionCollection();
      foreach (var c in rootFrame.ContentTransitions)
      {
        this.transitions.Add(c);
      }
    }
    rootFrame.ContentTransitions = null;
    rootFrame.Navigated += this.RootFrame_FirstNavigated;
#endif

    // メソッドの引数をIActivatedEventArgsインターフェースに変更したため、
    // 常にe.Argumentsが存在するとは限らなくなった。
    // 引数がILaunchActivatedEventArgsインターフェースでもあるとき
    // (=Argumentsプロパティが存在するとき)に限り、引数を取り出して使うようにする
    string launchArguments = string.Empty;
    var lea = e as ILaunchActivatedEventArgs;
    if(lea != null)
      launchArguments = lea.Arguments;

    if (!rootFrame.Navigate(typeof(MainPage), launchArguments))
    {
      throw new Exception("Failed to create initial page");
    }
  }
}

前回の状態を復元するためにOnLaunchedメソッドを改修(C#)
太字が追加/変更した部分。
これは共有プロジェクトのAppクラスのメソッドである。
なお、SuspensionManagerクラスが存在しない場合は、新しく[ハブ アプリケーション (ユニバーサル アプリ)]プロジェクトを別に作り、その共有プロジェクトの[Common]フォルダーを丸ごと移植する。その際には名前空間の変更を忘れずに。

 また、共有プロジェクトのAppクラスにあるOnSuspendingメソッドには、中断時に状態を保存するコードを追加する(次のコード)。

private async void OnSuspending(object sender, SuspendingEventArgs e)
{
  var deferral = e.SuspendingOperation.GetDeferral();
  // TODO: アプリケーションの状態を保存してバックグラウンドの動作があれば停止します

  // 現在の状態を保存する
  await Common.SuspensionManager.SaveAsync();

  deferral.Complete();
}

前回の状態を復元するためにOnSuspendingメソッドを改修(C#)
太字が追加した部分。これは共有プロジェクトのAppクラスのメソッドである。

 以上の変更で、画面遷移を伴うアプリであれば、起動時に前回終了時の画面が出るようになる(画面の内容を復元するにはそれぞれの画面での実装がさらに必要になるだろう)。

ファイルオープンピッカーを出すには?

 Phoneでファイルオープンピッカーを表示するには、次のようにする。

 画面には、前述したWindowsと同様にButtonコントロールとImageコントロールを配置する。そして、ボタンのクリックイベントを処理するコードを、次のようにコードビハインドに書けばよい。

// ボタンのクリックイベントハンドラー
private void Button1_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
  // ファイルオープンピッカーを準備する(Windowsと全く同じ)
  var picker = new Windows.Storage.Pickers.FileOpenPicker()
  {
    SettingsIdentifier = "FilePicker01",
    SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary,
  };
  picker.FileTypeFilter.Add(".png");
  picker.FileTypeFilter.Add(".jpeg");
  picker.FileTypeFilter.Add(".jpg");

  // ファイルオープンピッカーを出す
  picker.ContinuationData["Operation"] = "SetImage";
  picker.PickSingleFileAndContinue();
}

Phoneでファイルオープンピッカーを出すコード(C#)
このコードは、Phoneプロジェクトの「MainPage.xaml.cs」ファイルに追加する。
Phoneのファイルオープンピッカーには、WindowsにあったPickSingleFileAsyncメソッドが用意されておらず、代わりにPickSingleFileAndContinueメソッドを使う。このPickSingleFileAndContinueメソッドは値を返さない。
なお、ContinuationData["Operation"]にセットしている値は、本稿のサンプルコードでは使っていない。アプリ内で複数のファイルピッカーを使うなどの場合に、ファイルを受け取るときに識別するためのものである。

 PhoneのファイルオープンピッカーにあるPickSingleFileAndContinueメソッドは、値を何も返さない。次に説明するようにして、AppクラスのOnActivatedメソッドでエンドユーザーが選択したファイルを受け取る。

エンドユーザーが選択したファイルを受け取るには?

 ファイルオープンピッカーでエンドユーザーが選択したファイルは、AppクラスのOnActivatedメソッドの引数に入ってくる。

 そこで、AppクラスのOnActivatedメソッドでファイルを受け取って処理すればよい。ただし、前述したようにファイルオープンピッカーが表示されている間にアプリが終了させられてしまう場合もあるので、画面を構築/復元する処理(=前述のCreateRootFrameAsyncメソッド)も呼び出さねばならない(次のコード)。

protected override async void OnActivated(IActivatedEventArgs args)
{
  base.OnActivated(args);

  // 必要に応じて画面を構築/復元してくれる処理(前出)
  await CreateRootFrameAsync(args);

#if WINDOWS_PHONE_APP
  var fopArgs = args as FileOpenPickerContinuationEventArgs;
  if (fopArgs != null && fopArgs.Files.Count > 0)
  {
    // エンドユーザーが選択したファイルを引数から取り出す
    var storageFile = fopArgs.Files[0];

    // MainPage画面のSetImageAsyncメソッドを呼び出して、画像を表示する
    Frame rootFrame = Window.Current.Content as Frame;
    MainPage mainPage = rootFrame.Content as MainPage;
    await mainPage.SetImageAsync(storageFile);
  }
#endif

  Window.Current.Activate();
}

AppクラスのOnActivatedメソッドで選択されたファイルを受け取る(C#)
このメソッドは、共有プロジェクトのAppクラスに記述する。
なお、この他に、Phoneプロジェクトの「MainPage.xaml.cs」ファイルにSetImageAsyncメソッドを記述する。その内容は前述したWindowsのものと全く同じだが、メソッドのスコープだけはAppクラスから呼び出せるようにprivateではなくinternalとする。

 これで実行してみると、次の画像のようになる。

Phoneアプリでファイルオープンピッカーを使って画像ファイルを開く様子(Phoneエミュレーター) Phoneアプリでファイルオープンピッカーを使って画像ファイルを開く様子(Phoneエミュレーター)
ボタンをタップすると、ファイルオープンピッカーに切り替わる。
ファイルオープンピッカーで画像ファイルを選択すると、アプリの画面に戻ってその画像が表示される。
なお、ファイルオープンピッカーが表示されている間、アプリは中断、もしくは終了させられている。デバッグ実行時にそれをエミュレートするには、ファイルオープンピッカーが表示されているときに、VS 2013のツールバーにある[ライフサイクル イベント]ドロップダウンの[中断]、もしくは[中断とシャットダウン]を使う。

汎用的な仕組みにするには?

 Phoneでファイルピッカーを使う方法の基本は以上である。ただし、このようにAppクラスのOnActivatedメソッド内で処理をしていると、複数のファイルピッカーを使う場合にコードが煩雑になる。また、アプリにファイルピッカーを追加するたびに、画面のコードビハインドとAppクラスの両方を修正することになるが、それは面倒だしミスを誘発することにもなる。

 アプリにファイルピッカーを追加するときに、画面のコードビハインドだけの作業で済むようにならないだろうか? MSDNには、そのような汎用的に使えるサンプルコードが掲載されている。「AndContinue メソッドの呼び出し後に Windows Phone ストア アプリを続行する方法」に載っている「ContinuationManager」クラスだ。

 本稿では「ContinuationManager」クラスの実装を紹介する余裕はないが、上記のMSDNの解説に従ってPhoneのプロジェクトに「ContinuationManager」クラスを組み込んだ後では、ファイルオープンピッカーを使うコードは次のように書ける。

// 「ContinuationManager」クラスと同じソースに定義されている「IFileOpenPickerContinuable」
// インターフェースを画面に実装する
public sealed partial class MainPage : Page, IFileOpenPickerContinuable
{

  // ボタンクリックのイベントハンドラー
  private void Button1_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
  {
    var picker = new Windows.Storage.Pickers.FileOpenPicker()
    {
      SettingsIdentifier = "FilePicker01",
      SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary,
    };
    picker.FileTypeFilter.Add(".png");
    picker.FileTypeFilter.Add(".jpeg");
    picker.FileTypeFilter.Add(".jpg");

    picker.ContinuationData["Operation"] = "SetImage";
    picker.PickSingleFileAndContinue();
  }

  // ユーザーがファイルピッカーを使った後で、ContinuationManagerから呼び出されるメソッド
  // (IFileOpenPickerContinuableインターフェースの実装)
  public async Task ContinueFileOpenPickerAsync
    (Windows.ApplicationModel.Activation.FileOpenPickerContinuationEventArgs args)
  {
    // ファイルピッカーを開くときに設定した「ContinuationData["Operation"]」の値を見て
    // 処理を分岐させる(ここでは1つだけなのでif文にしたが、複数の場合はswitch文にする)
    if ((args.ContinuationData["Operation"] as string) == "SetImage"
        && args.Files.Count > 0)
    {
      await SetImageAsync(args.Files[0]);
    }
  }

  // 画像ファイルの画像をImageコントロールに表示するメソッド
  private async Task SetImageAsync(Windows.Storage.StorageFile file)
  {
    ……省略……

ContinuationManagerが組み込まれたPhoneプロジェクトでファイルオープンピッカーを開くコードの例(C#)
プロジェクトにContinuationManagerが組み込まれている場合には、このようにPhoneの画面のコードビハインドにコードを書くだけで済む。
表面的なコードの流れは、ファイルオープンピッカーのPickSingleFileAndContinueメソッドを呼び出す→ContinueFileOpenPickerAsyncメソッドが呼び出されるので処理を分岐させる→目的の処理を行うメソッド(ここではSetImageAsyncメソッド)を実行する、となる。
また、SetImageAsyncメソッドのスコープがprivateに戻っていることにも注目していただきたい(AppクラスのOnActivatedメソッドから呼び出す方法では、ここはinternalだった)。

 別途公開しているサンプルコードには「ContinuationManager」クラスを組み込んであるので、興味のある方はご覧いただきたい(MSDNのコードから一部変更している)。

共有コードにまとめるには?

 ここまでの説明では、Appクラスの改修は共有プロジェクトで行ったものの、画面のコードビハインドはWindowsとPhoneそれぞれのプロジェクトにコードを書いてきた。最後に、そのコードを共有プロジェクトにまとめよう。

 まず、WindowsとPhoneそれぞれの「MainPage.xaml.cs」ファイルから、ボタンのクリックイベントハンドラーのメソッドとSetImageAsyncメソッドを削除する。

 次に、共有プロジェクトに新しくクラスを作り、ファイル名を「MainPage.xaml.Shared.cs」とする。そこにはMainPageクラスのパーシャルクラスとして、次のようなコードを記述する。

using System;
using System.Threading.Tasks;

namespace MetroTips081CS
{
  public sealed partial class MainPage
  {
    // ボタンクリックのイベントハンドラー
    private
#if WINDOWS_APP
      async
#endif
      void Button1_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
    {
      var picker = new Windows.Storage.Pickers.FileOpenPicker()
      {
        SettingsIdentifier = "FilePicker01",
        SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary,
      };
      picker.FileTypeFilter.Add(".png");
      picker.FileTypeFilter.Add(".jpeg");
      picker.FileTypeFilter.Add(".jpg");

#if WINDOWS_PHONE_APP
      // PhoneではPickSingleFileAndContinueメソッドを使う
      picker.ContinuationData["Operation"] = "SetImage";
      picker.PickSingleFileAndContinue();
#endif
#if WINDOWS_APP
      // WindowsではPickSingleFileAsyncメソッドを使う
      var file = await picker.PickSingleFileAsync();
      await SetImageAsync(file);
#endif
    }

    // 画像ファイルの画像をImageコントロールに表示するメソッド
    // 注:このメソッドはPhoneではAppクラスのOnActivatedメソッドから呼び出される
    internal async Task SetImageAsync(Windows.Storage.StorageFile file)
    {
      if (file == null)
        return;

      Windows.Storage.Streams.IRandomAccessStream fileStream
        = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
      var bitmapImage = new Windows.UI.Xaml.Media.Imaging.BitmapImage();
      bitmapImage.SetSource(fileStream);
      this.Image1.Source = bitmapImage;
    }
  }
}

共有プロジェクトに置いた「MainPage.xaml.Shared.cs」(C#)
「MainPage.xaml/.xaml.cs」ファイルはWindowsとPhoneのプロジェクトに置いてあるが、そのパーシャルクラスをこのように共有プロジェクトに置ける。
なお、ここでは「ContinuationManager」クラスは使っていない。「エンドユーザーが選択したファイルを受け取るには?」の項で説明した、AppクラスのOnActivatedメソッド内から直接SetImageAsyncメソッドを呼び出している。
ちなみに、「ContinuationManager」クラスを利用する場合には、クラス宣言部にIFileOpenPickerContinuableインターフェースを追加し、クラス本体にContinueFileOpenPickerAsyncメソッドを追加するが、どちらも「#if WINDOWS_PHONE_APP」で囲っておくこと(別途公開のサンプルコードを参照)。

まとめ

 ファイルピッカーの扱いは、ファイルピッカーを表示してからがWindowsとPhoneとで大きく異なる。Phoneでは、エンドユーザーが選択したファイルを受け取るのはAppクラスになる。素直に実装すると、ファイルピッカーの処理が画面とAppクラスの2箇所に分散してしまう。しかし、「ContinuationManager」クラスをプロジェクトに組み込んでおけば、あとはコードビハインドに記述するだけでよくなる。また、ファイルピッカーを表示するコードと、エンドユーザーが選択したファイルを処理するコードは、ユニバーサルプロジェクトでは共有プロジェクトに置ける。

 ファイルピッカーの扱いについては、次のドキュメントも参照してほしい。

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

WinRT/Metro TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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