KatsuYuzuのブログ

.NET の開発をメインとした日記です。

ユニバーサルWindowsアプリ「さまざまなウィンドウ サイズに対応する」 #win8dev_jp #wpdev_jp

f:id:KatsuYuzu:20140818021847j:plain:w200 f:id:KatsuYuzu:20140818021853j:plain:w200
ユニバーサルWindowsアプリでさまざまなウィンドウ サイズに対応するためのまとめです。
サンプルはGithubからダウンロードしてください。

さまざまなウィンドウ サイズに適したレイアウト

必要になるレイアウトは3つです。

  1. 通常の横長のレイアウト
  2. 画面を分割した時や端末を縦に持った時の縦長のレイアウト
  3. マニフェストで最小幅320pxを指定した時の狭い幅のレイアウト

f:id:KatsuYuzu:20140818022529p:plain:w150 f:id:KatsuYuzu:20140818022635p:plain:w150 f:id:KatsuYuzu:20140818022642p:plain:w150
ウィンドウ サイズが変化した時に、現在の幅に合わせて画面のレイアウトを切り替えます。ここでは、レイアウトの切り替えはVisualStateManagerを使うこととします。

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="LayoutStateGroup">
            <VisualState x:Name="LandscapeState" />
            <VisualState x:Name="MinimalState">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextBlock.Text)"
                                                   Storyboard.TargetName="textBlock">
                        <DiscreteObjectKeyFrame KeyTime="0"
                                                Value="幅の狭いレイアウト" />
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
            <VisualState x:Name="PortraitState">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextBlock.Text)"
                                                   Storyboard.TargetName="textBlock">
                        <DiscreteObjectKeyFrame KeyTime="0"
                                                Value="縦長のレイアウト" />
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    
    <TextBlock x:Name="textBlock"
               Style="{StaticResource HeaderTextBlockStyle}"
               Text="横長のレイアウト"
               Margin="10"
               VerticalAlignment="Center"
               HorizontalAlignment="Center" />
</Grid>

ウィンドウ サイズの変化を購読する

ウィンドウ サイズの変化を購読します。

// VisualState を切り替えるために、購読は各ページで行います

Window.Current.SizeChanged += OnSizeChanged;
void OnSizeChanged(object sender, WindowSizeChangedEventArgs e)
{
    this.GoToState(e.Size.Width, e.Size.Height);
}

また、ページのロード時はウィンドウ サイズの変化が発生しないため、ロード時のレイアウトを切り替えるにはページのロードも購読します。

page.Loaded += OnLoaded;
void OnLoaded(object sender, RoutedEventArgs e)
{
    var rect = Window.Current.Bounds;
    this.GoToState(rect.Width, rect.Height);
}

ウィンドウ サイズによってレイアウトを切り替える

ウィンドウの幅と高さから適切なレイアウトを選択して切り替えます。

void GoToState(double width, double height)
{
    string stateName;

    if (width < 500)
    {
        // Windows PhoneアプリではWindowsストアアプリに比べて幅が狭くなるので、
        // この対応をしておくことで2つのレイアウトで済みます。

        // 狭い幅のレイアウトを指定していない場合は、縦長のレイアウトとする。
        stateName = this.MinimalLayoutState ?? this.PortraitLayoutState;
    }
    else if (width < height)
    {
        stateName = this.PortraitLayoutState;
    }
    else
    {
        stateName = this.LandscapeLayoutState;
    }

    VisualStateManager.GoToState(this.resolvedSource, stateName, true);
}

ビヘイビアー化する

これらの処理はウィンドウ サイズに対応するすべてのページに必要になるため、ビヘイビアー化して再利用します。
ビヘイビアーを作成するには、DependencyObjectIBehaviorの2つを継承して、IBehavior.Attach/Detachで購読と解除を行います。その他に、レイアウトを切り替えるための状態名を表すプロパティを作成します。

/// <summary>
/// 横長のレイアウトをあらわす状態名を取得または設定します。
/// </summary>
[CustomPropertyValueEditor(CustomPropertyValueEditor.StateName)]
public string LandscapeLayoutState
{
    get { return (string)GetValue(LandscapeLayoutStateProperty); }
    set { SetValue(LandscapeLayoutStateProperty, value); }
}

public static readonly DependencyProperty LandscapeLayoutStateProperty =
    DependencyProperty.Register(
        "LandscapeLayoutState",
        typeof(string),
        typeof(LayoutChangeBehavior),
        new PropertyMetadata(null));

ビヘイビアー化することで、ページ毎の処理は不要になり、簡単にさまざまなウィンドウ サイズに対応することができます。

<Interactivity:Interaction.Behaviors>
    <local:LayoutChangeBehavior LandscapeLayoutState="LandscapeState"
                                MinimalLayoutState="MinimalState"
                                PortraitLayoutState="PortraitState" />
</Interactivity:Interaction.Behaviors>

このビヘイビアーはNuGetで配布中のライブラリに含まれています。

Windows Phoneアプリの補足

Windows Phoneアプリでは必要となるレイアウトは横長と縦長の2つです。
f:id:KatsuYuzu:20140818022655p:plain:w150 f:id:KatsuYuzu:20140818022649p:plain:h150

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="LayoutStateGroup">
            <VisualState x:Name="LandscapeState">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextBlock.Text)"
                                                   Storyboard.TargetName="textBlock">
                        <DiscreteObjectKeyFrame KeyTime="0"
                                                Value="横長のレイアウト" />
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
            <VisualState x:Name="PortraitState" />
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    
    <TextBlock x:Name="textBlock"
               Style="{StaticResource HeaderTextBlockStyle}"
               Text="縦長のレイアウト"
               Margin="10"
               VerticalAlignment="Center"
               HorizontalAlignment="Center" />
</Grid>

また、レイアウトの切り替えでWindows Phone対応を行っているので、設定も明瞭です。

<Interactivity:Interaction.Behaviors>
    <local:LayoutChangeBehavior LandscapeLayoutState="LandscapeState"
                                PortraitLayoutState="PortraitState" />
</Interactivity:Interaction.Behaviors>