ユーザーコントロールに学ぶ様々なコンテンツ - WPF Vol 07

- - posted in wpf | Comments

設定メニューの作成を通じて、ユーザーコントロールについて紹介します。

wpf-08-01

WPFは柔軟で StyleやTemplate、添付プロパティで多くは対応できるのですが、場合によっては以下のようなレベルでコントロールを作成する場面も出てきます。

  • ユーザーコントロール
  • カスタムコントロール
  • カスタム要素

MSDNにも記載があります。

また、これらをクラス ライブラリとして開発する場合と、アプリケーションとして開発する場合で適用するプログラミング デザインパターンも変わってきます。

本シリーズのビューの基本クラスとなるBizViewクラスはUserControl派生です。 その基本機能を実装する上でMVVMパターンで作成しているかといえば、クラスライブラリとしての基本セットなのでそうではありません。

ユーザーコントロール

VisualStudioで、新しい項目を追加する場合に選択できます。
ベースクラスがUserControlクラスになります。

主な目的は、より要件に具体的なビューを提供済みのコンテンツを組み合わせて構築することです。
たとえば、BizViewクラスから派生したView/ViewModelの各種コンテンツ(サンプルのVol04View/Vol05Viewなど)も同様に、目的とする画面をボタンやラベルといった提供済みコンテンツを配置して作成しています。

それと比べて、カスタムコントロールやカスタム要素は、WPFの組込済みコントロール(Buttonのベースクラスは)がそうであるように、基本となるコンテンツ作成として利用します。

FrameworkElement派生ではパフォーマンスが向上しますが、実装する際には DrawingVisual クラスなどを利用して、描画を実装する必要が出てきます。

さて、今回は画面の右側に表示する設定画面をユーザーコントロールで作成します。
画面は以下の構成で、コンテンツ部には指定したView/ViewModelのセットが追加できるよう検討します。

wpf-08-03

ユーザーコントロールなので、Xamlで画面を開発する要領で作成します。

Xamlも上から下まで30行程度です。

  • グリッドをデザイン通りに上下分割
  • 上部にタイトルとアイコン
  • 下部に設定タイトル一覧とメインコンテンツ

を配置しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<UserControl x:Class="uEN.UI.Controls.Settings"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:uen="clr-namespace:uEN.UI"
             mc:Ignorable="d" 
             Background="{DynamicResource AppBrand}"
             Foreground="White"
             BorderBrush="Transparent"
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <uen:ViewDataTemplateSelector x:Key="templateSelector" />
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Margin="10,30,0,30" Orientation="Horizontal">
            <Button x:Name="IconButton" Width="35" Height="35" Margin="5" 
                    Foreground="White"
                    BorderBrush="White"
                    Style="{DynamicResource EllipseButtonStyle}" 
                    >
                <Viewbox Stretch="Fill">
                    <Path Style="{DynamicResource PathButtonStyle}"
                          Data="F1 M 33.6458,38L 49.4792,53.8333L 38.7917,53.8333L 22.1667,38L 38.7917,22.1667L 49.4792,22.1667L 33.6458,38 Z "/>
                </Viewbox>
            </Button>
            <TextBlock x:Name="Caption" Text="Settings" FontSize="20" VerticalAlignment="Center"/>
        </StackPanel>
        <ListBox Grid.Row="1" x:Name="SettingViewModels" Visibility="Visible" />
        <ContentPresenter Grid.Row="1" x:Name="ViewModelPresenter"  
                          ContentTemplateSelector="{StaticResource templateSelector}"
                          />
    </Grid>
</UserControl>

アイコンのジオメトリも前回同様にAlex Peattieさんのものを利用しています。

ボタンは丸く描画されるように Style="{DynamicResource EllipseButtonStyle}" としてスタイルをリソースとして切り出し、コンテンツにジオメトリを設定しています。

切り出したスタイルは単に <Setter Property="Template"> としてControlTemplateを設定する際に、Mindwos 8 Styleではフラットで角なしの線を描画するために <Border> としましたが、これを <Ellipse> として丸にすればそれだけでOKです。

ボタンを押したときの内部動作はコードビハインド上で記述しています。
(クラス ライブラリとしての開発)

アプリケーション構成ファイルに任意のViewModelを設定すると、それが設定画面の一覧に表示されるようにしました。

wpf-08-04

あとは、これをWindow Style で定義しているグリッド上にコンテンツとして配置するだけです。

アニメーション

ボタンを押下する度に先ほど作成したユーザーコントロールがWindowとしてモーダル表示されたり、画面上にパッとでたり消えたりするのは、利用者としては新鮮さにかけます。 XAMLで作成するアプリケーションのゴールは、やはり柔軟なアニメーションを利用者に体験してもらうことにあります。

今回は、そんなアニメーションを添付プロパティとして切り出して実装しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

  public enum TransitionStyle
    {
        None,

        Slide,
        VerticalSlide,

        SlideOut,
        VerticalSlideOut,
    }

    public class ViewTransition
    {
        public static TransitionStyle GetTransitionStyle(DependencyObject obj)
        {
            return (TransitionStyle)obj.GetValue(TransitionStyleProperty);
        }

        public static void SetTransitionStyle(DependencyObject obj, TransitionStyle value)
        {
            obj.SetValue(TransitionStyleProperty, value);
        }

        // Using a DependencyProperty as the backing store for TransitionStyle.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TransitionStyleProperty =
            DependencyProperty.RegisterAttached("TransitionStyle", typeof(TransitionStyle), typeof(ViewTransition), new UIPropertyMetadata(TransitionStyle.None, OnTransitionStyleChanged));

        private static void OnTransitionStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var fw = d as FrameworkElement;
            var style = e.NewValue as TransitionStyle?;
            if (!style.HasValue || style == TransitionStyle.None)
                return;

            fw.Loaded -= fw_Loaded;
            fw.Loaded += fw_Loaded;
        }

        static void fw_Loaded(object sender, RoutedEventArgs e)
        {
            var fw = (FrameworkElement)sender;
            var style = GetTransitionStyle(fw);

            Play(fw, style);
        }

        public static Storyboard Play(FrameworkElement target, TransitionStyle style, Action completedAction = null)
        {
            Storyboard storyboard = null;
            switch (style)
            {
                case TransitionStyle.None:
                    break;
                case TransitionStyle.Slide:
                    storyboard = CreateSlideStoryboard();
                    break;
                case TransitionStyle.VerticalSlide:
                    storyboard = CreateVerticalSlideStoryboard();
                    break;
                case TransitionStyle.SlideOut:
                    storyboard = CreateSlideStoryboard(false);
                    break;
                case TransitionStyle.VerticalSlideOut:
                    storyboard = CreateVerticalSlideStoryboard(false);
                    break;
                default:
                    break;
            }
            if (completedAction != null)
                storyboard.Completed += (x, y) => completedAction();
            storyboard.Begin(target);
            return storyboard;
        }

        private static Storyboard CreateSlideStoryboard(bool isFadeIn = true)
        {
            var storyboard = new Storyboard();

            var fromThickness = isFadeIn ? new Thickness(30, 0, -30, 0) : new Thickness(0);
            var toThickness = isFadeIn ? new Thickness(0) : new Thickness(30, 0, -30, 0);

            var slideAnimation = new ThicknessAnimation();
            slideAnimation.From = fromThickness;
            slideAnimation.To = toThickness;
            slideAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.3));

            Storyboard.SetTargetProperty(slideAnimation, new PropertyPath(FrameworkElement.MarginProperty));
            storyboard.Children.Add(slideAnimation);

            var fromOpacity = isFadeIn ? 0 : 1;
            var toOpacity = isFadeIn ? 1 : 0;

            var opacityAnimation = new DoubleAnimation();
            opacityAnimation.From = fromOpacity;
            opacityAnimation.To = toOpacity;
            opacityAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.5));
            Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath(FrameworkElement.OpacityProperty));
            storyboard.Children.Add(opacityAnimation);

            return storyboard;
        }

        private static Storyboard CreateVerticalSlideStoryboard(bool isFadeIn = true)
        {
            var storyboard = new Storyboard();

            var fromThickness = isFadeIn ? new Thickness(0, 30, 0, -30) : new Thickness(0);
            var toThickness = isFadeIn ? new Thickness(0) : new Thickness(0, 30, 0, -30);


            var slideAnimation = new ThicknessAnimation();
            slideAnimation.From = fromThickness;
            slideAnimation.To = toThickness;
            slideAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.3));

            Storyboard.SetTargetProperty(slideAnimation, new PropertyPath(FrameworkElement.MarginProperty));
            storyboard.Children.Add(slideAnimation);

            var fromOpacity = isFadeIn ? 0 : 1;
            var toOpacity = isFadeIn ? 1 : 0;

            var opacityAnimation = new DoubleAnimation();
            opacityAnimation.From = fromOpacity;
            opacityAnimation.To = toOpacity;
            opacityAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.5));
            Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath(FrameworkElement.OpacityProperty));
            storyboard.Children.Add(opacityAnimation);

            return storyboard;
        }

    }

Xaml上で設定すれば、コンテンツの読み込み時に自動でアニメーションをするようになります。

wpf-08-05

また静的メソッドを利用して、任意のタイミングでも動作します。

まとめ

ユーザーコントロールで作成したストアアプリスタイルの設定画面を設け、スタイルの変更ができるようになりました。

wpf-08-06

wpf-08-07

ただし、設定したユーザー固有のスタイルはまだ永続化していません。
また、TabControlを利用していますが、このコンテンツはどうしても旧来のWindows Formを彷彿させる、モダンではないビシュアルです。

次回の宿題は、ユーザー固有のスタイルは IsolatedStorage の機能を利用して永続化したいと思います。 それと、Controlから派生したItemsControlを利用して、モダンなカスタム コントロールを作成したいと思います。

サンプル成果物はGit管理で。

@s-ueno/uENLab on GitHub


Comments