消えないSettingsFlyoutを考える

設定関連のUI要素を載せるものとして便利なSettingsFlyoutですが、 ツイッタークライアントなどでユーザーの情報を表示するようなコントロールとして考えると ウィンドウサイズの変更などで消えてしまい若干不便です。

適当にゴニョゴニョしたところ構造が案外簡単だったので作りました。 デフォルトのStyle.xamlから必要なところを引っ張ってきていじるだけです。

わざわざStyle.xamlから持ってこないでSettingsFlyoutを継承してもできると思います。 今回はWindows10のために今後いじりやすいようにコピペして使ってます。

コード自体はながったらしいので続きから

先に説明

単純にIsLightDismissEnabledを指定するだけだと、SettingsFlyoutの後ろにあるコントロールも触れてしまうので、 透明なGridをまんべんなく敷き詰めています。 コメントになっているgradientのところは、影をつけようと思って書いてみたのですが、 複数のSettingsFlyoutを出すと重くなってしまうのでボツになってます。 SizeChangedにnewがついてるのは、継承元にSizeChangedがあることを知らなかったからです。 別の名前に変えて使ったほうが見のためです。

作り方

<Style TargetType="controls:ExtendedSettingsFlyout">
        <Setter Property="RequestedTheme" Value="Light" />
        <Setter Property="HeaderBackground" Value="{ThemeResource SettingsFlyoutHeaderBackgroundThemeBrush}" />
        <Setter Property="HeaderForeground" Value="{ThemeResource SettingsFlyoutHeaderForegroundThemeBrush}" />
        <Setter Property="Background" Value="{ThemeResource SettingsFlyoutBackgroundThemeBrush}" />
        <Setter Property="BorderThickness" Value="1,0,0,0" />
        <Setter Property="Padding" Value="39,33,40,33" />
        <Setter Property="HorizontalContentAlignment" Value="Left" />
        <Setter Property="VerticalContentAlignment" Value="Top" />
        <Setter Property="IsTabStop" Value="False" />
        <Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled" />
        <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
        <Setter Property="ScrollViewer.VerticalScrollMode" Value="Auto" />
        <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
        <Setter Property="ScrollViewer.ZoomMode" Value="Disabled" />
        <Setter Property="Width" Value="320" />
        <Setter Property="MinWidth" Value="320" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:ExtendedSettingsFlyout">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="#7F000000"
                            BorderThickness="1,0,0,0">
                        <Border.Resources>
                            <Style x:Key="BackButtonStyle" TargetType="Button">
                                <Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
                                <Setter Property="FontSize" Value="39" />
                                <Setter Property="Template">
                                    <Setter.Value>
                                        <ControlTemplate TargetType="Button">
                                            <Grid x:Name="RootGrid">
                                                <Grid Margin="-12,-16,-10,-10" Background="Transparent">
                                                    <Ellipse x:Name="Background"
                                                             Fill="Transparent"
                                                             Stroke="{TemplateBinding Foreground}"
                                                             StrokeThickness="2"
                                                             Height="30"
                                                             Width="30"
                                                             Margin="12,0,0,0"
                                                             HorizontalAlignment="Left" />
                                                    <TextBlock x:Name="NormalGlyph"
                                                               FontWeight="SemiLight"
                                                               Text="&#xE0A6;"
                                                               Margin="10,0,0,0"
                                                               Foreground="{TemplateBinding Foreground}"
                                                               AutomationProperties.AccessibilityView="Raw"/>
                                                    <Ellipse x:Name="PressedBackground"
                                                             Fill="{TemplateBinding Foreground}"
                                                             StrokeThickness="0"
                                                             Height="30"
                                                             Width="30"
                                                             Margin="12,0,0,0"
                                                             HorizontalAlignment="Left"
                                                             Opacity="0" />
                                                    <TextBlock x:Name="PressedGlyph"
                                                               FontWeight="SemiLight"
                                                               Text="&#xE0A6;"
                                                               Margin="10,0,0,0"
                                                               Foreground="{TemplateBinding Background}"
                                                               Opacity="0"
                                                               AutomationProperties.AccessibilityView="Raw"/>
                                                </Grid>
                                                <Rectangle x:Name="FocusVisualWhite"
                                                           Margin="-3,-6,-3,0"
                                                           IsHitTestVisible="False"
                                                           Stroke="{ThemeResource FocusVisualWhiteStrokeThemeBrush}"
                                                           StrokeEndLineCap="Square"
                                                           StrokeDashArray="1,1"
                                                           Opacity="0"
                                                           StrokeDashOffset="1.5" />
                                                <Rectangle x:Name="FocusVisualBlack"
                                                           Margin="-3,-6,-3,0"
                                                           IsHitTestVisible="False"
                                                           Stroke="{ThemeResource FocusVisualBlackStrokeThemeBrush}"
                                                           StrokeEndLineCap="Square"
                                                           StrokeDashArray="1,1"
                                                           Opacity="0"
                                                           StrokeDashOffset="0.5" />
                                                <VisualStateManager.VisualStateGroups>
                                                    <VisualStateGroup x:Name="CommonStates">
                                                        <VisualState x:Name="Normal" />
                                                        <VisualState x:Name="PointerOver">
                                                            <Storyboard>
                                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="Background"
                                                                                               Storyboard.TargetProperty="Fill">
                                                                    <DiscreteObjectKeyFrame Value="{ThemeResource SettingsFlyoutBackButtonPointerOverBackgroundThemeBrush}"
                                                                                            KeyTime="0" />
                                                                </ObjectAnimationUsingKeyFrames>
                                                            </Storyboard>
                                                        </VisualState>
                                                        <VisualState x:Name="Pressed">
                                                            <Storyboard>
                                                                <DoubleAnimation Storyboard.TargetName="PressedBackground"
                                                                                 Storyboard.TargetProperty="Opacity"
                                                                                 To="1"
                                                                                 Duration="0" />
                                                                <DoubleAnimation Storyboard.TargetName="PressedGlyph"
                                                                                 Storyboard.TargetProperty="Opacity"
                                                                                 To="1"
                                                                                 Duration="0" />
                                                            </Storyboard>
                                                        </VisualState>
                                                        <VisualState x:Name="Disabled">
                                                            <Storyboard>
                                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid"
                                                                                               Storyboard.TargetProperty="Visibility">
                                                                    <DiscreteObjectKeyFrame Value="Collapsed" KeyTime="0" />
                                                                </ObjectAnimationUsingKeyFrames>
                                                            </Storyboard>
                                                        </VisualState>
                                                    </VisualStateGroup>
                                                    <VisualStateGroup x:Name="FocusStates">
                                                        <VisualState x:Name="Focused">
                                                            <Storyboard>
                                                                <DoubleAnimation Storyboard.TargetName="FocusVisualWhite"
                                                                                 Storyboard.TargetProperty="Opacity"
                                                                                 To="1"
                                                                                 Duration="0" />
                                                                <DoubleAnimation Storyboard.TargetName="FocusVisualBlack"
                                                                                 Storyboard.TargetProperty="Opacity"
                                                                                 To="1"
                                                                                 Duration="0" />
                                                            </Storyboard>
                                                        </VisualState>
                                                        <VisualState x:Name="Unfocused" />
                                                        <VisualState x:Name="PointerFocused" />
                                                    </VisualStateGroup>
                                                    <VisualStateGroup x:Name="SettingsEdgeLocationStates">
                                                        <VisualState x:Name="Right" />
                                                        <VisualState x:Name="Left">
                                                            <Storyboard>
                                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="NormalGlyph"
                                                                                               Storyboard.TargetProperty="Text">
                                                                    <DiscreteObjectKeyFrame Value="&#xE0AB;" KeyTime="0" />
                                                                </ObjectAnimationUsingKeyFrames>
                                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="PressedGlyph"
                                                                                               Storyboard.TargetProperty="Text">
                                                                    <DiscreteObjectKeyFrame Value="&#xE0AB;" KeyTime="0" />
                                                                </ObjectAnimationUsingKeyFrames>
                                                            </Storyboard>
                                                        </VisualState>
                                                    </VisualStateGroup>
                                                </VisualStateManager.VisualStateGroups>
                                            </Grid>
                                        </ControlTemplate>
                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </Border.Resources>
                        <Border.Transitions>
                            <TransitionCollection>
                                <EdgeUIThemeTransition Edge="Right" />
                            </TransitionCollection>
                        </Border.Transitions>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="80" />
                                <RowDefinition Height="*" />
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>
                            <Grid x:Name="Header"
                                  Background="{TemplateBinding HeaderBackground}">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="*" />
                                    <ColumnDefinition Width="Auto" />
                                </Grid.ColumnDefinitions>
                                <Button x:Name="BackButton"
                                        Foreground="{TemplateBinding HeaderForeground}"
                                        Background="{TemplateBinding HeaderBackground}"
                                        Margin="39,0,0,12"
                                        Height="30"
                                        Width="30"
                                        Style="{StaticResource BackButtonStyle}"
                                        VerticalAlignment="Bottom" />
                                <TextBlock Text="{TemplateBinding Title}"
                                           Foreground="{TemplateBinding HeaderForeground}"
                                           FontFamily="{ThemeResource SettingsFlyoutHeaderThemeFontFamily}"
                                           FontWeight="SemiLight"
                                           FontSize="{ThemeResource SettingsFlyoutHeaderThemeFontSize}"
                                           TextTrimming="CharacterEllipsis"
                                           Grid.Column="1"
                                           VerticalAlignment="Bottom"
                                           Margin="10,0,0,13" />
                                <Image Height="30"
                                       Width="30"
                                       Source="{TemplateBinding IconSource}"
                                       Grid.Column="2"
                                       Margin="0,0,40,15"
                                       VerticalAlignment="Bottom" />
                            </Grid>
                            <ScrollViewer x:Name="ScrollViewer"
                                          Grid.Row="1"
                                          HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
                                          HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
                                          VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
                                          VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
                                          ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}"
                                          AutomationProperties.AccessibilityView="Raw">
                                <ContentPresenter x:Name="ContentPresenter"
                                                  Content="{TemplateBinding Content}"
                                                  ContentTemplate="{TemplateBinding ContentTemplate}"
                                                  ContentTransitions="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ContentTransitions}"
                                                  Margin="{TemplateBinding Padding}"
                                                  HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                                  VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                                <ScrollViewer.Transitions>
                                    <TransitionCollection>
                                        <EntranceThemeTransition FromHorizontalOffset="128" />
                                    </TransitionCollection>
                                </ScrollViewer.Transitions>
                            </ScrollViewer>
                            <Rectangle x:Name="InputPanePlaceholder" Grid.Row="2" />
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
  • ExtendedSettingsFlyout.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Documents;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;

// テンプレート コントロールのアイテム テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=234235 を参照してください

namespace Flantter.Cascade.Views.Controls
{
    public class ExtendedSettingsFlyout : ContentControl
    {
        public string Title
        {
            get { return (string)GetValue(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }
        public static readonly DependencyProperty TitleProperty =
            DependencyProperty.Register("Title", typeof(string), typeof(ExtendedSettingsFlyout), new PropertyMetadata("Title"));

        public Brush HeaderBackground
        {
            get { return (Brush)GetValue(HeaderBackgroundProperty); }
            set { SetValue(HeaderBackgroundProperty, value); }
        }
        public static readonly DependencyProperty HeaderBackgroundProperty =
            DependencyProperty.Register("HeaderBackground", typeof(Brush), typeof(ExtendedSettingsFlyout), new PropertyMetadata(new SolidColorBrush(Windows.UI.Color.FromArgb(255, 255, 255, 255))));

        public Brush HeaderForeground
        {
            get { return (Brush)GetValue(HeaderForegroundProperty); }
            set { SetValue(HeaderForegroundProperty, value); }
        }
        public static readonly DependencyProperty HeaderForegroundProperty =
            DependencyProperty.Register("HeaderForeground", typeof(Brush), typeof(ExtendedSettingsFlyout), new PropertyMetadata(new SolidColorBrush(Windows.UI.Color.FromArgb(255, 255, 255, 255))));

        public ImageSource IconSource
        {
            get { return (ImageSource)GetValue(IconSourceProperty); }
            set { SetValue(IconSourceProperty, value); }
        }
        public static readonly DependencyProperty IconSourceProperty =
            DependencyProperty.Register("IconSource", typeof(ImageSource), typeof(ExtendedSettingsFlyout), new PropertyMetadata(null));

        public bool IsLightDismissEnabled
        {
            get { return (bool)GetValue(IsLightDismissEnabledProperty); }
            set { SetValue(IsLightDismissEnabledProperty, value); }
        }
        public static readonly DependencyProperty IsLightDismissEnabledProperty =
            DependencyProperty.Register("IsLightDismissEnabled", typeof(bool), typeof(ExtendedSettingsFlyout), new PropertyMetadata(false));

        public event BackClickEventHandler BackClick;
        new public event SizeChangedEventHandler SizeChanged;
        
        public ExtendedSettingsFlyout()
        {
            this.DefaultStyleKey = typeof(ExtendedSettingsFlyout);
        }

        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            var backButton = GetTemplateChild("BackButton") as Button;
            backButton.Click += BackButton_Click;
            Window.Current.SizeChanged += Window_SizeChanged;
        }

        private void Window_SizeChanged(object sender, Windows.UI.Core.WindowSizeChangedEventArgs e)
        {
            if (isLightDismissEnabled)
                return;

            this.Height = Window.Current.Bounds.Height;

            if (SizeChanged != null)
                SizeChanged(this, null);

            if (contentPopup == null)
                return;

            contentPopup.HorizontalOffset = 0;
            contentPopup.VerticalOffset = 0;
            var popupGrid = contentPopup.Child as Grid;

            if (popupGrid == null)
                return;

            popupGrid.Width = Window.Current.Bounds.Width;
            popupGrid.Height = Window.Current.Bounds.Height;
        }

        private void BackButton_Click(object sender, RoutedEventArgs e)
        {
            this.Hide();
            var backClickEventArgs = new BackClickEventArgs() { Handled = false };

            if (BackClick != null)
                BackClick(sender, backClickEventArgs);
        }

        public bool IsOpen { get { return (contentPopup != null && contentPopup.IsOpen); } }
        private Popup contentPopup = null;
        private bool isLightDismissEnabled;
        public void Show()
        {
            if (IsLightDismissEnabled)
            {
                isLightDismissEnabled = true;
                this.Visibility = Windows.UI.Xaml.Visibility.Visible;
                this.Height = Window.Current.Bounds.Height;
                this.HorizontalAlignment = Windows.UI.Xaml.HorizontalAlignment.Right;
                this.contentPopup = new Popup
                {
                    Child = this,
                    IsLightDismissEnabled = true,
                    Opacity = 1,
                };

                this.contentPopup.HorizontalOffset = Window.Current.Bounds.Width - this.Width;
                this.contentPopup.VerticalOffset = 0;
                this.contentPopup.IsOpen = true;
            }
            else
            {
                this.Visibility = Windows.UI.Xaml.Visibility.Visible;
                this.Height = Window.Current.Bounds.Height;
                this.HorizontalAlignment = Windows.UI.Xaml.HorizontalAlignment.Right;

                var popupGrid = new Grid() { Background = new SolidColorBrush(Windows.UI.Color.FromArgb(0, 0, 0, 0)), Width = Window.Current.Bounds.Width, Height = Window.Current.Bounds.Height };
                popupGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
                popupGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Auto) });
                var popupTappedGrid = new Grid();
                //var gradient = new LinearGradientBrush();
                //gradient.StartPoint = new Point(1, 0.5);
                //gradient.EndPoint = new Point(0, 0.5);
                //gradient.GradientStops.Add(new GradientStop() { Color = Windows.UI.Color.FromArgb(72, 0, 0, 0), Offset = 0 });
                //gradient.GradientStops.Add(new GradientStop() { Color = Windows.UI.Color.FromArgb(36, 0, 0, 0), Offset = 0.01 });
                //gradient.GradientStops.Add(new GradientStop() { Color = Windows.UI.Color.FromArgb(0, 0, 0, 0), Offset = 0.02 });
                //gradient.GradientStops.Add(new GradientStop() { Color = Windows.UI.Color.FromArgb(0, 0, 0, 0), Offset = 1 });
                //popupTappedGrid.Background = gradient;
                popupTappedGrid.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(0, 0, 0, 0));
                popupTappedGrid.SetValue(Grid.ColumnProperty, 0);
                //popupTappedGrid.Transitions = new TransitionCollection();
                //popupTappedGrid.Transitions.Add(new EdgeUIThemeTransition() { Edge = EdgeTransitionLocation.Right });
                this.SetValue(Grid.ColumnProperty, 1);
                popupGrid.Children.Add(popupTappedGrid);
                popupGrid.Children.Add(this);
                this.contentPopup = new Popup
                {
                    Child = popupGrid,
                    IsLightDismissEnabled = false,
                    Opacity = 1,
                };

                this.contentPopup.HorizontalOffset = 0;
                this.contentPopup.VerticalOffset = 0;
                popupTappedGrid.Tapped += (s, e) => { this.Hide(); e.Handled = true; };
                popupTappedGrid.RightTapped += (s, e) => { this.Hide(); e.Handled = true; };
                this.contentPopup.IsOpen = true;
            }
        }
        public void Hide()
        {
            if (this.contentPopup != null)
            {
                this.contentPopup.IsOpen = false;

                if (this.contentPopup.Child is Grid)
                    (this.contentPopup.Child as Grid).Children.Clear();

                this.contentPopup.Child = null;
            }
                

            this.Visibility = Windows.UI.Xaml.Visibility.Collapsed;

            this.contentPopup = null;
        }
    }
}