Silverlightで再利用可能なカスタムコントロールを作る その2

テキストボックスの横幅は自動調整されるようになったけど、上下ボタンの縦高さも自動調整する必要があるのでこれもやる。

Controls/NumericUpDown.cs

private ButtonBase upButton;
private ButtonBase downButton;
private TextBox valueTextBox;
private FrameworkElement buttonsPane;

public override void OnApplyTemplate() {
    base.OnApplyTemplate();

    upButton = (ButtonBase)this.GetTemplateChild("upButton");
    downButton = (ButtonBase)this.GetTemplateChild("downButton");
    valueTextBox = (TextBox)this.GetTemplateChild("valueTextBox");
    buttonsPane = (FrameworkElement)this.GetTemplateChild("buttonsPane");

    upButton.Click += upButton_Click;
    downButton.Click += downButton_Click;
}

protected override Size ArrangeOverride(Size finalSize) {
    var margin1 = upButton.Margin;
    var margin2 = downButton.Margin;

    upButton.Height = downButton.Height =
        finalSize.Height / 2 - ((margin1.Top + margin1.Bottom) + (margin2.Top + margin2.Bottom));

    valueTextBox.Width = this.Width - buttonsPane.Width - 2;

    return base.ArrangeOverride(finalSize);
}

void upButton_Click(object sender, RoutedEventArgs e) {
}
void downButton_Click(object sender, RoutedEventArgs e) {
}

上下ボタンへの参照を取得し、ついでに「Click」イベントにイベントハンドラを登録しておく。

TemplatePart属性の宣言も忘れずに。

[TemplatePart(Name = "upButton", Type = typeof(ButtonBase))]
[TemplatePart(Name = "downButton", Type = typeof(ButtonBase))]
[TemplatePart(Name = "valueTextBox", Type = typeof(TextBox))]
[TemplatePart(Name = "buttonsPane", Type = typeof(FrameworkElement))]
public class NumericUpDown : Control {
}

これで外観はだいたいできたので、次は上下ボタンのテンプレートを外部から設定できるように変更してみよう。

やり方は、まずNumericUpDownクラスに「ButtonTemplate」という依存プロパティを「ControlTemplate」型で定義する。

Controls/NumericUpDown.cs

/// <summary>
/// ButtonTemplate
/// </summary>
public static readonly DependencyProperty ButtonTemplateProperty = DependencyProperty.Register(
   "ButtonTemplate", typeof(ControlTemplate), typeof(NumericUpDown), new PropertyMetadata((d, e) => { })
);
/// <summary>
/// 上下ボタンのテンプレートを取得、設定します。
/// </summary>
public ControlTemplate ButtonTemplate {
    get {
        return (ControlTemplate)this.GetValue(ButtonTemplateProperty);
    }
    set {
        this.SetValue(ButtonTemplateProperty, value);
    }
}

あとは「Generic.xaml」側で定義されている上下ボタンの「Template」プロパティにこの依存プロパティをバインディングすればいい。

Generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:my="clr-namespace:NumericUpDownDemo.Controls">

    <Style TargetType="my:NumericUpDown">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="my:NumericUpDown">
                    <Grid Background="{TemplateBinding Background}">
                        <TextBox x:Name="valueTextBox" HorizontalAlignment="Left" Margin="1" />

                        <StackPanel x:Name="buttonsPane" Grid.Column="1" Width="{TemplateBinding ButtonWidth}"
                                    VerticalAlignment="Center" HorizontalAlignment="Right">
                            <Button x:Name="upButton" Margin="0,0,0,0.5" Cursor="Hand" Template="{TemplateBinding ButtonTemplate}" >
                                <Polygon Points="0,3, 10,3, 5,0" Fill="Black"
                                         HorizontalAlignment="Center" VerticalAlignment="Center" />
                            </Button>

                            <Button x:Name="downButton" Margin="0,0.5,0,0" Cursor="Hand" Template="{TemplateBinding ButtonTemplate}" >
                                <Polygon Points="0,0, 10,0, 5,3" Fill="Black"
                                         HorizontalAlignment="Center" VerticalAlignment="Center" />
                            </Button>
                        </StackPanel>

                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

これだけでOK。

では実際にテンプレートを変更してみよう。

Page.xaml
<UserControl x:Class="NumericUpDownDemo.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:my="clr-namespace:NumericUpDownDemo.Controls"
    Width="400" Height="300">
    
    <Grid x:Name="LayoutRoot">
        <my:NumericUpDown>
            <my:NumericUpDown.ButtonTemplate>
                <ControlTemplate>
                    <Border BorderBrush="Black" BorderThickness="1" CornerRadius="4">
                        <Border.Background>
                            <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                                <GradientStop Offset="0" Color="Silver" />
                                <GradientStop Offset="0.7" Color="White" />
                            </LinearGradientBrush>
                        </Border.Background>
                        
                        <ContentPresenter />
                    </Border>
                </ControlTemplate>
            </my:NumericUpDown.ButtonTemplate>
        </my:NumericUpDown>
    </Grid>
</UserControl>

BorderとLinearGradientBrushを使って、ちょっとおしゃれなボタンにしている。

これを実行すると以下のように表示される。
http://sites.google.com/site/coma2n/Home/image-gallary/006.png

こんな感じで再利用可能なカスタムコントロールを作ることができる。思ったより簡単ですね!!

あと、コントロールとして必要な機能を一通り実装したバージョンは以下のようになる。

Controls/NumericUpDown.cs

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.ComponentModel;
using System.Diagnostics;

namespace NumericUpDownDemo.Controls {
    /// <summary>
    /// 値を増減できる入力コントロール
    /// </summary>
    [TemplatePart(Name = "upButton", Type = typeof(ButtonBase))]
    [TemplatePart(Name = "downButton", Type = typeof(ButtonBase))]
    [TemplatePart(Name = "valueTextBox", Type = typeof(TextBox))]
    [TemplatePart(Name = "buttonsPane", Type = typeof(FrameworkElement))]
    public class NumericUpDown : Control, INotifyPropertyChanged {
        /// <summary>
        /// デフォルトの背景色
        /// </summary>
        private static readonly Brush DefaultBackground = new SolidColorBrush(Colors.LightGray);

        private ButtonBase upButton;
        private ButtonBase downButton;
        private TextBox valueTextBox;
        private FrameworkElement buttonsPane;

        /// <summary>
        /// ButtonWidth
        /// </summary>
        public static readonly DependencyProperty ButtonWidthProperty = DependencyProperty.Register(
            "ButtonWidth", typeof(double), typeof(NumericUpDown), new PropertyMetadata((d, e) => { })
        );
        /// <summary>
        /// ボタンの横幅を取得、設定します。
        /// </summary>
        public double ButtonWidth {
            get {
                return (double)this.GetValue(ButtonWidthProperty);
            }
            set {
                this.SetValue(ButtonWidthProperty, value);
            }
        }

        /// <summary>
        /// ButtonTemplate
        /// </summary>
        public static readonly DependencyProperty ButtonTemplateProperty = DependencyProperty.Register(
            "ButtonTemplate", typeof(ControlTemplate), typeof(NumericUpDown), new PropertyMetadata((d, e) => { })
        );
        /// <summary>
        /// ボタンのテンプレートを取得、設定します。
        /// </summary>
        public ControlTemplate ButtonTemplate {
            get {
                return (ControlTemplate)this.GetValue(ButtonTemplateProperty);
            }
            set {
                this.SetValue(ButtonTemplateProperty, value);
            }
        }

        /// <summary>
        /// Value
        /// </summary>
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
            "Value", typeof(double), typeof(NumericUpDown),
            new PropertyMetadata((d, e) => ((NumericUpDown)d).OnValueChanged(e))
        );
        /// <summary>
        /// 値を取得、設定します。
        /// </summary>
        public double Value {
            get {
                return (double)this.GetValue(ValueProperty);
            }
            set {
                this.SetValue(ValueProperty, value);
            }
        }

        /// <summary>
        /// Increment
        /// </summary>
        public static readonly DependencyProperty IncrementProperty = DependencyProperty.Register(
            "Increment", typeof(double), typeof(NumericUpDown), new PropertyMetadata((d, e) => { })
        );
        /// <summary>
        /// 増減値を取得、設定します。
        /// </summary>
        public double Increment {
            get {
                return (double)this.GetValue(IncrementProperty);
            }
            set {
                this.SetValue(IncrementProperty, value);
            }
        }

        /// <summary>
        /// Maximum
        /// </summary>
        public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(
            "Maximum", typeof(double), typeof(NumericUpDown), new PropertyMetadata((d, e) => { })
        );
        /// <summary>
        /// 最大値を取得、設定します。
        /// </summary>
        public double Maximum {
            get {
                return (double)this.GetValue(MaximumProperty);
            }
            set {
                this.SetValue(MaximumProperty, value);
            }
        }

        /// <summary>
        /// Minimum
        /// </summary>
        public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(
            "Minimum", typeof(double), typeof(NumericUpDown), new PropertyMetadata((d, e) => { })
        );
        /// <summary>
        /// 最小値を取得、設定します。
        /// </summary>
        public double Minimum {
            get {
                return (double)this.GetValue(MinimumProperty);
            }
            set {
                this.SetValue(MinimumProperty, value);
            }
        }

        /// <summary>
        /// DoublePlaces
        /// </summary>
        public static readonly DependencyProperty DoublePlacesProperty = DependencyProperty.Register(
            "DecimalPlaces", typeof(int), typeof(NumericUpDown), new PropertyMetadata((d, e) => { })
        );
        /// <summary>
        /// 表示する少数桁数を取得、設定します。
        /// </summary>
        public int DoublePlaces {
            get {
                return (int)this.GetValue(DoublePlacesProperty);
            }
            set {
                this.SetValue(DoublePlacesProperty, value);
            }
        }

        /// <summary>
        /// Valueプロパティの値が変更された時に呼び出されます。
        /// </summary>
        public event RoutedEventHandler ValueChanged;
        /// <summary>
        /// ValueChangedイベントを呼び出します。
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnValueChanged(DependencyPropertyChangedEventArgs e) {
            if(valueTextBox != null) {
                valueTextBox.Text = FormatValue((double)e.NewValue);
            }
            if(ValueChanged != null) {
                ValueChanged(this, new RoutedEventArgs {
                    Source = this
                });
            }
            OnPropertyChanged(new PropertyChangedEventArgs("Value"));
        }

        /// <summary>
        /// 標準的なコンストラクタ
        /// </summary>
        public NumericUpDown() {
            this.DefaultStyleKey = typeof(NumericUpDown);
            // デフォルト値
            this.Width = 100;
            this.Height = 26;
            this.Background = DefaultBackground;
            this.ButtonWidth = 24;

            this.Value = 0;
            this.Increment = 1;
            this.Maximum = 100;
            this.Minimum = 0;
            this.DoublePlaces = 2;
        }

        public override void OnApplyTemplate() {
            base.OnApplyTemplate();

            upButton = (ButtonBase)this.GetTemplateChild("upButton");
            downButton = (ButtonBase)this.GetTemplateChild("downButton");
            valueTextBox = (TextBox)this.GetTemplateChild("valueTextBox");
            buttonsPane = (FrameworkElement)this.GetTemplateChild("buttonsPane");

            upButton.Click += upButton_Click;
            downButton.Click += downButton_Click;

            valueTextBox.Text = FormatValue(this.Value);
        }

        protected override Size ArrangeOverride(Size finalSize) {
            var margin1 = upButton.Margin;
            var margin2 = downButton.Margin;

            upButton.Height = downButton.Height =
                finalSize.Height / 2 - ((margin1.Top + margin1.Bottom) + (margin2.Top + margin2.Bottom));

            valueTextBox.Width = this.Width - buttonsPane.Width - 2;

            return base.ArrangeOverride(finalSize);
        }

        /// <summary>
        /// 指定した値を書式設定します。
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        [DebuggerStepThrough]
        string FormatValue(double value) {
            return value.ToString("f" + this.DoublePlaces.ToString());
        }

        /// <summary>
        /// [▲]ボタンがクリックされた時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void upButton_Click(object sender, RoutedEventArgs e) {
            if(this.Value + this.Increment > this.Maximum) return;

            this.Value += this.Increment;
        }

        /// <summary>
        /// [▼]ボタンがクリックされた時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void downButton_Click(object sender, RoutedEventArgs e) {
            if(this.Value - this.Increment < this.Minimum) return;

            this.Value -= this.Increment;
        }

        /// <summary>
        /// <see cref="INotifyPropertyChanged.PropertyChanged"/>
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        /// <summary>
        /// PropertyChangedイベントを呼び出します。
        /// </summary>
        /// <param name="e">イベント引数</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) {
            if(PropertyChanged != null) PropertyChanged(this, e);
        }
    }
}

Value」プロパティとかは本当はdecimal型で実装したかったけど、何故かXAML側からdecimal型の値が設定できなかった(コンバータが用意されていない?)ので、仕方なくdouble型にしておいた。

ソース

動作デモ