連載
» 2014年04月17日 12時30分 UPDATE

WinRT/Metro TIPS:アプリから複数のウィンドウを表示するには?[Windows 8.1ストアアプリ開発]

Windowsストアは原則として1アプリに1ウィンドウだが、Windows 8.1で複数のウィンドウを表示できるようになった。その実装方法を解説する。

[山本康彦(http://www.bluewatersoft.jp/),BluewaterSoft]
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

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

連載目次

 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」から無償で入手できる。


複数のウィンドウを出すWindowsストアアプリの例

 代表的なものに、Internet Explorer(以降、IE)がある(厳密にはWindowsストアアプリではない)。次の画像のように、複数のウィンドウを開くことができる。

IEで新しいウィンドウを開く IEで新しいウィンドウを開く
この画像のモニターは1920×1080ピクセルであり、横3つのウィンドウに分割している。そのうち2つのウィンドウには、すでにIEが開かれている。
Win 8.1のIEでは、例えばリンク先のページを開くときに新しいウィンドウを作成できる。それには、(1)リンクを長押し/右クリックしてメニューを出し(タッチではアプリバーが、マウスでは画像のようにコンテキストメニューが出てくる)、(2)メニューから[リンクを新しいウィンドウで開く]を選ぶと、(3)画面が分割されて新しくウィンドウが作られ(または、すでに最大限まで分割済みの場合は既存のウィンドウに)リンク先のページが表示される。

複数のウィンドウを扱うための新しいAPI

 Win 8.1では、上で紹介したIEのように複数のウィンドウを表示し、また、アプリからウィンドウ(アプリビュー)*3を切り替えるための新しいAPIが用意された。

  • ウィンドウ(アプリビュー)を作成するAPI

− CoreApplicationクラス(Windows.ApplicationModel.Core名前空間)に新設されたCreateNewViewメソッドで新しいウィンドウ(アプリビュー)を作る。

  • アプリビューの表示を切り替えるAPI

− 新設されたApplicationViewSwitcherクラス(Windows.UI.ViewManagement名前空間)のメソッドを使って、現在のウィンドウの中身を切り替える(SwitchAsyncメソッド)、あるいは、隣接するウィンドウに表示する(TryShowAsStandaloneAsyncメソッド)。

 一般的なコードの流れは、次のようになるだろう。

  • CreateNewViewメソッドで新しいウィンドウ(アプリビュー)を作る
  • 新しいウィンドウ(アプリビュー)にコンテンツ(一般にはFrameコントロール)をセットし、表示したいページにナビゲートする
  • ApplicationViewSwitcherクラスのメソッドで表示を切り替える

 以降で、具体的なコードを紹介していく。

*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つ目のウィンドウを開くには?

 これだけではあまり実用的ではないが、まずは理解のために、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)

2つ目のウィンドウを開くだけのコード(上:C#、下:VB)
コード中でawait/Awaitキーワードを使っているので、イベントハンドラーのシグネチャにはasync/Asyncキーワードが必要だ。
前述した一般的なコードの流れに付けた番号を、コメントにも付けてある(ただし、「2」は「2a」と「2b」に分割)。
なお、単にウィンドウを表示するだけなら、「2a」ではApplicationViewオブジェクトのIdを保持するだけでよい(「3」で必要なのはIdだけであるから)。

 いきなり「Dispatcher.RunAsync」などと出てきて面食らうかもしれないが、前述した一般的なコードの流れのうち2番目の処理は、新しく生成したウィンドウ(アプリビュー)のスレッドで行う必要があるのだ。取得できるApplicationViewオブジェクトとWindowオブジェクトは、コードを実行しているスレッドに結び付いているからだ。

 別途公開しているサンプルコードにはちょっとしたUIが作り込んである。そこで上のコードを実行すると次の画像のようになる。

単純に「SecondaryPage」を新しいウィンドウに開く
単純に「SecondaryPage」を新しいウィンドウに開く
単純に「SecondaryPage」を新しいウィンドウに開く 単純に「SecondaryPage」を新しいウィンドウに開く
上:2つ目のウィンドウを開いたところ。左側が「MainPage」、右側が「SecondaryPage」。引数が1つのTryShowAsStandaloneAsyncメソッドは、このように画面を等分割して新しいウィンドウを開く。
中:それぞれのウィンドウは独立しているから、それぞれ違うアプリに切り替えられる。左側はIE、右側が本サンプル。
下:前述のコードは、単純に「SecondaryPage」を新しいウィンドウに開くだけであるから、実行するたびに「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

同じウィンドウは開かないようにするコード(上:C#、下:VB)
省略したCreateKeyStringメソッドでは、引数の組み合わせごとに異なる文字列を返すように実装する。すると、「MainPage」から上記ShowSecondaryViewAsyncメソッドを2回以上呼び出すとき、引数が同じなら既存のウィンドウ(アプリビュー)に切り替わるだけとなり、また、引数が以前と違っていれば新しいウィンドウ(アプリビュー)が作られて表示されることになる。

 その他に、エンドユーザーの利便性を考えるなら前回紹介したようにタイトルバーに文字列を設定して、複数のウィンドウを識別できるようにしておこう。

 また、ApplicationViewオブジェクトには「Consolidated」というイベントがある。これはエンドユーザーがそのウィンドウを閉じたときに発生するものだ(タイトルバーの[X]ボタン、または上端から下端までのスライドによって)。

複数のウィンドウに情報を伝達するには?

 それぞれのウィンドウ(アプリビュー)は、異なるUIスレッドで動作している。そこで、情報伝達には主にイベントを利用することになる。

 例えば「App」クラスに次のコードのようなイベントを用意しておく。

// 各ウィンドウ(アプリビュー)にメッセージを伝えるためのイベント
public event Action<string> MessageEvent;

' 各ウィンドウ(アプリビュー)にメッセージを伝えるためのイベント
Public Event MessageEvent(msg As String)

「App」クラスにイベントを用意する(上:C#、下:VB)

 それぞれの画面では、初期化時に上のイベントにハンドラーを結び付けて情報を受け取る(次のコード)。

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」クラスのイベントにハンドラーを結び付けて情報を受け取る(上:C#、下:VB)
ここでは、イベントで受け取った文字列を、画面に配置したTextBlockコントロール(名前は「MessageTextBlock」)に表示している。

 これで、「App」クラス内から「MessageEvent」イベントを発火してやれば、表示されている全ての画面に情報が伝達される。

 以上の内容が別途公開のサンプルコードに全て実装されている(次の画像)。

別途公開のサンプルコードを実行しているところ 別途公開のサンプルコードを実行しているところ
この画像のモニターは1920×1080ピクセルである。
「SecondaryPage」のウィンドウは最大2つまで表示できる。それぞれ「2nd Window」/「3rd Window」とウィンドウ中央に表示されている。ウィンドウを開く/閉じるごとに、全てのウィンドウに共通のメッセージが表示される。タイトルバーには、ウィンドウごとに異なる文字列が設定されている。

まとめ

 WindowオブジェクトとApplicationViewオブジェクトの関係と、それらが同時に(しかも間接的に)生成されることが理解できれば、複数のウィンドウ(アプリビュー)を表示することは意外と簡単だ。ただし、ウィンドウ(アプリビュー)ごとにUIスレッドが異なる点には要注意である。

 複数のウィンドウ(アプリビュー)表示については、次のドキュメントも参照してほしい。

 本稿で説明しなかったプロジェクターへの表示については、次のドキュメントを参照してほしい。

Windows 8.1を扱う大規模カンファレンスのご紹介

 5月29日(木)〜5月30日(金)、マイクロソフトの最新技術情報(例えば本稿で解説したような内容)を日本語で日本人向けに提供するカンファレンス「de:code」が日本マイクロソフト主催で開催される。このカンファレンスは、米国時間で4月2〜4日に開催された「Build 2014」の内容をベースに、さらに日本向けのプラスアルファを含めたものになる。詳しい内容は(セッション内容は開催日までに決定していくとのこと)、リンク先を参照されたい。


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

WinRT/Metro TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

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

メールマガジン登録

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