for WPF developers
Home Profile Tips 全記事一覧

WPF で画面遷移する方法 1

(2017/05/17 23:41:11 created.)

(2017/05/24 15:13:11 modified.)

唐突に WPF で画面遷移する方法のひとつをここにまとめます。

ここで紹介する方法では、独自の UserControl を使用する方法で、 Content プロパティの値を切り替えると自動的にアニメーションで遷移するものです。

というわけで早速 UserControl 派生の TransitionPanel コントロールを次のように定義します。まずは XAML から。

TransitionPanel.xaml
  1. <UserControl x:Class="SlideContents.Views.TransitionPanel"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
  6.              mc:Ignorable="d" 
  7.              d:DesignWidth="400" d:DesignHeight="600"
  8.              x:Name="root">
  9.     <Grid>
  10.         <ContentControl Content="{Binding ContentA, ElementName=root}">
  11.             <ContentControl.RenderTransform>
  12.                 <TranslateTransform X="{Binding OffsetXA, ElementName=root}" Y="{Binding OffsetYA, ElementName=root}" />
  13.             </ContentControl.RenderTransform>
  14.         </ContentControl>
  15.         <ContentControl Content="{Binding ContentB, ElementName=root}">
  16.             <ContentControl.RenderTransform>
  17.                 <TranslateTransform X="{Binding OffsetXB, ElementName=root}" Y="{Binding OffsetYB, ElementName=root}" />
  18.             </ContentControl.RenderTransform>
  19.         </ContentControl>
  20.     </Grid>
  21. </UserControl>

Grid コントロールに 2 つの ContentControl を持たせていますが、それぞれ RenderTransform にTranslateTransform を指定することで、水平方向に自由に移動できるようにしています。

後でコードビハインドも紹介しますが、このコントロールは DisplayA/DisplayB という遷移状態を持っていて、DisplayA 状態のときは ContentA、DisplayB 状態のときは ContentB を表示するように TranslateTransform を調整します。このとき、両方の TranslateTransform を同時にアニメーションで操作することで 2 つのコンテンツがスライドしながら画面遷移するように見せることができます。

それではコードビハインドです。

TransitionPanel.xaml.cs
  1. namespace SlideContents.Views
  2. {
  3.     using System;
  4.     using System.Windows;
  5.     using System.Windows.Controls;
  6.     using System.Windows.Media.Animation;
  7.  
  8.     /// <summary>
  9.     /// TransitionPanel.xaml の相互作用ロジック
  10.     /// </summary>
  11.     public partial class TransitionPanel : UserControl
  12.     {
  13.         /// <summary>
  14.         /// 新しいインスタンスを生成します。
  15.         /// </summary>
  16.         public TransitionPanel()
  17.         {
  18.             InitializeComponent();
  19.  
  20.             this.Loaded += OnLoaded;
  21.         }
  22.  
  23.         /// <summary>
  24.         /// アニメーションの方向を表します。
  25.         /// </summary>
  26.         public enum TransitDirections
  27.         {
  28.             /// <summary>
  29.             /// 左へ移動します。
  30.             /// </summary>
  31.             ToLeft,
  32.  
  33.             /// <summary>
  34.             /// 右へ移動します。
  35.             /// </summary>
  36.             ToRight,
  37.         }
  38.  
  39.         /// <summary>
  40.         /// 遷移状態を表します。
  41.         /// </summary>
  42.         public enum TransitionStates
  43.         {
  44.             /// <summary>
  45.             /// A が表示されている状態を表します。
  46.             /// </summary>
  47.             DisplayA,
  48.  
  49.             /// <summary>
  50.             /// B が表示されている状態を表します。
  51.             /// </summary>
  52.             DisplayB,
  53.         }
  54.  
  55.         #region Content 依存関係プロパティ
  56.  
  57.         /// <summary>
  58.         /// Content 依存関係プロパティを定義し直します。
  59.         /// </summary>
  60.         public static readonly new DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(TransitionPanel), new UIPropertyMetadata(null, OnConentPropertyChanged));
  61.  
  62.         /// <summary>
  63.         /// コンテンツを取得または設定します。
  64.         /// </summary>
  65.         public new object Content
  66.         {
  67.             get { return GetValue(ContentProperty); }
  68.             set { SetValue(ContentProperty, value); }
  69.         }
  70.  
  71.         #endregion Content 依存関係プロパティ
  72.  
  73.         #region ContentA 依存関係プロパティ
  74.  
  75.         /// <summary>
  76.         /// ContentA 依存関係プロパティのキーを定義します。
  77.         /// </summary>
  78.         private static readonly DependencyPropertyKey ContentAPropertyKey = DependencyProperty.RegisterReadOnly("ContentA", typeof(object), typeof(TransitionPanel), new UIPropertyMetadata(null));
  79.  
  80.         /// <summary>
  81.         /// ContentA 依存関係プロパティを定義します。
  82.         /// </summary>
  83.         public static readonly DependencyProperty ContentAProperty = ContentAPropertyKey.DependencyProperty;
  84.  
  85.         /// <summary>
  86.         /// コンテンツのためのバッファ A を取得します。
  87.         /// </summary>
  88.         public object ContentA
  89.         {
  90.             get { return GetValue(ContentAProperty); }
  91.             private set { SetValue(ContentAPropertyKey, value); }
  92.         }
  93.  
  94.         #endregion ContentA 依存関係プロパティ
  95.  
  96.         #region ContentB 依存関係プロパティ
  97.  
  98.         /// <summary>
  99.         /// ContentB 依存関係プロパティのキーを定義します。
  100.         /// </summary>
  101.         private static readonly DependencyPropertyKey ContentBPropertyKey = DependencyProperty.RegisterReadOnly("ContentB", typeof(object), typeof(TransitionPanel), new UIPropertyMetadata(null));
  102.  
  103.         /// <summary>
  104.         /// ContentB 依存関係プロパティを定義します。
  105.         /// </summary>
  106.         public static readonly DependencyProperty ContentBProperty = ContentBPropertyKey.DependencyProperty;
  107.  
  108.         /// <summary>
  109.         /// コンテンツのためのバッファ B を取得します。
  110.         /// </summary>
  111.         public object ContentB
  112.         {
  113.             get { return GetValue(ContentBProperty); }
  114.             private set { SetValue(ContentBPropertyKey, value); }
  115.         }
  116.  
  117.         #endregion ContentB 依存関係プロパティ
  118.  
  119.         #region State 依存関係プロパティ
  120.  
  121.         /// <summary>
  122.         /// State 依存関係プロパティのキーを定義します。
  123.         /// </summary>
  124.         private static readonly DependencyPropertyKey StatePropertyKey = DependencyProperty.RegisterReadOnly("State", typeof(TransitionStates), typeof(TransitionPanel), new UIPropertyMetadata(TransitionStates.DisplayB));
  125.  
  126.         /// <summary>
  127.         /// State 依存関係プロパティを定義します。
  128.         /// </summary>
  129.         public static readonly DependencyProperty StateProperty = StatePropertyKey.DependencyProperty;
  130.  
  131.         /// <summary>
  132.         /// 遷移状態を取得します。
  133.         /// </summary>
  134.         public TransitionStates State
  135.         {
  136.             get { return (TransitionStates)GetValue(StateProperty); }
  137.             private set { SetValue(StatePropertyKey, value); }
  138.         }
  139.  
  140.         #endregion State 依存関係プロパティ
  141.  
  142.         #region TransitDirection 依存関係プロパティ
  143.  
  144.         /// <summary>
  145.         /// TransitDirection 依存関係プロパティを定義します。
  146.         /// </summary>
  147.         public static readonly DependencyProperty TransitDirectionProperty = DependencyProperty.Register("TransitDirection", typeof(TransitDirections), typeof(TransitionPanel), new UIPropertyMetadata(TransitDirections.ToLeft));
  148.  
  149.         /// <summary>
  150.         /// 画面遷移方向を取得または設定します。
  151.         /// </summary>
  152.         public TransitDirections TransitDirection
  153.         {
  154.             get { return (TransitDirections)GetValue(TransitDirectionProperty); }
  155.             set { SetValue(TransitDirectionProperty, value); }
  156.         }
  157.  
  158.         #endregion TransitDirection 依存関係プロパティ
  159.  
  160.         #region OffsetXA 依存関係プロパティ
  161.  
  162.         /// <summary>
  163.         /// OffsetXA 依存関係プロパティを定義します。
  164.         /// </summary>
  165.         public static readonly DependencyProperty OffsetXAProperty = DependencyProperty.Register("OffsetXA", typeof(double), typeof(TransitionPanel), new UIPropertyMetadata(0.0));
  166.  
  167.         /// <summary>
  168.         /// コンテンツのためのバッファ A の水平方向オフセットを取得または設定します。
  169.         /// </summary>
  170.         public double OffsetXA
  171.         {
  172.             get { return (double)GetValue(OffsetXAProperty); }
  173.             set { SetValue(OffsetXAProperty, value); }
  174.         }
  175.  
  176.         #endregion OffsetXA 依存関係プロパティ
  177.  
  178.         #region OffsetYA 依存関係プロパティ
  179.  
  180.         /// <summary>
  181.         /// OffsetYA 依存関係プロパティを定義します。
  182.         /// </summary>
  183.         public static readonly DependencyProperty OffsetYAProperty = DependencyProperty.Register("OffsetYA", typeof(double), typeof(TransitionPanel), new UIPropertyMetadata(0.0));
  184.  
  185.         /// <summary>
  186.         /// コンテンツのためのバッファ A の垂直方向オフセットを取得または設定します。
  187.         /// </summary>
  188.         public double OffsetYA
  189.         {
  190.             get { return (double)GetValue(OffsetYAProperty); }
  191.             set { SetValue(OffsetYAProperty, value); }
  192.         }
  193.  
  194.         #endregion OffsetYA 依存関係プロパティ
  195.  
  196.         #region OffsetXB 依存関係プロパティ
  197.  
  198.         /// <summary>
  199.         /// OffsetXB 依存関係プロパティを定義します。
  200.         /// </summary>
  201.         public static readonly DependencyProperty OffsetXBProperty = DependencyProperty.Register("OffsetXB", typeof(double), typeof(TransitionPanel), new UIPropertyMetadata(0.0));
  202.  
  203.         /// <summary>
  204.         /// コンテンツのためのバッファ B の水平方向オフセットを取得または設定します。
  205.         /// </summary>
  206.         public double OffsetXB
  207.         {
  208.             get { return (double)GetValue(OffsetXBProperty); }
  209.             set { SetValue(OffsetXBProperty, value); }
  210.         }
  211.  
  212.         #endregion OffsetXB 依存関係プロパティ
  213.  
  214.         #region OffsetYB 依存関係プロパティ
  215.  
  216.         /// <summary>
  217.         /// OffsetYB 依存関係プロパティを定義します。
  218.         /// </summary>
  219.         public static readonly DependencyProperty OffsetYBProperty = DependencyProperty.Register("OffsetYB", typeof(double), typeof(TransitionPanel), new UIPropertyMetadata(0.0));
  220.  
  221.         /// <summary>
  222.         /// コンテンツのためのバッファ B の垂直方向オフセットを取得または設定します。
  223.         /// </summary>
  224.         public double OffsetYB
  225.         {
  226.             get { return (double)GetValue(OffsetYBProperty); }
  227.             set { SetValue(OffsetYBProperty, value); }
  228.         }
  229.  
  230.         #endregion OffsetYB 依存関係プロパティ
  231.  
  232.         #region イベントハンドラ
  233.  
  234.         /// <summary>
  235.         /// Load イベントハンドラ
  236.         /// </summary>
  237.         /// <param name="sender">イベント発行元</param>
  238.         /// <param name="e">イベント引数</param>
  239.         private void OnLoaded(object sender, RoutedEventArgs e)
  240.         {
  241.             var storyboard = new Storyboard();
  242.  
  243.             storyboard.Children = new TimelineCollection()
  244.             {
  245.                 CreateMoveAnimation(TimeZero, TimeZero, this.HorizontalOffset, "OffsetXB"),
  246.             };
  247.  
  248.             storyboard.Begin();
  249.         }
  250.  
  251.         /// <summary>
  252.         /// Content 依存関係プロパティ変更イベントハンドラ
  253.         /// </summary>
  254.         /// <param name="d">イベント発行元</param>
  255.         /// <param name="e">イベント引数</param>
  256.         private static void OnConentPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  257.         {
  258.             var control = d as TransitionPanel;
  259.             if (control.IsInitialized) control.SwapDisplay();
  260.         }
  261.  
  262.         #endregion イベントハンドラ
  263.  
  264.         #region ヘルパ
  265.  
  266.         /// <summary>
  267.         /// コンテンツを入れ替えます。
  268.         /// </summary>
  269.         private void SwapDisplay()
  270.         {
  271.             if (this.State == TransitionStates.DisplayA)
  272.             {
  273.                 this.ContentB = this.Content;
  274.                 this.State = TransitionStates.DisplayB;
  275.             }
  276.             else
  277.             {
  278.                 this.ContentA = this.Content;
  279.                 this.State = TransitionStates.DisplayA;
  280.             }
  281.  
  282.             if ((this.ContentA != null) && (this.ContentB != null))
  283.                 StartAnimation();
  284.         }
  285.  
  286.         /// <summary>
  287.         /// 画面遷移を開始します。
  288.         /// </summary>
  289.         private void StartAnimation()
  290.         {
  291.             var storyboard = this.State == TransitionStates.DisplayA ? CreateAnimationBtoA(this.TransitDirection) : CreateAnimationAtoB(this.TransitDirection);
  292.             storyboard.Begin();
  293.         }
  294.  
  295.         /// <summary>
  296.         /// ContentB から ContentA へ遷移するためのストーリーボードを生成します。
  297.         /// </summary>
  298.         /// <param name="direction">遷移する方向を指定します。</param>
  299.         /// <returns>生成したストーリーボードを返します。</returns>
  300.         private Storyboard CreateAnimationBtoA(TransitDirections direction)
  301.         {
  302.             var storyboard = new Storyboard();
  303.  
  304.             storyboard.Children = direction == TransitDirections.ToLeft ?
  305.             new TimelineCollection()
  306.             {
  307.                 CreateMoveAnimation(TimeZero, TimeZero, this.HorizontalOffset, "OffsetXA"),
  308.                 CreateMoveAnimation(TimeZero, AnimationTime, 0, "OffsetXA"),
  309.                 CreateMoveAnimation(TimeZero, AnimationTime, -this.HorizontalOffset, "OffsetXB"),
  310.             } :
  311.             new TimelineCollection()
  312.             {
  313.                 CreateMoveAnimation(TimeZero, TimeZero, -this.HorizontalOffset, "OffsetXA"),
  314.                 CreateMoveAnimation(TimeZero, AnimationTime, 0, "OffsetXA"),
  315.                 CreateMoveAnimation(TimeZero, AnimationTime, this.HorizontalOffset, "OffsetXB"),
  316.             };
  317.  
  318.             return storyboard;
  319.         }
  320.  
  321.         /// <summary>
  322.         /// ContentA から ContentB へ遷移するためのストーリーボードを生成します。
  323.         /// </summary>
  324.         /// <param name="direction">遷移する方向を指定します。</param>
  325.         /// <returns>生成したストーリーボードを返します。</returns>
  326.         private Storyboard CreateAnimationAtoB(TransitDirections direction)
  327.         {
  328.             var storyboard = new Storyboard();
  329.  
  330.             storyboard.Children = direction == TransitDirections.ToLeft ?
  331.             new TimelineCollection()
  332.             {
  333.                 CreateMoveAnimation(TimeZero, TimeZero, this.HorizontalOffset, "OffsetXB"),
  334.                 CreateMoveAnimation(TimeZero, AnimationTime, 0, "OffsetXB"),
  335.                 CreateMoveAnimation(TimeZero, AnimationTime, -this.HorizontalOffset, "OffsetXA"),
  336.             } :
  337.             new TimelineCollection()
  338.             {
  339.                 CreateMoveAnimation(TimeZero, TimeZero, -this.HorizontalOffset, "OffsetXB"),
  340.                 CreateMoveAnimation(TimeZero, AnimationTime, 0, "OffsetXB"),
  341.                 CreateMoveAnimation(TimeZero, AnimationTime, this.HorizontalOffset, "OffsetXA"),
  342.             };
  343.  
  344.             return storyboard;
  345.         }
  346.  
  347.         /// <summary>
  348.         /// Double 型のプロパティに対するアニメーションを生成します。
  349.         /// </summary>
  350.         /// <param name="beginTime">アニメーションの開始時間を指定します。</param>
  351.         /// <param name="duration">アニメーションの実行時間を指定します。</param>
  352.         /// <param name="to">プロパティ値の最終値を指定します。</param>
  353.         /// <param name="targetPropertyName">対象とするプロパティ名を指定します。</param>
  354.         /// <returns>Storyboard の添付プロパティを設定したアニメーションを返します。</returns>
  355.         private DoubleAnimation CreateMoveAnimation(TimeSpan beginTime, TimeSpan duration, double to, string targetPropertyName)
  356.         {
  357.             var animation = new DoubleAnimation()
  358.             {
  359.                 To = to,
  360.                 BeginTime = beginTime,
  361.                 Duration = new Duration(duration),
  362.                 AccelerationRatio = 0.3,
  363.                 DecelerationRatio = 0.3,
  364.             };
  365.             Storyboard.SetTarget(animation, this);
  366.             Storyboard.SetTargetProperty(animation, new PropertyPath(targetPropertyName));
  367.  
  368.             return animation;
  369.         }
  370.  
  371.         #endregion ヘルパ
  372.  
  373.         #region private フィールド
  374.  
  375.         /// <summary>
  376.         /// 時刻ゼロ
  377.         /// </summary>
  378.         private static readonly TimeSpan TimeZero = TimeSpan.FromMilliseconds(0);
  379.  
  380.         /// <summary>
  381.         /// アニメーション時間
  382.         /// </summary>
  383.         private static readonly TimeSpan AnimationTime = TimeSpan.FromMilliseconds(500);
  384.  
  385.         /// <summary>
  386.         /// 水平方向の遷移量
  387.         /// </summary>
  388.         private double HorizontalOffset { get { return this.ActualWidth + 10; } }
  389.  
  390.         #endregion private フィールド
  391.     }
  392. }

長い…。

Content プロパティが変更されたタイミングで 269 行目の SwapDisplay() メソッドを呼ぶようにします。すると 300 行目の CreateAnimationBtoA() によって生成される ContentB から ContentA へ遷移するアニメーション、または 326 行目の CreateAnimationAtoB() メソッドによって生成される ContentA から ContentB へ遷移するアニメーションが実行されます。

この TransitionPanel を実際に使ってみるとこんな感じになります。

ちなみに、Content プロパティに指定するオブジェクトはなんでもいいですが、ここでの例では表示するコンテンツに対する ViewModel のクラスを指定し、型に対する DataTemplate を App クラスの Resoureces にあらかじめ指定する方法を使っています。長くなってきたのでコードは割愛。