[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();
}
}