C#で小数がある数値入力専用のTextBoxを作る(WinForms)

下記仕様のTextBoxを作成します。

  • 数字と小数点のみを入力可能とする

  • 入力可能桁数の制限を行う。

  • 入力可能桁数は整数部と小数部を分けて指定する

  • フォーカスが当たっていないときは3桁毎のカンマ区切りで表示する

  • 小数がない場合、整数のみを表示する

  • 小数は存在する桁だけを表示する。

クリップボードからペーストされた時のチェックは未対応です。
Validatingイベント、Validatedイベント等でのチェック処理や入力値を用いる処理直前での値チェックは必要不可欠です。

数字、小数点のみを入力可能にするため、
KeyPressイベントを下記のように実装します。

private void textBox2_KeyPress(object sender, KeyPressEventArgs e)
{
    try
    {
        //バックスペースと小数点使用可能とする
        if (e.KeyChar == 0x08 || e.KeyChar == '.')
        {
            return;
        }

        //数字キー以外の入力をキャンセルする
        if (e.KeyChar < 0x30 || e.KeyChar > 0x39)
        {
            e.Handled = true;
        }
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine(ex);
    }
}

これから入力可能桁数の制限を実装していきますが、小数がない場合と比べて考慮が必要な点があります。

既に小数点付きの数字が入力されている場合に小数点を削除されると、
それまで小数部だった桁が整数部へ移動することになるため、
このときに整数の溢れ分をまとめて削除する必要があります。

この場合、カーソル位置にある数字を削除してしまうと違和感が発生するため、
上桁から削除していくこととします。

桁数の制限はTextChangedイベントで行う予定ですが、
TextChangedイベントでは文字が削除されたのかどうかを判断することができません。

前の文字列を保存しておいて比較する手もあるかと思いますが、
今回はDeleteキーやBackSpaceが押されたかどうかを判断することにします。

KeyPressイベントではDeleteキーを検知できませんので、
KeyDownイベントまたはKeyUpイベントを使用します。

ただし、イベントが呼び出される順番が KeyDown→TextChanged→KeyUp なので
KeyDownイベントを使用します。

bool isDelete = false;

private void textBox2_KeyDown(object sender, KeyEventArgs e)
{
    isDelete = (e.KeyCode == Keys.Delete || e.KeyCode == Keys.Back);
}

TextChangedイベントで入力可能桁数を制限します。

//整数部桁数
private readonly int LENGTH_INT_PART = 7;
//小数部桁数
private readonly int LENGTH_DECIMAL_PLACES = 2;

private void textBox2_TextChanged(object sender, EventArgs e)
{
    var txtBox = sender as TextBox;

    txtBox.TextChanged -= textBox2_TextChanged;

    try
    {
        if (!string.IsNullOrWhiteSpace(txtBox.Text))
        {
            var value = txtBox.Text;
            //整数と小数を分割する
            string[] values = value.Split('.');

            int currentPoint = 0;
            switch (values.Length)
            {
                case 1:
                    //------------------------------
                    //整数部のみで構成されている場合
                    //------------------------------
                    //整数部の最大桁数を超えた場合、今回の入力値を無効にする
                    if (value.Length > LENGTH_INT_PART)
                    {
                        //DELETEやBackSpaceによる削除で桁あふれが発生しているなら
                        //小数点の削除によるものなので、先頭桁から削除する
                        if (isDelete)
                        {
                            txtBox.Text = value.Substring(value.Length - LENGTH_INT_PART);

                            //カーソル位置を移動
                            if (currentPoint - (values.Length - LENGTH_INT_PART) >= 0)
                            {
                                txtBox.SelectionStart = currentPoint -
                                	(values.Length - LENGTH_INT_PART);
                            }
                            else
                            {
                                txtBox.SelectionStart = 0;
                            }
                        }
                        else
                        {
                            //入力後の現カーソル位置を取得
                            currentPoint = txtBox.SelectionStart;

                            //カーソル位置の前の1文字が今回入力された文字、
                            //よって、それを省いた文字列に編集する
                            var left = value.Substring(0
                                , currentPoint > LENGTH_INT_PART
                                    ? LENGTH_INT_PART : currentPoint - 1);
                            var right = left.Length >= LENGTH_INT_PART
                                ? "" : value.Substring(currentPoint);
                            txtBox.Text = left + right;

                            //カーソル位置を入力前の位置に戻す
                            txtBox.SelectionStart = currentPoint - 1;
                        }
                    }
                    break;

                case 2:
                    //----------------------------------
                    //整数部+小数部で構成されている場合
                    //----------------------------------
                    //入力後の現カーソル位置を取得
                    currentPoint = txtBox.SelectionStart;

                    //今回の入力値が"."の場合、小数点を基準点として桁あふれ分を除外する
                    if (value.Substring(currentPoint - 1, 1) == ".")
                    {
                        //=== 整数部の処理 ===
                        var intPart = values[0];
                        if (values[0].Length > LENGTH_INT_PART)
                        {
                            intPart = values[0].Substring(LENGTH_INT_PART - values[0].Length);
                        }

                        //=== 小数部の処理 ===
                        var decimalPart = values[1];
                        if (values[1].Length > LENGTH_DECIMAL_PLACES)
                        {
                            decimalPart = values[1].Substring(0, LENGTH_DECIMAL_PLACES);
                        }

                        //整数と小数を結合
                        if (values[0].Length > LENGTH_INT_PART || values[1].Length > LENGTH_DECIMAL_PLACES)
                        {
                            txtBox.Text = string.Format("{0}.{1}", intPart, decimalPart);

                            //小数点入力時なら小数点の後ろにカーソルをセット
                            txtBox.SelectionStart = value.IndexOf(".") + 1;
                        }
                    }
                    else
                    {
                        //=== 整数部の処理 ===
                        var intPart = values[0];
                        if (values[0].Length > LENGTH_INT_PART)
                        {
                            //カーソル位置の前の1文字が今回入力された文字、
                            //よって、それを省いた文字列に編集する
                            var left = values[0].Substring(0
                                , currentPoint > LENGTH_INT_PART
                                    ? LENGTH_INT_PART : currentPoint - 1);
                            var right = left.Length >= LENGTH_INT_PART
                                ? "" : values[0].Substring(currentPoint);

                            //桁数調整後の整数部文字列
                            intPart = left + right;
                        }

                        //=== 小数部の処理 ===
                        var decimalPart = values[1];
                        if (values[1].Length > LENGTH_DECIMAL_PLACES)
                        {
                            //整数部と小数点を除いたときのカーソル位置を算出
                            var tempPoint = currentPoint - values[0].Length - 1;
                            //カーソル位置の前の1文字を除外する
                            var left = values[1].Substring(0
                                , tempPoint > LENGTH_DECIMAL_PLACES
                                    ? LENGTH_DECIMAL_PLACES : tempPoint - 1);
                            var right = left.Length >= LENGTH_DECIMAL_PLACES
                                    ? "" : values[1].Substring(tempPoint);

                            //桁数調整後の小数部文字列
                            decimalPart = left + right;
                        }

                        //整数と小数を結合
                        if (values[0].Length > LENGTH_INT_PART || values[1].Length > LENGTH_DECIMAL_PLACES)
                        {
                            txtBox.Text = string.Format("{0}.{1}", intPart, decimalPart);

                            //カーソル位置を入力前の位置に戻す
                            txtBox.SelectionStart = currentPoint - 1;
                        }
                    }

                    break;

                default:
                    //"."で文字列を分割したときに3以上になるのなら、
                    //既に"."が存在しているのに今回"."が入力されたことを示す
                    //よって、今回の入力を無効にしてしまう。
                    {
                        currentPoint = txtBox.SelectionStart;
                        var left = value.Substring(0, currentPoint - 1);
                        var right = value.Substring(currentPoint);
                        txtBox.Text = left + right;
                        txtBox.SelectionStart = currentPoint - 1;
                    }
                    break;
            }

            //先頭が小数点なら先頭に 0 を入れる
            if (txtBox.Text.StartsWith("."))
            {
                currentPoint = txtBox.SelectionStart;
                txtBox.Text = "0" + txtBox.Text;
                txtBox.SelectionStart = currentPoint + 1;
            }
        }
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine(ex);
    }
    finally
    {
        txtBox.TextChanged += textBox2_TextChanged;
    }
}

フォーカスが外れたときに3桁毎のカンマ区切りで表示させるために
Validated イベントを下記のように実装します。

private void textBox2_Validated(object sender, EventArgs e)
{
    var txtBox = sender as TextBox;
    txtBox.TextChanged -= textBox2_TextChanged;
    try
    {
        if (!string.IsNullOrWhiteSpace(txtBox.Text))
        {
            string fmt = "#,0." + "#".PadRight(LENGTH_DECIMAL_PLACES, '#');
            txtBox.Text = decimal.Parse(txtBox.Text.Replace(",", "")).ToString(fmt);
        }
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine(ex.Message);
    }
    finally
    {
        txtBox.TextChanged += textBox2_TextChanged;
    }
}

フォーカスが当たったときに3桁毎のカンマ区切りを解除します。
Enter イベントを下記のように実装します。

private void textBox2_Enter(object sender, EventArgs e)
{
    var txtBox = sender as TextBox;
    txtBox.TextChanged -= textBox2_TextChanged;
    try
    {
        txtBox.Text = txtBox.Text.Replace(",", "");
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine(ex.Message);
    }
    finally
    {
        txtBox.TextChanged += textBox2_TextChanged;
    }
}

最後にテキストボックスで右クリックメニューを表示させないようにするため、
フォームのコンストラクタで下記のようにコードを追加します。

public Form1()
{
    InitializeComponent();

	//これを追加
    textBox2.ContextMenu = new ContextMenu();
}