
Excelのユーザーフォームを使わないで、フォームコントロールとオートシェイプを使って、データを選択するリストボックスを作っていきます。
今回、サンプルで使うデータは次のような、今の野菜相場データです。

このデータはあくまでサンプル用です。
皆さんは、業務で使う実際のデータの形を想像しながら見ていただければと思います。
はい、最初の一手は、データの横にフォームコントロールボタンを配置してください。ActiveXじゃない方ですよ。

次に、配置したボタンが隠れるように、リストボックスウインドウを作成します。
サンプルでは次のようにしました。

デザインは皆さん好き好きで作ってください。もっと影の距離伸ばしたり、立体的にしたりと……。普通のオートシェイプと同じように扱って大丈夫です。
このサンプルを作る上で、やって頂きたい事は、「オブジェクトの選択と表示」の一覧から、オブジェクトの名前の変更です。
変更するのは、マクロ(VBA)を登録したり、マクロから動かしたい図形だけで大丈夫です。先ほど、ウインドウの下に作ったボタンも、ここで名前変更お願いします。その他の動かさない図形は変更しなくても大丈夫です。
この変更しなくてはいけない理由なのですが、 過去記事でも触れましたが、勝手に割り振られた図形の名前は、ファイルのコピーなどによって、名前が割り振り直される事があります。
図形が少なければ振り直されても、偶然一致するかもしれませんが、このように多い場合はそうはいきません。
なので、VBAのコード上で図形を名前で指定するような場合、名前が変更されないように、先に名前をつけておきます。ユーザーがつけた名前は、割り振りはされませんからね。
形が決まったら、ウインドウに使っている図形全部をまとめてグループ化しましょう。そして、グループにも名前をつけてください。
グループの中にさらにグループを作りたい方は、先に小さいグループを作っておいて、最後にまとめてグループ化すればよいでしょう。
上のスクショでは、矢印の上にテキストボックスが乗っている状態です。ここは、テキストボックスの方が大きくて上にあるので、選択されるのはテキストボックスになります。そのため、名前はテキストボックスだけ変更してもらえれば大丈夫です。
スクショをよく見ると、実は図形の矢印も名前が変更されています。これは私が最初にサンプルを作成した際に、テキスト無しの矢印だけで作成したんです。ですが、ちょっとわかりにくいかな…と思いましたので、テキストをあとから足したというわけです。その名残りです(笑)
また、図形につける名前ですが、あとあとVBAのコード上で入力するので、できればアルファベットが良いと思います。
もちろん、カナ、かな、漢字でも全く問題ないですが、ごくまれに、VBA上で予想外のことが起こることもあって、日本語はちょっとやっかい(特にMacは)なので、そのあたりは、好みにあわせてやってください。WindowsのVBAに限って言えば、日本語変数だってありです。(Macは論外ですが(笑))
それでは、このリストボックスでどんな動きをさせるのかを説明していきます。
まず、最初に作ったボタンを押すと、データが入ったシート上に、先ほど作ったリストボックスウインドウが開き、上の野菜リストボックスに一覧が表示されます。

上の野菜リストボックスの中から、1つデータを選択し、追加矢印ボタンを押すと、選択されたリストが下の選択リストボックスに移ります。

逆に、下から上のリストボックスへ移動するには、下の選択リストボックスで1つデータを選んで、削除矢印ボタンで上に移動します。
ここで、1つ注意点です。
以前の記事でも書いてますが、このフォームコントロールのリストボックスは、残念ながら複数選択が使えません。
ユーザーフォーム、AcitveXは使えるんですけどね〜。
コントロールの書式の中に複数選択はあるし、選択だけはできるようになるんです。
でも、選択したリストはデータとして扱う方法がありません。選んだリスト番号が拾えないのです。ひょっとしたら扱える人が世界のどこかにいるかもしれません。私は無理でした。もし、使える人いらっしゃいましたら、ぜひコメントで教えてください!
そして、生産地コンボボックスで生産地を選ぶと、上の野菜リストボックスで生産地フィルターをかけられます。このフィルターは下の選択ボックスには連動しないので、先に選んでいる別の生産地の野菜は消えません。

そして、全選択ボタンは、野菜リストボックスに表示されているもの全部が選択されて下に移動します。

当然、すべて表示していれば全部移動します。
同様に、全削除ボタンは、下から上のリストボックスにデータが移ります。生産地フィルターかかっている場合は、フィルターが適用されて表示されますので、フィルター外のものは表示されません。

もちろん、消えたわけではないので、生産地フィルータを変えるか、外せば、野菜リストボックスに再度表示されます。
そして、下の選択リストボックスにデータが表示されている時に、決定ボタンを押すと、メッセージウインドウに選択したデータが表示されます。

これはサンプルなので、意味のないメッセージウインドウ表示になってますが、実運用にする場合、この結果を、書類作成にしたり、なにか別のソフト用にデータをエクスポートしたりなど、業務に合わせて使っていただければ……と、ちょっとは使える?!かなと思います。
さて、どんなコードで動いているか…ですが、その前に、どういう仕組みでリストボックスが制御されているか説明していきます。
動いているウィンドウを見ると、リストボックスに表示されているデータが、実際に移動して、動いているよう見えますが、実際はそうではなく、リストボックスの表示は、各操作のたび、毎回全部消え、再度データを表示しているだけです。
動いているのは、1つのデータテーブルの表示判定カラム(列)の内容だけなのです。
なので、画面のリストボックスウィンドウは、各ボタンがデータ操作用コントローラー、ウインドウが結果表示用の画面というのが一番しっくりきます。
判定用のカラムが動くと言いましたが、実際どうなっているかと言いますと、0がデータ一覧リストボックス(上側)に表示、1が選択リストボックス(下側)に表示、2がフィルター外の非表示、となっています。これをボタンによって変化させているだけです。図にするとこんな感じですかね。

なので、大きなコードの流れとしては、
リスト操作→リスト全クリア→リスト再表示
が繰り返されるだけです。
それでは、順番にコードを見ていきます。
まずは環境整備のための前準備します。
関数のスコープ(有効範囲)の対策や、画面や自動計算、イベントの停止などプロシージャが動くごとに必要になるものを先にまとめます。
関数スコープの話は、関数をどこで宣言するのか?という過去記事もありますので、なんのことかわからない方は、あとで見てみてください。
簡単にいうと、普通は、コードのまとまりごとにしか使えない関数を、コードをまたいで使えるように準備しましょう!ってことです。
今回、リストを操作するデータは、シートデータを2次元配列に入れて使っています。
配列ではなく、シートのデータをそのまま使うことも可能ですが、今回は詳しく書きません。のちほど、さらっと触れています。
それでは、変数をプロジェクト内すべてで使えるように宣言していきます。今回コードを書くモジュールの一番上に書いてもらえればよいです。
Public item_list() As Variant, l_sheet As Worksheet, area() As Variant
パブリック宣言で使う変数は、リスト操作のためのデータリスト用の動的配列、データ一覧を格納するワークシート、産地リスト用の動的配列の3つです。
次は、VBA定番の画面更新、自動計算、イベントの操作用です。
Sub sc_update(ck As Boolean)
'画面の更新、自動計算、イベントの一時中止と再開
'Falseで一時中止、Trueで再開
With Application
.ScreenUpdating = ck
.EnableEvents = ck
If ck=False Then
.Calculation = xlCalculationManual’手動計算
Else
.Calculation = xlCalculationAutomatic'自動計算
End If
End With
End Sub
今回のサンプルではあまり多用しないので、別に準備しなくてもよかったなと思ってます(笑)
それでも、このコードはVBAで何かしら作成している方は、本当によく使うと思います。
使ったことなくて、処理速度で困っている人は、ひとまず、コードの初めにFalse、コードの終わりにTrueで使ってみて!
あとですね、これ、Funcitonにしてないですが、引数を渡せるプロシージャにしてます。このようにプロシージャでも、カッコ内に引数の変数を宣言すれば、Functionみたいに受けた引数で処理ができるようになります。
次はパブリック宣言した2次元配列にデータを格納します。
Sub list_load()
Dim i As Long, co As Long, ro As Long
Dim j As Long, fl As Boolean
'シートにあるデータの列数をカウント
i = 1
Do While l_sheet.Cells(1, i) <> ""
i = i + 1
Loop
'Do~Loopを1で抜けてくるのはループ回ってない=データ無し
If i = 1 Then
MsgBox "リストデータが見当たりません", , "データエラー"
Exit Sub'処理中断
End If
'+1で抜けるので−1して配列カラム数を登録
'配列は0スタートなので列数としては+1
co = i-1
i = 2'シートデータの行数はじまり
ro = 0'野菜データ配列の列数
a_ro = 0'産地データ配列の列数
'産地動的配列〜使う前に数指定(リセット)
ReDim area(0)
'野菜動的配列〜使う前に数指定(リセット)
ReDim item_list(co,0)
'シートデータをループで下方向にチェックしながら配列に格納
Do While l_sheet.Cells(i, 1) <> ""
ReDim Preserve item_list(co, ro)'野菜動的配列の列追加処理(データ維持)
'リストを横に移動しながら配列に格納
For j = 0 To co - 1
item_list(j, ro) = l_sheet.Cells(i, j + 1)
Next j
'配列カラムの最後はリスト表示判定を入れる
item_list(co, ro) = 0 'リスト表示にしておく
ro = ro + 1'野菜配列の列をカウントアップ
fl = False'同じ産地の判定用スイッチ
'産地配列の中とシートデータの配列を比較して同じものがないかチェック
For j = 0 To UBound(area)
If area(j) = l_sheet.Cells(i, 2) Then
'同じものがあったらスイッチONにしてループ抜ける
fl = True
Exit For
End If
Next j
'産地がデータになかったら処理
If fl = False Then
a_ro = a_ro + 1'産地配列の列数を増やす(1から代入)
ReDim Preserve area(a_ro)'産地動的配列の列追加処理
area(a_ro) = l_sheet.Cells(i, 2)'データ追加(1つ目はわざと空白)
End If
'次のシートデータへ
i = i + 1
Loop
End Sub
これでリスト一覧が配列に格納されました。
配列は慣れないとやっかいに感じるかもしれませんが、2次元配列は仮想ワークシートとして処理速度を上げるのに使ったりと、けっこう応用できるので、ぜひ慣れて頂きたいな〜と思います。
そう言いながら、私、上手に配列を説明する自信がないので、詳しく説明はやめておきます。
申し訳ないですが、ご自身で勉強してください(笑)
ここでは、2次元配列を仮想シートと考えたとき、ちょっとわからなくなる点だけ、さらっといきます。
2次元配列を仮想シートと考えると、データの位置を表す時、コード上では、
「配列名(列数、行数)」
と書きます。
ワークシートのセルの位置を指定する時は、
「Worksheet(n).Cell(行数、列数)」
と、行と列が反対になります。
この違いがわからなくなるんですよね。
この原因は、VBAの多次元の動的配列の特性にあります。それは、「最後の次元しか数を増やせない」という特性です。
この配列の特性の中で、仮想ワークシートを表現する訳です。
では、ワークシートの方をみてみましょう。
今回の野菜データサンプルです。

どうでしょうか?データが増える可能性って縦方向か横方向かどちらだと思います?
そうですよね?縦方向の方が可能性高いですよね。
野菜の種類が増える、産地が増える、価格が変わる、単位が変わる、どの場合も縦方向にデータが増えていきます。データベースもそうですけど、レコード(行)は増えても、あまり項目(カラム)って増えないですよね。
産地に項目として市が加わるとか、全く可能性がないとは言いませんが、データの形が決まっている場合、なかなか形を変えるってないですよね。
この配列とデータ、2つの特性をすりあわせて考えると、2次元配列は2次元目が増える、データは行側が増える、、、、ね、、、行、列が反対になるというわけですね。
なので、配列は「項目数、レコード数」と書くって、覚えちゃうしかない(笑)です。
そして、もう1点。
ワークシートのセルの左上端は「1」です。「Cell」で座標を指定するのも、「1、1」始まりですね。これ、配列は基本「0、0」始まりです。1つずれます。まあ、1スタートにもできますが、1つずれるよって覚えておいた方が早いです(笑)
あと、これもVBA特有のルールなんですが、動的配列の数を変更したいときは、変更する数を先に「宣言」しなくてはいけません。宣言の方法は、上のコード内にもありますが、サイズ変更ができる「ReDim」をつかいます。
ただですね、これそのまま「ReDim」だけ使うと、サイズ変更と同時にデータも初期化しちゃいます。
つまり、配列に入っていたデータがすべて消えるということです。
なので、データを維持したままサイズ変更したいときは、「Redim Preserve」を使います。
はい、つぎはいよいよウインドウの動きを作っていきます。
まず、データ一覧をリストボックスに表示させるものを作ります。これは追加、削除どちらの操作でも使うパーツになります。
Sub list_set()
Dim li_name As Variant
Dim i As Long, co As Long, ro As Long
'ウインドウの名前を数字で変更できるように配列にする
li_name = Array("item_list", "select_list")
'表示判定のカラム番号を得る
co = UBound(item_list, 1)
'2つのリストボックスの内容を全て消す
With l_sheet
.Shapes("item_list").ControlFormat.RemoveAllItems
.Shapes("select_list").ControlFormat.RemoveAllItems
End With
'判定用の数字と表示ウインドウをあわせてリスト表示する
For i = 0 To UBound(item_list, 2)
If item_list(co, i) <> 2 Then'2は表示しない
With l_sheet.Shapes(li_name(item_list(co, i))).ControlFormat
.AddItem item_list(0, i) & " --- " & item_list(1, i) & " --- " & item_list(5, i)
End With
End If
Next i
End Sub
フォームコントロールのリストボックスを操作するのは、上のコードのように
Worksheet(n).Shapes(名前).ControlFormat.
とします。これでオブジェクトを取得できるわけです。ここから派生するプロパティを使って操作していくわけです。コントロールの書式ウインドウで、できる範囲のこと操作可能です。
上のコード内では、「RemoveAllItems」でリストをクリア、「AddItem」をループさせてリストを1行ずつ登録してます。
ちょっと、変わったことしているのは、リストボックスの名前の使い方ですかね。
これは、配列にリストボックスの名前をいれると、自動的に1つ目に「0」が、2つ目に「1」の要素数が割り当てられます。この数字を、リスト表示判定用の数字とリンクさせたという訳です。こうすることで、ループ内ででてきた判定用の数字がそのまま使えるようになっています。
あと、これ、単体で動かすとエラー出ます。ワークシート変数の中が空っぽになってます。さきに動作確認するときは、変数「l_sheet」にワークシートを代入してください。くわしくは次に出てきます。
それでは、次は、ウインドウを開いたり、閉じたりをさせます。
まず、開きます。
Sub window_open()
'画面の更新など一時停止
Call sc_update(False)
'関数にワークシートを代入
Set l_sheet = ThisWorkbook.Worksheets("データ一覧")
'ワークシート内の図形の編集を無効にする
l_sheet.Protect DrawingObjects:=True
'データを配列に格納
Call list_load
'産地コンボボックスをリセット
With l_sheet
.Shapes("p_area").ControlFormat.RemoveAllItems
End With
'産地コンボボックスのリストを登録
For i = 0 To UBound(area)
l_sheet.Shapes("p_area").ControlFormat.AddItem area(i)
Next i
'ウインドウを表示する
l_sheet.Shapes("select_window").Visible = True
'リスト表示
Call list_set
End Sub
ウインドウを開く前に準備してきたプロシージャーをここで一気に使っています。
「Call」で始まるコードの箇所ですね。画面を一時停止、データを配列に格納、リスト表示がそうです。
そして、変数「l_sheet」にワークシートを代入してます。ワークシートは様々な情報の集まりであるオブジェクトになります。そのため、代入するには、「Set」を代入式の前につけないといけません。
そのあと、オートシェイプの図形にプロテクトかけてます。これは一回かけてしまえば、コードが止まっても状態は維持されます。イベントの停止なども同様です。復帰にはリボンから操作するか、コード上で復帰させなくてはいけません。
このワークシートの代入とオートシェイプのガードの実行タイミングですが、ここではこの仕組みが動き始めるウインドウのオープン動作に入れますが、ワークシートかワークブックのOpenとかActiveとかのイベントにおいてもよいかもしれません。
次にウインドウを閉じます。
Sub window_close()
'リストボックスとコンボボックスをリセット
With l_sheet
.Shapes("item_list").ControlFormat.RemoveAllItems
.Shapes("select_list").ControlFormat.RemoveAllItems
.Shapes("p_area").ControlFormat.RemoveAllItems
.Shapes("select_window").Visible = False'ウインドウ非表示
End With
'画面の更新の再開
Call sc_update(True)
End Sub
消すだけですので、シンプルですね。
リストボックスとコンボボックスの値をリセットして、ウインドウを隠すだけです。イベントも最初に止めてますので、最後に再開させて終了です。
実は、オープンの時も、最初にリストボックスとコンボボックスの値をリセットしてます。前の値が残らないようにする処置なのですが、この処理が走るなら、ウインドウが閉じるとき、リストボックスの値は消さなくても、この仕組み上問題ありません。
それでもですね、VBAを使わない状態でウインドウを編集したりするなど、意外とコード以外でもリストボックスを見る機会があります。値が残っていると、いろいろ余計なリスクがありますので、きれいにして終わる方が良いと思います。
次は、全追加ボタン処理です。
Sub all_item_add()
Dim i As Long, co As Long
'配列内の1次元目の最大数を得る(判定カラム)
co = UBound(item_list, 1)
For i = 0 To UBound(item_list, 2)
'ループさせながら判定が0のリストを1に変える
If item_list(co, i) = 0 Then item_list(co, i) = 1
Next i
'リスト表示
Call list_set
End Sub
もう表示させるコードは書いてますので、リストの判定の数字だけをループで調べて変更するだけです。
フィルターを設置してますので、表示されている「0」だけを「1」にしてます。こうすることで「2」は「2」のままになり、フィルターがかかったままになるわけです。
フィルターがなければ無条件で全部「1」にすればよいです。簡単ですね。
あと、最初に判定するカラム(列)を見つけるために「UBound」で配列の上限要素数、つまりアッパー(U)を調べてます。要素というのは、まあ、配列枠の枠番みたいなものです。なので、配列の中で一番大きな枠番は何番ですか?って聞いてます。関数名のあと、カンマ区切りで指定する数字は、次元数です。仮想ワークシートとして考えると、1次元目は列数ですね。なので、ループのFor文の上限は「2」になってますので、仮想ワークシートなら「行数」を調べている事になります。データ総数ですね〜。
このサンプルでは、判定カラムの場所は調べなくても、自分で設定して7列目にあることはわかってます。配列の要素(枠番)は0スタートですから、ベタ打ちで「6」としてループ内で判定してもよいのですが、、、まあ、こうしておけば、判定カラムが常に最後のカラムというルールさえあれば、カラム(項目)数が増えてもそのまま対応できる なあって、だけです。
この「UBound」ですが、VBAはやたら使います。配列を全部ループさせるときの上限しらべたり、要素数を先に得たりと、、、で逆に「LBound」で下限(ロウL)も調べられます。コードにあわせて配列の開始要素数を変えたりすることもあるので、下限も得る機会はでてきます。私はほとんど使わないですが(笑)
はい、それでは、次いきます。
順番からすると、全削除、、、ですが、全削除は、全追加と違って動作後の産地フィルターの影響を考えなくてはなりません。
全追加は、フィルターがかかっているもの(結果)について確定されるので、動作後にフィルターが影響することはありませんでした。
ですが、削除の場合は、フィルターがかかっているものがすでに選択した中にある場合、そのまま戻すと設定されているフィルターが無視されている状態になります。
なので、フィルター処理から作っていきます。
Sub area_filter()
Dim i As Long, co As Long, li_cnt As Long, li_no As Long
'産地コンボボックスで選択されているリスト番号を得る
li_cnt = 0
With l_sheet
li_no = .Shapes("p_area").ControlFormat.ListIndex
End With
'産地の選択ない(0)場合はリスト番号を1(空白)にする
'リスト作成時に1行目にわざと空白いれてます。
If li_no = 0 Then li_no = 1
'判定カラムを調べる
co = UBound(item_list, 1)
For i = 0 To UBound(item_list, 2)
If area(li_no - 1) = "" Then
'空白ならフィルー解除(2→0)
If item_list(co, i) = 2 Then item_list(co, i) = 0
ElseIf item_list(1, i) = area(li_no - 1) Then
'産地が一致ならフィルター解除(2→0)
If item_list(co, i) = 2 Then item_list(co, i) = 0
Else
'産地が不一致ならフィルター設定(0→2)
If item_list(co, i) = 0 Then item_list(co, i) = 2
End If
Next i
'リスト表示
Call list_set
End Sub
リスト表示、大活躍ですよね。同じ処理は分けておくと楽になることが、よくわかるかと思います。
ループ内の処理については特に解説なしで良さそうですね。
なので、リストボックスの使い勝手の悪さ(笑)を解説します。
見た目にリストを選択できて、ユーザービリティの面から言えば、とても親切でわかりやすくなりますよね。
でも、作成する側からすれば、もうちょっとなんとかならんのか?!という面もあります。私だけ感じてるだけかもしれませんが、、、、。
その1つがこの上のコード中にもあります。
コンボボックスで選択されているリスト番号をゲットしている箇所ありますね。
「li_no = .Shapes("p_area").ControlFormat.ListIndex」
これっす。
これ、値はわかなんないんすよ(笑)
なので、上のコード内では、配列とリスト一覧をリンクさせておき、選ばれた番号から、値がすぐにわかるように作ってます。
せめてさ〜、1つでもいいからリストを判別できる値がわかるようにならんかね。
そうすれば、判別IDをリストにいれておいて、あとでデータベースの中をループさせて、選ばれたIDに該当するデータを探すみたいなことができるようになります。
言っても、できないので、リストに対応する一覧データをつねに持つようにしましょう。シート上でやろうと思ったら、それこそ別シートつくたり、列つくったりが必要です。ああ、使い勝手は悪い〜。
「表計算ソフトだからさ。そういうのはAccessとかSQL serverでやってよ ┐(´д`)┌ヤレヤレ」
とか言ってそう(笑)
次は全削除ボタン処理いきます。
Sub all_item_del()
Dim i As Long, co As Long
'判定カラムを調べる
co = UBound(item_list, 1)
For i = 0 To UBound(item_list, 2)
'ループさせながら判定が1のリストを0に変える
If item_list(co, i) = 1 Then item_list(co, i) = 0
Next i
'フィルター処理を実行
Call area_filter
End Sub
今度は最後がリスト表示でななく、フィルター処理になってます。これは先ほど説明したとおり、下のリストから上のリストに戻す時、フィルターがかかっていた場合、フィルターを優先させるためですね。リスト表示のフローはフィルターの最後にも入ってますので、この中ではフィルターだけ実行すればよいことになります。その他は、全選択とリスト判定が逆になるだけでフローは一緒です。
次、選択されたリストを追加する処理をみてみます。
Sub item_add()
Dim i As Long, co As Long, li_cnt As Long, li_no As Long
'野菜リストボックスで選択されているリスト番号を得る
li_cnt = 0
With l_sheet
li_no = .Shapes("item_list").ControlFormat.ListIndex
End With
'リストが選択されてなかったら処理中断
If li_no = 0 Then
MsgBox "リストが選択されていません", , "リスト選択エラー"
Exit Sub
End If
'判定カラムを調べる
co = UBound(item_list, 1)
li_cnt = 0
For i = 0 To UBound(item_list, 2)
'表示判定が0のリストだけカウントする
If item_list(co, i) = 0 Then
li_cnt = li_cnt + 1
'リストカウントと0カウントが一緒なら選択データである
If li_no = li_cnt Then
'表示判定を1にする
item_list(co, i) = 1
'ループ抜ける
Exit For
End If
End If
Next i
'リスト表示
Call list_set
End Sub
ここのポイントはリストボックスで選択されているリスト番号をどうやってデータ一覧の番号と一致させるか?ですね。
先ほど書きましたが、リストボックスから得られる情報は、「選択されたデータはリストの中で何番目なのか?」だけです。
産地コンボボックスの時は、表示リストと配列リストは一致していました。なので、順番も同じですからすぐに選択データを導きだせました。
でも、このサンプルの野菜データ一覧は、2つのリストに分けて表示するし、フィルターがかかれば表示されないリストも出てきます。つまり、リストが3つに分かれます。そして、ユーザーの選択によって状態はコロコロ変わります。
そうなると、リストボックスから得られる情報は、データ一覧の全体の順番の中では使えない、意味が無いものになります。
そう、「全体の順番」では意味がない。
「選択されたリストでの順番」つまり表示判定に使っている、同じ番号を数えていけばいいわけです。
ここでは、上のリストに表示される判定番号は「0」です。なので、コード中も「0」だけ数えて、リストインデックスと一致したら、ループ抜けるわけですね〜。
ちょっとわかりにくいですかね?
リスト表示を想像していただいたらよいと思います。毎回表示するときは、一回全部消して、リストを上から表示判定をみて順番に表示させてますよね?
はい、次は選択されたリストを削除します。
Sub item_del()
Dim i As Long, co As Long, li_cnt As Long, li_no As Long
'選択リストボックスで選択されているリスト番号を得る
li_cnt = 0
With l_sheet
li_no = .Shapes("select_list").ControlFormat.ListIndex
End With
'リストが選択されてなかったら処理中断
If li_no = 0 Then
MsgBox "リストが選択されていません", , "リスト選択エラー"
Exit Sub
End If
'判定カラムを調べる
co = UBound(item_list, 1)
li_cnt = 0
For i = 0 To UBound(item_list, 2)
'表示判定が1のリストだけカウントする
If item_list(co, i) = 1 Then
li_cnt = li_cnt + 1
'リストカウントと1カウントが一緒なら選択データである
If li_no = li_cnt Then
'表示判定を0にする
item_list(co, i) = 0
'ループ抜ける
Exit For
End If
End If
Next i
'フィルター処理を実行
Call area_filter
End Sub
全選択と全削除の関係と一緒で、選択リストを追加する、削除するも、判定と最後のCallで呼び出すプロシージャがリスト表示かフィルター処理 かの違いで、フロー自体は一緒ですね。
はい、これでコードは最後です。
決定ボタン処理です。
Sub result_item()
Dim i As Long, co As Long, re_come As String
Dim j As Long
'判定カラムを調べる
co = UBound(item_list, 1)
For i = 0 To UBound(item_list, 2)
'表示判定が1のリストだけ探す
If item_list(co, i) = 1 Then
'表示判定が1だったら、コメント用変数にデータを入れる
For j = 0 To co - 1
re_come = re_come & item_list(j, i)
If j < co - 2 Then
re_come = re_come & " -- "
Else
re_come = re_come & vbCrLf
End If
Next j
End If
Next i
'コメント表示
MsgBox re_come, , "選択結果"
End Sub
最後はメッセージボックスを出すだけの、なんだこりゃの処理ですが、確定されたデータの抽出方法だけ見てもらえればと思います。
選択されたリストの追加、削除と同様なので、あえて説明は不要だとおもいますが、決定ボタンを押された時、どのリストが選択されているのかを判定する方法は、判定カラムの数字が1になっているものを探せばいいということになります。
あとは、一番最初につくったオートシェイプとフォームコントロールにマクロを登録して完了です!
登録は画面をみながらできるので省略しますが、解説が必要な方はコメントください。記事に追加します。
ひとつだけ気をつけておきたいことですが、マクロをすべて登録し終わったら、手動でウインドウを見えなくしてください。
オブジェクトの選択から目のマークを閉じてもらえればよいです。
このサンプルコードの起点は、ウインドウオープンになってます。そのため、起点コードから始まらないと、エラーが鬼のように出ます(笑)
そう言った理由で、ウインドウを出しっぱなしでファイルを保存して閉じた場合も同じことが起きます(笑)
回避したい場合、ワークシート、ブックのイベントで強制的にウインドウを閉じるコードを入れるか、どこから始めてもエラーがなくなるように、ワークシートの変数設定、配列リスト作成の処理をすべての動作起点のプロシージャの最初に持ってきて、先に実行させてください。それでいけると思います。
ここから、おまけです。
配列を使わずに、ワークシートのデータを直接リストボックスへ表示した場合のコードをさらっと紹介します。
この方法は処理速度も落ちますし、判定用の列(カラム)を作らなくてなりません。また、このサンプルの産地データのように、リスト用のリストが必要な場合、別の場所に、そのリストを作らなくてはなりません。
それでも、配列より視覚でリストを見ながら作れるので、理解は100倍しやすいとは思います。
それでは、リスト表示するプロシージャを使って、配列じゃないパターンで書いていきます。
リスト表示用の判定カラムは列の最後に作成済みという仮定です。
Sub list_set()
Dim li_name As Variant
Dim i As Long, co As Long, ro As Long
'ウインドウの名前を数字で変更できるように配列にする
li_name = Array("item_list", "select_list")
'表示判定のカラム番号を得る
co = UBound(item_list, 1)
'2つのリストボックスの内容を全て消す
With l_sheet
.Shapes("item_list").ControlFormat.RemoveAllItems
.Shapes("select_list").ControlFormat.RemoveAllItems
End With
'判定用の数字と表示ウインドウをあわせてリスト表示する
i=2
Do While l_sheet.Cells(i,1)<>""
If l_sheet.Cells(i,7) <> 2 Then'2は表示しない
With l_sheet.Shapes(li_name(Val(l_sheet.Cells(i,7).Value))).ControlFormat
.AddItem l_sheet.Cells(i,1) & " --- " & l_sheet.Cells(i,2) & " --- " & l_sheet.Cells(i,6)
End With
End If
i=i+1
Loop
End Sub
こんな感じですかね。
あとは各ボタンによって表示判定用の数字をワークシート上で直に変更するようにすれば、同じように動くと思います。
ただ、産地コンボボックスは、別の場所にリストつくらないとダメですよ。リストは手で作ってもいいですし、VBAで自動化してもいいです。
それでは今回はここまでです。
ながながとありがとうございます。



















