2022/03/11

一定桁数の入力後、Enter無しで次セルへ移動




1.背景

ネットでの買い物でカード払いの時には、ブラウザ上でカード番号(4桁 × 4ヶ所=16桁の数字)を入力する画面があると思います。その時の入力方法は「4桁毎にEnterキーを押すこと無く、次の桁へカーソルが移動」して行きます。
PCなどの有償ソフトウェアをインストールする場合も、大文字小文字を含んだ長い英数字の入力を求められる事があります。

この入力方法(Enterを押さなくても一定の桁数になったら次のセルへ移動)をExcelで再現しようとすると、結構大変そうです。「ユーザーがタイプした値を受取る」→「記憶しながら個数を数える」→「設定の数に達したらまとめてセルに吐き出す」→「次のセルに移動」のような流れにでもなるでしょうか。
しかし考えてみたら、これは「個数を数える」部分を除けば、Excelの編集モードで行っているPC側の作業そのものです。言ってみれば、Excelを自分で作るようなものです。ワークシートやマクロ・関数を使って、自分の作業を効率良くするのがExcelの目的だとすれば、その作業はもはやExcelマクロでやるものでは無い気がします。

そこで今回は、セルそのもので桁数を数えている訳ではありませんが、それと似た形で実現させたものを紹介します。
Excelのシート上にはActiveXコントロール等を置くことが出来、そのコントロールの中にはユーザーがタイプした値を取得できるものがあります。 そのコントロール(TextBox)を使って、目的の機能を達成させようとしています。
なお今回紹介のものは単独では役に立ちません。何かのシステムの一部分の参考になれば嬉しいです。

2.システム概要

ボタンをクリックし、システムを起動(図2-1の①)すると、まず「セル位置・桁数・順番」を変数として登録②します。その情報は、事前にマクロ内に記述しておきます。
そのあと、先頭セルをマクロ側からセル選択③すると、SelectionChangeイベントが発生し、そのセル上にTextBoxを作成します。このTextBoxが「ユーザーの入力枠」となります。
システム起動と入力TextBox作成
図2-1

作られたTextBoxにユーザーがキー入力していき、設定した桁数(図2-2は、3桁設定の枠に「123」と入れる途中の、最後の「3」を入力する直前を表している)の値を入れると、設定の3桁の値がセルに反映されます。反映後、1番目のセルの上にあったTextBoxは削除され、2番目のセル(図2-2ではD2セル)にセル選択が移動すると共に、そのセル上に新たな入力用のTextBox⑤が作られます。
設定桁数を入力すると、次のセルへ移動
図2-2

図2-3でF2セルが最後の入力セルだとします。そのセル上のTextBoxに設定の桁数が入力⑥されると、セルに入力値が反映され、帰着セル(このセル位置も、事前にマクロ内に記述)に移動⑦し、入力が完了します。
最後のセル入力完了すると、帰着セルへ戻る
図2-3

このように 3桁+5桁+4桁の枠であれば「123012340012」等と、EnterキーやTABキーを使うこと無く12桁を連続入力することが出来ます。なおボタンを押して、先頭セルから入力している最中は、シート保護を掛けて「違うセルには入れない」ようにしています。
また、入力できる文字として英数記号はOKですが、スペースはNGとしています。また半角のみになるようにTextBox上ではIMEを切っています。

また図2-4のように、入力途中で「入力中止」という意味の「ESCキー」を押した⑧場合は、途中までの入力値は破棄して帰着セルに移動⑨します。
入力途中でESCキーを押すと入力終了
図2-4

入力終了後、値を修正したい時には、図2-5のように「修正したいセルをクリック⑩」します。すると、そのクリックしたセル上に再度TextBoxが表示され、かつ入力済みの値がTextBox上に編集モードのように入ります⑪。
修正で途中のセルに入る事は可能
図2-5

バックスペースや矢印キー等を使って値を修正し、設定した桁数になる⑫と、次の順番のセルに移動⑬して編集モードのTextBoxが表示されます。
修正後は設定した次のセルへ移動
図2-6

例えば「F2セルは編集の必要無し」のため、次に行きたい意思として「Enterキー」や「矢印キー」を押す方は多いと思います。しかし、Enterキーや矢印キーでは次セルへの移動は出来ません。ESCキーで入力を終了させるか、またはスペースキー等を押すことで次へ次へと入力済みのセルを移動させることが出来ます。
なお、連続入力が目的ですので、図2-4以降の「途中で入力中止」や「途中の値を修正」というのは邪道な機能なのかもしれません。

3.プログラムの流れ

今回システムは、シート上の「ボタン」からスタートします。スタートすると、まずSelectionChangeイベントを有効にさせた後、先頭の入力セルにセル選択を移動させます。
するとSelectionChangeイベントが発生し、選択しているセルに被せる様に「TextBox」を作成します。また作成すると同時に、作成したTextBoxのKeyPressイベントを有効にします。
ユーザーがそのTextBoxに文字を入力するとKeyPressイベントが発生し、そのイベントプロシージャ内で「TextBoxに入力された文字数」をカウントします。設定された文字数に達すると、TextBoxに記入された文字列をセル側に書き出し、TextBoxを削除します。そして、次のセルへセル選択を移動させます。
移動させるとSelectionChangeイベントが発生し、TextBoxを新たに作り・・・という工程を繰り返します。
プログラムの流れ
図3-1

なお、設定している最後のセルに達したら、終了します。また、どのセルを入力セルにしどの順番にするか、各セルの桁数(文字数)を何文字にするかはマクロ内に記載されており、スタートのボタンをクリックした時に設定値がロード(メモリー上に記憶)されます。

4.入力用シート(Sheet1)

入力シート上に必要なのは、図4-1の右側のように、ユーザーに入力してもらうセル位置と順番、そして入力桁数(半角の文字数)です。「今回サンプル」では図4-1のように、黄色背景のセルを入力セルとしています。

なお図4-1の赤吹き出しのように、起動ボタンの下に「TextBoxを1個」隠してあります。これは、図5-1の3行目で「MsForms.TextBox型のオブジェクトをWithEvents宣言」しているのですが、事前にTextBoxが存在しないと、エラーが出るためです。またエラーが出ないまでも、起動後最初に作ったTextBoxはTBオブジェクトとして登録されない様子で、「1番目の入力TextBoxが未制御状態(桁数に達しても次のセルに移動せず、ESCでもTextBoxが消えてくれない)になる」のです。
一度実行したファイルを保存すれば次からは正常に動作します(TextBoxが存在した情報がExcel本体かファイル本体に残るようです)が、初回からでも正常に動くように、ダミーのTextBoxを置いています。
(もしかしたら不十分かもしれませんので、エラーとなる場合は一度保存をしてみて下さい。)
入力用シート
図4-1

これをまとめると、図4-2のようになります。
順番セル番地入力桁数
1B23桁
2D25桁
3F24桁
図4-2

なお、今回は入力セルを連続セルにしていませんが、連続した隣同士でも、また縦方向に並べてもOKです。但し、シート間を跨ぐ入力には対応させていません。また、最後に戻ってくるセル(帰着セル)は、A3セルとしました。

また、シート上の「ボタン」には、図6-2の「systemStartプロシージャ」を登録してあります。このプロシージャが言わばシステム起動プロシージャになります。

5.クラスモジュール(Class1)

今回システムは、シートモジュール上のみでも作れそうだったのですが、TextBoxを作ったり消したりしますし、元のシート上に今回とは無関係のTextBoxが有ったりするとTextBoxの番号の指定が面倒になるとの事から、作ったTextBoxを別な名前のObjectとして管理し、そのイベントをクラス上で取得することとしました。
同時に(ついでに?)SelectionChangeイベントもクラス上に置くことで、モジュールの無い入力シートにすることが出来ますので、この方が汎用性が高まるのではないか と考えました。

クラスモジュール宣言部では、図5-1のようにWithEventsの宣言をしています。
今回はSelectionChangeイベントを取得する為に「Application」のWithEventsと、TextBoxのKeyPressイベントを取得するための「MSForms.TextBox」のWithEventsの2種を宣言しています。
  1. '========== ⇩(1) WithEventsの宣言 ============
  2. Public WithEvents App As Application
  3. Public WithEvents TB As MSForms.TextBox
図5-1

2行目「Public WithEvents App As Application」では、オブジェクト変数Appを「Application(=Excel)」の型として宣言しています。この後の図6-2の48行目でオブジェクト生成することで、図5-2が有効になり「Application(=Excel)のSheetSelectionChangeイベント」が使えるようになります。

3行目「Public WithEvents TB As MSForms.TextBox」は、オブジェクト変数TBを「MSForms.TextBox」の型として宣言しています。これは図6-5の74~75行目で、選択したセル上に作るTextBoxを指します。そして、図6-5の80行目でオブジェクト生成することで、図5-3が有効になり「TextBoxのKeyPressイベント」が使えるようになります。
なお、TextBoxが存在しない状態では「MSForms.TextBox」型が認識されないみたいなので、シート上のボタンの下に「ダミーのTextBox」を配置しています。

「Application(=Excel)のSheetSelectionChangeイベント」が図5-2になります。
  1. '========== ⇩(2) SheetSelectionChangeイベント ============
  2. Private Sub App_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
  3.  Call Sel_Cng(Sh, Target(1))
  4. End Sub
図5-2

Excel内のどこかでセル選択位置が変わると、このイベントが呼び出され、7行目「Call Sel_Cng(Sh, Target(1))」で図6-5を呼び出します。引数として、変更されたセル選択位置のワークシート(Sh)と、セル選択位置(Target)を渡します。なお複数セルを選択した時でも、その左上の「単一セル」を渡すように「Target(1)」としています。

「TextBoxのKeyPressイベント」が図5-3になります。
  1. '========== ⇩(3) TextBoxのKeyPressイベント ============
  2. Private Sub TB_KeyPress(ByVal KeyAscii As MSForms.ReturnInteger)
  3.  Call TB_KP(KeyAscii)
  4. End Sub
図5-3

このイベントは、図6-5の74~75行目で作られたTextBoxを編集中に、ユーザーがキーを押したときに呼び出されます。そして12行目「Call TB_KP(KeyAscii)」で、図6-6を呼び出します。その時に引数として渡すのが「KeyAscii」で、これは「キーを押したときに、TextBox内に入力される文字のコード」です。
すぐ下の「よりみち」の「違い2」でも説明していますが、「最後にタイプした文字がまだTextBoxに表示されない内にKeyPressイベントが発生」しますので、最後の文字もセルに反映させるためには、受け取った「最後の文字」を処理系に渡してあげる必要がある、引数に割り当てています。

寄り道
KeyPressと似たイベントとして「KeyDown」「KeyUp」があります。キーが押された時に発生する順番は、およそ「KeyDown」→「KeyPress」→「KeyUp」のようです(詳しく調べていませんが、キーにより逆転するものがあります)。
しかし、この3つのイベントは、「キーを押した・離した」というような単純なものでは無いようです。

まず違いの1つ目は、押されたキーを示す第一引数の違いです。
KeyPressイベント:KeyAscii
KeyDown・KeyUpイベント:KeyCode
違いは引数の名前だけでは無く、KeyAsciiが「キーが押されたときに入力される文字のコード」であり、KeyCodeが「キーボードのキーそのものに割り当てられている値」です。1つのキーには大文字小文字・記号など複数の意味を持たせており、どの意味にするかは「Shiftキー」「Ctrlキー」「Altキー」を押しながらタイプしたか、で割り当てています。そのため、第一引数KeyCodeだけではタイプされた文字を特定できないために、KeyDown・KeyUpイベントでは第二引数「Shift」も使って、文字を特定できるようにしています。

違いの2つ目は、キー入力した文字がTextBoxに表示されるタイミングです。キーを押してから離すまでに3つのイベントが発生しますが、その順序は図5-4のようになるようです。

キーイベントの発生順番
図5-4

つまりどのイベントを使うかで、TextBox上の文字列を「TextBox.Valueで取得(KeyUpイベント)」できるか、「TextBox.Value + 引数のキーコードから計算した文字(KeyDown、KeyPressイベント)」と計算をするか が変わります。

違いの3つ目は、反応するキーが異なることです。
色々なキーを押してみて、3つのイベントが受け取るか否か、またその時に受け取るコード(第一引数の値)を調べたのが図5-5です。尚このデータは、自分のノートPCで調べたものなので、他のPCと異なっている場合があるかもしれません。御了承下さい。また図5-5は、全て半角状態での値です。
KeyDownKeyPressKeyUp
a(小文字)659765
A(大文字)656565
1(数字)494949
BS88
Tab99
Enter1313
Shift1616
Ctrl1717
Alt1818
ESC2727
スペース323232
3737
3838
3939
4040
Del4646
漢字キー229243/244
図5-5

空の場所は「TextBox内で、そのキーを押しても反応しない」ことを意味しています。一見してKeyPressイベントは「操作に使うキーでは、ほとんど反応しない」ことが分かります。

今回システムで、3つのイベントのどれを使うのが良いのか迷い、色々試してみました。その結果、正規の方法(1番目のセルから、順番に入力していく)では、どのイベントでもほぼ同じ(何文字目かを数える部分と、入力された文字列をセルに書き込む部分のコードが少し異なる程度)でした。
しかし入力完了後、遡って値を編集する機能を考えると、3つのイベントで下記のようなデメリットがありました。
KeyDown:編集に使用するキー(BS、Delなど)でもイベントが反応してしまい、すぐに次のセルに移動してしまう。
そのため、全てのコードを制御か否かに振り分けなくてはいけなそう。
KeyPress:Enterや矢印で反応しない為、そのまま次のセルへ行く場合は、スペースや文字キーをタイプする必要有り。
KeyUp:正規入力途中で中断したい時のESCキーに反応しないため、中断キーに他のキーを割り当てる必要有り。
どのイベントもデメリットがありますが、今回は「編集時そのまま次のセルに移動するのにEnterや矢印キーを使う」のをあきらめるのが最もコードを簡単にできるため、KeyPressイベントを採用しました。

6.標準モジュール(Module1)

6-1.定数・変数の宣言

標準モジュールの宣言部(先頭)では、システム内で使用する変数の宣言を行っています。
  1. '========== ⇩(4) 変数の宣言 ============
  2. Dim X As New Class1      '←イベント取得用のクラス
  3. Dim TX As OLEObject      '←入力の為のTextBox
  4. Dim inputSh As Worksheet   '←入力するワークシート
  5. Dim Rarray() As Variant    '←入力セルとその順番が入った配列
  6. Dim Runion As Range     '←入力セルの結合体
  7. Dim EndCell As Range     '←終了後に移動するセル位置
  8. Dim order As Long       '←入力する順番
図6-1

16行目「Dim X As New Class1」は、クラスモジュール(Class1)型のインスタンス「X」を宣言します。
17行目「Dim TX As OLEObject」は、セル上に作成する「入力用のTextBoxオブジェクト」です。

19行目「Dim inputSh As Worksheet」は、入力するワークシートの変数inputShを宣言しています。
20行目「Dim Rarray() As Variant」は、入力するセル位置・順番・入力桁数の情報を入れる配列です。
21行目「Dim Runion As Range」は、入力するセルをUnionメソッドで一体のRange型にしたものです。
22行目「Dim EndCell As Range」は、入力終了した後、最後に戻ってくるセル位置(帰着セル)です。

24行目「Dim order As Long」は、何番目の入力セルの作業をしているかを表す値です。

6-2.システム設定

シート上のボタンから呼び出されるプロシージャが図6-2です。
  1. '========== ⇩(5) システム初期設定 ============
  2. Public Sub systemStart()
  3.  Set inputSh = ThisWorkbook.Sheets("sheet1")
  4.  ReDim Rarray(1 To 3, 1 To 2)
  5.  Rarray(1, 1) = "B2": Rarray(1, 2) = 3
  6.  Rarray(2, 1) = "D2": Rarray(2, 2) = 5
  7.  Rarray(3, 1) = "F2": Rarray(3, 2) = 4
  8.  Set EndCell = inputSh.Range("A3")
  9.  Set Runion = inputSh.Range(Rarray(1, 1))
  10.  For order = 2 To UBound(Rarray, 1)
  11.   Set Runion = Union(Runion, inputSh.Range(Rarray(order, 1)))
  12.  Next order
  13.  Runion.ClearContents
  14.  Runion.NumberFormatLocal = "@"
  15.  Runion.HorizontalAlignment = xlCenter
  16.  Set X.App = Application
  17.  inputSh.Protect UserInterfaceOnly:=True
  18.  inputSh.EnableSelection = xlNoSelection
  19.  EndCell.Select
  20.  inputSh.Range(Rarray(1, 1)).Select
  21. End Sub
図6-2

29行目「Set inputSh = ThisWorkbook.Sheets("sheet1")」は、ユーザーが入力をするワークシートを変数inputShに設定します。

31行目「ReDim Rarray(1 To 3, 1 To 2)」は、入力セル位置・順番・入力桁数を入れる配列のサイズを設定しています。「入力セルの数=配列の行数」とします。例えば31行目のサイズだと行方向のサイズは「1 To 3」ですので、入力セルは「3つ」という事になります。
配列Rarrayの列方向のサイズ「1 To 2」は固定で、1列目に「セル位置」を、2列目に「入力桁数」を代入します。
また、入力の順番は「配列の行の順番」としています。

33~35行目は、今回サンプルのセル情報を代入しています。
33行目「Rarray(1, 1) = "B2": Rarray(1, 2) = 3」では、1番目に「B2」セルで「3」桁を入力
34行目「Rarray(2, 1) = "D2": Rarray(2, 2) = 5」では、2番目に「D2」セルで「5」桁を入力
35行目「Rarray(3, 1) = "F2": Rarray(3, 2) = 4」では、3番目に「F2」セルで「4」桁を入力
という意味の情報を、配列Rarrayに入れています。

37行目「Set EndCell = inputSh.Range("A3")」は、入力終了した後、最後に戻ってくるセル位置(帰着セル)を「A3セル」に設定しています。なお、このEndCellを入力セルのどれかに設定してしまうと、「入力が終了しない」「ESCキーを押しても元の状態に戻らない」ことになりますので注意して下さい。

39~42行目は、配列Rarrayの入力セル情報に基づいて、入力セルを集合させたRange型を作成しています。この「セル範囲」は、入力セルの書式設定や、SelectionChangeイベントで「入力セルに入ったか否かの判断」に使用します。
作り方はUnionメソッドでセルを結合していくのですが、先頭セルは事前に設定しておく必要がありますので、39行目「Set Runion = inputSh.Range(Rarray(1, 1))」で、まず「先頭セルをセル範囲Runionに設定」しています。

そして40行目「For order = 2 To UBound(Rarray, 1)」で、カウンタ変数orderを2番目のセルから最後のセルまでを処理します(1番目のセルは、既にRunionに代入済みです)。
41行目「Set Runion = Union(Runion, inputSh.Range(Rarray(order, 1)))」では、それまでに結合した「セル範囲Runion」に、次のセル範囲「inputSh.Range(Rarray(order, 1))」をUnionで結合し、それをまた「セル範囲Runion」にします。
追加するセルの位置を表す文字列は、配列Rarrayの「order」番目に入っていますので「Rarray(order, 1)」で表すことができますので、その文字列をRangeのカッコ内で指定しています。

なお、Rarrayを1行のみの配列とした場合(=入力セルが1つのみ)は、40行目の「UBound(Rarray, 1)」が「1」になってしまうため、41行目は実行されずに、39行目で「1番目のセルのみ」がRunionに設定された状態になります。
1セルのみを入力セルとするのでは、今回システムを使用する意味が半減してしまいますが、問題無く動作します。

44行目「Runion.ClearContents」では、これから入力するセル(Runion)の値をクリアします。
45行目「Runion.NumberFormatLocal = "@"」は、入力セルの書式を「文字列」に設定します。これは、たとえば「0123」など先頭文字がゼロの数字を入力する場合、TextBox上では文字列ですので入力できるのですが、標準書式のセルに書き込むと数値と見なされて「先頭のゼロが表示されない」ことになってしまうからです。
46行目「Runion.HorizontalAlignment = xlCenter」は、表示を中央揃えにしています。

48行目「Set X.App = Application」は、クラス(Class1)で設定したオブジェクト変数AppにApplication(=Excel)を設定します。これにより、図5-2の「App_SheetSelectionChangeイベント」が有効になり、どこかのセル選択を変更した時に反応するようになります。

50行目「inputSh.Protect UserInterfaceOnly:=True」は、入力シートのシート保護を有効にし、ロック解除しているセル以外には入力不可となります。なおパラメータの「UserInterfaceOnly:=True」は、図6-3のように「マクロ側からは操作可」という設定です。今回システムでは、TextBoxに入力された値をマクロ側からセルに書き込む必要があるためです。
UserInterfaceOnlyパラメータ
 値内容
Falseマクロからも、画面上からも変更することができない(既定値)
True画面上からの変更は保護されるが、マクロからの変更は保護されない
図6-3

なお手動で、入力セルを「ロック解除」設定にしていると、ユーザーの操作によっては「TextBoxを無視して、入力セルに直接(設定桁数以外の)文字を入力出来てしまう」状態を作り出すことが可能です。
ですので51行目「inputSh.EnableSelection = xlNoSelection」で、入力シートを「全セルに対し、ユーザー操作不可」にしています。なお、EnableSelectionの設定値は図6-4のように3種あります。
EnableSelectionの設定値
定数 値内容
xlNoRestrictions0どのセルの選択も許可
xlUnlockedCells1ロック解除のセルに限り選択が許可(初期値)
xlNoSelection-4142シートのすべてのセルの選択が禁止
図6-4

なお、この設定はシートに残ってしまいますので、他の目的でEnableSelection値を設定している場合は、システム起動時に設定値を記憶しておき、Unprotect時に元に戻す等の処理が必要になります。

53~54行目では、入力セルに選択セルを移動させています。
まず53行目「EndCell.Select」で、一旦「帰着セル」を選択した後、54行目「inputSh.Range(Rarray(1, 1)).Select」で「1番目の入力セル」を選択しています。この一旦帰着セルを経由する意味は、システム起動時にもし「1番目のセル」が選択されていたとすると、「選択セルの移動が無い」ために「SelectionChangeイベントが発生しない→TextBoxが作られない」という不具合を避けるためです。そのためにも、帰着セル≠入力セルである必要があります。

54行目の実行により、セル位置が1番目の入力セルに移動し、図5-2のApp_SheetSelectionChangeイベントを経由して、図6-5が呼び出されます。

6-3.SelectionChangeでの処理

選択セル位置が変更されると、まず図5-2のApp_SheetSelectionChangeイベントが呼び出され、その中から図6-5が呼び出されます。引数としては、選択されたシート(Sh)とセル(Target)を受け取ります。
なおApp_SheetSelectionChange内で、選択されたセル範囲を「左上の単一セル(1番目のセル)」に絞っていますので、ここで受け取るTargetは「単一セル範囲」となります。
  1. '========== ⇩(6) セル選択時の処理 ============
  2. Public Sub Sel_Cng(Sh As Object, Target As Range)
  3.  Dim i As Long    '←設定した入力情報の行位置
  4.  If Not Sh.Name = inputSh.Name Then Exit Sub
  5.  If Intersect(Target, Runion) Is Nothing Then Exit Sub
  6.  For i = 1 To UBound(Rarray, 1)
  7.   If Not Intersect(Target, Range(Rarray(i, 1))) Is Nothing Then
  8.    order = i
  9.    Exit For
  10.   End If
  11.  Next i
  12.  If Not TX Is Nothing Then TX.Delete
  13.  Set TX = inputSh.OLEObjects.Add(ClassType:="Forms.TextBox.1", _
  14.       Left:=Target.Left, Top:=Target.Top, Width:=Target.Width, Height:=Target.Height)
  15.  TX.Object.Value = Target.Value
  16.  TX.Activate
  17.  Set X.TB = TX.Object
  18.  TX.Object.IMEMode = fmIMEModeDisable
  19. End Sub
図6-5

62行目「If Not Sh.Name = inputSh.Name Then Exit Sub」では、選択されたシート(Sh.Name)が「入力シート(inputSh.Name)」か否かを調べています。入力シートで無い場合は、「Exit Sub」で抜け出して処理を中止します。
62行目を抜けると、選択されたセルは「入力シート上のセル」ということになります。
63行目「If Intersect(Target, Runion) Is Nothing Then Exit Sub」は、選択されたセル(Target)が入力セル(Runion)の一部か否かを調べています。Intersectメソッドは、引数に指定したセル範囲同士が「重なっている範囲」を返しますので、「Is Nothing」という事は「選択されたセルは、入力セルの一部では無い」ことになります。その時は「Exit Sub」で抜け出して処理を中止します。
なお、Intersectメソッドの引数に「異なるシートのセル範囲」を指定するとエラーとなります。今回は62行目で入力シート以外を排除済みですので、エラーは出ません。

65~70行目では、「選択されたセルは、入力セルの何番目のセルか」を計算しています。
65行目「For i = 1 To UBound(Rarray, 1)」で、カウンタ変数iを入力セル情報の配列Rarrayの行数分だけ回します。
66行目「If Not Intersect(Target, Range(Rarray(i, 1))) Is Nothing Then」では、配列内に書き込んだセル範囲と、選択セル(Target)が重なっているか否かをIntersectメソッドで調べます。重なっている(=i番目の入力セルと判明する)場合は67~68行目を実行します。
ここでIntersectを使わずに、「If Target.Address = Range(Rarray(i, 1)).Address Then」と「アドレスの文字列を比較」する方法でもOKです。

「i番目の入力セル」と判明した為、67行目「order = i」では、モジュール変数orderにi値を代入します。この変数orderは、TextBoxの処理(図6-6)で使用します。
何もしなければ、For~Nextは次の入力セル情報との比較を続けるのですが、既に「何番目かは判明してしまった」ので残りの処理は不要です。ですので68行目「Exit For」でFor~Nextを抜け出し、その後の処理に移ります。

なお、65~70行目で「何番目の入力セルか」を調べているのですから、63行目の「入力セル範囲にいるか」の調査は省略しても良いのかもしれません。しかし、入力セル以外をセル選択する可能性が多くあるならば、毎回For~Nextを回すのもどうか と思い63行目は残しました。

72行目「If Not TX Is Nothing Then TX.Delete」は、「TextBoxが既に存在する場合は、まず削除」します。これは、スタートボタンにより1番目のセルから順に入力していくモードでは必要ありません。しかしSelectionChangeイベントが有効+シート保護が無効(どのセルも選択可)の時に、「手動で入力セルをクリックし、そのセルのTextBoxが表示された状態で、異なる入力セルをクリック」すると、最初のTextBoxが残ったまま次のTextBoxが表示されてしまう現象が発生します。
一旦複数のTextBoxが作成されてしまうと、削除するには「開発」タブ→「コントロール」グループ→「デザインモード」をONにしてからTextBoxを手動削除するしか手がありません。

74~75行目は、選択したセルの上に被せる形でTextBoxを作る工程です。
74行目「Set TX = inputSh.OLEObjects.Add(ClassType:="Forms.TextBox.1", _」では、入力シート上にTextBoxを作成し、そして75行目「Left:=Target.Left, Top:=Target.Top, Width:=Target.Width, Height:=Target.Height)」と、Left・Top・Width・Heightを選択セル(Target)と全く同じ位置・サイズにしています。

なお、ここでは「Sheet.OLEObjects.Add(ClassType:="Forms.TextBox.1")」としましたが、似たようなものに「Sheet.TextBoxes.Add」があります。これによって作られるTextBoxは「ActiveXコントロール」のTextBoxでは無く「挿入タブ→テキストグループ→テキストボックス」で作られるTextBoxのようです(見た目で判断)。それに「データの重みを考慮したComboBox入力補助」で紹介した「作成できるコントロール」の中にもTextBoxは入っていません。
そのためか「Sheet.TextBoxes.Add」で作ったTextBoxをクラスの変数TBに設定することが出来ませんでした。何か手はあるのかもしれませんが、今回は74行目のやり方でTextBoxの作成を行いました。

77行目「TX.Object.Value = Target.Value」では、セルに入っていた値をTextBox側にコピーしています。スタートボタンにより1番目の入力セルから入力していく場合は、スタート時点で入力セルの値を全て削除(図6-2の44行目)していますので、77行目は意味の無いコードになりますが、入力終了後に修正しようとして入力セルに入った時には「編集モード」として機能をします。
78行目「TX.Activate」では、作成したTextBoxにカーソルを移します(SetFocus的なもの)。このコードにより、タイプした値が、セルからセルへと連続入力できることになります。
寄り道
サンプルファイルで何回か入力してみて、気が付いた方もいるかもしれません。テキストボックスに「1つ前に入力した値が一瞬だけ見える」のです。タイミング的には、78行目「TX.Activate」の実行直後です。
見える値は、設定した桁よりも1つ少ないので、そのセル上にあったTextBoxのKeyPressイベントが発生した時点でのTextBoxのValue値のようです。
3ヶ所の入力セルがあれば、3ヶ所とも異なる元の値が見えますので、「DeleteしたはずのTextBoxが持っていた値」では無く、「そのセル上に存在していたTextBoxの1つ前の値」のようです。

Endステートメントで終了させても「見える」のですが、ファイルを一旦閉じたり、Excelを終了したりすると「見えない」ことまでは分かりました。しかし、それ以上は掴めませんでした。
本当は「一瞬でも、1つ前の値を見せたく無い」のですが、残念です。

80行目「Set X.TB = TX.Object」は、作成したTextBoxをクラス(Class1)のオブジェクト変数TXに設定しています。これにより、TextBoxを編集中にキーを押すと、図5-3のTB_KeyPressイベントが発生することになります。
82行目「TX.Object.IMEMode = fmIMEModeDisable」は、作成したTextBoxに入力する際には「IMEモードを半角のみ」の状態にしています。
このIMEモード設定をおこなわずに全角文字を指定文字数だけ入力することは可能ですが、全角ですので確定させるためにEnterキーを押さなくてはならず、今回テーマの「Enterを押さずに・・・」に当てはまらなくなります。

6-4.KeyPressでの処理

図6-5で作られたTextBoxでキー入力すると、図5-3のTB_KeyPressイベントが発生し、そこから呼び出されるのが図6-6です。引数としては、入力した文字のコード(K)を受け取ります。
  1. '========== ⇩(7) TextBox上でキーを押した時の処理 ============
  2. Public Sub TB_KP(K As MSForms.ReturnInteger)
  3.  Select Case K
  4.   Case 27    '←ESCキー
  5.    TX.Delete
  6.    Set TX = Nothing
  7.    EndCell.Select
  8.    inputSh.Unprotect
  9.    Exit Sub
  10.   Case 32    '←スペースキー
  11.    K = 0
  12.    Exit Sub
  13.  End Select
  14.  If Len(TX.Object.Value) >= Rarray(order, 2) - 1 Then
  15.   inputSh.Range(Rarray(order, 1)).Value = Left(TX.Object.Value & Chr(K), Rarray(order, 2))
  16.   TX.Delete
  17.   Set TX = Nothing
  18.   order = order + 1
  19.   If order <= UBound(Rarray, 1) Then
  20.    inputSh.Range(Rarray(order, 1)).Select
  21.   Else
  22.    EndCell.Select
  23.    inputSh.Unprotect
  24.   End If
  25.  End If
  26. End Sub
図6-6

まず、KeyPressイベントが受け取れるのは、図5-5で示したように「英数文字」+「ESCキー」「スペース」です。「英数文字」は、セルに入力するために使用しますが、ESCキーは今回システムでは「入力を途中で止める」という意味に使用しています。また、スペースを「英数字の1つ」と言い切るのは難しく、通常は「無視する文字」の扱いだと思います。
ですので、まずは何のキーが来たかをチェックする必要があります。

そこで、89行目「Select Case K」では、入力された文字コード(k)を調べます。

まず、ESCキーのAsciiコードは「27(10進数)」ですので、90行目「Case 27」以下で「ESCキーの時の処理」を行います。
91行目「TX.Delete」で「TextBoxを削除」し、92行目「Set TX = Nothing」でオブジェクト変数TXを解放します。わざわざオブジェクト変数も解放している理由は、TextBoxが存在しているか否かを調べる手がオブジェクト変数で行っているためです。TX.Deleteで実体を削除してもオブジェクト変数TXはそのまま残ってしまうため、92行目での解放が必要です。

94行目「EndCell.Select」では、図6-2の37行目で設定しておいた帰着セル(EndCell)を選択します。
最後に95行目「inputSh.Unprotect」で、シート保護を解除し、96行目「Exit Sub」で処理を終わります。

次に、スペースキーのAsciiコードは「32(10進数)」ですので、97行目「Case 32」以下で「スペースキーの時の処理」を行います。
98行目「K = 0」は、「TextBoxに入力してきたAsciiコードをゼロにする」という意味ですが、AsciiコードゼロはNull文字ですので「文字入力をキャンセル」するという意味になります。
入力しなかった事にするので、その後の処理をしてはダメですので、99行目「Exit Sub」で処理を終わります。

ESCキー・スペースキー以外の場合は、102行目「If Len(TX.Object.Value) >= Rarray(order, 2) - 1 Then」で、TextBoxに入力されている文字数を数えます。
KeyPressイベントを使用してますので、図5-4のように「TextBox内(TX.Object.Value)には最後にタイプした文字が含まれていない」状態です。ですので設定文字数が4桁(4文字)の場合、文字数(Len(TX.Object.Value))が「4 - 1 = 3」になった時に「設定文字数になった」と判断できます。設定文字数に達したら、104~115行目を実行します。

104行目「inputSh.Range(Rarray(order, 1)).Value = Left(TX.Object.Value & Chr(K), Rarray(order, 2))」は、入力用TextBoxの下に隠れているセルに、TextBoxの値を貼り付けています。
現在操作しているセルは、セル情報の入った配列Rarrayの「order行目」のセルですので「inputSh.Range(Rarray(order, 1))」となります。
式の右辺は貼り付ける値です。まず、最後の桁が入力されていないTextBoxの値は「TX.Object.Value」です。そのTextBoxの値の最後尾に、引数として受け取ったAsciiコードの文字を結合します。Asciiコードを文字に変換するにはChr関数を使用しますので「TX.Object.Value & Chr(K)」となります。
ただし、設定桁数よりも多く入ってしまう場合があります。それはコピペで長い桁数の値をTextBoxに貼り付けられた場合です。実はこの方法を使われると、全角文字も入ってしまいますが、今回システムではそれを防止する機能は入れていません。
とりあえずは「設定桁数だけをセルに入れる」のを目的に、Left関数を使って、桁数「Rarray(order, 2)」になるように文字列を切り取ってからセルへ貼り付けています。

セルへの貼り付けが完了したら、入力のためのTextBoxは不要ですので、106行目「TX.Delete」でTextBoxを削除し、107行目「Set TX = Nothing」で、オブジェクト変数を解放します。
そして、次の入力セルに移動するために、108行目「order = order + 1」で変数orderを「+1」増やします。

110行目「If order <= UBound(Rarray, 1) Then」では、増やしたorde変数の値が「入力セル情報の配列の行数(=入力セルの数)」よりも小さいかを確認しています。
まだ最後の入力セルに達していない(=次の入力セルが存在する)場合は、111行目「inputSh.Range(Rarray(order, 1)).Select」で、次(=order番目)のセルへ移動をさせます。

一方、次の順番が入力セル数よりも大きければ「今処理をしたTextBoxが最後のセル」だった事になります。その時は113行目「EndCell.Select」で帰着セル(最後に戻るセル)へ移動させ、114行目「inputSh.Unprotect」でシート保護を解除します。

7.最後に

現在のセル位置が入力セルの何番目かを共通変数orderで管理したつもりなのですが、入力後、途中の入力セルからでも編集可能な仕様としたため、「自分の中でも順番を計算」し「共通変数でも順番の値を保持」しているという二重管理状態になってしまった気がします。もう少し上手な管理が必要かもしれません。



一定桁数の入力後Enter無しで次セルへ移動(it-077.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。