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>

ユニバーサルWindowsアプリ「共有」 #win8dev_jp #wpdev_jp

f:id:KatsuYuzu:20140801004303j:plain
ユニバーサルWindowsアプリの[共有]機能のまとめです。
過去の記事のまとめになります。それぞれの機能の詳細は過去の記事をご覧ください。

サンプルはGithubからダウンロードしてください。

共有を呼び出す

任意の操作から共有を呼び出します。

DataTransferManager.ShowShareUI();

共有を送信する

共有操作の開始を購読します。

// アプリケーション内で1度のみ購読する、または、画面毎などで適切に購読と解除を行う
DataTransferManager.GetForCurrentView().DataRequested += OnDataRequested;

共有を処理します。

void OnDataRequested(DataTransferManager sender, DataRequestedEventArgs e)
{
    // 必須
    e.Request.Data.Properties.Title = "共有するデータのタイトル";
    e.Request.Data.Properties.Description = "共有するデータの説明";

    // 以下、適宜

    // 共有する文字列
    e.Request.Data.SetText("共有するテキスト!");
}

ファイルなどの時間や処理を伴う共有はデリゲートを通して共有します。

void OnDataRequested(DataTransferManager sender, DataRequestedEventArgs e)
{
    // 必須
    e.Request.Data.Properties.Title = "共有するデータのタイトル";
    e.Request.Data.Properties.Description = "共有するデータの説明";

    // 以下、適宜

    // 共有するファイルの拡張子
    e.Request.Data.Properties.FileTypes.Add(".jpg");

    // 共有するファイルを処理するデリゲート
    e.Request.Data.SetDataProvider(
        StandardDataFormats.StorageItems,
        this.OnDeferredImageRequestedHandler);
}

async void OnDeferredImageRequestedHandler(DataProviderRequest request)
{
    // 非同期処理の開始
    var deferral = request.GetDeferral();
    try
    {
        // ファイルの非同期操作
        var files = await ...

        // 共有するファイル
        request.SetData(files);
    }
    finally
    {
        // 非同期処理の終了
        deferral.Complete();
    }
}

共有を受信する

共有によるアプリケーションのアクティブ化を処理します。

// App.xaml.cs

protected override void OnShareTargetActivated(ShareTargetActivatedEventArgs args)
{
    var rootFrame = new Frame();
    rootFrame.Navigate(typeof(HogePage), args.ShareOperation);
    Window.Current.Content = rootFrame;
    Window.Current.Activate();
}

共有されたデータを処理します。

// HogePage.xaml.cs

protected async override void OnNavigatedTo(NavigationEventArgs e)
{
    var shareOperation = e.Parameter as ShareOperation;
    if (shareOperation == null)
    {
        return;
    }

    // 共有されたデータのタイトルと説明
    var receivedDataTitle = shareOperation.Data.Properties.Title;
    var receivedDataDescription = shareOperation.Data.Properties.Description;

    // StorageItems の共有を確認
    if (shareOperation.Data.Contains(StandardDataFormats.StorageItems))
    {
        // 共有されたデータの取得
        var storageItems= await shareOperation.Data.GetStorageItemsAsync();

        // 以下、適宜
    }
}

ストアアプリで自分のアプリへデータを共有する #win8dev_jp #wpdev_jp

f:id:KatsuYuzu:20140807003450j:plain
これまで、共有の呼び出し、他のアプリへ共有するデータの引き渡しを紹介しました。

今回は他のアプリが共有したデータを自分のアプリで受け取ります。

データの共有を受け取る

共有の受信では、あらかじめ受信できるデータを宣言する必要があります。宣言はマニフェストで行い、宣言をした時点で共有ターゲットのアプリ一覧に表示されます。適切に処理する形式のみ宣言しましょう。

  1. ユニバーサルアプリプロジェクトを準備
  2. マニフェストで共有ターゲットの宣言
  3. App.xaml.csでOnShareTargetActivatedによるアプリの起動を処理
  4. ShareOperationからデータを受信する

ユニバーサルアプリプロジェクトを準備

プロジェクトを作成します。前回と同じものに受信用のページを追加しました。
f:id:KatsuYuzu:20140730234130p:plain:w400
f:id:KatsuYuzu:20140730234433p:plain:h300
f:id:KatsuYuzu:20140807005454p:plain:w400
f:id:KatsuYuzu:20140807031011p:plain:h100

<StackPanel>
    <TextBlock x:Name="receivedDataTitle"></TextBlock>
    <TextBlock x:Name="receivedDataDescription"></TextBlock>
    <Image x:Name="receivedDataImage"></Image>
</StackPanel>

マニフェストで共有ターゲットの宣言

Package.appxmanifestのGUIで[宣言]を開き[使用可能な宣言]の中から[共有ターゲット]を選び、追加します。
f:id:KatsuYuzu:20140807010253p:plain:w400
f:id:KatsuYuzu:20140807010324p:plain
次に受信のための説明を[プロパティ]の[説明の共有]で指定します。説明はWindows Phoneでは表示されません。
f:id:KatsuYuzu:20140807031038p:plain
f:id:KatsuYuzu:20140807031049p:plain
最後にどんなデータを受信できるかを追加して、詳細を指定します。
f:id:KatsuYuzu:20140807011332p:plain

App.xaml.csでOnShareTargetActivatedによるアプリの起動を処理

OnShareTargetActivatedでアプリが起動されます。OnLaunchedと同様にフレームの生成やコンテンツの割り当てを行います。
今回は通常起動時はメインページ、共有起動時は専用のページへ遷移というようにしました。また、遷移後のページで共有データを処理できるようにNavigateに引き渡しています。

protected override void OnShareTargetActivated(ShareTargetActivatedEventArgs args)
{
    var rootFrame = new Frame();
    rootFrame.Navigate(typeof(ReceivedPage), args.ShareOperation);
    Window.Current.Content = rootFrame;
    Window.Current.Activate();
}

同一ページで表現する場合はスナップの画面サイズへの対応をしておくと良いでしょう。

ShareOperationからデータを受信する

遷移後のページではOnNavigatedToで共有データの受信を行います。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    var shareOperation = e.Parameter as ShareOperation;
    if (shareOperation == null)
    {
        return;
    }

    this.receivedDataTitle.Text = shareOperation.Data.Properties.Title;
    this.receivedDataDescription.Text = shareOperation.Data.Properties.Description;
}

タイトルなどの通常のプロパティは普通に参照できますが、共有データはContainsで確かめてから取得する必要があります。

// StorageItems の共有の確認
if (shareOperation.Data.Contains(StandardDataFormats.StorageItems))
{
    // 共有されたアイテムの取得
    IReadOnlyList<IStorageItem> storageItems= await shareOperation.Data.GetStorageItemsAsync();

    // ファイルの抽出
    var file = storageItems
        .OfType<StorageFile>()
        .First();

    // 画像の読み込み
    using (var stream = await file.OpenReadAsync())
    {
        var bitmapImage = new BitmapImage();
        bitmapImage.SetSource(stream);
        this.receivedDataImage.Source = bitmapImage;
    }
}

確かめずに取得して、存在しなかった場合は例外が発生するので気を付けてください。
f:id:KatsuYuzu:20140807014559p:plain

サンプル

記事で紹介した共有の呼び出し、送信、受信を含んだサンプルをGitHubに置きました。
KatsuYuzu/universal-Windows-apps-Sample · GitHub
Windows ストアアプリではサンプル自身からサンプル自身へ共有して動作確認ができます。Windows Phoneアプリでは自身がアプリ一覧に表示されないので他のアプリから共有を呼び出してください。フォトで写真を共有するのが手頃かと思います。

Windows ストアアプリ

f:id:KatsuYuzu:20140807015935j:plain

Windows Phoneアプリ

f:id:KatsuYuzu:20140807024401j:plain:h300
f:id:KatsuYuzu:20140807024423j:plain:h300