やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

WPFでCarouselPanelを作る

前回、PathListBoxを使って遊んでみましたが、今回もPathListBoxを使ってみます。
iyemon018.hatenablog.com

ルーセルパネル(Carousel Panel)は、左右にコンテンツをスライドするあれです。
まずはサンプルプログラムの動作を見てみます。

f:id:iyemon018:20170216154556g:plain

このように複数のコンテンツを無限にスライドし続けることができます。
以下、サンプルソースです。
必要なアセンブリ名前空間は、前回の記事を参照してください。

MainWindow.xaml

<Window x:Class="CarouselPanelSample01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:ec="http://schemas.microsoft.com/expression/2010/controls"
        xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:local="clr-namespace:CarouselPanelSample01"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="516"
        Height="350"
        mc:Ignorable="d">
    <Grid>
        <ec:PathListBox x:Name="pathListBox"
                        Width="0"
                        Height="0"
                        HorizontalAlignment="Left"
                        VerticalAlignment="Top"
                        WrapItems="True">
            <ec:PathListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="OverridesDefaultStyle" Value="True" />
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                                <Border>
                                    <Grid>
                                        <ContentPresenter Margin="{TemplateBinding Padding}"
                                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                                          SnapsToDevicePixels="True" />
                                    </Grid>
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                    <Style.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter Property="Background" Value="Transparent" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ec:PathListBox.ItemContainerStyle>
            <i:Interaction.Behaviors>
                <local:PathListBoxBehavior />
            </i:Interaction.Behaviors>
            <ec:PathListBox.LayoutPaths>
                <ec:LayoutPath Padding="0" SourceElement="{Binding ElementName=path}" />
            </ec:PathListBox.LayoutPaths>
            <ed:RegularPolygon Width="100"
                               Height="100"
                               Fill="Blue"
                               InnerRadius="0.47211"
                               PointCount="5"
                               Stretch="Fill"
                               Stroke="Black" />
            <ed:RegularPolygon Width="100"
                               Height="100"
                               Fill="#FFFFF300"
                               InnerRadius="0.47211"
                               PointCount="5"
                               Stretch="Fill"
                               Stroke="Black" />
            <ed:RegularPolygon Width="100"
                               Height="100"
                               Fill="#FF17FF00"
                               InnerRadius="0.47211"
                               PointCount="5"
                               Stretch="Fill"
                               Stroke="Black" />
            <ed:RegularPolygon Width="100"
                               Height="100"
                               Fill="#FF00C5FF"
                               InnerRadius="0.47211"
                               PointCount="5"
                               Stretch="Fill"
                               Stroke="Black" />
            <ed:RegularPolygon Width="100"
                               Height="100"
                               Fill="#FF4E00D6"
                               InnerRadius="0.47211"
                               PointCount="5"
                               Stretch="Fill"
                               Stroke="Black" />
            <ed:RegularPolygon Width="100"
                               Height="100"
                               Fill="#FFB900FF"
                               InnerRadius="0.47211"
                               PointCount="5"
                               Stretch="Fill"
                               Stroke="Black" />
            <ed:RegularPolygon Width="100"
                               Height="100"
                               Fill="Red"
                               InnerRadius="0.47211"
                               PointCount="5"
                               Stretch="Fill"
                               Stroke="Black" />
        </ec:PathListBox>
        <Path x:Name="path"
              VerticalAlignment="Top"
              Data="M-50,160 L550,160" />
        <Button x:Name="PreviousButton"
                Width="32"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Click="PreviousButton_OnClick"
                Content="<" />
        <Button x:Name="NextButton"
                Width="32"
                HorizontalAlignment="Right"
                VerticalAlignment="Top"
                Click="NextButton_OnClick"
                Content=">" />
    </Grid>
</Window>

MainWindow.xaml.cs

namespace CarouselPanelSample01
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Windows;

    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        
        private void NextButton_OnClick(object sender, RoutedEventArgs e)
        {
            // 次の要素を選択する。
            var selectedIndex = pathListBox.SelectedIndex;
            if (0 < selectedIndex)
            {
                pathListBox.SelectedIndex--;
            }
            else
            {
                pathListBox.SelectedIndex = pathListBox.Items.Count - 1;
            }
        }

        private void PreviousButton_OnClick(object sender, RoutedEventArgs e)
        {
            // ひとつ前の要素を選択する。
            var selectedIndex = pathListBox.SelectedIndex;
            if (selectedIndex < pathListBox.Items.Count - 1)
            {
                pathListBox.SelectedIndex++;
            }
            else
            {
                pathListBox.SelectedIndex = 0;
            }
        }
    }
}

また、今回はスライドしたときにアニメーションを実施するためのBehaviorも作りました。
このBehaviorのアニメーション部分を変更するだけでも結構グラフィカルな外観になりそうです。

PathLixtBoxBehavior.cs

namespace CarouselPanelSample01
{
    using System;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Interactivity;
    using System.Windows.Media.Animation;
    using Microsoft.Expression.Controls;

    public class PathListBoxBehavior : Behavior<PathListBox>
    {
        /// <summary>
        /// LayoutPath Start の最大値
        /// </summary>
        private const double MaximumLength = 1.0;

        /// <summary>
        /// 前回選択されていたインデックス
        /// </summary>
        private int _previousSelectedIndex;

        /// <summary>
        /// LayoutPath のStart の初期値を設定、または取得します。
        /// </summary>
        public double DefaultStart { get; set; }

        protected override void OnAttached()
        {
            base.OnAttached();

            if (AssociatedObject != null)
            {
                AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged;
            }
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            if (AssociatedObject != null)
            {
                AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged;
            }
        }

        private void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.AddedItems == null || e.AddedItems.Count < 1 || !AssociatedObject.LayoutPaths.Any()) return;

            LayoutPath target = AssociatedObject.LayoutPaths.First();
            int selectedIndex = AssociatedObject.SelectedIndex;
            int itemCount = AssociatedObject.Items.Count;

            // アニメーションする移動量を計算する。
            double from;
            double to = DefaultStart - MaximumLength / itemCount * selectedIndex;
            target.Start = to;

            if (_previousSelectedIndex == 0 && selectedIndex == itemCount - 1)
            {
                from = DefaultStart - MaximumLength;
            }
            else if (_previousSelectedIndex == itemCount - 1 && selectedIndex == 0)
            {
                from = DefaultStart + MaximumLength / itemCount;
            }
            else
            {
                from = target.Start;
            }

            // Storyboardを作成する。
            var frames = new DoubleAnimationUsingKeyFrames();
            frames.KeyFrames.Add(new EasingDoubleKeyFrame(from, KeyTime.FromTimeSpan(TimeSpan.Zero)));
            frames.KeyFrames.Add(new EasingDoubleKeyFrame(to, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(200))));
            Storyboard.SetTarget(frames, target);
            Storyboard.SetTargetProperty(frames, new PropertyPath(LayoutPath.StartProperty));
            frames.Freeze();
            var storyboard = new Storyboard();
            storyboard.Children.Add(frames);
            storyboard.Freeze();

            AssociatedObject.BeginStoryboard(storyboard);
            _previousSelectedIndex = selectedIndex;
        }
    }
}

このようにPathListBoxを使用すると更にグラフィカルな外観を簡単に実装することができます。
今回使用したサンプルはこちら↓↓↓↓↓
github.com