2023/02/09

選択不可のリストボックス




リストボックスは、通常「提示されたリストの中から、必要なデータ行を選択」する時に用いられます。リストを選択した後は、その選択行データを使った次の処理に進みます。
一方「選択する必要のないリスト」も存在します。例えば、図書館で既に借用している本のリストが挙げられます。ユーザー側からすると「重複して借りてしまわないためのリスト」ですので、選択する必要が無く「確認するだけ」のリストです。
(逆に図書館側のシステムでは、ユーザーが本を返して来た時の返却処理が必要なので、選択し処理ができるリストボックスとすることが必要となります。)

そのような場面でのリストでは、仮に選択が出来てしまうと、ユーザーとしては「選択できたのだから、別の操作ができるのでは?」との勘違いを引き起こしてしまいます。今回は、選択は出来ない(≒選択行が青色反転しない)リストを紹介します。

1.概要

今回紹介するのは、以下の4種類です。
手法動作デメリット
Enabled=False操作不可縦スクロール不可
Locked=True編集不可
イベントで未選択化縦スクロール可能ダブルクリックで選択される場合有り
No.3 + MouseUpイベントダブルクリックでもすぐに未選択化
図1


サンプルファイル」では、上記4種を試行するために図2のようなものを作成しました。
システム概要
図2


ワークシート上のボタンをクリックすると、ユーザーフォームが起動します。フォーム上には4つのリストボックスがあり、左側から図1のNo.1~No.4の仕様となっています。

また「セルをダブルクリックすることでフォームが起動」するシステムの場合、ダブルクリックした場所に「起動したフォーム上のリストボックス」があると、No.3の仕様では「リストボックスが選択された状態で起動」してしまう時があります。
これを防ぐため、No.4仕様のリストボックスには「MouseUpイベントで処理」を行っているのですが、その「セルのダブルクリックからフォームを起動」するためのイベント処理も、サンプルファイルには入れています。

2.システム補助関係

2ー1.ワークシート(Sheet1)

ワークシート上には、フォームを起動するためのボタン(フォームコントロールで作成)を1つ配置しています。そのボタンには図3のようにマクロ(標準モジュール上の「UFstart」プロシージャ)を登録します。
また今回の4つのリストボックスのデータは、ワークシートのA1~A10セルから取得しています。リストボックス表示からはみ出す(=縦スクロールバーが表示される)ように、わざと多めのデータ数にしています。
ワークシート上のレイアウト
図3


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

図3のワークシート上のボタンをクリックすると呼び出されるのが図4の「UFstart」プロシージャです。
  1. '========== ⇩(1) フォーム起動(ボタン) ============
  2. Public Sub UFstart()
  3.  UserForm1.Show
  4. End Sub
図4


02行目「UserForm1.Show」では、ユーザーフォーム(UserForm1)を起動させます。パラメータ無しなのでモーダルで起動しますが、モードレスで起動しても問題ありません。

2ー3.シートモジュール(Sheet1)

今回は、ワークシート(Sheet1)のセルをダブルクリックするとフォームが起動するようにしています。ダブルクリックすると、シートモジュール上の図5の「BeforeDoubleClick」イベントが呼び出されます。
  1. '========== ⇩(2) フォーム起動(シートをダブルクリック) ============
  2. Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
  3.  UserForm1.Show
  4. End Sub
図5


セルをダブルクリックすると、12行目「UserForm1.Show」でユーザーフォーム(UserForm1)が起動します。

2ー4.ユーザーフォーム(UserForm1)

2ー4ー1.フォームレイアウト

フォームのレイアウトは図6のようにしました。 フォームのレイアウト
図6


4つのListBoxを横に並べています。ListBoxの縦サイズ(Height)は、今回用意したデータ数(10個)が入りきらない(=縦スクロールバーが現れる)ようなの高さにしています。またListBoxのタイトル用のLabelは、配置時にCaptionプロパティを変更しています。

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

フォームが起動した時に呼び出されるのが図7です。
  1. '========== ⇩(3) フォーム起動時の静的設定 ============
  2. Private Sub UserForm_Initialize()
  3.  Me.ListBox1.Enabled = False
  4.  Me.ListBox2.Locked = True
  5. End Sub
図7


22行目「Me.ListBox1.Enabled = False」では、ListBox1(図1のNo.1仕様)のEnabledプロパティをFalseに設定します。これにより、ListBox1は「操作不可」の状態になります。

23行目「Me.ListBox2.Locked = True」では、ListBox2(図1のNo.2仕様)のLockedプロパティをTrueに設定します。これにより、ListBox2は「編集不可」の状態になります。

フォームが表示される時に呼び出されるのが図8です。
  1. '========== ⇩(4) フォーム起動時の動的設定 ============
  2. Private Sub UserForm_Activate()
  3.  Dim Data As Variant   '←リストのデータ配列
  4.  Data = Range("A1:A10").Value
  5.  Me.ListBox1.List = Data
  6.  Me.ListBox2.List = Data
  7.  Me.ListBox3.List = Data
  8.  Me.ListBox4.List = Data
  9. End Sub
図8


33行目「Data = Range("A1:A10").Value」では、ワークシートのA1~A10セルの値を取得しています。データは「縦方向の複数行」ですので、代入された変数Dataは「二次元配列」となります。

35行目「Me.ListBox1.List = Data」では、33行目で取得したデータ配列をListBoxのリストに格納します。
36~38行目も同様に、各ListBoxのリストにデータを格納しています。

3.仕様1(操作不可:Enabled=False)

ListBox1は図7の22行目で「Me.ListBox1.Enabled = False」と操作不可の状態にしています。
表示されたListBox1の外観は図9の1番左側になります。
仕様1の結果(操作不可のリストボックス)
図9


見かけは普通に見えますが、マウスでクリックしても全く反応しません。またリスト行数が10行なのに対して5行しか表示できない高さのため、縦スクロールバーが表示されていますが、それも操作できません。
つまり、リストの上部のデータは確認可能なものの、その下に隠れている残りのデータは確認不可となります。

また、Tabキーでコントロール間を移動する機能がありますが、操作不可のListBox1にはTabがストップすることはできません。

なお、他のコントロール(例えばComboBox)では、Enabled=False の設定にすると「ドロップダウンボタンの三角印がグレー色」になったりするのですが、リストボックスの場合には見かけ上は「Locked=True 」と変わらない様です。

4.仕様2(編集不可:Locked=True)

ListBox2は図7の23行目で「Me.ListBox2.Locked = True」と編集不可の状態にしています。
表示されたListBox2の外観は図10の左から2番目になります。
仕様2の結果(編集不可のリストボックス)
図10


見かけは普通に見えますが、マウスでクリックしても全く反応しません。また縦スクロールバーも操作できません。これらは、操作不可のListBox1と同様です。

但し操作不可の時とは異なり、TabキーでListBox2にFocusを当てることは可能です。その証拠として図10のListBox2の1番目の項目が破線の枠で囲われています。これがFocusが当たっている証拠です。
このフォーム上のコントロールは、ListBox1 → ListBox2 → ・・・という順番で作ったので、TabIndexもその順番で付けられていますが、ListBox1が操作不可状態なので除外され、ListBox2がフォーム起動時には「Tabストップの先頭」になっているのです。

5.仕様3(イベントで未選択化)

ListBox3のリストを選択した時には、図11のイベントが発生し、選択を解除させています。
  1. '========== ⇩(5) リスト選択時 ============
  2. Private Sub ListBox3_AfterUpdate()
  3.  If Not Me.ListBox3.ListIndex = -1 Then
  4.   Me.ListBox3.ListIndex = -1
  5. '  Me.ListBox3.Selected(Me.ListBox3.ListIndex) = False 
  6.  End If
  7. End Sub
図11


まずリスト選択を検出するイベントですが、マウスでクリックした場合には以下の順番でイベントが発生します。
「MouseDown」→「Change」→「Click」→「BeforeUpdate」→「AfterUpdate」→「MouseUp」
この中で、リストボックスが選択された(ListIndexが変わった)と判断できるは「Changeイベント」以降です。
ですのでChange以降のイベントで処理すれば「未選択状態を作る」ことが可能なはずなのですが、試してみると「Changeイベント、Clickイベントでは選択解除が失敗」することが分かりました。図11で言うと53行目は確かに実行されているのですが、選択解除されずに「選択されたまま」の状態になります。

選択解除されるのは「BeforeUpdate」以降であり、Change・Clickで解除失敗となる原因が今のところ不明な為、できるだけ遅く反応するイベントが確実だろうという判断で、今回は「AfterUpdateイベント」を使っています。
なお「BeforeUpdateイベント」でも試行したところ、正しく動きます。

52行目「If Not Me.ListBox3.ListIndex = -1 Then」では、リストが選択された時に53行目を実行します。と言うより、53行目を実行してListIndex値が変更された際に再度「AfterUpdateイベント」が発生しますので、再帰呼び出しされた時にスルーさせる意味合いが強いものです。
53行目「Me.ListBox3.ListIndex = -1」では、リストを未選択状態にしています。53行目の代わりに、見え消しにしてある54行目「Me.ListBox3.Selected(Me.ListBox3.ListIndex) = False」のように「選択した行を未選択(False)にする」ことでもOKです。

図11の実行により、ユーザーがリスト選択しても、すぐに未選択状態になります。
且つ通常のリストボックスですので、図12のように縦スクロールバーを移動することで全リストの閲覧が可能です。
仕様3の結果(イベントで未選択化する)
図12


6.仕様4(未選択化+ダブルクリック選択の解除)

実は上記の仕様3では、リストが選択されてしまう条件があります。それは以下のような場合です。
・セル(含:コンボボックスやリストボックス等)をダブルクリックすることで、フォームを起動するシステム
・ダブルクリックした位置の上にフォームが起動し、且つリストボックスのリストが丁度ある
 (現象が発生しない場合もあるので、他にも要因・条件がありそうです。)

図で表すと図13のようになります。マウスのクリック①の1つが、起動中のフォーム②のリストを選択③してしまう動きにつながってしまうようです。
セルのダブルクリックでフォーム上のリストが反応
図13


調べてみると、フォーム起動時には既にリストが選択された状態(ListBox.ListIndex ≒ -1 )になっていますが、リストのChangeやAfterUpdateイベントには引っ掛かりません。

この、マウスクリックの位置とタイミングの関係で「リストが選択された状態でフォームが起動」しますが、フォーム起動後にリストのどこかを選択すると、当然ながらAfterUpdateイベントが発生し、選択を解除してくれます。

この現象を防ぐ手段として、AfterUpdateイベントでの処理(図14)に加え、ダブルクリックが丁度リスト上に来てしまった時に「マウスボタンを離す」のを検知した処理(図15)を考えました。
図14は、仕様3の図11と全く一緒です(処理するListBoxだけが異なります)。
  1. '========== ⇩(6) リスト選択時 ============
  2. Private Sub ListBox4_AfterUpdate()
  3.  If Not Me.ListBox4.ListIndex = -1 Then
  4.   Me.ListBox4.ListIndex = -1
  5.  End If
  6. End Sub
図14


62行目「If Not Me.ListBox4.ListIndex = -1 Then」でリストが選択された時に、63行目「Me.ListBox4.ListIndex = -1」でリストを未選択状態にします。63行目の代わりに、図11の54行目で示した様に「Me.ListBox4.Selected(Me.ListBox4.ListIndex) = False」を使ってもOKです。

マウスをダブルクリックしてフォームを起動させ、そのマウスが「起動したフォームのリスト上」にあった時に、ダブルクリックを終えて「マウスボタンを離した時に発生するイベント」を利用したものが図15です。
  1. '========== ⇩(7) ダブルクリック起動でマウスボタンを離した時 ============
  2. Private Sub ListBox4_MouseUp(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
  3.  If Not Me.ListBox4.ListIndex = -1 Then
  4.   Me.ListBox4.ListIndex = -1
  5.  End If
  6. End Sub
図15


イベント内で実行するコードは、図14のAfterUpdateと全く同じです。
72行目「If Not Me.ListBox4.ListIndex = -1 Then」でリストが選択された時に、73行目「Me.ListBox4.ListIndex = -1」でリストを未選択状態にしています。

なお、通常操作でフォーム起動後に「リストをマウスで選択」した際には、まずAfterUpdateイベントが発生し、その後でMouseUpイベントが発生します。
ですので選択解除は図14のAfterUpdate内の処理で行い、選択が解除された後で図15のMouseUpが呼び出されますので、72行目のIf文が成立しない(既にListIndex = -1 になっている)ため、処理を行わずそのまま終了します。

まとめると、図14により「リスト選択してもすぐに未選択状態」にし、且つ図15により「ダブルクリックのマウスがリストボックスに引っ掛かって選択状態になっても、未選択状態」となります。
且つ通常のリストボックスですので、図16のように縦スクロールバーを移動することで全リストの閲覧が可能です。
仕様4の結果(イベントで未選択化+起動時も未選択化)
図16


7.まとめ

今回は、リストデータを閲覧するだけの用途でのListBoxを紹介しました。仕様4が最適と思われるかもしれませんが、システムによって使い分けるべきだと思います。以下の表を参考にしていただければ良いかと思います。
仕様動作適するシステム
Enabled=Falseリストデータが少ない
(全行が必ず見える)
Locked=True
AfterUpdate
イベント
ダブルクリック起動が無い 又は
ダブルクリック位置ではフォーム起動せず
AfterUpdate+MouseUpダブルクリック位置でのフォーム起動が有り得る
図17


アプリ実例

複数の備品を同時予約可能な貸出台帳

サンプルファイル

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