[C#][WPF] 数値のみ入力可能なTextBoxを作る (カスタムコントロール)

更新:

WindowsFormアプリには、NumericUpDownという数値入力に特化したコントロールがあります。しかしWPFには同じような機能を持つコントロールが存在しません。WPFでこれを解決するには、2つの方法があります。1つはWindowsFormsHostクラスを使ってWPF上でWindowsFormコントロールをホストできるようにする方法です。もう1つはWPFの既存コントロールを継承して新たな機能を付けたコントロールを作る方法です。

今回は後者の、新しくコントロールを作る方法を試してみます。

作るコントロールの機能

数値のみ入力可能
入力できる文字を半角数値に制限します。半角アルファベットや2バイト文字は入力できません。例外として、先頭のマイナス記号だけは入力できるようにします。
デフォルト値を指定可能
テキストボックスに何も入力されていない状態でフォーカスが外れた時は、デフォルト値が自動挿入されるようにします。
値変化のイベントは数値が確定したときのみ発動
キーボードから文字入力をした場合は、1文字入力ごとにイベントを発動(=TextBoxのTextChangedイベント)しないようにします。エンターキーを押したとき、またはフォーカスが外れた時に発動するようにします。
数値増減ボタンを大きく表示
NumericUpDownコントロールのUPボタン/DOWNボタンは小さいので大きくします。表示スタイルを手軽に変えられるのがWPFのいいところです。
数値増減ボタンを長押し中は、値を増減させる
NumericUpDownコントロールと同じく、UPボタン/DOWNボタンを押している間は繰り返し数値を加算するようにします。

ソースコード全文

コントロールのソースコード全文です。UI部分を含め全てプログラムコードで書いています。

C#

using System;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;

class NumericTextBox : Border {
	//値変化のイベントハンドラ
	public delegate void ValueChangedHandler(object sender, int value);
	public event ValueChangedHandler ValueChanged;

	//最小値
	public int Min {
		get { return (int)GetValue(MinProperty); }
		set { SetValue(MinProperty, value); }
	}
	public static readonly DependencyProperty MinProperty = DependencyProperty.Register(nameof(Min), typeof(int), typeof(NumericTextBox), new PropertyMetadata(0, CheckMinMax));
	//最大値
	public int Max {
		get { return (int)GetValue(MaxProperty); }
		set { SetValue(MaxProperty, value); }
	}
	public static readonly DependencyProperty MaxProperty = DependencyProperty.Register(nameof(Max), typeof(int), typeof(NumericTextBox), new PropertyMetadata(100, CheckMinMax));

	private static void CheckMinMax(DependencyObject d, DependencyPropertyChangedEventArgs e) {
		var con = d as NumericTextBox;
		con.Value = con.RoundValue(con.Value);
	}
	//デフォルト値
	public int Def {
		get { return (int)GetValue(DefProperty); }
		set { SetValue(DefProperty, value); }
	}
	public static readonly DependencyProperty DefProperty = DependencyProperty.Register(nameof(Def), typeof(int), typeof(NumericTextBox), new PropertyMetadata(0));
	//増加値
	public int Increment {
		get { return (int)GetValue(IncrementProperty); }
		set { SetValue(IncrementProperty, value); }
	}
	public static readonly DependencyProperty IncrementProperty = DependencyProperty.Register(nameof(Increment), typeof(int), typeof(NumericTextBox), new PropertyMetadata(1));
	//現在値
	public int Value {
		get { return (int)GetValue(ValueProperty); }
		set { SetValue(ValueProperty, value); }
	}
	public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(int), typeof(NumericTextBox), new PropertyMetadata(0, ValuePropertyChanged, RoundValue));
	private static object RoundValue(DependencyObject d, object baseValue) {
		var con = d as NumericTextBox;
		return con.RoundValue((int)baseValue);
	}
	private static void ValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
		var con = d as NumericTextBox;
		con.textBox.Text = con.Value.ToString();
		con.ValueChanged?.Invoke(con, con.Value);
	}

	private DateTime? pressedTime;
	private TextBox textBox;
	private DispatcherTimer timer;

	public NumericTextBox() {

		var grid = new Grid();
		textBox = new TextBox();

		var btns = new Button[2];
		for (var i = 0; i < 2; i++) {
			var btn = new Button();
			btn.Tag = (i == 0) ? -1 : 1;
			btn.Content = (i == 0) ? "-" : "+";
			btn.BorderThickness = new Thickness(0);
			btn.PreviewMouseLeftButtonDown += Btn_PreviewMouseLeftButtonDown;
			btn.PreviewMouseLeftButtonUp += Btn_PreviewMouseLeftButtonUp;
			btns[i] = btn;
		}
		textBox.BorderThickness = new Thickness(0);
		textBox.HorizontalContentAlignment = HorizontalAlignment.Right;
		textBox.MouseWheel += TextBox_MouseWheel;
		textBox.PreviewTextInput += TextBox_PreviewTextInput;
		InputMethod.SetIsInputMethodEnabled(textBox, false);
		CommandManager.AddPreviewExecutedHandler(textBox, ExecuteCommandEvent);
		textBox.LostFocus += TextBox_LostFocus;
		textBox.KeyDown += TextBox_KeyDown;
		textBox.ContextMenu = null;

		grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(10, GridUnitType.Pixel) });
		grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
		grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(10, GridUnitType.Pixel) });
		grid.Children.Add(btns[0]);
		grid.Children.Add(textBox);
		grid.Children.Add(btns[1]);
		Grid.SetColumn(btns[0], 0);
		Grid.SetColumn(textBox, 1);
		Grid.SetColumn(btns[1], 2);

		this.BorderThickness = new Thickness(1);
		this.BorderBrush = new SolidColorBrush(Color.FromRgb(160, 160, 160));
		this.Child = grid;
		timer = new DispatcherTimer();
		timer.Interval = new TimeSpan(0, 0, 0, 0, 30);
		timer.Tick += Timer_Tick;
	}



	private void TextBox_KeyDown(object sender, KeyEventArgs e) {
		if (e.Key == Key.Enter) {
			CheckValue();
		}
	}

	private void TextBox_LostFocus(object sender, RoutedEventArgs e) {
		CheckValue();
	}

	private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) {
		e.Handled = true;
		if (e.Text == "-" && textBox.CaretIndex == 0 && !Regex.IsMatch(textBox.Text, "^-")) {
			e.Handled = false;
		} else if (Regex.IsMatch(e.Text, @"[0-9]") && !(textBox.CaretIndex == 0 && textBox.Text.Contains("-"))) {
			e.Handled = false;
		}
	}

	private void TextBox_MouseWheel(object sender, MouseWheelEventArgs e) {
		var val = e.Delta > 0 ? Increment : -Increment;
		AddValue(val);
	}

	private void Btn_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
		var val = Increment * (int)(sender as Button).Tag;
		AddValue(val);
		pressedTime = DateTime.Now;
		timer.Tag = val;
		timer.Start();
	}

	private void Btn_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) {
		timer.Stop();
	}

	private void Timer_Tick(object sender, EventArgs e) {
		if (pressedTime != null) {
			var ts = (TimeSpan)(DateTime.Now - pressedTime);
			if (ts.TotalMilliseconds < 500) {
				return;
			}
			pressedTime = null;
		}
		var val = (int)timer.Tag;
		AddValue(val);
	}

	private void ExecuteCommandEvent(object sender, ExecutedRoutedEventArgs e) {
		if (e.Command == ApplicationCommands.Paste) {
			e.Handled = true;
		}
	}

	private int RoundValue(int value) {
		return Math.Max(Math.Min(value, Max), Min);
	}

	private void AddValue(int val) {
		Value = Value + val;
	}

	private void CheckValue() {
		Value = int.TryParse(textBox.Text, out var v) ? v : Def;
		textBox.Text = Value.ToString();
	}


}