WindowsアプリとPhoneアプリでは多くの部分でコードを共有できるが、そうではない部分もある。今回はファイルピッカーを扱う際に両者の違いをどのように吸収すればよいか。その方法を説明する。
powered by Insider.NET
アプリから任意のファイルを開くには、ファイルピッカーを出してエンドユーザーに選択してもらう。Windowsストアアプリ(以降、Windowsアプリ)とWindows Phone 8.1のWindows Runtimeアプリ(以降、Phoneアプリ)では、ファイルピッカーを用意する部分は同じなのだが、ファイルピッカーを出してエンドユーザーの選択結果を受け取る部分が大きく異なる。それをできるだけ分かりやすい形にして、ユニバーサルプロジェクトの共有コードにまとめて記述するにはどうしたらよいだろうか? 本稿ではその方法を解説する。なお、本稿のサンプルは「Windows Store app samples:MetroTips #81」からダウンロードできる。
ユニバーサルプロジェクトを使ってユニバーサルWindowsアプリを開発するには、以下の開発環境が必要である。本稿では、無償のVisual Studio Express 2013 for Windowsを使っている。
*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」はデスクトップで動作するアプリ用)。
本稿では、紛らわしくない限り次の略称を用いる。
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ではうまく実装できない。
例えば、画像ファイルを開いて画面に表示することを考えてみよう。
画面には、ButtonコントロールとImageコントロールを配置する(次のコード)。
<StackPanel ……省略…… >
<Button Click="Button1_Click" ……省略…… >画像ファイルを開く</Button>
<Border Background="Gray" ……省略…… >
<Image x:Name="Image1" ……省略…… />
</Border>
</StackPanel>
あとはボタンのクリックイベントを処理するコードを、次のようにコードビハインドに書けばよい。
// ボタンのクリックイベントハンドラー
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;
}
これで実行してみると、次の画像のようになる。
なお、実行中に画面がファイルオープンピッカーに切り替わっているときでも、Windowsアプリは動作している。この点が、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");
}
}
}
また、共有プロジェクトのAppクラスにあるOnSuspendingメソッドには、中断時に状態を保存するコードを追加する(次のコード)。
private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
// TODO: アプリケーションの状態を保存してバックグラウンドの動作があれば停止します
// 現在の状態を保存する
await Common.SuspensionManager.SaveAsync();
deferral.Complete();
}
以上の変更で、画面遷移を伴うアプリであれば、起動時に前回終了時の画面が出るようになる(画面の内容を復元するにはそれぞれの画面での実装がさらに必要になるだろう)。
ファイルオープンピッカーを出すには?
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のファイルオープンピッカーにある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();
}
これで実行してみると、次の画像のようになる。
汎用的な仕組みにするには?
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」クラスを組み込んであるので、興味のある方はご覧いただきたい(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;
}
}
}
ファイルピッカーの扱いは、ファイルピッカーを表示してからがWindowsとPhoneとで大きく異なる。Phoneでは、エンドユーザーが選択したファイルを受け取るのはAppクラスになる。素直に実装すると、ファイルピッカーの処理が画面とAppクラスの2箇所に分散してしまう。しかし、「ContinuationManager」クラスをプロジェクトに組み込んでおけば、あとはコードビハインドに記述するだけでよくなる。また、ファイルピッカーを表示するコードと、エンドユーザーが選択したファイルを処理するコードは、ユニバーサルプロジェクトでは共有プロジェクトに置ける。
ファイルピッカーの扱いについては、次のドキュメントも参照してほしい。
Copyright© Digital Advantage Corp. All Rights Reserved.