Special Windows 10 issue 2015

Volume 30 Number 11

UI デザイン - Windows 10 向けアダプティブ アプリ

Clint Rutkas | Windows 2015

Windows 10 のユニバーサル Windows プラットフォーム (UWP) により、アプリは UWP のコントロールがサポートするさまざまなデバイス ファミリ上で実行し、多種多様な画面サイズやウィンドウ サイズに合わせて表示を自動調整できるようになります。このようなデバイス ファミリは、アプリでのユーザー操作をどのようにサポートするのでしょう。また、実行中のデバイスにアプリをどのように対応させ、適合させるのでしょう。今回は、この 2 つの疑問に答え、マイクロソフトが UWP で提供するツールとリソースを紹介します。こうしたツールとリソースにより、各種デバイスで実行するアプリ向けに複雑なコードを記述してメンテナンスする必要がなくなります。

まず、さまざまなデバイス ファミリに合わせて UI を最適化するレスポンシブ手法から説明します。その後、さらに掘り下げ、デバイスの特定の機能にアプリを適応させる方法を見ていきます。

コントロール、API、コードに関する話を始める前に、ここで言うデバイス ファミリについて触れておきます。簡潔に言うと、デバイス ファミリとは、特定のフォーム ファクターに当てはまるデバイスのグループです。フォーム ファクターは、IoT デバイス、スマートフォン、タブレット、デスクトップ PC から、Xbox ゲーム機本体、大画面 Surface Hub デバイス、ウェアラブル デバイスまで広範囲にわたります。アプリはこのどのデバイス ファミリでも動作しますが、アプリをデザインするときは、そのアプリが使われるデバイス ファミリを考慮しなくてはなりません。

数多くのデバイス ファミリがありますが、UWP は、アプリを実行するデバイス ファミリとは関係なく、すべてのアプリが UWP の API のうち 85% にアクセスできるようデザインされています。さらに、よく使われる上位 1,000 個のアプリを見てみると、使用している全 API のうち 96.2% を占めているのが基本的なユニバーサル Windows API のセットです。機能の大部分が UWP の一部として存在し、使用可能になっており、各デバイスにはアプリをさらにカスタマイズできるデバイス専用の API が用意されています。

ウィンドウへの回帰

Windows でのアプリの使われ方についての最も大きな変化の 1 つが、再びウィンドウ内で実行されるようになることです。Windows 8 と Windows 8.1 では、アプリを全画面で実行するか、最大 4 つのアプリを同時にサイドバイサイドで実行することができます。これとは対照的に、Windows 10 では、アプリの並べ方、サイズ、位置をユーザーが自由に変えられるようになります。Windows 10 の新しいアプローチにより、ユーザーは UI を柔軟に操作できるようになりますが、開発者にとっては、このアプローチに対応するためにアプリを最適化する作業が必要になります。Windows 10 では XAML の機能が強化され、さまざまな方法でレスポンシブ手法をアプリに実装し、画面やウィンドウのサイズに関係なく最適なかたちでアプリを表示できるようになります。ここでは、このうち 3 つのアプローチを取り上げます。

VisualStateManager: Windows 10 では、VisualStateManager クラスが拡張され、XAML ベースのアプリにレスポンシブ デザインを実装する 2 つのメカニズムが導入されています。新しい VisualState.StateTriggers API と VisualState.Setters API は、特定の条件に応じて表示状態を定義できるようにします。組み込みの AdaptiveTrigger を VisualState の StateTrigger に使用し、MinWindowHeight プロパティと MinWindowWidth プロパティを設定すると、アプリ ウィンドウの高さと幅に応じて表示状態が変化します。また、Windows.UI.Xaml.StateTriggerBase を拡張して、デバイス ファミリや入力の種類に応じたトリガーなど、独自のトリガーを作成することもできます。図 1 のコードをご覧ください。

図 1 カスタム状態トリガーの作成

<Page>
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup>
        <VisualState>
          <VisualState.StateTriggers>
          <!-- VisualState to be triggered when window
            width is >=720 effective pixels. -->
            <AdaptiveTrigger MinWindowWidth="720" />
          </VisualState.StateTriggers>
          <VisualState.Setters>
            <Setter Target="myPanel.Orientation"
                    Value="Horizontal" />
          </VisualState.Setters>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <StackPanel x:Name="myPanel" Orientation="Vertical">
      <TextBlock Text="This is a block of text. It is text block 1. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
      <TextBlock Text="This is a block of text. It is text block 2. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
      <TextBlock Text="This is a block of text. It is text block 3. "
                 Style="{ThemeResource BodyTextBlockStyle}"/>
    </StackPanel>
  </Grid>
</Page>

図 1 の例では、既定の状態では 3 つの TextBlock 要素が上下に積み重ねられた状態でページに表示されます。VisualStateManager では、MinWindowWidth を 720 にした AdaptiveTrigger が定義されています。これにより、ウィンドウの幅が有効ピクセルで 720 以上の場合、StackPanel の向きが水平方向に変わります。これにより、ユーザーがウィンドウのサイズを変更した場合、またはスマートフォンやタブレットを縦向きから横向きに変えた場合に、横に広く水平方向の画面領域を利用するようにできます。幅と高さ両方のプロパティを定義すると、アプリが両方の条件を同時に満たした場合にしかトリガーが発生しないことに注意してください。GitHub (wndw.ms/XUneob、英語) で「State triggers」サンプルを操作すると、多数のカスタム トリガーを含め、トリガーを使用するさまざまなシナリオを確認できます。

RelativePanel: 図 1 の例では、StackPanel の Orientation プロパティを変更するのに StateTrigger を使用しています。XAML の多くのコンテナー要素は、StateTrigger と組み合わせることで、さまざまな方法で UI を操作できるようになります。しかし、相互に相対関係にあるレイアウトで要素を配置する複雑なレスポンシブ UI を作成するのは簡単ではありません。そこで用意されたのが、新しい RelativePanel です。RelativePanel を使用すると、要素どうしの空間関係を表現することで要素をレイアウトできます (図 2 参照)。つまり、RelativePanel と AdaptiveTrigger を組み合わせると、使用可能な画面領域に応じて要素が移動するレスポンシブ UI の作成が簡単になります。

図 2 RelativePanel を使った空間関係の表現

<RelativePanel BorderBrush="Gray" BorderThickness="10">
  <Rectangle x:Name="RedRect" Fill="Red" MinHeight="100" MinWidth="100"/>
  <Rectangle x:Name="BlueRect" Fill="Blue" MinHeight="100" MinWidth="100"
             RelativePanel.RightOf="RedRect" />
  <!-- Width is not set on the green and yellow rectangles.
       It's determined by the RelativePanel properties. -->
  <Rectangle x:Name="GreenRect" Fill="Green"
             MinHeight="100" Margin="0,5,0,0"
             RelativePanel.Below="RedRect"
             RelativePanel.AlignLeftWith="RedRect"
             RelativePanel.AlignRightWith="BlueRect"/>
  <Rectangle Fill="Yellow" MinHeight="100"
             RelativePanel.Below="GreenRect"
             RelativePanel.AlignLeftWith="BlueRect"
             RelativePanel.AlignRightWithPanel="True"/>
</RelativePanel>

添付プロパティを使用する構文には、以下に示すように追加のかっこが必要になることに注意してください。

<VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
    <VisualState>
      <VisualState.StateTriggers>
        <AdaptiveTrigger MinWindowWidth="720" />
      </VisualState.StateTriggers>
      <VisualState.Setters>
        <Setter Target="GreenRect.(RelativePanel.RightOf)"
                Value="BlueRect" />
      </VisualState.Setters>
    </VisualState>

GitHub (wndw.ms/cbdL0q、英語) の「Responsiveness techniques」サンプルでは、RelativePanel を使用した他のシナリオを確認できます。

SplitView: アプリのウィンドウ サイズには、アプリ ページに表示されるコンテンツよりも大きな影響力があります。そのため、ナビゲーション要素が、ウィンドウ自体のサイズ変化に対応する必要が生じることがあります。Windows 10 で新しく導入された SplitView コントロールは、通常、アプリ ウィンドウのサイズに応じてさまざまに動作を調整できる最先端のナビゲーション エクスペリエンスを生み出すのに使用します。これが SplitView の一般的なユース ケースの 1 つですが、この用途だけに厳密に制限されているわけではありません。SplitView は、ペインとコンテンツという 2 つの領域に分割されます。

このコントロールのプロパティの多くは、表示を操作するために使用します。まず、DisplayMode によって、利用可能な Overlay、Inline、CompactOverlay、および CompactInline の 4 つのモードの 1 つを使用して、コンテンツ領域と相対にペインをレンダリングする方法を指定します。Inline モード、Overlay モード、および CompactInline モードでアプリをレンダリングした例を、図 3 に示します。

ナビゲーション要素の DisplayMode
図 3 ナビゲーション要素の DisplayMode

PanePlacement プロパティにより、コンテンツ領域の左側 (既定) または右側にペインを表示します。OpenPaneLength プロパティは、要素を最大に広げたときのペインの幅を指定します (既定は有効ピクセル数 320 です)。

SplitView コントロールには、モバイル アプリでよく見られる「ハンバーガー」メニューのように、ペインの状態をユーザーが切り替えられる組み込みの UI 要素はありません。この動作を利用できるようにする場合は、アプリでこの UI 要素を定義し、SplitView の IsPaneOpen プロパティを切り替えるコードを記述しなければなりません。

SplitView に用意されているすべての機能セットについては、GitHub (wndw.ms/qAUVr9、英語) の XAML ナビゲーション メニュー サンプルを確認してください。

[戻る] ボタンの導入

以前のバージョンの Windows Phone 向けにアプリを開発していた方は、すべてのデバイスにハードウェアまたはソフトウェアの [戻る] ボタンが用意されていて、ユーザーがアプリの前の場所に戻ることができることに慣れているかもしれません。しかし、Windows 8 と 8.1 では、この戻る操作用に独自の UI を作成しなければなりませんでした。Windows 10 アプリでは、複数のデバイス ファミリをターゲットにする際に物事を簡単にするため、すべてのユーザーが一貫した戻る操作メカニズムを利用できるようにしています。これにより、アプリの UI 領域をいくぶん広げることができるようになります。

ハードウェアまたはソフトウェアの [戻る] ボタンを用意していないデバイス ファミリ (ノート PC やデスクトップ PC など) でも、アプリでシステムの [戻る] ボタンを使用できるようにするには、SystemNavigationManager クラスの AppViewBackButtonVisibility プロパティを使用します。次のコードに示すように、単純に現在のビューの SystemNavigationManager を取得し、[戻る] ボタンを表示するかどうかを設定します。

SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
  AppViewBackButtonVisibility.Visible;

SystemNavigationManager クラスは、BackRequested イベントも公開します。このイベントは、戻る操作を実行するために、ユーザーがシステム提供のボタン、ジェスチャー、または音声によってコマンドを起動したときに発生します。つまり、この 1 つのイベントを処理すれば、アプリの戻る操作をすべてのデバイス ファミリで一貫した方法で実行できます。

Continuum のメリット

最後に、大事なことを 1 つ取り上げます。筆者の個人的お気に入りでもある Windows 10 の Continuum です。Continuum により、Windows 10 は、ユーザーが希望する操作を望みどおりの方法で実行できるようにエクスペリエンスを調整します。たとえば、アプリがツーインワンの Windows PC で実行される場合、アプリに Continuum を実装すると、ユーザーは自身の生産性を上げるために、タッチを使用するか、マウスとキーボードを使用するかを選択できます。UIViewSettings クラスの UserInteractionMode プロパティにより、ビューの操作にタッチを使用しているか、マウスとキーボードを使用しているかを、たった 1 行のコードで判断できるようになります。

UIViewSettings.GetForCurrentView().UserInteractionMode;
// Returns UserInteractionMode.Mouse or UserInteractionMode.Touch

操作のモードを検出した後、余白の増減や、複雑な機能の表示/非表示などを行って、アプリの UI を最適化します。Lee McPherson が執筆した TechNet 記事「Windows 10 アプリ: Continuum 機能を使用して、カスタム StateTrigger でマウス/キーボード ユーザー向けに UI を変更する」(wndw.ms/y3gB0J、英語) では、新しい StateTriggers と UserInteractionMode を組み合わせて、カスタマイズした独自の Continuum StateTrigger を作成する方法が紹介されています。

アダプティブなアプリ

画面のサイズや向きの変化に反応するアプリは便利ですが、UWP は、魅力的なクロスプラットフォーム機能を実現するため、さらに 2 種類のアダプティブな動作を用意しています。

  • バージョンに対してアダプティブなアプリ: 使用可能な API とリソースを検出することで、UWP のさまざまなバージョンに適応します。たとえば、最新バージョンの UWP を実行しているデバイスにしか存在しない新しい API をアプリで採用し、そのうえで UWP をアップグレードしていないユーザーにもサポートを提供し続けるような状況が考えられます。
  • プラットフォームに対してアダプティブなアプリ: デバイス ファミリそれぞれで利用可能な独特の機能に適応します。つまり、すべてのデバイス ファミリで動作するように作成したアプリでも、スマートフォンなどのモバイル デバイスでアプリを実行する場合は、モバイル特有の API を使用するような場合です。

前述のとおり、Windows 10 では、アプリを実行するデバイスに関係なく、すべてのアプリから UWP API の大多数に完全にアクセスできます。したがって、各デバイス ファミリに関連付けられた特殊な API により、開発者はアプリをさらにカスタマイズできるようになります。

アダプティブ アプリ本来の目的は、アプリで必要な機能をチェックし、使用可能な場合にのみその機能を使用することです。以前は、アプリで OS バージョンをチェックし、そのバージョンに関連付けられた API を呼び出していました。Windows 10 では、現在の OS でクラス、メソッド、プロパティ、イベント、または API コントラクトがサポートされているかどうかを、アプリ実行中にチェックできます。それらがサポートされていれば、アプリから適切な API を呼び出します。Windows.Foundation.Metadata 名前空間にある ApiInformation クラスには、API のクエリに使用するいくつかの静的メソッド (IsApiContractPresent、IsEventPresent、IsMethodPresent など) が用意されています。次に例を示します。

using Windows.Foundation.Metadata;
if(ApiInformation.IsTypePresent("Windows.Media.Playlists.Playlist"))
{
  await myAwesomePlaylist.SaveAsAsync( ... );
}

上記のコードは、2 つのことを行います。Playlist クラスの有無を実行時にチェックしてから、そのクラスのインスタンスの SaveAsAsync メソッドを呼び出しています。また、IsTypePresent API を使用することで、現在の OS に型が存在するかどうかを簡単にチェックできます。以前であれば、このようなチェックには、言語とフレームワークに応じて、LoadLibrary、GetProcAddress、QueryInterface、または Reflection が必要であったり、"dynamic" キーワードなどのキーワードを使用しなければなりませんでした。また、メソッド呼び出しを行う際は、参照を厳密に型指定します。Reflection や "dynamic" を使用すると、メソッド名のスペルを間違えていることなどを知らせる、静的なコンパイル時診断を使用できなくなります。

API コントラクトによる検出

API コントラクトとは、本質的には API のセットです。たとえば、2 つのクラス、5 つのインターフェイス、1 つの構造体、2 つの列挙体などを含む API のセットを表現する仮の API コントラクトを考えることができます。論理的に関連性を持つ型を API コントラクトにグループ化します。いろいろな意味で、API コントラクトは 1 つの機能 (関連性があり、併用することで特定の機能性を提供する API のセット) を表します。Windows 10 以降に組み込まれる各 Windows ランタイム API は、なんらかの API コントラクトのメンバーになります。msdn.com/dn706135 (英語) にあるドキュメントで、使用可能なさまざまな API コントラクトが説明されています。このドキュメントから、ほとんどの API コントラクトが、機能に関して関連性のある API のセットになっていることがわかります。

API コントラクトを使用することで、開発者はなんらかの追加保証を得られます。最も重要なのは、プラットフォームが API コントラクトの中の任意の API を実装する場合は、そのコントラクトのあらゆる API を実装する必要があることです。つまり、API コントラクトは 1 つの原子単位であり、その API コントラクトのサポートをテストすることで、セット内のすべての API に関するサポートをテストしていることになります。アプリでは、API を個別にチェックしなくても、検出した API コントラクトの任意の API でも呼び出すことができます。

最もよく使用されている最大の API コントラクトは、Windows.Foundation.UniversalApiContract です。この API コントラクトには、UWP のほぼすべての API が含まれています。現在の OS が UniversalApiContract をサポートしているかどうかを確認する場合は、以下のコードを記述します。

if (ApiInformation.IsApiContractPresent(
  "Windows.Foundation.UniversalApiContract"), 1, 0)
{
  // All APIs in the UniversalApiContract version 1.0 are available for use
}

現時点では、実在する UniversalApiContract のバージョンがバージョン 1.0 だけであるため、上記のチェックはあまり意味がないように思えます。しかし、今後の Windows 10 更新プログラムで API が追加され、新しいユニバーサル API を含む UniversalApiContract のバージョン 2.0 が導入されることも考えられます。今後、アプリをすべてのデバイスで実行しながら、バージョン 2.0 の新しい API も使用できるようにするには、以下のコードを使用します。

if (ApiInformation.IsApiContractPresent(
  "Windows.Foundation.UniversalApiContract"), 2, 0)
{
  // This device supports all APIs in UniversalApiContract version 2.0
}

アプリがバージョン 2.0 の 1 つのメソッドしか呼び出す必要がなければ、IsMethodPresent を使用して直接メソッドをチェックできます。この場合は、開発者が便利だと考える方のアプローチを使用します。

UniversalApiContract のほかにも API コントラクトはあります。ほとんどの API コントラクトは、すべての Windows 10 プラットフォームに普遍的に存在する API ではなく、1 つまたは複数の特定のデバイス ファミリに存在する 1 つの機能 (API のセット) を表します。前述のように、特定の種類のデバイスをチェックし、API のサポートを推測する必要がなくなります。アプリで使用する API のセットをチェックするだけです。

これで、Playlist クラスの存在だけをチェックするのではなく、Windows.Media.Playlists.PlaylistsContract の存在をチェックするように最初の例を書き直すことができます。

if(ApiInformation.IsApiContractPresent(
  "Windows.Media.Playlists.PlaylistsContract"), 1, 0)
{
  // Now I can use all Playlist APIs
}

すべてのデバイス ファミリに存在するわけではない API を呼び出す必要があるときは、必ず API を定義する適切な拡張機能 SDK への参照を追加します。Visual Studio 2015 では、[参照マネージャー] ダイアログ ボックスにアクセスし、拡張機能タブを開きます。このタブでは、モバイル拡張機能、デスクトップ拡張機能、IoT 拡張機能という、特に重要な 3 つの拡張機能が表示されます。

ただし、アプリで必要な操作は、目的の API コントラクトの存在をチェックして、条件に応じた適切な API を呼び出すことだけです。デバイスの種類を意識する必要はありません。ここで疑問が浮かびます。Playlist API を呼び出す必要がありますが、Playlist API は普遍的に提供される API ではありません。ドキュメント (https://msdn.microsoft.com/ja-jp/library/windows/apps/windows.media.playlists.playlist.aspx) では、クラスが属する API コントラクトを使用するよう指示されています。しかし、そのクラスを定義する拡張機能 SDK はどれでしょう。

結論から言うと、Playlist クラスは (現時点では) デスクトップ デバイスのみで使用でき、モバイル、Xbox、およびその他のデバイス ファミリでは使用できません。したがって、前述のどのコードをコンパイルするよりも前に、Desktop Extension SDK への参照を追加することが必要です。

Visual Studio チームのメンバーで、MSDN マガジンで執筆することもある Lucian Wischik が、便利なツールを作成しました。このツールは、プラットフォーム固有の API を呼び出す際にアプリ コードを分析し、アプリ コードに関連した適性チェックが完了していることを確認します。チェックが完了していない場合は、Ctrl + ピリオド キーを押すか電球をクリックするだけで、警告をレポートし、適切なチェックをコードに挿入する便利な "応急処置" を提供します (詳細については、bit.ly/1JdXTeV (英語) を参照してください)。この分析ツールは NuGet (bit.ly/1KU9ozj) からもインストールできます。

締めくくりとして、アダプティブなコーディングに関する、より完成度の高い例を見ていきます。まず、アダプティブとしては不適切なコードを以下に示します。

// This code will crash if called from IoT or Mobile
async private Task CreatePlaylist()
{
  StorageFolder storageFolder = KnownFolders.MusicLibrary;
  StorageFile pureRockFile = await storageFolder.CreateFileAsync("myJam.mp3");
  Windows.Media.Playlists.Playlist myAwesomePlaylist =
    new Windows.Media.Playlists.Playlist();
  myAwesomePlaylist.Files.Add(pureRockFile);
  // Code will crash here as this is a Desktop-only call
  await myAwesomePlaylist.SaveAsAsync(KnownFolders.MusicLibrary,
    "My Awesome Playlist", NameCollisionOption.ReplaceExisting);
}

次に、同じコードで、メソッドを呼び出す前にターゲット デバイスでオプションの API がサポートされていることを確認する行を追加します。これにより、実行時のクラッシュを防ぐことができます。アプリがデバイスに再生リスト機能が搭載されていないことを検出した場合は、この例にさらに手を加え、CreatePlaylist メソッドを呼び出す UI を表示しないようにすることが必要になるでしょう。

async private Task CreatePlaylist()
{
  StorageFolder storageFolder = KnownFolders.MusicLibrary;
  StorageFile pureRockFile = await storageFolder.CreateFileAsync("myJam.mp3");
  Windows.Media.Playlists.Playlist myAwesomePlaylist =
    new Windows.Media.Playlists.Playlist();
  myAwesomePlaylist.Files.Add(pureRockFile);
  // Now I'm a safe call! Cache this value if this will be queried a lot
  if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent(
    "Windows.Media.Playlists.Playlist"))
  {
      await myAwesomePlaylist.SaveAsAsync(
        KnownFolders.MusicLibrary, "My Awesome Playlist",
        NameCollisionOption.ReplaceExisting);
  }
}

最後に、多くのモバイル デバイスに搭載されている専用のカメラ ボタンにアクセスするコード例を紹介します。

// Note: Cache the value instead of querying it more than once
bool isHardwareButtonsAPIPresent =
  Windows.Foundation.Metadata.ApiInformation.IsTypePresent(
  "Windows.Phone.UI.Input.HardwareButtons");
if (isHardwareButtonsAPIPresent)
{
  Windows.Phone.UI.Input.HardwareButtons.CameraPressed +=
    HardwareButtons_CameraPressed;
}

検出ステップに注目してください。デスクトップ PC の使用時に、HardwareButtons オブジェクトが存在するかどうかをチェックしないで CameraPressed イベントの HardwareButtons オブジェクトを直接参照すると、アプリはクラッシュします。

Windows 10 では、レスポンシブ UI とアダプティブ アプリに関して多数の取り組みが行われています。詳細を知りたい方は、Build 2015 カンファレンス (wndw.ms/IgNy0I、英語) で Brent Rector が API コントラクトについて行ったすばらしい講演内容をチェックしてください。豊富な情報が盛り込まれた、アダプティブ コードに関する Microsoft Virtual Academy のビデオ (bit.ly/1OhZWGs、英語) でも、このトピックについてさらに詳しく説明しているので視聴をお勧めします。


Clint Rutkasは、開発者向けプラットフォームを担当している Windows のシニア プロダクト マネージャーです。これまでに、マイクロソフトで Halo at 343 Industries および Channel 9 に携わり、コンピューターで制御するディスコ ダンス フロア、カスタムのフォード マスタング、T シャツ シューティング ロボットなど、Windows テクノロジを使って数々の奇妙なプロジェクトを立ち上げてきました。

Rajen Kishnaは、ワシントン州レドモンドにあるマイクロソフトで、Windows Platform Developer Marketing チームのシニア プロダクト マーケティング マネージャーを務めています。以前は、オランダのマイクロソフトでコンサルタント兼テクニカル エバンジェリストでした。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Sam Jarawan、Harini Kannan、および Brent Rector に心より感謝いたします。