すほーいログ

140字以上のものを残しておくために開設しました

0~Fまでの16進数文字のみを受け付け2桁ごとに空白を挿入し文字数に制限のないTextBoxの作成 (Visual C#)

16進文字列のみを受け付けて,”1F 33 25 CA"みたいな感じで表示してくれるTextBoxが欲しいという話です.

マイコンとPCを通信させるあたりで使うと言えば何となくお察しいただけるのではないかなと思います.

こんな感じで動いてくれます

f:id:Fragmented:20181123011854g:plain

目次

経緯

PCからマイコン等のシリアルデバイスにASCIIじゃなくて16進を送りたくなるので,練習がてら内製ツールでも作るかって感じのことをやりだしたんですね.

そんな中タイトル通りの機能が欲しくなったわけです.

ほしい機能があったらまずコードが落ちてないか探すんですが,

  • 「0~Fまでの16進数文字のみを受け付け」というだけならTextBox.KeyPressイベント内で目的外の文字を捨てれば事足りる

  • 「2桁ごとに空白を挿入する」という部分までは↓の記事で事足りるが,マスクで設定されるため文字数に制限があるmy-web-note.com

とまぁ大体こんな感じで,あとはString(16進文字列)→byte[]の方法とかみたいな感じで軽く探したうちではなかったわけです. 結局作るしかないかなーってことで作りました.

要件

  • TextBoxで0 ~9およびA ~F,a ~fまでのキーボード入力のみを受け付ける

  • 16進数2桁ごとに空白を挿入して表示する

  • 桁数が無制限かつ動的に変化する(実際はTextBox.MaxLengthで制限されるが気にしないことにする)

  • Backspace,Deleteで文字列の編集が行える

  • 範囲選択での編集機能は無し

  • Ctrl+VやShift+Insertは無視する

  • コントロールを右クリックしての貼り付けは無効化する

  • 通常のTextBoxを使う(MaskedTextBoxではない)

環境

プロパティとソースコード

プロパティ

要件で述べた通り,

  • Ctrl+VやShift+Insertは無視する
  • コントロールを右クリックしての貼り付けは無効化する

となるようShortcutsEnabledをfalseに設定します.

このプロパティがfalseだと

  • 全てのショートカットキー
  • 右クリックでのコンテクストメニュー

が無効化されます.

そもそも何故これが要件に入るのか,ですが,単純にクリップボード貼り付けに対するマスクは煩雑になりそうだからです

あとIMEを有効にして日本語入力されると処理が面倒なのでしまう事故が無いように,ImeModeをDisableにします.

ソースコード

テンプレートから空のC#フォームを作成し,テキストボックスを置いただけだとこんな感じ.

クリックでソースコード展開

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace HexOnlyTextBox
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.textBox1.ShortcutsEnabled = false;
            this.textBox1.ImeMode = System.Windows.Forms.ImeMode.Disable;
            this.textBox1.KeyDown += new System.Windows.Forms.KeyEventHandler(this.textBox_KeyDown_FormattedHex);
        }

        private void textBox_KeyDown_FormattedHex(object sender, KeyEventArgs e)
        {
            if (!sender.GetType().IsSubclassOf(typeof(TextBoxBase)))
            {
                return;
            }
            else
            {
                e.SuppressKeyPress = true;  // 通常KeyDown後に発生するKeyPressイベントを発生させない
                var tBox = (TextBoxBase)sender;
                var caret = tBox.SelectionStart;  // キャレットの位置を取得(入力フォームの ’I’)
                var str = tBox.Text;
                if (e.KeyCode == Keys.Delete || e.KeyCode == Keys.Back)
                {
                    // 削除対象(Delならstr[caret], BSならstr[caret-1])がスペースの場合2文字分消去することに留意
                    if (e.KeyCode == Keys.Delete && caret < str.Count())
                    {
                        if (str[caret] == ' ')
                        {
                            if (caret + 1 < str.Count())
                            {
                                str = str.Remove(caret, 2);
                            }
                        }
                        else
                        {
                            str = str.Remove(caret, 1);
                        }
                    }
                    else if (e.KeyCode == Keys.Back && caret > 0)
                    {
                        if (str[caret - 1] == ' ')
                        {
                            if (caret - 1 > 0)
                            {
                                str = str.Remove(caret - 2, 2);
                                caret -= 2;
                            }
                        }
                        else
                        {
                            str = str.Remove(caret - 1, 1);
                            caret -= 1;
                        }
                    }
                }
                else if ((Keys.D0 <= e.KeyCode && e.KeyCode <= Keys.D9)
                       || (Keys.A <= e.KeyCode && e.KeyCode <= Keys.F)
                       || (Keys.NumPad0 <= e.KeyCode && e.KeyCode <= Keys.NumPad9))
                {
                    char keyChar;
                    if ((Keys.D0 <= e.KeyCode && e.KeyCode <= Keys.D9)
                     || (Keys.A <= e.KeyCode && e.KeyCode <= Keys.F))
                        keyChar = (char)e.KeyCode;  // 0~Zの場合はキーコードとASCIIコードが同じなのでそのままキャスト
                    else
                        keyChar = (char)(e.KeyCode - 48);  // NumPadの場合はASCIIコードの数値と合わせるために計算
                    str = str.Insert(caret, keyChar.ToString());
                    if (caret + 1 < str.Count())
                    {
                        if (str[caret + 1] == ' ')
                            caret += 2;
                        else
                            caret += 1;
                    }
                    else
                    {
                        caret += 1;
                    }
                }
                else if (e.KeyCode == Keys.Left  // キャレットの移動に使う文字はKeyPressイベントを許可
                      || e.KeyCode == Keys.Right
                      || e.KeyCode == Keys.Home
                      || e.KeyCode == Keys.End)
                {
                    e.SuppressKeyPress = false;
                }

                // 一度空白を削除し,2文字ごとに空白を挿入
                str = str.Replace(" ", "");
                List<char> istr = new List<char>();
                for (int i = 0; i < str.Count(); i++)
                {
                    istr.Add(str[i]);
                    if (i % 2 == 1)
                    {
                        istr.Add(' ');
                    }
                }

                tBox.Text = new string(istr.ToArray());
                tBox.SelectionStart = caret;
            }
        }
    }
}

TextBox.KeyDownに自作したイベントハンドラを追加して処理します. 自作したイベントハンドラはTextBoxBaseを継承していれば動くはずなので,TextBox,RichTextBoxなら動作してくれるはず. 一般的にキー入力に対する処理はTextBox.KeyPressが多く用いられるようですが,デリートキーを検出する必要があるためKeyDownを用いています.

16進数へ変換可能か判別する

textBox1.Text.Split(' ')で返される文字列配列の要素数と末尾要素の要素数をチェックすれば16進へ変換可能か判別できます.

クリックでソースコード展開

var res = textBox1.Text.Split(' ');
if (res.Last().Count() == 1)
{
    // 入力された16進文字列が1桁
}
else if (res.Count() == 1)
{
    // textBox1が空
}
else if (res.Last().Count() == 0)
{
    // 変換可能
}

サンプルプログラム

github.com

あとがき

モノ自体は悪くないと思います.満足のいく出来というやつですね.

実は最初は,リンクを張ったサイトのものをベースに,MaskedTextBoxを使って桁数を変えるようなものを作ってて,かなりいいところまで行ったもののDeleteとBackSpace周りの動作がうまくいかず,結局TextBoxで実装しなおしたという経緯があります.

MaskedTextBoxのマスク設定をうまく使うと16進文字列がすべて2桁入力になっているかとかがMaskedTextBox.MaskCompletedで取得できて面白いかなと思ってやってたものの,変な挙動踏んで数時間溶かして嫌になったのでやめました.

そのうちまた時間があれば挑戦して,うまくいけば公開すると思います.

もしかして正規表現使って秒で終わる処理だったりしないかと震えて寝ます.

2018/12/25 編集 TextBox,RichTextBox両方に対応しつつイベントハンドラ化することで処理をスッキリさせました