かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

ちょっとした確認のためのフライアウトを出す部品作成過程

RC版の情報です。

以下古い情報

1つ前の記事では手作り感満載のフライアウトを出してました。これでもまぁいいっちゃいいんだけど、いけてないですよね。ということで部品化!初日は、UserControlにカプセル化してみました。

<UserControl
    x:Class="FlyoutSampleApp.FlyoutWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:FlyoutSampleApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400" 
    DataContext="{Binding Settings, RelativeSource={RelativeSource Mode=Self}}"
    d:DataContext="{d:DesignInstance Type=local:FlyoutWindowSettings, IsDesignTimeCreatable=True}">
    <UserControl.Resources>
        <Style x:Key="ButtonStyle" TargetType="Button">
            <Setter Property="Background" Value="{StaticResource ButtonBackgroundThemeBrush}"/>
            <Setter Property="Foreground" Value="{StaticResource ButtonForegroundThemeBrush}"/>
            <Setter Property="BorderBrush" Value="{StaticResource ButtonBorderThemeBrush}"/>
            <Setter Property="BorderThickness" Value="{StaticResource ButtonBorderThemeThickness}"/>
            <Setter Property="Padding" Value="12,4,12,4"/>
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="FontFamily" Value="{StaticResource ContentControlThemeFontFamily}"/>
            <Setter Property="FontWeight" Value="SemiBold"/>
            <Setter Property="FontSize" Value="{StaticResource ControlContentThemeFontSize}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Grid>
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal"/>
                                    <VisualState x:Name="PointerOver">
                                        <Storyboard>
                                    		<ColorAnimation Duration="0" Storyboard.TargetProperty="(ContentPresenter.Foreground).(SolidColorBrush.Color)" Storyboard.TargetName="ContentPresenter" d:IsOptimized="True" To="White"/>
                                    		<ColorAnimation Duration="0" To="Purple" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" Storyboard.TargetName="Border" d:IsOptimized="True"/>
                                    	</Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Pressed">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="Border">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource ButtonPressedBackgroundThemeBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="Border">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource ButtonDisabledBackgroundThemeBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="Border">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource ButtonDisabledBorderThemeBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="FocusStates">
                                    <VisualState x:Name="Focused">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="FocusVisualWhite"/>
                                            <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="FocusVisualBlack"/>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Unfocused"/>
                                    <VisualState x:Name="PointerFocused"/>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <Border x:Name="Border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="3">
                                <ContentPresenter x:Name="ContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTransitions="{TemplateBinding ContentTransitions}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Foreground="White"/>
                            </Border>
                            <Rectangle x:Name="FocusVisualWhite" IsHitTestVisible="False" Opacity="0" StrokeDashOffset="1.5" StrokeEndLineCap="Square" Stroke="Purple" StrokeDashArray="1,1"/>
                            <Rectangle x:Name="FocusVisualBlack" IsHitTestVisible="False" Opacity="0" StrokeDashOffset="0.5" StrokeEndLineCap="Square" Stroke="{StaticResource FocusVisualBlackStrokeThemeBrush}" StrokeDashArray="1,1"/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

    <Grid Background="White" MinWidth="300">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBlock TextWrapping="Wrap" Text="{Binding Message}" Style="{StaticResource BodyTextStyle}" Margin="5" Foreground="Black"/>
        <ItemsControl Grid.Row="1" ItemsSource="{Binding Commands}" Margin="5">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button Content="{Binding Label}" Foreground="White" BorderBrush="White" Background="Blue" MinWidth="75" Click="ComandButton_Click" Style="{StaticResource ButtonStyle}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</UserControl>

ざっくりとXAMLを作ります。メッセージ出すTextBlockとItemsControlでコマンドに対応するボタン出してるところくらいがポイントかな?コードビハインドはこんな感じです。

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;

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

namespace FlyoutSampleApp
{
    public sealed partial class FlyoutWindow : UserControl
    {
        public FlyoutWindow()
        {
            this.Settings = new FlyoutWindowSettings();
            this.InitializeComponent();

            this.CommandClicked += FlyoutWindow_CommandClicked;
            this.Tapped += (_, e) => e.Handled = true;

            if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
            {
                this.LoadDesignModeData();
            }
        }

        private void FlyoutWindow_CommandClicked(FlyoutWindow sender, IUICommand command)
        {
            this.Cleanup();
        }

        private void Cleanup()
        {
            this.currentPopup.IsOpen = false;
            this.currentPopup = null;
            Window.Current.Activated -= Current_Activated;
        }

        private void Current_Activated(object sender, WindowActivatedEventArgs e)
        {
            if (e.WindowActivationState == CoreWindowActivationState.Deactivated)
            {
                this.Cleanup();
            }
        }


        private void LoadDesignModeData()
        {
            this.Settings.Message = "Design time message";
            this.Settings.Commands.Add(new UICommand("Button1"));
            this.Settings.Commands.Add(new UICommand("Button2"));
            this.Settings.Commands.Add(new UICommand("Button3"));
        }

        public FlyoutWindowSettings Settings { get; private set; }

        private void ComandButton_Click(object sender, RoutedEventArgs e)
        {
            var command = ((FrameworkElement)sender).DataContext as IUICommand;
            if (command == null)
            {
                return;
            }

            if (command.Invoked != null)
            {
                command.Invoked(command);
            }

            var h = this.CommandClicked;
            if (h != null)
            {
                h(this, command);
            }
        }

        public event Action<FlyoutWindow, IUICommand> CommandClicked;

        private Popup currentPopup;

        public async Task Popup(Control target)
        {
            if (this.currentPopup != null)
            {
                this.currentPopup.IsOpen = false;
            }

            this.currentPopup = new Popup
            {
                Width = Window.Current.Bounds.Width,
                Height = Window.Current.Bounds.Height,
                IsOpen = true,
            };

            // 左上に来るように調整
            this.HorizontalAlignment = HorizontalAlignment.Left;
            this.VerticalAlignment = VerticalAlignment.Top;

            var grid = new Grid
            {
                Background = new SolidColorBrush(Colors.Transparent),
                Width = Window.Current.Bounds.Width,
                Height = Window.Current.Bounds.Height,
            };
            grid.Children.Add(this);
            grid.Tapped += (_, __) => currentPopup.IsOpen = false;

            // コントロールの右上の座標を取得
            var gt = target.TransformToVisual(null);
            var p = gt.TransformPoint(new Point());
            p.X += target.ActualWidth;

            // UserControlの表示場所を指定
            var dummyTransform = new TranslateTransform { X = -1000, Y = -1000 };
            this.RenderTransform = dummyTransform;

            // 準備が終わったのでPopupにGridを設定
            currentPopup.Child = grid;
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                var transform = new TranslateTransform
                {
                    X = p.X - this.ActualWidth,
                    Y = p.Y - 20 - this.ActualHeight
                };
                this.RenderTransform = transform;
            });
        }

        private void grid_Tapped(object sender, TappedRoutedEventArgs e)
        {
            this.Cleanup();
        }
    }

    public sealed class FlyoutWindowSettings : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private bool SetProperty<T>(ref T store, T value, [CallerMemberName]string propertyName = null)
        {
            if (Equals(store, value))
            {
                return false;
            }

            store = value;
            var h = this.PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(propertyName));
            }

            return true;
        }

        private string message;

        public string Message
        {
            get { return this.message; }
            set { this.SetProperty(ref this.message, value); }
        }

        private ObservableCollection<IUICommand> commands = new ObservableCollection<IUICommand>();

        public ObservableCollection<IUICommand> Commands
        {
            get { return this.commands; }
            set { this.SetProperty(ref this.commands, value); }
        }

    }

}

コメントもろくにないですが、備忘録のためのコードはっときます。解説は、満足いくものができたら、その時にするとして…。使い方は、いたって簡単。画面右側のAppBarのボタンをクリックしたときに上のほうに出るのにしか対応してないのはご愛嬌。

private async void Button_Click_1(object sender, RoutedEventArgs e)
{
    var flyout = new FlyoutWindow();
    flyout.Settings.Message = "削除してもいいですか";
    flyout.Settings.Commands.Add(new UICommand("削除", async _ =>
    {
        var dialog = new MessageDialog("削除しました");
        await dialog.ShowAsync();
    }));

    await flyout.Popup(sender as Control);
}

これだけのコードでこんな感じに動きます。AppBarのボタンを押すと、フライアウトが出てくる。

フライアウトのボタンをクリックするとUICommandで指定した処理が実行されます。

一歩前進!!というか、正式版ではこういうの簡単にできるようになっててほしい or MS製 Toolkitとかで追加されるかなぁ。