ずっと思ってた。WPFをやり始めたころからずっと。
カスタムコントロールコントロールを作りたい!!!ということでコツコツDependencyPropertyとかCommandとかやってきたのが実を結んで、ついにカスタムコントロール作成を入門してみるよ。
 プロジェクトの作成
 WpfMyControlという名前でプロジェクトを作成した。あえて見出しをつけるまでもないけど、はじめを大事にね。
 コントロールの作成
 右クリックメニューからさくっと追加。カスタムコントロール(WPF)っていうのを選ぼう。
WPFついてないのを選ぶとWindowsFormのになっちゃうので要注意。
 名前はGreetControlにしてみた。コントロールを作成すると、Themes\Generic.xamlとGreetControl.csというファイルが作られる。
 
 Generic.xamlは、見た目を定義するのに使います。GreetControlに、CommandやDependencyPropertyを定義する。
 今回の目標
 最初に書いておけって感じがしなくもないけど、今回の目標を書いておく。といあえず、テキストボックスとボタンがあって、ボタンを押すと、テキストボックスの中身がメッセージボックスで表示されるものを目指す。
 見た目の作成
 見た目を作っていく。見た目はGeneric.xamlにあるStyleに書いていく。最初の状態だとGeneric.xamlには、下のようなStyleが定義されている。
  
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfMyControl">
    <Style TargetType="{x:Type local:GreetControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:GreetControl}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
GreetControlをTargetTypeにして、Templateを設定するStyleが定義されているのがわかる。
ここにStackPanelを置いて、TextBoxとButtonをとりあえず置いてみた。
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfMyControl">
    <Style TargetType="{x:Type local:GreetControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:GreetControl}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <StackPanel>
                            <TextBox />
                            <Button Content="Greet!!" />
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
ちょっと見た目を弄ったので、Window1.xamlに置いてみる。namespaceを定義してWindowにぽちっと置いてみた。
<Window x:Class="WpfMyControl.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfMyControl"
    Title="Window1" Height="300" Width="300">
    <StackPanel>
        <local:GreetControl />
    </StackPanel>
</Window>
実行してみると、確かに見た目は出来てる!
 
 
次はプロパティを作ってみよう。とりあえず、TextBoxに入力された値を保持するためのプロパティが必要になりそう。ということでプロパティをつくってみようと思う。
プロパティの定義
プロパティは、普通に依存プロパティで作ることになる。string型のValueプロパティなので、こんな感じでいける。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfMyControl
{
    public class GreetControl : Control
    {
        #region Value 依存プロパティ
        public string Value
        {
            get { return (string)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }
        // GreetControlのstring型のValueプロパティで、デフォルト値が空文字
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", 
                typeof(string), 
                typeof(GreetControl), 
                new UIPropertyMetadata(""));
        #endregion
        static GreetControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(GreetControl), new FrameworkPropertyMetadata(typeof(GreetControl)));
        }
    }
}
usingとかが長いけど、追加したのはregion~endregionまでの間になる。これで、Window1.xamlでValueプロパティに値を設定できるようになる。
<Window x:Class="WpfMyControl.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfMyControl"
    Title="Window1" Height="300" Width="300">
    <StackPanel>
        <local:GreetControl Value="こんにちは" />
    </StackPanel>
</Window>
ビルドは通るけど、ぜんぜん動きとしては変わらない。プロパティの値を見た目に反映させるには、Generic.xamlをいじくることになる。
プロパティの値をバインドして画面に出すよ
ということでGeneric.xamlに戻って編集を再開。ValueプロパティをTextBoxにバインドして出してみようと思う。
ということでTemplateBindingでさくっとバインドをする。
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfMyControl">
    <Style TargetType="{x:Type local:GreetControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:GreetControl}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <StackPanel>
                            <!-- Binding!! -->
                            <TextBox Text="{TemplateBinding Value}"/>
                            <Button Content="Greet!!" />
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
これで実行すると、画面にWindow1.xamlで設定した「こんにちは」が表示されるようになる。
 
 
動きをつけよう
最後に動きをつけて完成かな。このコントロールの動きは、ボタンを押したときにポローンとメッセージボックスが出るってだけ。単純にイベントを登録するのではなく、Commandを使ってボタンのクリックを補足します。
とりあえずCommandの定義をGreetControl.csに追加。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfMyControl
{
    public class GreetControl : Control
    {
        #region Value 依存プロパティ
        public string Value
        {
            get { return (string)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }
        // GreetControlのstring型のValueプロパティで、デフォルト値が空文字
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", 
                typeof(string), 
                typeof(GreetControl), 
                new UIPropertyMetadata(""));
        #endregion
        #region GreetCommand
        public static ICommand GreetCommand = new RoutedCommand(
            "GreetCommand", typeof(GreetControl));
        #endregion
        static GreetControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(GreetControl), new FrameworkPropertyMetadata(typeof(GreetControl)));
        }
    }
}
Commandが定義できたらStaticコンストラクタにCommandBindingを追加する。Staticメソッド経由でインスタンスメソッドを呼び出してる。そこでMessageBoxの表示処理を書くって寸法。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfMyControl
{
    public class GreetControl : Control
    {
        #region Value 依存プロパティ
        public string Value
        {
            get { return (string)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }
        // GreetControlのstring型のValueプロパティで、デフォルト値が空文字
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", 
                typeof(string), 
                typeof(GreetControl), 
                new UIPropertyMetadata(""));
        #endregion
        #region GreetCommand
        public static ICommand GreetCommand = new RoutedCommand(
            "GreetCommand", typeof(GreetControl));
        #endregion
        static GreetControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(GreetControl), new FrameworkPropertyMetadata(typeof(GreetControl)));
            
            // CommandBingingを登録
            var greetCommandBinding = new CommandBinding(
                GreetCommand, OnGreetCommand);
            CommandManager.RegisterClassCommandBinding(
                typeof(GreetControl), greetCommandBinding);
        }
        #region Command
        private static void OnGreetCommand(object sender, ExecutedRoutedEventArgs e)
        {
            // senderからコントロールを取得して、インスタンスメソッドに処理を丸投げ
            var control = (GreetControl)sender;
            control.OnGreetCommand();
        }
        /// <summary>
        /// GreetCommandの処理の実体
        /// </summary>
        public void OnGreetCommand()
        {
            MessageBox.Show(this.Value);
        }
        #endregion
    }
}
そして、Generic.xamlで、ButtonとCommandを関連付ける。
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfMyControl">
    <Style TargetType="{x:Type local:GreetControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:GreetControl}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <StackPanel>
                            <!-- Binding!! -->
                            <TextBox Text="{TemplateBinding Value}"/>
                            <!-- Command!! -->
                            <Button Content="Greet!!" Command="{x:Static local:GreetControl.GreetCommand}" />
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
動作確認
ということで、実行して動作を確認してみる。実行してボタンをぽちっとすると…
 
 
でた~!ということで始めての、作り方は正統派のコントロールの作り方でした。