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ストアアプリではない)。次の画像のように、複数のウィンドウを開くことができる。
 IEで新しいウィンドウを開く
IEで新しいウィンドウを開く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が作り込んである。そこで上のコードを実行すると次の画像のようになる。
 単純に「SecondaryPage」を新しいウィンドウに開く
単純に「SecondaryPage」を新しいウィンドウに開く上の画像のように新しいウィンドウが増え続けるのは、困る場合もあるだろう。また、コードからウィンドウ(アプリビュー)を切り替えたいこともあるだろう。それには、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.