2023/04/21

複数チェックボックスのON-OFF情報を単一値で管理




複数のチェックボックスが存在するアプリで、ユーザーが「どのチェックボックスをONにしたか、OFFにしたか」という情報を保存する方法を考えた場合、1つとしては、各チェックボックスの情報(例えばON=True、OFF=False)を1つずつ保存する方法があります。

メリットとしては、データそのものから「どのチェックボックスがONかOFFか」が分かりますし、一見するとプログラムも簡単そうです。しかし「1つ1つのチェックボックス」に対してデータの取得保存を行わなくてはならず、同じような処理コードをチェックボックスの数だけ繰り返す必要が出てきます。
なお保存にBoolean型を使えば、必要容量はそれほど気にする事は無いかもしれません。

今回は、各チェックボックスの情報を「2進数の各ビット」に格納する事で、1つの値として取得保存する方法を紹介します。
イメージとしては図01のように、チェックボックスの状態を「レ点有り=True=1」「レ点無し=False=0」に置き換え、順番に並べることで完成する「2進数」を「10進数」に変換した値を保存しています。
複数チェックボックスON-OFF情報の管理状況
図01


値取り出してチェックボックスに反映する時も、10進数を2進数に変換し、0(=True)か1(=False)かを読み取りチェックボックスのValue値を変更させています。

1.チェックボックスのON-OFF値を取得保存するツールの概要

今回のツールは、チェックボックスの数で2種類に分けています。「少ない(9個以下)」場合は図02で、「多い(10個以上)」場合が図03です。見掛けはチェックボックスの数が違うだけですが、2進数~10進数の変換に使用している「WorksheetFunctionの進数変換関数」が9個(「よりみち」参照)までしか対応できないため、内部の処理コードを変える必要があるのです。
(多い方に統一する事は可能ですが、WorksheetFunctionを使った方が簡単に実現できるというメリットがあります。)

サンプルファイル」の上段が、チェックボックスが「少ない(9個以下)」場合です(図02)。
ツール概要(チェックボックスが9個以下)
図02


スクロールバー①を操作することで、黄色セル②にスクロールバーの値が表示されます。設定範囲は「ゼロ~31」となります。値の設定後、ボタン1をクリック③するとフォーム(UserForm1)が表示されます。フォーム上にはチェックボックスが5個配置されており、①②で設定した値に対応して「チェックボックスのON-OFFが反映」されます。
フォーム上のチェックボックスのON-OFFを手動で変更④した後、終了ボタンをクリックすると、変更したチェックボックスのON-OFF状態がワークシート上の黄色セル②に戻されます。
イメージとしては、黄色セル②をデータ保管場所(ワークシートやデータベーステーブル等)として見立てている感じです。

チェックボックスが「多い(10個以上)」場合が図03です(サンプルファイルの下段)。
操作方法は、少ない場合(図02)と何も変わりません。但しフォーム上には10個のチェックボックスがありますので、スクロールバー⑤の設定範囲は「ゼロ~1023」となります。
ツール概要(チェックボックスが10個以上)
図03


寄り道
WorksheetFunctionの進数変換の関数(例えばBin2Dec)の入力値制限は「10文字(2進数だと10ビット)」です。上記では「9個まで」と書きましたが、実際には「チェックボックス10個まで」を扱う事ができます。但し10ビット目は符号となるため、プラスとマイナスの値が混ざった値で管理しなければならなくなります。
2進数のマイナス計算方法については「2進数のマイナス値について」で紹介しますが、方法として「2の補数」と「1の補数」の2種類があります。そのためマイナス値を使ってON-OFF情報を保管した場合には、そのデータを処理する側に制限が出来てしまい、最悪は間違った処理をすることも考えられます。

また、現在の進数変換関数は10文字という制限(10ビット目が符号)ですが、将来もっと文字数の多い関数が出てこないとも限りません。その時、例えば「12ビット目が符号」となると、下記のように情報量が変わってしまいます。
 10ビットでの「-10」=   "11 1111 0110"
 12ビットでの「-10」="1111 1111 0110"
ビット数が変わっても、元の情報は保持しているので正しく処理すれば良いのですが、勘違いの元ともなります。

そのため今回は、符号ビットを除いた9ビット(=チェックボックス9個分)を進数変換関数の上限としています。

2.進数の変換

2ー1.対応するワークシート関数(WorksheetFunction)

今回、図17内で使用している「Dec2Bin」と「Bin2Dec」について先に説明します。
コンピュータで良く扱う進数は「2進数」「8進数」「10進数」「16進数」の4つです。各進数から異なる進数へ変換する関数は、図04のように全部で12種類です。
今回使用している「10進数 → 2進数」への変換関数「Dec2Bin」を例にした構文は以下の様になり、また全ての進数変換関数について図04で一覧にまとめました。

 2進数(文字列) = Application.WorksheetFunction.Dec2Bin( 10進数 , [出力する2進数の文字数] )

InputOutput関数引数戻り値
第一引数第二引数
制限値の範囲内容値の範囲
2進数8進数Bin2Oct10文字(10bit)
最上位bitは符号
2の補数を使用
正:0~1 1111 1111
負:10 0000 0000~11 1111 1111
出力文字数String正:0~777
負:7 777 777 000~7 777 777 777
10進数Bin2Dec正:0~+511
負:-512~-1
16進数Bin2Hex出力文字数正:0~1FF
負:FF FFFF FE00~FF FFFF FFFF
8進数2進数Oct2Bin10文字(30bit)
最上位bitは符号
2の補数を使用
正:0~777
負:7 777 777 000~7 777 777 777
出力文字数String正:0~1 1111 1111
負:10 0000 0000~11 1111 1111
10進数Oct2Dec正:0~3 777 777 777
負:4 000 000 000~7 777 777 777
正:0 ~ +536,870,911
負:-536,870,912 ~ -1
16進数Oct2Hex出力文字数正:0~1FFF FFFF
負:FF E000 0000~FF FFFF FFFF
10進数2進数Dec2Bin出力が10文字
以内となる
正負整数
正:0 ~ +511
負:-512 ~ -1
出力文字数String正:0~1 1111 1111
負:10 0000 0000~11 1111 1111
8進数Dec2Oct正:0 ~ +536,870,911
負:-536,870,912 ~ -1
出力文字数正:0~3 777 777 777
負:4 000 000 000~7 777 777 777
16進数Dec2Hex正:0 ~ +549,755,813,887
負:-549,755,813,888 ~ -1
出力文字数正:0~7F FFFF FFFF
負:80 0000 0000~FF FFFF FFFF
16進数2進数Hex2Bin10文字(40bit)
最上位bitは符号
2の補数を使用
正:0~1FF
負:FF FFFF FE00~FF FFFF FFFF
出力文字数String正:0~1 1111 1111
負:10 0000 0000~11 1111 1111
8進数Hex2Oct正:0~1FFF FFFF
負:FF E000 0000~FF FFFF FFFF
出力文字数正:0~3 777 777 777
負:40 0000 0000~77 7777 7777
10進数Hex2Dec正:0~7F FFFF FFFF
負:80 0000 0000~FF FFFF FFFF
正:0 ~ +549,755,813,887
負:-549,755,813,888 ~ -1
図04


入力・出力が10進数以外の時を、まず説明します。関数に指定する引数は2つです。
第一引数には「変換前の進数」を指定します。制限は「10文字以内」ですが、出力の進数も「10文字以内」という制限がありますので、「入力・出力とも10文字以内」となるような制限を受けます。
2進数の10文字は10ビット、8進数の10文字は30ビット、16進数の10文字は40ビットですが、その先頭ビットはプラスマイナスの符号を表します(ビットが1の時に負の数)。また「2の補数」によりマイナス値の計算をします。

第二引数には「出力する進数の文字数」を指定します。変換後の進数の文字数が指定文字数より少なかった場合は、先頭側を「ゼロ」で埋めて出力されます。但し、マイナス側は「先頭ビットが1」のために、常にフル(10文字)で出力されます(指定文字数は無視される形になる)。
第二引数は省略可で、省略した場合は「必要な文字数のみ」が出力されます。

入力・出力が10進数の時を説明します。
まず出力側が10進数の時には、関数の第二引数はありません。これは、10進数の場合は「123,456」を「000,123,456」などと先頭にゼロを並べる事をせず、Long型の数値として扱うためだと思われます。
また入力側が10進数の場合、10進数には「文字数制限(≒桁数制限)が無い」ので、出力側の進数はフルの10文字までが可能となります。

なお、図04の中の「8進数」は3桁ごとにスペースで区切り、「2進数」「16進数」は4桁ごとにスペースで区切っています。また「10進数」はカンマで3桁区切りをしています。これは「よりみち」でも説明する「Windowsの電卓アプリ」の表示と合わせています。もしかしたら色々な区切り方があるかもしれませんが、御了承下さい。

2ー2.進数のマイナス値について

今回は関係無いのですが、図04の関数に指定する「2進数・8進数・16進数のマイナス値」について説明しておきます。
第一引数に指定する進数は「最大10文字」「最上位ビットは符号」「マイナスは2の補数を使用」となっています。

2ー2ー1.2進数の場合

このマイナス値をまず2進数で見てみます。
「最大10文字」「最上位ビットは符号」ですので、10文字の割当は図05のようになります。
関数の進数の文字数
図05


次にマイナスは「2の補数」を使います。2の補数とは「足し合わせるとちょうど桁が一つ増える最小の数」という意味ですが、コンピュータ上でマイナス値を求める時に使う場合では、例えば「+1を表す2進数(10桁)」と「-1を表す2進数(10桁)」を足し合わせると、桁が一つ上がって「1000・・・(先頭が1の11桁)」となる事を示します。
求め方としては、図06のように引き算で求める事になります。

マイナスの計算方法(-1の場合)
図06


図06は「-1」を求める計算式ですが、まず「10ビットの最大値(=全てのビットが立つ状態)に1を足したもの」を考えます。最大値に1を足せば、桁があふれて11ビットになり「11ビット目が1、それ以下は全てゼロ」になります(図06の1行目)。
その値から「求めたいマイナス値のプラス側の値(=+1)」を引き算します(図06の2行目)。この結果が「2の補数を使った-1」になります(図06の3行目)。ちなみに2の補数の「2」は、2進数の「2」です。

10ビット目は符号ですので、プラス側の最大値は9ビットまでを全て1にした「01 1111 1111(10進数にすると+511)」になります。このマイナス計算は図07のようになります。

マイナスの計算方法(プラス側最大の511の場合)
図07


ここまでの「最大値+1」から「プラス側の値」を引いた計算の対象から漏れた「10 0000 0000」は、10ビット目にビットが立っているのでマイナス側の「-512」ということになります。
ちなみに、なぜこのような(人間の目には)分かりにくいマイナス値を使うのかですが、例えば「511 - 511 =」という減算をコンピュータが実行する際、2進数だと「01 1111 1111」+「10 0000 0001」という加算に置き換えられ、結果は最上位の桁が繰り上がって「100 0000 0000」となりますが、10ビットしか枠が無いので「先頭の1は無視」されて「00 0000 0000」、つまり「ゼロ」が得られるのです。

なお2の補数との対で「1の補数」もあります。1の補数は2の補数とは異なり「足しても桁があふれず、最も大きい数」になることです。最も大きい数ですので、2進数なら「全てのビットが1」となる数です。計算方法としては図08のようになります。
1の補数を使ったマイナス値計算法
図08


桁あふれしませんので、元の10ビットのままで全て1の値(=10ビットの最大値)から、正の数(図08では +1)を引き算します。この計算は「ビット反転(NOT演算)」で実現できます。

2ー2ー2.8進数数の場合

以上の説明は2進数のものでしたが、8進数も同様です。
8進数では「0~7」の数値が各桁(=各文字)を構成していますが、その1文字の中身としては図09のように「2進数×3ビット」です。そのため図04内でも「10文字30bit」と指定されています。

8進数の表し方
図09


この8進数「30ビット(10文字)」の最上位ビットが符号ですので、図10の一番上のように「残りの29ビットが1」の値がプラス側の最大値になります(8進数で3 777 777 777 、10進数だと+536,870,911)。
またマイナス計算には「2の補数」を使うとの説明から「2進数×30ビット」レベルで計算し、マイナス側の最大値は図10の2段目(10進数で -536,870,912)になります。そして30ビット全てに1が入った時(図10の一番下)が「-1」を表す事になります。
8進数でのマイナス表示
図10


2ー2ー3.16進数の場合

また16進数も同様です。16進数では「0~9、A~F」の16文字を使い、その1文字の中身としては図11のように「2進数×4ビット」です。そのため図04内でも「10文字40bit」と指定されています。

16進数の表し方
図11


16進数も「40ビット(10文字)」の最上位ビットが符号ですので、図12の一番上のように「残りの39ビットが1」の値がプラス側の最大値(10進数だと+549,755,813,887)となり、マイナス側の最大値は図12の2段目(10進数で -549,755,813,888)になります。そして40ビット全てに1が入った時(図12の一番下)が「-1」を表す事になります。
16進数でのマイナス表示
図12


3.ツールの説明

3ー1.ワークシート(今回Sheet1)

3ー1ー1.ワークシート上のコントロール類の配置

シート上には、図13のように「チェックボックス保存値を変更する為のスクロールバー」を2個、フォーム起動用ボタンを2個配置します。
ワークシート上のコントロール類の配置
図13


2つのボタンには、それそれシートモジュールのUF1showプロシージャ、UF2showプロシージャをマクロ登録します。
スクロールバーの設定はシートモジュールの「SBiniプロシージャ」から設定していますが、今回特に実行ボタンは設けていません。一度設定しておけば充分だからです(サンプルファイルでは、実行済みです)。
各スクロールバーでの設定値(=フォームと授受する値)は、A2セル・A4セルに書き込みますので、目立つようにセル背景色を黄色にしています。

3ー1ー2.シートモジュール

3ー1ー2ー1.スクロールバーの初期設定
ワークシート上の2個のスクロールバーの設定を行うのが図14です。スクロールバー配置後に1回だけ実行すればOKです。なお、サンプルファイルでは実行済みです。
  1. '========== ⇩(1) スクロールバーの初期設定 ============
  2. Private Sub SBini()
  3.  With Me.ScrollBar1
  4.   .Min = 0
  5.   .Max = 31
  6.   .Value = 0
  7.   .LinkedCell = "A2"
  8.  End With
  9.  With Me.ScrollBar2
  10.   .Min = 0
  11.   .Max = 1023
  12.   .Value = 0
  13.   .LinkedCell = "A4"
  14.  End With
  15. End Sub
図14


02~07行目は、ScrollBar1(チェックボックスの数が少ない時)の設定を行っています。
03行目「.Min = 0」では、最小をゼロに、04行目「.Max = 31」では最大を31に設定します。今回のUserForm1上のチェックボックスの数は5個ですので、2進数で表すと「1 1111」と31になります。
05行目「.Value = 0」では、初期値をゼロとし、06行目「.LinkedCell = "A2"」では設定した値をA2セルに書き込むようにしています。但し今回は「どのチェックボックスがON-OFFか」の情報を「ゼロ以上の正数」で保存・取得していますのでLinkedCellプロパティが使えますが、もしマイナス値を使う場合はLinkedCellプロパティが使えません(「スクロールバー動的設定時の注意点」参照)ので注意が必要です。

09~14行目は、ScrollBar2(チェックボックスの数が多い時)の設定を行っています。
今回のUserForm2上のチェックボックスの数は10個ですので、2進数で表すと「11 1111 1111」と1023になるので、11行目は「.Max = 1023」としています。
スクロールバー値の出力先はA4セルですので、13行目「.LinkedCell = "A4"」としています。

3ー1ー2ー2.フォームの呼び出し
シート上のボタンをクリックした時に呼び出されるのが図15です。ボタン1(チェックボックスの数が少ない時)の時は21~27行目のUF1showプロシージャを、ボタン2(チェックボックスの数が多い時)の時は30~37行目のUF2showプロシージャを実行します。
  1. '========== ⇩(2) フォーム呼び出し(チェックボックス少ない時) ============
  2. Sub UF1show()
  3.  Dim CB As Range   '←セルRange
  4.  Set CB = Range("A2")
  5.  CB.Value = UserForm1.UFstart(CB.Value)
  6.  Me.ScrollBar1.Value = CB.Value
  7. End Sub
  8. '========== ⇩(3) フォーム呼び出し(チェックボックス多い時) ============
  9. Sub UF2show()
  10.  Dim CB As Long   '←セルの値
  11.  CB = Range("A4").Value
  12.  CB = UserForm2.UFstart(CB)
  13.  Range("A4").Value = CB
  14.  Me.ScrollBar2.Value = CB
  15. End Sub
図15


UF1showとUF2showは内容はほぼ同じですが、操作する対象(変数CB)を「Rangeセル(UserForm1)」と「セル値(UserForm2)」とに変えて作ってみました。最終的な結果はもちろん同じですが、セル値を使った方が「セル自体にアクセスする回数が1回少ない」動きになります。

ボタン1(UF1showプロシージャ)では、まず22行目「Dim CB As Range」で変数CBをRange型で宣言します。そして24行目「Set CB = Range("A2")」で、A2セルを変数CBに設定します。
25行目「CB.Value = UserForm1.UFstart(CB.Value)」では、UserForm1上の「UFstart関数プロシージャ(図17)」を呼び出します。その際引数として「CB.Value(=A2セルの値)」を渡します。
フォーム上で処理を行った後、UFstart関数は「どのチェックボックスがON-OFFかの情報」を数値にして戻してきますので、それを受け取り「CB.Value(=A2セルの値)」に書き込みます。

A2セルの値は変更された可能性があるため、値変更のためのスクロールバーのValue値を26行目「Me.ScrollBar1.Value = CB.Value」で変更します。

ボタン2(UF2showプロシージャ)では、まず31行目「Dim CB As Long」で変数CBをLong型で宣言します。そして33行目「CB = Range("A4").Value」で、A4セルの値を変数CBに代入します。
34行目「CB = UserForm2.UFstart(CB)」では、UserForm2上の「UFstart関数プロシージャ(図22)」を呼び出します。その際引数として「CB(=A4セルの値)」を渡します。
フォーム上で処理を行った後、UFstart関数は「どのチェックボックスがON-OFFかの情報」を数値にして戻してきますので、それを受け取り「変数CB」に代入し、35行目「Range("A4").Value = CB」でA4セル値として書き込みます。

A4セルの値は変更された可能性があるため、値変更のためのスクロールバーのValue値を36行目「Me.ScrollBar2.Value = CB」で変更します。

ちなみにセル値へのアクセスは、以下の通りとなります。
UF1start:25行目(引数用の読み取り)、25行目(戻り値をセル書き込み)、26行目(セル値読み取り)
UF2start:33行目(セルち読み取り)、35行目(セル値書き込み)
今回は処理時間に差が出るほどではありませんが、コードの可読性と併せてアクセス回数にも注意を払う事が大切と思います。

3ー2.チェックボックス数が少ない時(UserForm1)

3ー2ー1.フォーム上のコントロール類

フォーム上には、チェックボックスを5個と、フォーム終了のためのボタンを配置しています。ボタンの表面文字は配置時にプロパティ設定しています。
フォーム上のコントロール類配置
図16


3ー2ー2.フォームモジュール

3ー2ー2ー1.呼び出されるフォーム内関数
シート側プロシージャ(図15の25行目)から呼び出される、フォームモジュール上の関数が図17です。引数として、シート側からの指定の「5個のチェックボックスのON-OFF情報」の値を受け取ります。
  1. '========== ⇩(4) フォーム起動とチェックボックスの処理 ============
  2. Function UFstart(CB As Integer) As Integer
  3.  Dim CBs(1 To 5) As Variant   '←チェックボックスの集合体(配列)
  4.  Dim BitStr As String     '←2進数の文字列
  5.  Dim i As Integer      '←チェックボックスの数
  6.  Set CBs(1) = Me.CheckBox1
  7.  Set CBs(2) = Me.CheckBox2
  8.  Set CBs(3) = Me.CheckBox3
  9.  Set CBs(4) = Me.CheckBox4
  10.  Set CBs(5) = Me.CheckBox5
  11.  BitStr = WorksheetFunction.Dec2Bin(CB, UBound(CBs, 1))
  12.  For i = 1 To UBound(CBs, 1)
  13.   CBs(i).Value = CBool(Mid(BitStr, UBound(CBs, 1) + 1 - i, 1))
  14.  Next i
  15.  Me.Show vbModal
  16.  BitStr = ""
  17.  For i = 1 To UBound(CBs, 1)
  18.   BitStr = -1 * CBs(i).Value & BitStr
  19.  Next i
  20.  UFstart = WorksheetFunction.Bin2Dec(BitStr)
  21. End Function
図17


今回、5個のチェックボックスコントロールを配列の形にして処理する為、52行目「Dim CBs(1 To 5) As Variant」で配列宣言をしています。
そして56行目「Set CBs(1) = Me.CheckBox1」で、CheckBox1を配列CBsにオブジェクトの形で格納していきます。57~60行目も同様に配列CBsに、CheckBox2~5のオブジェクトを格納しています。

62行目「BitStr = WorksheetFunction.Dec2Bin(CB, UBound(CBs, 1))」では、ワークシート関数(図04)の「Dec2Bin」を呼び出します。Dec2Binには2つの引数を渡しますが、1つ目の引数は「10進数の値」ですので、シート側から受け取った変数CBを指定します。

2つ目の引数は「出力する2進数の文字数」です。例えば10進数の「15」は2進数だと「1111」と4ビットになりますが、第二引数に「5」を指定すると「01111」と値の無い先頭部分のビットにゼロを入れて出力してくれます。省略すると、必要のないゼロは省略されます。

今回引数で受け取った値を2進数に変換し、その各ビットの値により64~66行目で「チェックボックスをON-OFF」に設定しますが、「全てのチェックボックスに相当したビットが存在」する事を前提にしていますので、チェックボックスの数(ここでは、 UBound(CBs, 1) を使用)のビット数だけ出力するようにしています。
なお、例えば「1111」と4ビット出力されるのに、第二引数に 3 のように「出力ビット数を下回る値」を指定した場合にはエラーが発生します。また第一引数にマイナス値を指定すると、必ず「10ビットが出力」されますので、第二引数は無視されます。

「Dec2Bin関数」で変換された2進数の文字列は、左辺の変数BitStrに代入されます。その2進数文字列を使って、64~66行目で各チェックボックスへON-OFFを設定していきます。
64行目「For i = 1 To UBound(CBs, 1)」では、カウンタ変数iをチェックボックスの数だけ回します。
65行目「CBs(i).Value = CBool(Mid(BitStr, UBound(CBs, 1) + 1 - i, 1))」では、2進数の各ビットの値を各チェックボックスのValue値に振り分けています。
なお変数BitStrは文字列で、1ビット目(=一番右側のビット)をチェックボックスの1番目、2ビット目をチェックボックスの2番目、・・・という順番にならべていますので、図18のように「一番右側から文字列を切り出す」ようにしています。
2進数の各ビットをチェックボックスのValue値に振り分け
図18


なお、切り出した文字列は「"1"」か「"0"」です。これをValue値である「True」「False」に変換するため、CBool関数を使用しています。CBool関数では「0 はFalse」となりますが、他は全てTrueとなります。

これで、フォーム上のチェックボックスのON-OFF設定が完了しましたので、68行目「Me.Show vbModal」でフォームを起動します。なおvbModalで起動していますが、モードレスで起動してしまうと制御が次のコードに移ってしまい「フォーム上のチェックボックスのレ点変更が、ワークシート上に戻せない」事になります。

フォーム上の「終了ボタン」によりフォームが閉じられる(フォームの右上×印をクリックしても同じ)と、制御は70行目に移ります。
70行目「BitStr = ""」では、まず変数BitStrをクリアします。
71行目「For i = 1 To UBound(CBs, 1)」では、カウンタ変数iをチェックボックスの数だけ回します。
72行目「BitStr = -1 * CBs(i).Value & BitStr」では、各チェックボックスの値をつなげていきます。なおチェックボックスのValue値(CBs(i).Value)は「True」か「False」ですが、そのBoolean値に数値を掛け算することで数値になります。Excelの場合は「True → -1」「False → 0」ですので、「-1 *」を掛けることで「True → +1」「False → 0」になります。

71~73行目のFor~Nextにより「2進数の文字列」が出来ます。75行目「UFstart = WorksheetFunction.Bin2Dec(BitStr)」では、図04の「Bin2Dec」関数を呼出し、その引数に「2進数の文字列」を渡すことで「10進数」の値に変換し、関数プロシージャの戻り値に設定します。
フォームを閉じた後の流れを図で表すと、図19になります。
チェックボックスのValue値を2進数の各ビットにしてシートに戻す
図19


3ー2ー2ー2.フォームの終了
フォーム上の「終了」ボタンをクリックした時に呼び出されるのが図20です。92行目「Unload Me」でフォームを閉じます。
  1. '========== ⇩(5) フォーム終了 ============
  2. Private Sub CommandButton1_Click()
  3.  Unload Me
  4. End Sub
図20


3ー3.チェックボックス数が多い時(UserForm2)

今回紹介しているシステムは、1つのチェックボックスを2進数の1つのビットに変換し、その値を保存するものです。UserForm1のコード(図17)では、ワークシート関数のDec2BinとBin2Decを使用して2進数⇔10進数の変換を行っていますが、この関数で使用できる2進数のビット数は「(符号を除いて)9ビット」です。つまり「9個までのチェックボックス」にしか対応できないことになります。

ですのでチェックボックスが10個以上になる場合は、「2進数⇔10進数」の変換コードは自作する必要があります。

3ー3ー1.フォーム上のコントロール類

フォーム上には、チェックボックスを10個と、フォーム終了のためのボタンを配置しています。ボタンの表面文字は配置時にプロパティ設定しています。
フォーム上のコントロール類
図21


3ー3ー2.フォームモジュール

3ー3ー2ー1.呼び出されるフォーム内関数
シート側プロシージャ(図15の34行目)から呼び出される、フォームモジュール上の関数が図22です。引数として、シート側から「10個のチェックボックスのON-OFF情報」の値を受け取ります。
  1. '========== ⇩(6) フォーム起動とチェックボックスの処理 ============
  2. Function UFstart(CB As Long) As Long
  3.  Dim CBs(1 To 10) As Variant   '←チェックボックスの集合体(配列)
  4.  Dim BitLng As Long       '←チェックボックスON-OFF情報の10進数
  5.  Dim i As Integer       '←チェックボックスの数
  6.  Set CBs(1) = Me.CheckBox1
  7.  Set CBs(2) = Me.CheckBox2
  8.  Set CBs(3) = Me.CheckBox3
  9.  Set CBs(4) = Me.CheckBox4
  10.  Set CBs(5) = Me.CheckBox5
  11.  Set CBs(6) = Me.CheckBox6
  12.  Set CBs(7) = Me.CheckBox7
  13.  Set CBs(8) = Me.CheckBox8
  14.  Set CBs(9) = Me.CheckBox9
  15.  Set CBs(10) = Me.CheckBox10
  16.  For i = 1 To UBound(CBs, 1)
  17.   CBs(i).Value = CBool((2 ^ (i - 1)) And CB)
  18.  Next i
  19.  Me.Show vbModal
  20.  For i = 1 To UBound(CBs, 1)
  21.   BitLng = BitLng + (-1 * CBs(i).Value) * 2 ^ (i - 1)
  22.  Next i
  23.  UFstart = BitLng
  24. End Function
図22


107~116行目では、10個のチェックボックスをオブジェクトとして配列に格納しています。例えば107行目「Set CBs(1) = Me.CheckBox1」では、CheckBox1を配列CBsの要素1に格納しています。

118~120行目は各チェックボックスのValue値(True または False)を「引数CB」を使って設定しています。
まず118行目「For i = 1 To UBound(CBs, 1)」で、カウンタ変数iをチェックボックスの数だけ回しています。
119行目「CBs(i).Value = CBool((2 ^ (i - 1)) And CB)」でチェックボックスのON-OFF設定をしているのですが、右辺内の「(2 ^ (i - 1)) And CB」では「調査対象のビットがTrueかFalseか」を、ANDを使ったビット演算で調べています。

通常ANDは「If 条件式A And 条件式B Then ・・・」のように使われます。この時のANDは理論積で、図23のように「A も B もTrueの時のみ True」となります。
理論積としてのAND
ABA AND B
FalseFalseFalse
TrueFalseFalse
FalseTrueFalse
TrueTrueTrue
図23


しかし、例えば「If 5 And 3 Then ・・・」というIf文はどうなるでしょうか? 答えはTrueとなり、Then以下のコードが実行されます。一方「If 4 And 3 Then ・・・」の答えはFalseです。
と言って「5」や「3」がTrueで「4」がFalse という訳ではありません。「If 5 And 4 Then ・・・」はTrueになるからです。

実は、ANDの左右が「TrueやFalseの値が得られる条件式」では無く「数値」となった時には、ANDは「ビット演算子」として機能します。内容としては「ANDの左右の値を2進数」で表し、各ビットごとの値を比較します。そして図24のように両方とも1の時に「そのビットは1」となります。
ビット演算としてのAND
ABA AND B
000
100
010
111
図24


先ほど例で示した「If 5 And 3 Then」等をビット演算で表したものが図25です。「5」「4」「3」をそれぞれ2進数で表し、各桁ごとに図24の規則に従ってビット演算すると、「5 And 3」と「5 And 4」の時には「どこかの桁が1」の2進数となります。ExcelではゼロはFalse、ゼロ以外がTrueですので、先ほどの「If 5 And 3 Then ・・・」がTrueとして処理されるのです。

ビット演算の例
図25


寄り道
なお、Excel内で条件式の評価(TrueやFalse)をどのような値で扱っているかは分かりませんが、もしBoolean型の1ビットだとすれば、フラグが立っている1がTrue、フラグが下りている0がFalseという事になると思います。すると「True同士のみがTrue」という規則は「1同士のみが1」と置き換えられます。
またExcelではTrueを数値にすると-1です。-1を2の補数で表すと「11 1111 1111(10ビットの時)」となります。一方Falseは「00 0000 0000」ですので、やはりTrue同士のみしかビットが立たない事になります。

つまり、図23の表は図24の表に統一できる事になり、「ANDの左右が条件式の時は・・・」や「ANDの左右が数値の時は・・・」等と場合分けせずに、「ANDの左右を2進数と考えて各ビットごとを比較し、得られた2進数がゼロか否か」で考えれば良いのではないかとも思えてきます。
なお「If "A" And "B" Then ・・・」のように、最終的には2進数になるはずの文字列ではエラーが発生しますが、「If #2/2/2020# And #2/3/2020# Then ・・・」のような日付はOKで、数値系であれば問題なさそうです。

ちなみにExcel内でTrueを指す定数「vbTrue」は「&HFFFFFFFF」と説明されています。Windowsが64ビットで動いているので、1が64個続く2進数です。

今回はANDをビット演算子として使用し「各ビットごとに演算」をしています。119行目では、「2 ^ (i - 1)」と「CB値」の演算を図26の様に行っています。「2 ^ (i - 1)」は図26でも分かるように、「i桁目のみが1」となる2進数です。
なお、119行目の時点では「CB値(チェックボックスの全ON-OFF情報)」も下段のフィルター「2 ^ (i - 1)」)も10進数ですが、AND演算子は数値を2進数として演算します。

ANDを使った理論積
図26


図26では、上段に「CB値(チェックボックスの全ON-OFF情報=引数で受け取った値)」を置き、下段に「各桁(≒各チェックボックス)のフィルター」を置く事で、「対象のチェックボックスがTrueかFalseか」を取得しています。ちなみに図26の左側は2桁目でフィルターを掛けており、右側は3桁目でフィルターを掛けています。
そしてその演算結果は、例えば図26の左側では「00 0000 0000」と全てゼロなので、10進数で言っても「ゼロ」です。また図26の右側は「00 0000 0100」と下から3ビット目が1となっていて、10進数で言えば「4」です。

「ゼロ以外はTrue」なので「0」や「4」をそのままCheckBoxのValue値に指定すれば良さそうなのですが、Boolean型(True・False)以外を指定すると、Value値が「Null」になって、表示のレ点が「灰色」になってしまいます。
ですので、この2進数の演算結果を「CBool関数」でBoolean型に変換します。「ゼロ=False」「ゼロ以外=True」が設定されます。
つまりフィルターを掛けたビット位置が「1の場合はTrue」「0の場合はFalse」となるのです。
このTrue・False値を各チェックボックスのValue値に設定することで、チェックボックスにレ点が付いたり消えたりします。

チェックボックスの設定が完了したら、122行目「Me.Show vbModal」でフォームを起動します。

フォーム上の「終了ボタン」によりフォームが閉じられる(フォームの右上×印をクリックしても同じ)と、制御は124行目に移ります。
124~126行目では、各チェックボックスのValue値を使って、直接「10進数の値」を作成します。

まず変数BitLngは、103行目で宣言したままなので、値としてはゼロです。
124行目「For i = 1 To UBound(CBs, 1)」では、カウンタ変数iをチェックボックス(CBs)の数だけ回します。
125行目「BitLng = BitLng + (-1 * CBs(i).Value) * 2 ^ (i - 1)」では、チェックボックスがONの時のみ、その番号に対応した値を累積していきます。

125行目の右辺内の「CBs(i).Value」は、各チェックボックスのValue値(レ点有り=True、レ点無し=False)ですが、Boolean値を数値にすると「True=-1、False=ゼロ」になります(Excelの場合)。今回は、そのValue値に「-1 * 」を掛けて、プラス側の値(レ点有り=1、レ点無し=0)にします。
これだけでは「どのチェックボックスがONなのかOFFなのか分からない」ので、チェックボックスの判別できるように「2 ^ (i - 1)」を更に掛け合わせます。この様子は図27のように、2進数を組み立てているのと同じ事になります。
(図27は、図03の状態のフォームの後処理をしている所と考えて下さい)。

2進数の生成と10進数の合計
図27


124~126行目のFor~Nextが全て処理されると、変数BitLngには「どのチェックボックスがON-OFFかの情報」である数値が収まっている事になります。
この値を128行目「UFstart = BitLng」で、関数プロシージャの戻り値に設定することで「チェックボックスのON-OFF情報値」がシート側に戻ります。

寄り道
この2進数⇔10進数の計算内容は、Windowsに標準搭載されている電卓アプリで確認できます。
2進数の生成と10進数の合計
図28


まず電卓アプリを起動し標準の電卓が表示されたら、左上の横棒三本の印の「ナビゲーション①」を開きます。すると電卓の種類が表示されますので、今回は「プログラマー」を選択すると、2進数・8進数・10進数・16進数のデータが同時に操作できる電卓になります。
2進数入力をする場合は、左横の「BIN(2進数の意味)」をクリックすると先頭に青いバーが表示され、テンキーも1と0しか受け付けないようになります。入力した値が、例えば10進数だと何になるかは、DEC(10進数の意味)の行を確認します。その時にDECをクリックすれば、10進数基準の電卓に変わります。

なお、テンキーには「+/-」の印があり、今回行ったような「2の補数」でのマイナス計算も出来ますが、ビット数は64ビット固定(Windowsのバージョンによっては異なるかもしれません)のようです。

3ー3ー2ー2.フォームの終了
フォーム上の「終了」ボタンをクリックした時に呼び出されるのが図29です。142行目「Unload Me」でフォームを閉じます。
  1. '========== ⇩(7) フォーム終了 ============
  2. Private Sub CommandButton1_Click()
  3.  Unload Me
  4. End Sub
図29


寄り道
「10進数と2進数の変換」のコードについては、様々なサイトで紹介されています。今回紹介したワークシート関数を使うものよりは、2進数を分解していくロジック的なものの方が多い感じで、一般的には別プロシージャにした方が扱い易くなります。

しかし今回のようにユーザーフォームの中で実行しようとする時は注意が必要です。図29のようにフォームを「Unload」すると、プログラムは次のコード(図22で言えば、124行目以降)を実行するのですが、もし途中に「Sub プロシージャを呼び出すCall文」や「Functionプロシージャを呼び出すコード」が存在していても、全て「無視」されます。つまりメインプロシージャ以外のものは実行してくれません(Me.Hide でフォームを隠すだけなら、正常に呼び出しが行われます)。

このメカニズムは良く分からないのですが、Unloadするとモジュール変数が初期化される(「フォームのUnload時に値を戻せないデータ型」参照)事と関係があるのかもしれません。

4.まとめ

この「複数コントロールの情報を1つの値」にする手法は、今回のチェックボックスだけでなく、オプションボタンやトグルボタン等についても使えます。

ただ、2進数に苦手意識のある人は多いと思います。と言って、良く分からないまま「コードをコピペ」して使うのは非常に危険です。2進数を使う以外にも、良い方法がきっとあるはずなので、探してみて下さい。
大切なのは「もっと良くできないか、もっとうまい方法はないか」と色々調べて考えて試してみることが、進歩につながる唯一の道だと思っています。

アプリ実例・関連する項目

画像を直接シートに貼りつける
2つの日付間の平日の日数計算
週ごとに切り替え可能な業務日程線
Book内で完結するフォーム表示のHelp画面
指定フォルダ配下のファイル情報を取得
マウス操作で日程の開始・完了を設定できるタスク表
DVD等の内容・保管場所等管理システム

サンプルファイル

複数チェックボックスのON-OFF情報を単一値で管理(its-036.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。