Excel (VBA)

Excel VBAに関するフォーラムです。
  • 解決済みのトピックにはコメントできません。
このトピックは解決済みです。
質問

 
(Windows 11 Pro : Excel 2016)
選択肢の範囲を変数にした複数選択が可能なプルダウンリストを作りたい
投稿日時: 24/04/19 10:08:48
投稿者: nari99

ActiveXコントロールのリストボックスを使って"選択肢を複数選択可能で、選択されたデータをセルに書き込むプルダウンリスト"をExcelに実装しようとしています。選択肢となる範囲は、Excelの機能である「名前の定義」でOFFSET関数で設定した可変の範囲を、リストボックスのプロパティの"Listfillrange"に設定しています。リストボックスには"ショップ"と名前を付けています。
 
図形挿入で作成したボタンを押すとプルダウンの表示/非表示を切り替えることができるようにしたいと思い、はじめに以下で試してみました。
 

Option Explicit

Sub リスト表示非表示()

    With ActiveSheet.Shapes("ショップ")
        If .Visible = True Then
            Call リスト非表示
        Else
            Call リスト表示
        End If
    End With

End Sub

Sub リスト表示()

    With ActiveSheet.ショップ
        .Visible = True
        .Activate
    End With

End Sub

Sub リスト非表示()

    With ActiveSheet.ショップ
        .Visible = False
    End With
    
End Sub

Sub セルの値表示()

    Dim セルの値 As String
    Dim I As Long
    
    セルの値 = ""
     
    With ActiveSheet.ショップ
        For I = 0 To .ListCount - 1                         ' リストの先頭から最後までループ
            If .Selected(I) = True Then                     ' リストが選択されていたら
                セルの値 = セルの値 & "," & .List(I)        ' カンマ区切りでリストの値をつなげる
            End If
        Next I
    End With
    セルの値 = Mid(セルの値, 2)
    Range("B2").Value = セルの値

End Sub

 
上記のコードで
・コード実行時の選択肢範囲を正しく取得し、プルダウンリストに反映すること
・選択肢から選択されたデータをセルに書き込むこと
これはできたのですが
・選択肢の範囲が増えたときに増えた分の選択肢をプルダウンに反映できない
・選択肢範囲が減った時にプルダウンに減った分の空白の選択肢が残ってしまう
という問題がありました。
 
原因はListfillrangeに設定された範囲の広さが変わった時(選択肢の数が変わった時)にリストボックスが更新されないことかと想定し、いろいろやって現在以下のようになっています。
 
Option Explicit

Dim 初期化済 As Boolean

Sub リスト表示非表示()
    With ActiveSheet.Shapes("ショップ")
        If .Visible = True Then
            Call リスト非表示
        Else
            Call リスト表示
        End If
    End With
End Sub

Sub リスト表示()
    With ActiveSheet.Shapes("ショップ")
        .Visible = True
        ' 初めてリストを表示する場合にのみ初期化を行う
        If Not 初期化済 Then
            初期化リストボックス
            初期化済 = True
        End If
    End With
End Sub

Sub リスト非表示()
    With ActiveSheet.Shapes("ショップ")
        .Visible = False
        ' リスト非表示時にリストボックスの選択をクリアする
        With .OLEFormat.Object.Object
            .MultiSelect = fmMultiSelectMulti
            .Selected(-1) = False
        End With
    End With
End Sub

Sub セルの値表示()
    Dim セルの値 As String
    Dim i As Long
    セルの値 = ""
    With ActiveSheet.Shapes("ショップ").OLEFormat.Object.Object
        For i = 0 To .ListCount - 1
            If .Selected(i) = True Then
                セルの値 = セルの値 & "," & .List(i)
            End If
        Next i
    End With
    セルの値 = Mid(セルの値, 2)
    Range("B2").Value = セルの値
End Sub

Sub 初期化リストボックス()
    Dim ws As Worksheet
    Dim rng As Range
    Dim lb As MSForms.listBox
    Dim i As Long
    
    Set ws = ThisWorkbook.Sheets("Sheet2") ' 
    Set rng = ws.Range("SHOP") ' 
    Set lb = ActiveSheet.Shapes("ショップ").OLEFormat.Object.Object
    
    ' リストボックスの内容をクリア
    lb.Clear
    
    ' リストボックスに新しい範囲の値をセット
    For i = 1 To rng.Rows.Count
        lb.AddItem rng.Cells(i, 1).Value
    Next i
End Sub


 
現在発生しているエラーは
lb.Clear
ここで「予期せぬエラー」が発生しています。
 
何かヒントをご教示いただければと思います。
よろしくお願い致します。
 
 

投稿日時: 24/04/19 10:28:02
投稿者: nari99

プルダウンが表示されていないときにマクロ登録で"リスト表示非表示"を登録したボタンを押すと
 
lb.Clear
 
ここで「予期せぬエラー」が発生しています。
 
プルダウンが表示されていないときにボタンを押すと
 
.Selected(-1) = False
 
ここで「selectプロパティを設定できません。プロパティの値が無効です」となります

回答
投稿日時: 24/04/19 11:24:29
投稿者: sk

引用:
選択肢となる範囲は、Excelの機能である「名前の定義」で
OFFSET関数で設定した可変の範囲を、リストのプロパティの
"Listfillrange"に設定しています。

引用:
現在発生しているエラーは
lb.Clear
ここで「予期せぬエラー」が発生しています。

ListFillRange プロパティの設定を活かしたまま
そのコードを実行しているからではないかと。

投稿日時: 24/04/19 12:16:44
投稿者: nari99

nari99 さんの引用:
プルダウンが表示されていないときにマクロ登録で"リスト表示非表示"を登録したボタンを押すと
 
lb.Clear
 
ここで「予期せぬエラー」が発生しています。
 
プルダウンが表示されていないときにボタンを押すと
 
.Selected(-1) = False
 
ここで「selectプロパティを設定できません。プロパティの値が無効です」となります

 
コメントの種類を間違えていたので再度コメントします

投稿日時: 24/04/19 12:21:30
投稿者: nari99

skさん
 
ありがとうございます。
 
ご指摘の意図は
>ListFillRange プロパティの設定を活かしたまま
>そのコードを実行しているからではないかと。
選択肢データを都度ListFillRageから再取得できるようにすれば、リストボックスのクリアは必要ない、という認識で間違いないでしょうか。
試しに"lb.Clear"を削除したところまた別のエラーが出ましたが、上記の認識でしたら消した後出るエラーは別の原因かと思うので。

回答
投稿日時: 24/04/19 14:16:34
投稿者: sk

引用:
選択肢データを都度ListFillRageから再取得できるようにすれば、
リストボックスのクリアは必要ない、という認識で間違いないでしょうか。

ListFillRange プロパティに任意のセル範囲のアドレス/名前が
設定されているリストボックスについては、参照された範囲から
取得したリストを直接編集できないため、Clear, AddItem, RemoveItem など
リストのアイテムの追加/削除に関わるメソッドを呼び出すことは出来ません。
 
それらのメソッドを使用したければ、そのリストボックスの
ListFillRange プロパティの値は空白にしておけばよい、
と申し上げただけです。
 
引用:
選択肢となる範囲は、Excelの機能である「名前の定義」でOFFSET関数で設定した可変の範囲

引用:
Set rng = ws.Range("SHOP")

引用:
・選択肢の範囲が増えたときに増えた分の選択肢をプルダウンに反映できない
・選択肢範囲が減った時にプルダウンに減った分の空白の選択肢が残ってしまう
という問題がありました。

いずれにせよ、名前定義[SHOP]の参照範囲として設定した数式(のみ)によって
そのリストボックスに表示させるリストの増減を適切に制御することが
出来ているのか否かはまた別の問題です。
 
例えば、あるセルに何らかの値を入力すると OFFSET 関数によって
返される範囲の行数が増減するような数式が名前定義[SHOP]の
参照範囲として設定されているのであれば、そのセルの値が
変更されたタイミングでリストボックスの ListFillRange プロパティを
再設定するようになさればよいでしょう。
 
実際にどのような手法を採るべきか、どのモジュールに
どのようなコードを記述すべきかについては、
[SHOP]に設定されている具体的な数式が判らない限り
今のところ回答のしようがありません。

投稿日時: 24/04/19 14:41:24
投稿者: nari99

引用:

ListFillRange プロパティに任意のセル範囲のアドレス/名前が
設定されているリストボックスについては、参照された範囲から
取得したリストを直接編集できないため、Clear, AddItem, RemoveItem など
リストのアイテムの追加/削除に関わるメソッドを呼び出すことは出来ません。

これを理解できておりませんでした。ご教示頂きありがとうございます。
 
引用:
あるセルに何らかの値を入力すると OFFSET 関数によって
返される範囲の行数が増減するような数式が名前定義[SHOP]の
参照範囲として設定されているのであれば

おっしゃる通りで、名前定義[SHOP]に設定している数式は以下です。
=OFFSET(Sheet2!$A$4,0,0,COUNTA(Sheet2!$A$4:$A))

 
引用:
実際にどのような手法を採るべきか、どのモジュールに
どのようなコードを記述すべきかについては、
[SHOP]に設定されている具体的な数式が判らない限り
今のところ回答のしようがありません。

大変恐縮ですが、もしよろしければご教示頂きたいです。
よろしくお願い致します。[/quote]

回答
投稿日時: 24/04/19 16:43:51
投稿者: sk

引用:
COUNTA(Sheet2!$A$4:$A)

こういう参照の仕方は Excel では出来ないでしょう、
という点は置いておくとして。
 
引用:
=OFFSET(Sheet2!$A$4,0,0,COUNTA(Sheet2!$A$4:$A))

COUNTA 関数によって得られるのは、あくまで
「参照範囲の中に含まれる空白セル以外のセルの個数」です。
 
例えば、その時点における Sheet2 の最後のデータ行が 13 行目であるとして、
A4 セルから A13 セルまでの範囲に空白セルが含まれていなければ
A4 セル以降の行を参照範囲とする COUNTA 関数の戻り値は 10 となります。
その値が更に OFFSET 関数の第 4 引数(高さ)に渡された結果、
OFFSET 関数が返す範囲は A4:A13 となります。
 
しかし、もし A5 セルのみが空白セルだった場合、A4 セル以降の
行を参照範囲とする COUNTA 関数の戻り値は 9 となります。
その結果、OFFSET 関数が返す範囲は A4:A12 となり、
空白である A5 セルを含み、かつ空白セルではない A13 セルが
含まれていない状態となります。
 
前者のように「先頭行から最終行までの間に空白セルが含まれていない」ことが
確実に保証されているのであればそのような数式でもよいでしょうが、
後者のような状態になり得るのであれば、上記のような数式を使用した
名前定義を用いるのはなるべく止めた方がよいでしょう。
 
他にも懸念すべき問題はありますが、ひとまず前提条件を
次のように仮定します。
 
・Sheet2 の A 列の最初のデータ行は 4 行目からとする。
 
・Sheet2 の A 列の最初のデータ行から最後のデータ行までの間に
 空白セルが含まれることはない。
 
引用:
あるセルに何らかの値を入力すると OFFSET 関数によって
返される範囲の行数が増減するような数式が名前定義[SHOP]の
参照範囲として設定されているのであれば、そのセルの値が
変更されたタイミングでリストボックスの ListFillRange プロパティを
再設定する

ここでの「あるセル」とは Sheet2 の A 列のいずれかのセルですので、
Sheet2 の Change イベントの発生時に次のようなイベントプロシージャを
実行するようにします。
 
( Sheet2 のシートモジュール)
---------------------------------------------------------------
Private Sub Worksheet_Change(ByVal Target As Range)
 
    If Intersect(Target, Me.Range("A:A")) Is Nothing Then
        Exit Sub
    End If
     
    Dim rngFirst As Range
    Dim rngLast As Range
    Dim rngListSource As Range
    Dim oleListBox As Excel.OLEObject
     
    With Me
         
        Set rngFirst = .Cells(4, 1)
        Set rngLast = .Cells(.Rows.Count, 1).End(xlUp)
        Set rngListSource = .Range(rngFirst, rngLast)
         
        Debug.Print "参照範囲のアドレス:" & rngListSource.Address
         
        Set oleListBox = .OLEObjects("ショップ")
        oleListBox.ListFillRange = rngListSource.Address
     
    End With
     
    Set oleListBox = Nothing
    Set rngFirst = Nothing
    Set rngLast = Nothing
    Set rngListSource = Nothing
      
End Sub
---------------------------------------------------------------

投稿日時: 24/04/19 17:58:48
投稿者: nari99

質問させて頂いていることに加え、別の懸念点までご指摘いただきありがとうございます。

引用:
こういう参照の仕方は Excel では出来ないでしょう、
という点は置いておくとして。

そうでした。スプレッドシートをよく使う癖で記述してしまいました。失礼いたしました。
 
引用:
しかし、もし A5 セルのみが空白セルだった場合、A4 セル以降の
行を参照範囲とする COUNTA 関数の戻り値は 9 となります。
その結果、OFFSET 関数が返す範囲は A4:A12 となり、
空白である A5 セルを含み、かつ空白セルではない A13 セルが
含まれていない状態となります。

確かにおっしゃる通りですね、、まったく考慮できていませんでした。
であれば、例えばSheet2のB4セルに
=FILTER(IF(A4:A2000="","",A4:A2000),A4:A2000<>"")

のような関数を入れて形で間に空白行が入らないようにした範囲を作り、名前の定義設定する式を
=OFFSET(Sheet2!$B4,0,0,COUNTA(Sheet2!$B4:$B2000))

とすれば、OFFSET関数の4 引数=範囲の高さになるようにできるかと思いました。
 
引用:
Sheet2 の Change イベントの発生時に次のようなイベントプロシージャを
実行するようにします

こちら教えて頂いてありがとうございます。
範囲指定の仕方さえ正しくできていれば、おそらく正しく動かせていると思います。

回答
投稿日時: 24/04/19 18:26:40
投稿者: sk

引用:
=FILTER(IF(A4:A2000="","",A4:A2000),A4:A2000<>"")

FILTER 関数を使用できる環境( Microsoft 365 など)ならば
そういう方法も可能ですが、Excel 2016 では FILTER 関数を
使用することはできません。
 
引用:
=OFFSET(Sheet2!$B4,0,0,COUNTA(Sheet2!$B4:$B2000))

また懸念すべき問題の 1 つとして、OFFSET 関数も COUNTA 関数も
揮発性関数である、ということが挙げられます。
これらの関数は、シートの再計算が発生する都度に呼び出されます。
 
今回の場合、リストボックスの ListFillRange プロパティに
名前定義[SHOP]を設定しているわけですが、[SHOP]の参照範囲として
上記の数式が設定されているならば、シートの再計算が発生するたびに
OFFSET 関数と COUNTA 関数が呼び出され、それに伴ってリストボックスの
リストの再読み込み処理が実行され、その時点で複数選択されていたアイテムの
選択状態も全てリセットされることになります。
 
シートの再計算が発生してもリストのアイテムの選択状態を維持して
おきたいのであれば、動的範囲を返す名前を ListFillRange プロパティを
設定しないことをお奨めします。

投稿日時: 24/04/22 10:49:04
投稿者: nari99

引用:
Excel 2016 では FILTER 関数を
使用することはできません。

そうなんですね、、、何か別の方法を考えます。
 
引用:
シートの再計算が発生するたびに
OFFSET 関数と COUNTA 関数が呼び出され、それに伴ってリストボックスの
リストの再読み込み処理が実行され、その時点で複数選択されていたアイテムの
選択状態も全てリセットされることになります。

この点は実際に運用するときに不都合になることがありそうなので、範囲設定の仕方も考え直す必要がありそうです。
ご指摘いただきありがとうございます。
当初お伺いした質問の内容は解決して頂いたので解決済みにさせて頂きます。