読者です 読者をやめる 読者になる 読者になる

熊小屋日誌

Windows 10 UWPやXamarin, Python、mbed/NetMF/Arduino/Edison, Azureなどぼちぼちと。たまにPCや勉強会、セミナーなどの話題も

StateTriggersでの画面制御(画面の回転、Continuum・PC)

これは、Windows 10 Mobile / Windows Phone Advent Calendar 2016の7日目の記事です。

StateTriggersでの画面制御

Windows 10 UWPでは、VisualStateManagerでのVisualStateの定義にStateTriggersが追加されて、コントロールがVisualStateの状態になるトリガーを記述することができるようになりました。

StateTriggersのサンプルコードは、画面の幅・高さを扱うAdaptiveTriggerを使った例が多いのですが、他のStateTriggersを作成して組み合わせることで、それ以外の条件に応じてレイアウト変更などの画面制御をXAMLで行うことができます。

既に広く知られた手法かとも思ったのですが、Webを検索しても詳しく書いた例が少ないため、参考として、画面を回転した場合と、ContinuumやPCで表示した場合のそれぞれについて、StateTriggersを使ったXAMLベースでの画面レイアウト変更を行う方法を紹介します。

サンプルアプリの画面仕様

今回、例として作成するアプリの画面仕様は以下の通りです。画面が広いContinuumやPC画面では、2画面を1つにまとめて表示する仕様を想定しました。

  • リストのあるマスター画面と、選択された項目の情報を表示する詳細画面から構成される。
  • マスター画面は、ロゴの画像とリストから構成される。ポートレート表示では縦に並べて、ランドスケープ表示では横に並べる。
  • 詳細画面はテキスト欄から構成される。
  • マスター画面と詳細画面は、電話ではリストの選択と戻るボタンで相互に画面遷移する別画面とする。一方、ContinuumとPCでは1画面に並べて表示する。

f:id:kumar:20161206220648p:plain:w400

画面の実装方針

電話のポートレート表示とランドスケープ表示、そしてContinuum・PC画面の表示を統一的に扱うために、2行×3列のGridを作成して、マスター画面のロゴ画像・リスト、詳細画面のテキスト欄を各セルに並べます。

マスター画面は、電話のポートレート表示では、1列目の1行目・2行目にロゴ画像とリストを配置して、残りは非表示にします。

f:id:kumar:20161206221721p:plain:w300

また、電話のランドスケープ表示では、1行目の1列目・2列目にロゴ画像とリストを配置して、残りは非表示にします。

f:id:kumar:20161206221810p:plain:w300

電話の詳細画面は、3列目の1行目にテキスト欄を表示して、残りは非表示にします。

f:id:kumar:20161206230821p:plain:w300

最後に、Continuum・PC画面では、1列目の1行目・2行目にロゴ画像とリストを配置して、3列目の1行目・2行目にまたがってテキスト欄を表示します。2列目は非表示にします。

f:id:kumar:20161206222559p:plain:w300

これらの画面レイアウトの変更は、電話のポートレート表示・ランドスケープ表示、Continuum・PC画面であることをStateTriggersで判定して、各状態のVisualState.SettersでGridの表示状態を変更することで実現します。

画面制御の実装

サンプルアプリを実装したコードを以下で公開しています。本記事では、ポイントとなる箇所だけ説明します。

github.com

電話のマスター画面(ポートレート表示)

この画面が表示されるのは、以下の条件を満たした場合です。

  • 画面の向きがポートレートである
  • 電話かつ非Continuumで実行されている(*)
    • Windows.System.Profile.AnalyticsInfo.VersionInfo.DeviceFamilyがMobileである
    • UIViewSettings.GetForCurrentView().UserInteractionModeがTouchである
  • リストが選択されていない

(*)電話・PCおよびContinuumの判定方法は高橋さんのブログ記事「UWPをPCとMobileで動かしたときの環境チェック方法 – 高橋 忍のブログ」で紹介されている通り、DeviceFamilyとUserInteractionModeで判定できます。

f:id:kumar:20161207035836p:plain:w300

(高橋さんのブログ記事から引用)

この複合的な条件をVisualState.StateTriggersで実装するのですが、そのときに便利なのが WindowsStateTriggersと呼ばれるパッケージです。

WindowsStateTriggersには、デバイスの状態を判定するトリガーだけでなく、CompositeStateTriggerというAND条件を判定するトリガーも含まれます。これは子要素にトリガーをとり、それらがすべてTrueのときにTrueと判定されます。

これらを使うと、電話のマスター画面(ポートレート表示)を判定するStateTriggersは以下のように書けます。

<VisualState.StateTriggers>
    <wintrigger:CompositeStateTrigger Operator="And">
        <wintrigger:CompositeStateTrigger.StateTriggers>
            <wintrigger:OrientationStateTrigger Orientation="Portrait"/>
            <wintrigger:DeviceFamilyStateTrigger DeviceFamily="Mobile"/>
            <wintrigger:UserInteractionModeTrigger InteractionMode="Touch"/>
            <wintrigger:IsFalseStateTrigger Value="{x:Bind ViewModel.IsItemsListSelected, Mode=OneWay}"/>
        </wintrigger:CompositeStateTrigger.StateTriggers>
    </wintrigger:CompositeStateTrigger>
</VisualState.StateTriggers>

リストが選択されていないことは、ViewModelのIsItemsListSelectedプロパティ(リストの選択状態に連動)がFalseであることをIsFalseStateTriggerを使って判定します。

このトリガーが満たされた時に、Gridの行・列の表示・非表示を設定して、画面要素の行列位置を変更します。電話のマスター画面(ポートレート表示)の場合、2列目・3列目を非表示にしますが、これはColumnDefinitionにx:Name属性を設定して、

<Grid.RowDefinitions>
    <RowDefinition x:Name="Row0" Height="Auto"/>
    <RowDefinition x:Name="Row1" Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
    <ColumnDefinition x:Name="Column0" Width="1*"/>
    <ColumnDefinition x:Name="Column1" Width="1*"/>
    <ColumnDefinition x:Name="Column2" Width="1*"/>
</Grid.ColumnDefinitions>

この名前を使って、行・列のWidthを0に設定することで実現できます。

    <VisualState.Setters>
        <Setter Target="Row0.Height" Value="Auto"/>
        <Setter Target="Row1.Height" Value="*"/>

        <Setter Target="Column0.Width" Value="*"/>
        <Setter Target="Column1.Width" Value="0"/>
        <Setter Target="Column2.Width" Value="0"/>
                        
        <Setter Target="IconImage.(Grid.Column)" Value="0" />
        <Setter Target="IconImage.(Grid.Row)" Value="0" />
        <Setter Target="IconImage.(Grid.ColumnSpan)" Value="1" />
        <Setter Target="IconImage.(Grid.RowSpan)" Value="1" />

        <Setter Target="ItemsList.(Grid.Column)" Value="0" />
        <Setter Target="ItemsList.(Grid.Row)" Value="1" />
        <Setter Target="ItemsList.(Grid.ColumnSpan)" Value="1" />
        <Setter Target="ItemsList.(Grid.RowSpan)" Value="1" />
                        
        <Setter Target="ItemInfo.(Grid.Column)" Value="1"/>
        <Setter Target="ItemInfo.(Grid.Row)" Value="0"/>
        <Setter Target="ItemInfo.(Grid.ColumnSpan)" Value="1"/>
        <Setter Target="ItemInfo.(Grid.RowSpan)" Value="2"/>
    </VisualState.Setters>
</VisualState>   

電話のマスター画面(ランドスケープ表示)

この画面が表示されるのは、以下の条件を満たした場合です。

  • 画面の向きがランドスケープである
  • 電話かつ非Continuumで実行されている
    • Windows.System.Profile.AnalyticsInfo.VersionInfo.DeviceFamilyがMobileである
    • UIViewSettings.GetForCurrentView().UserInteractionModeがTouchである
  • リストが選択されていない

これを記述するStateTriggersは以下の通りです。

<VisualState.StateTriggers>
    <wintrigger:CompositeStateTrigger Operator="And">
        <wintrigger:CompositeStateTrigger.StateTriggers>
            <wintrigger:OrientationStateTrigger Orientation="Landscape"/>
            <wintrigger:DeviceFamilyStateTrigger DeviceFamily="Mobile"/>
            <wintrigger:UserInteractionModeTrigger InteractionMode="Touch"/>
            <wintrigger:IsFalseStateTrigger Value="{x:Bind ViewModel.IsItemsListSelected, Mode=OneWay}"/>
        </wintrigger:CompositeStateTrigger.StateTriggers>
    </wintrigger:CompositeStateTrigger>
</VisualState.StateTriggers>

このトリガーが満たされたときのVisualState.Settersは省略します。

電話の詳細画面

この画面が表示されるのは、以下の条件を満たした場合です。

  • 電話かつ非Continuumで実行されている
    • Windows.System.Profile.AnalyticsInfo.VersionInfo.DeviceFamilyがMobileである
    • UIViewSettings.GetForCurrentView().UserInteractionModeがTouchである
  • リストが選択されている

これを記述するStateTriggersは以下の通りです。リストが選択されていることの判定に使ったIsTrueStateTriggerは、WindowsStateTriggersのIsFalseStateTriggerを修正して作成しています。

<VisualState.StateTriggers>
    <wintrigger:CompositeStateTrigger Operator="And">
        <wintrigger:DeviceFamilyStateTrigger DeviceFamily="Mobile"/>
        <wintrigger:UserInteractionModeTrigger InteractionMode="Touch"/>
        <trigger:IsTrueStateTrigger  Value="{x:Bind ViewModel.IsItemsListSelected, Mode=OneWay}"/>
    </wintrigger:CompositeStateTrigger>
</VisualState.StateTriggers>

このトリガーが満たされたときのVisualState.Settersは省略します。

Continuum・PC画面

電話のContinuum表示、もしくはPC画面であることは、UIViewSettings.GetForCurrentView().UserInteractionModeがMouseであることで判定できます。

<VisualState.StateTriggers>
    <wintrigger:UserInteractionModeTrigger InteractionMode="Mouse"/>
</VisualState.StateTriggers>

このトリガーが満たされたときのVisualState.Settersは省略します。

実行結果

ポートレート表示

f:id:kumar:20161207034155p:plain:w250 f:id:kumar:20161207034202p:plain:w250

ランドスケープ表示

f:id:kumar:20161207034214p:plain:w250 f:id:kumar:20161207034216p:plain:w250

Continuum

f:id:kumar:20161207034314p:plain:w300

PC

f:id:kumar:20161207034452p:plain:w300

注意点

1つのXAMLに全てのコントロールを配置して、動的にその位置と表示・非表示を変更して画面制御を実装したため、戻るキーでの画面遷移に注意する必要があります。

電話で実行した場合、詳細画面で戻るキーが押されたらマスター画面に戻る必要があります。そのため、DeviceGestureServiceのGoBackRequestedイベントハンドラを実装して、画面の状態に応じた処理を行います。

このイベントハンドラを実装するためには、ViewModelでVisualStateGroupのVisualStateを取得する必要があるのですが、ViewModelでViewの状態をどのように取得すればよいのかまだ分かっていません。そのため、UIViewSettings.GetForCurrentView().UserInteractionModeがTouchかMouseか調べることで暫定的に対応しています。ここは今後の改善点としたいと思います。

private void DeviceGestureService_GoBackRequested(object sender, DeviceGestureEventArgs e)
{
    var view = Windows.UI.ViewManagement.UIViewSettings.GetForCurrentView();
    if (view.UserInteractionMode == Windows.UI.ViewManagement.UserInteractionMode.Mouse)
    {
        // PCもしくはContinuumの場合、アプリを終了する
        e.Handled = false;
        e.Cancel = false;
    }
    else
    {
        // 電話で、アイテムが選択されている画面の場合、リスト画面へと戻る
        if (this.IsItemsListSelected)
        {
            this.SelectedIndex = -1;
            e.Handled = true;
            e.Cancel = true;
        }
        else
        {
            e.Handled = false;
            e.Cancel = false;
        }
    }
}

まとめ

VisualStateManagerのStateTriggersを使って、画面の各状態に遷移するトリガーと、各状態でのコントロールの表示を定義することで、XAML上でレイアウト変更などの画面制御を行うこと、そしてこのStateTriggersの記述には、WindowsStateTriggersを利用することで、複合的な条件にも対応できることを見ました。

このように、XAML上で画面制御を定義することで、ViewModelやViewのコードビハインドで画面遷移に関する処理(状態の判定や画面遷移)を書く必要がなくなり、それぞれに書かれるべきコードの記述に専念できます。

Windows 10 Mobile / Windows Phone Advent Calendar 2016

以上がWindows 10 Mobile / Windows Phone Advent Calendar 2016の7日目の記事でした。

明日はpikepikeidさんです。よろしくお願いします。