かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Universal Windows Platform appっぽいSplitViewを使った画面の作り方

Windows 10 Insider Preview 10074 + VS2015 RC時点の情報です

ハンバーガーメニューがぱかぱか開いたりするあの画面です。

ヘッダー部の作成

簡単にヘッダー部分をさくっと作ります。

<Page
    x:Class="App17.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App17"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <!-- Header -->
        <RelativePanel>
            <ToggleButton x:Name="toggleButton" 
                          Content="&#xE700;"
                          FontFamily="{ThemeResource SymbolThemeFontFamily}"
                          Background="Transparent"
                          Width="40"
                          Height="40" />
            <Button x:Name="buttonBack" 
                    Style="{ThemeResource NavigationBackButtonNormalStyle}"
                    RelativePanel.RightOf="toggleButton"/>
            <TextBlock Text="My App" Style="{ThemeResource TitleTextBlockStyle}" 
                       Margin="10, 0"
                       RelativePanel.RightOf="buttonBack"
                       RelativePanel.AlignVerticalCenterWith="buttonBack"/>
        </RelativePanel>
        <!-- Body -->
        <SplitView x:Name="splitView" Grid.Row="1">
            
        </SplitView>
    </Grid>
</Page>

ToggleButtonでハンバーガーメニューのボタンを準備します。文字コードは0xE700なので、それを指定しましょう。ハンバーガーメニューのボタンの横には戻るボタンを用意しておきます。そしてその横には、アプリタイトルを表示しています。

SplitViewの作成

メインのコンテンツ部分はSplitViewで作成しています。これからここを作りこんでいきます。

<!-- Body -->
<SplitView x:Name="splitView" Grid.Row="1" IsPaneOpen="{Binding IsChecked, ElementName=toggleButton, Mode=TwoWay}" CompactPaneLength="40">
    <SplitView.Pane>
        <ListView>
            <ListViewItem>
                <StackPanel Orientation="Horizontal">
                    <FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}" Glyph="&#xE701;" FontSize="20" />
                    <TextBlock Text="ItemItemItem" Style="{ThemeResource BodyTextBlockStyle}" Margin="10,0" />
                </StackPanel>
            </ListViewItem>
            <ListViewItem>
                <StackPanel Orientation="Horizontal">
                    <FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}" Glyph="&#xE702;" FontSize="20" />
                    <TextBlock Text="ItemItemItem" Style="{ThemeResource BodyTextBlockStyle}" Margin="10,0" />
                </StackPanel>
            </ListViewItem>
            <ListViewItem>
                <StackPanel Orientation="Horizontal">
                    <FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}" Glyph="&#xE703;" FontSize="20" />
                    <TextBlock Text="ItemItemItem" Style="{ThemeResource BodyTextBlockStyle}" Margin="10,0" />
                </StackPanel>
            </ListViewItem>
        </ListView>
    </SplitView.Pane>
    <Frame x:Name="rootFrame" />
</SplitView>

ListViewに適当なアイテムを3つ置いています。SplitViewは、ここに画面遷移するためのコンテンツを置くためのFrameを置いています。これだけで、大分それっぽくなります。

f:id:okazuki:20150506152337p:plain

サイズに応じた見た目の作成

次に、VisualStateを作りこんでいきます。ルートのGridの下に以下のようなVisualStateを定義します。

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="LayoutVisualStateGroup">
        <!-- 最小幅 -->
        <VisualState x:Name="NarrowLayout">
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="0" />
            </VisualState.StateTriggers>
        </VisualState>
        <!-- 普通 -->
        <VisualState x:Name="NormalLayout">
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="321" />
            </VisualState.StateTriggers>
            <VisualState.Setters>
                <Setter Target="splitView.DisplayMode" Value="CompactOverlay" />
            </VisualState.Setters>
        </VisualState>
        <!-- 広い -->
        <VisualState x:Name="WideLayout">
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="1025" />
            </VisualState.StateTriggers>
            <VisualState.Setters>
                <Setter Target="splitView.DisplayMode" Value="Inline" />
                <Setter Target="toggleButton.IsChecked" Value="True" />
                <Setter Target="toggleButton.Visibility" Value="Collapsed" />
            </VisualState.Setters>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

こうすると、画面の幅に応じてそれっぽい見た目になってくれます。

電話

f:id:okazuki:20150506153604p:plain f:id:okazuki:20150506153631p:plain

パソコンで幅が狭いとき

f:id:okazuki:20150506153747p:plain f:id:okazuki:20150506153802p:plain

パソコンで幅が広いとき

f:id:okazuki:20150506153857p:plain

RootFrameの差し替え

SplitView内に置いたFrameをアプリケーションのルートのFrameにします。App.xaml.csのOnLaunchedを以下のように書き換えます。(BlankPage1.xamlに初期状態で遷移するようにしています)

        /// <summary>
        /// Invoked when the application is launched normally by the end user.  Other entry points
        /// will be used such as when the application is launched to open a specific file.
        /// </summary>
        /// <param name="e">Details about the launch request and process.</param>
        protected override void OnLaunched(LaunchActivatedEventArgs e)
        {

#if DEBUG
            if (System.Diagnostics.Debugger.IsAttached)
            {
                this.DebugSettings.EnableFrameRateCounter = true;
            }
#endif

            var root = Window.Current.Content as MainPage;
            var rootFrame = root?.FindName("rootFrame") as Frame ?? null;

            // Do not repeat app initialization when the Window already has content,
            // just ensure that the window is active
            if (root == null)
            {
                // Create a Frame to act as the navigation context and navigate to the first page
                root = new MainPage();
                rootFrame = root.FindName("rootFrame") as Frame;

                rootFrame.NavigationFailed += OnNavigationFailed;

                if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
                {
                    //TODO: Load state from previously suspended application
                }

                // Place the frame in the current Window
                Window.Current.Content = root;
            }
            

            if (rootFrame.Content == null)
            {
                // When the navigation stack isn't restored navigate to the first page,
                // configuring the new page by passing required information as a navigation
                // parameter
                rootFrame.Navigate(typeof(BlankPage1), e.Arguments);
            }
            // Ensure the current window is active
            Window.Current.Activate();
        }

仕上げ

最後にSplitViewのPaneのListViewのListViewItemをクリックしたら画面遷移するようにします。

普通のBehaviorのNavigateToPageActionだと動かないので、自前の画面遷移のアクションを定義します。

using Microsoft.Xaml.Interactivity;
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App17
{
    public class CustomNavigateToPageAction : DependencyObject, IAction
    {
        public string Page
        {
            get { return (string)GetValue(PageProperty); }
            set { SetValue(PageProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Page.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PageProperty =
            DependencyProperty.Register("Page", typeof(string), typeof(CustomNavigateToPageAction), new PropertyMetadata(null));

        public object Execute(object sender, object parameter)
        {
            var root = Window.Current.Content as MainPage;
            var frame = root?.FindName("rootFrame") as Frame;
            frame?.Navigate(Type.GetType(this.Page));
            return null;
        }
    }
}

あとは、ListViewItemをクリックしたときにこのActionが呼ばれるようにすればOK。全体のXAMLは以下のようになります。

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App17"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
    x:Class="App17.MainPage"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="LayoutVisualStateGroup">
                <!-- 最小幅 -->
                <VisualState x:Name="NarrowLayout">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="0" />
                    </VisualState.StateTriggers>
                </VisualState>
                <!-- 普通 -->
                <VisualState x:Name="NormalLayout">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="321" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="splitView.DisplayMode" Value="CompactOverlay" />
                    </VisualState.Setters>
                </VisualState>
                <!-- 広い -->
                <VisualState x:Name="WideLayout">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="1025" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="splitView.DisplayMode" Value="Inline" />
                        <Setter Target="toggleButton.IsChecked" Value="True" />
                        <Setter Target="toggleButton.Visibility" Value="Collapsed" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <!-- Header -->
        <RelativePanel>
            <ToggleButton x:Name="toggleButton" 
                Content="&#xE700;"
                FontFamily="{ThemeResource SymbolThemeFontFamily}"
                Background="Transparent"
                Width="40"
                Height="40" />
            <Button x:Name="buttonBack" 
                Style="{ThemeResource NavigationBackButtonNormalStyle}"
                RelativePanel.RightOf="toggleButton" />
            <TextBlock Text="My App" Style="{ThemeResource TitleTextBlockStyle}" 
                Margin="10, 0"
                RelativePanel.RightOf="buttonBack"
                RelativePanel.AlignVerticalCenterWith="buttonBack"/>
        </RelativePanel>
        <!-- Body -->
        <SplitView x:Name="splitView" Grid.Row="1" IsPaneOpen="{Binding IsChecked, ElementName=toggleButton, Mode=TwoWay}" CompactPaneLength="40">
            <SplitView.Pane>
                <ListView>
                    <ListViewItem>
                        <Interactivity:Interaction.Behaviors>
                            <Core:EventTriggerBehavior EventName="Tapped">
                                <local:CustomNavigateToPageAction Page="App17.BlankPage1"/>
                            </Core:EventTriggerBehavior>
                        </Interactivity:Interaction.Behaviors>
                        <StackPanel Orientation="Horizontal">
                            <FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}" Glyph="&#xE701;" FontSize="20" />
                            <TextBlock Text="ItemItemItem" Style="{ThemeResource BodyTextBlockStyle}" Margin="10,0" />
                        </StackPanel>
                    </ListViewItem>
                    <ListViewItem>
                        <Interactivity:Interaction.Behaviors>
                            <Core:EventTriggerBehavior EventName="Tapped">
                                <local:CustomNavigateToPageAction Page="App17.BlankPage2"/>
                            </Core:EventTriggerBehavior>
                        </Interactivity:Interaction.Behaviors>
                        <StackPanel Orientation="Horizontal">
                            <FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}" Glyph="&#xE702;" FontSize="20" />
                            <TextBlock Text="ItemItemItem" Style="{ThemeResource BodyTextBlockStyle}" Margin="10,0" />
                        </StackPanel>
                    </ListViewItem>
                    <ListViewItem>
                        <StackPanel Orientation="Horizontal">
                            <Interactivity:Interaction.Behaviors>
                                <Core:EventTriggerBehavior EventName="Tapped">
                                    <local:CustomNavigateToPageAction Page="App17.BlankPage3"/>
                                </Core:EventTriggerBehavior>
                            </Interactivity:Interaction.Behaviors>
                            <FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}" Glyph="&#xE703;" FontSize="20" />
                            <TextBlock Text="ItemItemItem" Style="{ThemeResource BodyTextBlockStyle}" Margin="10,0" />
                        </StackPanel>
                    </ListViewItem>
                </ListView>
            </SplitView.Pane>
            <Frame x:Name="rootFrame" />
        </SplitView>
    </Grid>
</Page>