Windowsストアは原則として1アプリに1ウィンドウだが、Windows 8.1で複数のウィンドウを表示できるようになった。その実装方法を解説する。
powered by Insider.NET
Windowsストアは原則として1アプリに1ウィンドウである。しかしそうはいっても、複数のウィンドウを表示したいことはないだろうか? 例えば、1つのアプリからモニターとプロジェクターに異なる画面を表示したいとき。あるいは、コンテンツを表示するウィンドウを複数出したいときなどだ。Windows 8.1(以降、Win 8.1)ではそれが可能になった。本稿ではその方法を解説する。なお、本稿のサンプルは「Windows Store app samples:MetroTips #71」からダウンロードできる。
Win 8.1用のWindowsストアアプリを開発するには、Win 8.1とVisual Studio 2013(以降、VS 2013)が必要である。本稿ではOracle VM VirtualBox上で64bit版Windows 8.1 Pro(日本語版)*1とVisual Studio Express 2013 for Windows(日本語版)*2を使用している。
*1 Win 8.1 Update(2014年4月)を適用済み。なお、このアップデートは必須とされている。
*2 マイクロソフト公式サイトの「Microsoft Visual Studio Express 2013 for Windows」から無償で入手できる。
代表的なものに、Internet Explorer(以降、IE)がある(厳密にはWindowsストアアプリではない)。次の画像のように、複数のウィンドウを開くことができる。
Win 8.1では、上で紹介したIEのように複数のウィンドウを表示し、また、アプリからウィンドウ(アプリビュー)*3を切り替えるための新しいAPIが用意された。
− CoreApplicationクラス(Windows.ApplicationModel.Core名前空間)に新設されたCreateNewViewメソッドで新しいウィンドウ(アプリビュー)を作る。
− 新設されたApplicationViewSwitcherクラス(Windows.UI.ViewManagement名前空間)のメソッドを使って、現在のウィンドウの中身を切り替える(SwitchAsyncメソッド)、あるいは、隣接するウィンドウに表示する(TryShowAsStandaloneAsyncメソッド)。
一般的なコードの流れは、次のようになるだろう。
以降で、具体的なコードを紹介していく。
*3 MSDNでは画面分割に関して、ウィンドウとアプリビューを文章の上できちんと区別していない。例えば、ApplicationViewクラスの説明に「ウィンドウ (アプリ ビュー) のインスタンス」などと同一視して書かれている。実際には、ウィンドウはWindowクラス(Windows.UI.Xaml名前空間)、アプリビューはApplicationViewクラス(Windows.UI.ViewManagement名前空間)であり、これらは別のものである。WindowクラスとApplicationViewクラスをきちんと分けて理解しないと、複数ウィンドウ表示のプログラミングは難しい。画面に表示するコンテンツ(一般にはFrameコントロール(Windows.UI.Xaml.Controls名前空間)およびその中に配置された複数のコントロール)を格納するのがWindowクラスで、そのWindowクラスをどのようにモニターに表示するかを調整するのがApplicationViewクラスだと考えてほしい。また、Windowクラスは目に見えるUIを持つのでInspectツールに表示されるが、ApplicationViewクラスはUIを持たないので表示されない。
これだけではあまり実用的ではないが、まずは理解のために、2つ目のウィンドウを開くだけのコードを考えてみよう。
アプリ起動時に表示される画面は「MainPage.xaml」ファイルに定義されていて、2つ目のウィンドウに表示したい画面は「SecondaryPage.xaml」ファイルに定義されているものとする。「MainPage.xaml」にクリックイベントを持つ何らかのコントロールを配置し、そのイベントハンドラーに次のようなコードを記述する(コメント中の数字は上記の手順1.〜3.に対応する)。
// 1. 新しいCoreApplicationViewオブジェクトを作る(間接的にWindowオブジェクトとApplicationViewオブジェクトが一緒に生成される)
var coreApplicationView
= Windows.ApplicationModel.Core.CoreApplication.CreateNewView();
Windows.UI.ViewManagement.ApplicationView newAppView = null;
// 生成されたCoreApplicationViewオブジェクトのスレッドで、2.の処理を行う
await coreApplicationView.Dispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
() =>
{
// 2a. 生成されたApplicationViewオブジェクトを取得する
newAppView = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView();
// GetForCurrentViewメソッドの名前にある「CurrentView」とは、生成されたCoreApplicationViewオブジェクトの
// スレッドに結び付けられているApplicationViewオブジェクトのことである
// 2b. 生成されたWindowオブジェクトに画面をセットする
var newFrame = new Frame();
newFrame.Navigate(typeof(SecondaryPage));
Window.Current.Content = newFrame;
// このWindow.Currentプロパティは、生成されたCoreApplicationViewオブジェクトの
// スレッドに結び付けられているWindowオブジェクトである
}
);
// 3. 新しいウィンドウ(アプリビュー)を隣に表示する
int viewId = newAppView.Id;
await Windows.UI.ViewManagement.ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewId);
' 1. 新しいCoreApplicationViewオブジェクトを作る(間接的にWindowオブジェクトとApplicationViewオブジェクトが一緒に生成される)
Dim coreApplicationView _
= Windows.ApplicationModel.Core.CoreApplication.CreateNewView()
Dim newAppView As Windows.UI.ViewManagement.ApplicationView = Nothing
' 生成されたCoreApplicationViewオブジェクトのスレッドで、2.の処理を行う
Await coreApplicationView.Dispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
Sub()
' 2a. 生成されたApplicationViewオブジェクトを取得する
newAppView = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView()
' GetForCurrentViewメソッドの名前にある「CurrentView」とは、生成されたCoreApplicationViewオブジェクトの
' スレッドに結び付けられているApplicationViewオブジェクトのことである
' 2b. 生成されたWindowオブジェクトに画面をセットする
Dim newFrame = New Frame()
newFrame.Navigate(GetType(SecondaryPage))
Window.Current.Content = newFrame
' このWindow.Currentプロパティは、生成されたCoreApplicationViewオブジェクトの
' スレッドに結び付けられているWindowオブジェクトである
End Sub
)
' 3. 新しいウィンドウ(アプリビュー)を隣に表示する
Dim viewId As Integer = newAppView.Id
Await Windows.UI.ViewManagement.ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewId)
いきなり「Dispatcher.RunAsync」などと出てきて面食らうかもしれないが、前述した一般的なコードの流れのうち2番目の処理は、新しく生成したウィンドウ(アプリビュー)のスレッドで行う必要があるのだ。取得できるApplicationViewオブジェクトとWindowオブジェクトは、コードを実行しているスレッドに結び付いているからだ。
別途公開しているサンプルコードにはちょっとしたUIが作り込んである。そこで上のコードを実行すると次の画像のようになる。
上の画像のように新しいウィンドウが増え続けるのは、困る場合もあるだろう。また、コードからウィンドウ(アプリビュー)を切り替えたいこともあるだろう。それには、ApplicationViewオブジェクトのIdを管理すればよい。
そのような管理をする場所としては、「App」クラスが適切だ。画面は、どの画面であれ、複数表示する可能性があるからだ。
次のコードのようにして「App」クラスに「Dictionary<string, ApplicationView>」クラスのオブジェクトをメンバー変数として配置し、作成したApplicationViewオブジェクトを格納しておくようにする。そして、ウィンドウを切り替えようとしたときに、まだApplicationViewオブジェクトが存在していない場合だけ新しいウィンドウ(アプリビュー)を作成するようにする。これで冒頭に挙げた希望がかなう。
// ApplicationViewオブジェクトを保持しておくコレクション
private Dictionary<string, Windows.UI.ViewManagement.ApplicationView> _viewDictionary
= new Dictionary<string, Windows.UI.ViewManagement.ApplicationView>();
// SecondaryPageのウィンドウ(アプリビュー)を、必要なら作成してから隣に表示する
public async System.Threading.Tasks.Task ShowSecondaryViewAsync(Type page, string param)
{
var viewKey = CreateKeyString(page, param); // このメソッドは下記参照
if (!_viewDictionary.ContainsKey(viewKey))
{
// まだ存在しないウィンドウ(アプリビュー)なので、作成する。
// ここから2a/2bまでは前述のコードと同様
var coreApplicationView
= Windows.ApplicationModel.Core.CoreApplication.CreateNewView();
Windows.UI.ViewManagement.ApplicationView newAppView = null;
await coreApplicationView.Dispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
() =>
{
// 2a. 生成されたApplicationViewを取得する
newAppView = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView();
…… 2b.は省略(前述のコードと同様)……
}
);
// 2c. 生成されたApplicationViewをメモリに保持しておく
_viewDictionary[viewKey] = newAppView;
}
// 3. viewKeyで特定されるウィンドウ(アプリビュー)を隣に表示する
bool success = await Windows.UI.ViewManagement.ApplicationViewSwitcher
.TryShowAsStandaloneAsync(_viewDictionary[viewKey].Id);
}
// Dictionaryに格納するときのキー文字列を生成する
private string CreateKeyString(Type page, string param)
{
……省略……
}
' ApplicationViewオブジェクトを保持しておくコレクション
Private _viewDictionary As Dictionary(Of String, Windows.UI.ViewManagement.ApplicationView) _
= New Dictionary(Of String, Windows.UI.ViewManagement.ApplicationView)()
' SecondaryPageのウィンドウ(アプリビュー)を、必要なら作成してから隣に表示する
Public Async Function ShowSecondaryViewAsync(page As Type, param As String) _
As System.Threading.Tasks.Task
Dim viewKey = CreateKeyString(page, param) ' このメソッドは下記参照
If (Not _viewDictionary.ContainsKey(viewKey)) Then
' まだ存在しないウィンドウ(アプリビュー)なので、作成する。
' ここから2a/2bまでは前述のコードと同様
Dim coreApplicationView _
= Windows.ApplicationModel.Core.CoreApplication.CreateNewView()
Dim newAppView As Windows.UI.ViewManagement.ApplicationView = Nothing
Await coreApplicationView.Dispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
Sub()
' 2a. 生成されたApplicationViewを取得する
newAppView = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView()
…… 2b.は省略(前述のコードと同様)……
End Sub
)
' 2c. 生成されたApplicationViewをメモリに保持しておく
_viewDictionary(viewKey) = newAppView
End If
' 3. viewKeyで特定されるウィンドウ(アプリビュー)を隣に表示する
Dim success As Boolean = Await Windows.UI.ViewManagement.ApplicationViewSwitcher _
.TryShowAsStandaloneAsync(_viewDictionary(viewKey).Id)
End Function
' Dictionaryに格納するときのキー文字列を生成する
Private Function CreateKeyString(page As Type, param As String) As String
……省略……
End Function
その他に、エンドユーザーの利便性を考えるなら前回紹介したようにタイトルバーに文字列を設定して、複数のウィンドウを識別できるようにしておこう。
また、ApplicationViewオブジェクトには「Consolidated」というイベントがある。これはエンドユーザーがそのウィンドウを閉じたときに発生するものだ(タイトルバーの[X]ボタン、または上端から下端までのスライドによって)。
それぞれのウィンドウ(アプリビュー)は、異なるUIスレッドで動作している。そこで、情報伝達には主にイベントを利用することになる。
例えば「App」クラスに次のコードのようなイベントを用意しておく。
// 各ウィンドウ(アプリビュー)にメッセージを伝えるためのイベント
public event Action<string> MessageEvent;
' 各ウィンドウ(アプリビュー)にメッセージを伝えるためのイベント
Public Event MessageEvent(msg As String)
それぞれの画面では、初期化時に上のイベントにハンドラーを結び付けて情報を受け取る(次のコード)。
private Windows.UI.Core.CoreDispatcher _currentDispatcher;
// コンストラクター
public SecondaryPage()
{
……省略……
_currentDispatcher = Window.Current.Dispatcher;
App.CurrentApp.MessageEvent += App_MessageEvent;
}
private async void App_MessageEvent(string msg)
{
try
{
// このイベントハンドラーは別のスレッドから呼び出されるので、
// 画面作成時に保持しておいたディスパッチャーを使って、この画面のUIスレッドで実行する
await _currentDispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
() =>
{
this.MessageTextBlock.Text = msg;
});
}
catch { }
}
Private _currentDispatcher As Windows.UI.Core.CoreDispatcher
' コンストラクター
Public Sub New()
……省略……
_currentDispatcher = Window.Current.Dispatcher
AddHandler App.CurrentApp.MessageEvent, AddressOf App_MessageEvent
End Sub
Private Async Sub App_MessageEvent(msg As String)
Try
' このイベントハンドラーは別のスレッドから呼び出されるので、
' 画面作成時に保持しておいたディスパッチャーを使って、この画面のUIスレッドで実行する
Await _currentDispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
Sub()
Me.MessageTextBlock.Text = msg
End Sub
)
Catch ex As Exception
End Try
End Sub
これで、「App」クラス内から「MessageEvent」イベントを発火してやれば、表示されている全ての画面に情報が伝達される。
以上の内容が別途公開のサンプルコードに全て実装されている(次の画像)。
WindowオブジェクトとApplicationViewオブジェクトの関係と、それらが同時に(しかも間接的に)生成されることが理解できれば、複数のウィンドウ(アプリビュー)を表示することは意外と簡単だ。ただし、ウィンドウ(アプリビュー)ごとにUIスレッドが異なる点には要注意である。
複数のウィンドウ(アプリビュー)表示については、次のドキュメントも参照してほしい。
本稿で説明しなかったプロジェクターへの表示については、次のドキュメントを参照してほしい。
5月29日(木)〜5月30日(金)、マイクロソフトの最新技術情報(例えば本稿で解説したような内容)を日本語で日本人向けに提供するカンファレンス「de:code」が日本マイクロソフト主催で開催される。このカンファレンスは、米国時間で4月2〜4日に開催された「Build 2014」の内容をベースに、さらに日本向けのプラスアルファを含めたものになる。詳しい内容は(セッション内容は開催日までに決定していくとのこと)、リンク先を参照されたい。
Copyright© Digital Advantage Corp. All Rights Reserved.