for WPF developers
Home Profile Tips 全記事一覧

WPF で画面遷移する方法 1 についての考察

(2017/05/18 9:35:22 created.)

(2017/05/24 16:50:21 modified.)

前回では WPF で画面遷移する方法を UserControl 派生の TransitionPanel を使って実現しました。また、コンテンツの指定方法は DataTemplate を利用した方法を使いました。ここではその具体的なコードについて紹介し、ちょっとした特徴をまとめます。

まずはプロジェクトのファイル構造から。

Views フォルダに MainView を始め、前回作成した TransitionPanel と、コンテンツとして表示する Content01View、Content02View、Content03View というユーザコントロールがあります。

ViewModel フォルダにはそれぞれの View に対応した ViewModel があります。ただし、Content01ViewModel、Content02ViewModel、Content03ViewModel は ViewModelBase クラスの派生クラスとしています。

App.xaml では次のように View と ViewModel を紐付けるための DataTemplate をリソースとして定義しています。

App.xaml
  1. <Application x:Class="WpfApplication1.App"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:vw="clr-namespace:WpfApplication1.Views"
  5.              xmlns:vm="clr-namespace:WpfApplication1.ViewModels">
  6.     <Application.Resources>
  7.         <DataTemplate DataType="{x:Type vm:Content01ViewModel}">
  8.             <vw:Content01View />
  9.         </DataTemplate>
  10.         <DataTemplate DataType="{x:Type vm:Content02ViewModel}">
  11.             <vw:Content02View />
  12.         </DataTemplate>
  13.         <DataTemplate DataType="{x:Type vm:Content03ViewModel}">
  14.             <vw:Content03View />
  15.         </DataTemplate>
  16.     </Application.Resources>
  17. </Application>

各 DataTemplate に x:Key 属性を指定していないため、コンテンツとして各 ViewModel が指定されると自動的に各 DataTemplate にしたがって対応する View に変換されて表示されるようになります。ちなみに、変換された View の DataContext は変換前のデータ、すなわちここでは ViewModel となるため、Content01View などに明示的に DataContext を指定する必要はありません。

MainViewModel ではコンテンツを入れ替えるために各コンテンツの ViewModel を保持します。

MainViewModel.cs
  1. namespace WpfApplication1.ViewModels
  2. {
  3.     using System.Collections.Generic;
  4.     using YKToolkit.Bindings;
  5.  
  6.     internal class MainViewModel : NotificationObject
  7.     {
  8.         private List<ViewModelBase> _viewModels = new List<ViewModelBase>()
  9.         {
  10.             new Content01ViewModel(),
  11.             new Content02ViewModel(),
  12.             new Content03ViewModel(),
  13.         };
  14.         public List<ViewModelBase> ViewModels
  15.         {
  16.             get { return this._viewModels; }
  17.         }
  18.     }
  19. }

MainViewModel が公開するプロパティを使って MainView を次のように定義します。

MainView.xaml
  1. <YK:Window x:Class="WpfApplication1.Views.MainView"
  2.            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.            xmlns:YK="clr-namespace:YKToolkit.Controls;assembly=YKToolkit.Controls"
  5.            xmlns:vw="clr-namespace:WpfApplication1.Views"
  6.            Title="MainView"
  7.            Width="300" Height="200">
  8.     <DockPanel>
  9.         <ComboBox x:Name="combobox" DockPanel.Dock="Bottom" ItemsSource="{Binding ViewModels}" DisplayMemberPath="Caption" SelectedIndex="0" />
  10.         <vw:TransitionPanel Content="{Binding SelectedItem, ElementName=combobox}" />
  11.     </DockPanel>
  12. </YK:Window>

ComboBox で ViewModel のリストを表示し、選択された ViewModel を TransitionPanel のコンテンツとして表示するようにしています。ちなみに Content01ViewModel クラスの中身は次のようになっています。

Content01ViewModel.cs
  1. namespace WpfApplication1.ViewModels
  2. {
  3.     internal class Content01ViewModel : ViewModelBase
  4.     {
  5.         public string Caption { get { return "Content01"; } }
  6.     }
  7. }

Content02ViewModel と Content03ViewModel も同様になっています。

Content01View は例えば次のようにチェックボックスを配置しておきます。

Content01View.xaml
  1. <UserControl x:Class="WpfApplication1.Views.Content01View"
  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:DesignHeight="300" d:DesignWidth="300">
  8.     <Grid>
  9.         <CheckBox Content="Content01" />
  10.     </Grid>
  11. </UserControl>

Content02View と Content03View も適当にコントロールを配置しておきます。

それでは実行してみましょう。

無事画面遷移できました。

ところで、せっかく配置したチェックボックスにチェックを入れて画面遷移してみましょう。

なんと、チェックを入れた状態で画面遷移して戻ってくると、チェック状態がリセットされています。もう少し解析するために、Content01View のコードビハインドに次のようなコードを追加します。

Content01View.xaml.cs
  1. namespace WpfApplication1.Views
  2. {
  3.     using System.Windows.Controls;
  4.  
  5.     /// <summary>
  6.     /// Content01View.xaml の相互作用ロジック
  7.     /// </summary>
  8.     public partial class Content01View : UserControl
  9.     {
  10.         public Content01View()
  11.         {
  12.             System.Diagnostics.Debug.WriteLine("Content01View() コンストラクタ");
  13.  
  14.             InitializeComponent();
  15.         }
  16.     }
  17. }

Content02View、Content03View も同様にコンストラクタでデバッグ出力をおこなうようにしてからもう一度実行してみましょう。

Content02View、Content03View は初めて表示されるのでコンストラクタが走るのは当然ですが、その後に一度表示したはずの Content01View がまたコンストラクタから処理されていることがわかります。

実は、これは DataTemplate を使って View と ViewModel を紐付けていることが原因です。DataTemplate は指定された型のデータを指定されたテンプレートに変換して表示するためのテンプレートです。つまり、データが指定される度にテンプレートからコンテンツを毎回構築してしまいます。

このサンプルのようにチェックボックスの状態くらいなら ViewModel 側でデータを保持し、それをデータバインディングすることで解決できそうですが、それ以外にも、例えば ListBox の選択状態だったり、そのアイテムを表示するためのスクロールバーの状態だったり、そのすべてを ViewModel 側で保持しようとすると、技術的には可能かもしれませんが、あまり現実的ではありません。

今回の現象の肝は、ViewModel のインスタンスは MainViewModel クラスが保持しているのに対し、Content01View などの View のインスタンスは誰も保持していないことにあります。DataTemplate によって View と ViewModel を紐付けることで実現する画面遷移はお手軽ではありますが、システム要件によってはちょっと使い勝手が悪くなってしまうので注意が必要です。

というわけで View のインスタンスもちゃんと保持する画面遷移について後日紹介します。




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 にあらかじめ指定する方法を使っています。長くなってきたのでコードは割愛。




Visual Studio の拡張機能の開発を始められなかった

(2017/04/19 16:58:28 created.)

以前の記事で Visual Studio の拡張機能を開発すべく SDK をインストールしたわけですが、
どういうわけかプロジェクトを新規作成しようとするとエラーが発生してしまい、まったく手がつけられませんでした。
今思えばそのときのスクリーンショットを取っておくべきでした…。
確か "Project 要素の下の #Text 要素が認識できない" とかなんとかいうエラーメッセージで、
プロジェクトファイルの生成に失敗しているような内容でした。 ただ、プロジェクトファイルをテキストエディタで覗いて見ても "#Text" なる要素は見当たらず。
Google 先生に聞いてみても真面目に回答してくれる様子もなかったので諦めました。
気になった点といえば、日本語版の Visual Studio に英語版の SDK を入れていたことくらいですが、SDK の日本語版はどこにも見当たりませんでした。

そういうわけでインストールした SDK をアンインストールしたわけですが、ここでもまた一難。
アンインストール後、Visual Studio でプロジェクトを新規に作成しようとしたり、既存プロジェクトを立ち上げた状態でクラスなどを追加しようとするとフリーズ!

どうやらプロジェクトなどの追加項目に関するテンプレートにゴミが入り込んだのか、テンプレート読み込みの段階でフリーズするようになってしまいました。
仕方がないので修復作業へ。

なぜか修復作業に 10 時間以上もかかるという所業。なぜだ。
新しいことを始めようというウキウキ感からの絶望を味わったのでもう二度と Visual Studio 2013 による拡張機能開発をやるつもりはありません。




Visual Studio の拡張機能の開発を始めてみる

(2017/04/07 8:39:42 created.)

(2017/04/07 9:50:29 modified.)

普段お世話になっている Visual Studio の拡張機能ですが、自分でも作ってみたくなって調べたところ、
とりあえず Visual Studio SDK なるものが必要ということで、
さっそくダウンロード&インストール。



自分が普段 Visual Studio 2013 使ってるから 2013 の SDK で開発するけど、 もしかして上位バージョンに対応するにはそのバージョンの SDK じゃないとできないのかな?
互換性があるものと信じたいけど、"2017 に対応しました" の更新をよく見かけるからあまり期待してはいけない。
というわけで結局オレオレ拡張機能になってしまいそうだ。ま、いっか。

 

興味本位でインストールしたけど 1 時間かかった…。長かった。





モダンなインストーラへの道 3 ~ カスタム UI を msi インストーラパッケージと紐付ける ~

(2017/04/05 14:35:06 created.)

(2017/05/24 15:14:16 modified.)

前回までで、簡単なインストーラを作成するこ]とができるようになりました。
今回は、いよいよカスタム UI をインストーラとして使用できるようにしたいと思います。

WiX では Bootstrapper Project によって msi インストーラパッケージやカスタム UI の dll ファイルをすべてペイロードとして exe パッケージを作成します。
イメージとしては次のようになります。


そんなわけでカスタム UI の参照は DLL 経由となります。
そこで、まずインストーラとして使用する UI を作成するためのプロジェクトを追加しましょう。
ここでは WPF カスタムコントロールライブラリのプロジェクトを "InstallerUI" という名前で追加します。


WPF カスタムコントロールライブラリのプロジェクト構成はデフォルトでは次のようになっています。


インストーラ用の UI として WiX と紐付けるために、参照設定などを次のように変更します。

  • Themes フォルダならびに CustomControl1.cs を削除
  • 参照設定に以下を追加
    BootstrapperCore.dll
    Microsoft.Deployment.WindowsInstaller.dll
    どちらも C:\Program Files\WiX Toolset v3.10\SDK\ にあります。
  • アプリケーション構成ファイル BootstrapperCore.config を追加
  • ウィンドウとして MainView クラスを追加
  • YKBootstrapperApplication クラスを追加


アプリケーション構成ファイルの中身は次のようにします。

BootstrapperCore.config
  1. xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.   <configSections>
  4.     <sectionGroup name="wix.bootstrapper" type="Microsoft.Tools.WindowsInstallerXml.Bootstrapper.BootstrapperSectionGroup, BootstrapperCore">
  5.       <section name="host" type="Microsoft.Tools.WindowsInstallerXml.Bootstrapper.HostSection, BootstrapperCore" />
  6.     </sectionGroup>
  7.   </configSections>
  8.  
  9.   <startup>
  10.     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
  11.   </startup>
  12.  
  13.   <wix.bootstrapper>
  14.     <host assemblyName="InstallerUI" />
  15.   </wix.bootstrapper>
  16. </configuration>

また、YKBootstrapperApplication クラスは Microsoft.Tools.WindowsInstallerXml.Bootstrapper.BootstrapperApplication クラスを基本クラスとして次のように定義します。

YKBootstrapperAplication.cs
  1. namespace InstallerUI
  2. {
  3.     using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
  4.     using System.Windows.Threading;
  5.  
  6.     public class YKBootstrapperApplication : BootstrapperApplication
  7.     {
  8.         protected override void Run()
  9.         {
  10.             var dispatcher = Dispatcher.CurrentDispatcher;
  11.             var w = new MainView();
  12.             w.Closed += (_, __) => dispatcher.InvokeShutdown();
  13.  
  14.             this.Engine.Detect();
  15.             w.Show();
  16.             Dispatcher.Run();
  17.             this.Engine.Quit(0);
  18.         }
  19.     }
  20. }

WiX の Bootstrapper Project からこの WPF カスタムコントロールライブラリの DLL ファイルを参照されるようになりますが、
実は、このときのエントリポイントが BootstrapperApplication クラスの Run() メソッドになります。
したがって、通常の WPF アプリケーションプロジェクトで Application クラスの Startup() メソッドでおこなっていたような処理を、この Run() メソッドでおこなうことになります。
ここでは MainView ウィンドウを表示するようにしています。
14 行目の Detect() メソッドは Microsoft.Tools.WindowsInstallerXml.Bootstrapper.Engine クラスに用意されているメソッドで、インストール条件がすべて満たされているかどうかを確認しています。

それでは、WiX の Bootstrapper Project のプロジェクトをソリューションに追加しましょう。


Bundle.wxs というファイルでインストーラの exe ファイルを生成するための XML コードを記述します。
デフォルトのままでは少し足りないので、次のような変更をしています。

  • References に WixUtilExtension.dll への参照を追加
    "References" を右クリックし、「参照の追加」メニューから簡単に追加できます。
  • .NET Framework のインストーラ dotNetFx40_Full_x86_x64.exe をプロジェクトに追加


WiX によって生成するインストーラは、通常は .NET Framework を必要としないインストーラですが、
カスタム UI を使用する場合は .NET Framework が必要となるため、インストーラ用に .NET Framework のインストーラが必要になります。
web 経由で指定することもできるようですが、ここではローカルに .NET Framework のインストーラを保存し、これを参照するようにするため、ダウンロードしてきた .NET Framework のインストーラをプロジェクトのファイルとして含めています。
このファイルはこちらからダウンロードできます。

それでは、Bundle.wxs の中身について見てみましょう。

Bundle.wxs
  1. xml version="1.0" encoding="UTF-8"?>
  2. <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
  3.   <Bundle Name="AppSetup" Version="1.0.0.0" Manufacturer="YKSoftware" UpgradeCode="b8dbe519-5748-4e6e-a73d-9404b7fccbec">
  4.     <BootstrapperApplicationRef Id="ManagedBootstrapperApplicationHost">
  5.       <Payload SourceFile="../InstallerUI/BootstrapperCore.config" />
  6.       <Payload SourceFile="../InstallerUI/bin/Release/InstallerUI.dll" />
  7.       <Payload SourceFile="C:/Program Files/WiX Toolset v3.10/SDK/Microsoft.Deployment.WindowsInstaller.dll" />
  8.     </BootstrapperApplicationRef>
  9.  
  10.     <Chain>
  11.       <PackageGroupRef Id="Netfx4Full" />
  12.       <MsiPackage Id="SampleWpfApplicationInstallerPackage" SourceFile="../Installer/bin/Release/Installer.msi" Cache="yes" Vital="no" />
  13.     </Chain>
  14.   </Bundle>
  15.  
  16.   <Fragment>
  17.     <WixVariable Id="WixMbaPrereqPackageId" Value="Netfx4Full" />
  18.     <WixVariable Id="WixMbaPrereqLicenseUrl" Value="NetfxLicense.rtf" />
  19.  
  20.     <util:RegistrySearch Root="HKLM" Key="SOFTWARE/NET Framework Setup/NDP/v4/Full" Value="Version" Variable="Netfx4FullVersion" />
  21.     <util:RegistrySearch Root="HKLM" Key="SOFTWARE/NET Framework Setup/NDP/v4/Full" Value="Version" Variable="Netfx4x64FullVersion" Win64="yes" />
  22.  
  23.     <PackageGroup Id="Netfx4Full">
  24.       <ExePackage Id="Netfx4Full" Cache="no" Compressed="yes" PerMachine="yes"
  25.                   Permanent="yes" Vital="yes" SourceFile="../../dotNetFx40_Full_x86_x64.exe"
  26.                   DetectCondition="Netfx4FullVersion AND (NOT VersionNT64 OR Netfx4x64FullVersion)"
  27.                   InstallCondition="(VersionNT < v6.0 OR VersionNT64 < v6.0) AND (NOT (Net4FullVersion OR Net4x64FullVersion))" />
  28.     </PackageGroup>
  29.   </Fragment>
  30. </Wix>

ペイロードするものをすべて BootstrapperApplicationRef に登録しています。
ここではカスタム UI のアプリケーション構成ファイルと生成物である dll ファイルを指定しています。
また、カスタム UI に必要な外部参照ファイルを指定しなければいけないため、Microsoft.Deployment.WindowsInstaller.dll も追加しています。

Chain 要素にインストーラパッケージを詰め込みます。
ここではインストーラに必要な .NET Framework のインストーラと、本来の目的である Installer.msi ファイルを登録しています。
.NET Framework のインストーラは、条件によってはインストールする必要がないため、 PackageGroupRef 要素を使って ExePackage を参照させることで条件を付加しています。
その条件とは、Windows Vista 以降か、または .NET Framework 4.0 以降がインストール済みかどうか、をレジストリを探索することで確認しています。

それではビルドしてみましょう。プロジェクトの依存関係を適切に設定しておくことで、ビルド順序を自動的に判別して各プロジェクトをビルドしてくれるので、必ず設定しておきましょう。


すると exe ファイルが生成されます。


それでは早速 exe ファイルを実行してみましょう。


MainView ウィンドウが表示されれば成功です。
.NET Framework 4.0 以降がインストールされていない PC で実行すると、まず .NET Framework 4.0 のインストーラが起動するはずです。

ちょっと冒頭のイメージ図で状況を整理しておきましょう。


WiX の Bootstrapper Project は本来インストールさせたい SampleWpfApplication という WPF アプリケーションのインストーラパッケージである msi ファイルと、そのインストーラの UI として使用する WPF カスタムコントロールライブラリの dll を持っています。また、インストーラが必要とする .NET Framework のインストーラも持っています。
実際のインストール処理は元々の msi ファイルがおこないますが、そのキックをかけるのは BootstrapperApplication クラスを持つカスタム UI になります。
したがって、今後はカスタム UI を編集していくことで、インストーラを作り上げていくことになります。

これでいよいよ好きな UI でインストーラを作成することができるようになってきました。
次回はここで作成した MainView ウィンドウでインストール/アンインストールを実行できるようにカスタマイズしていきます。