Excelの入力規則(リスト)を「複数選択」できない…そんなときは、Worksheet_Change と Application.Undo を使うVBAで、選択値をカンマ区切りで追記し、再選択で削除(トグル)できます。別シートに候補がある場合の注意点と、実運用で安定する修正版コードをまとめます。
Excelの入力規則(リスト)は基本的に「単一選択」
Excelの入力規則(データの入力規則)の「リスト」は、標準機能だけでは同じセルに複数の候補を選んで保持することができません。ドロップダウンから選び直すと、以前の値が上書きされます。
そのため、チェックボックスのような複数選択を疑似的に実現したい場合は、次のような仕組みをVBAで作るのが定番です。
- 入力規則リストのセルで値が変わったタイミング(
Worksheet_Change)を検知 - 今選ばれた値(New)を取得し、
Application.Undoで直前の値(Old)に戻して取得 - OldにNewが含まれていなければ「追記」、含まれていれば「削除」(再選択でトグル)
「候補リストが別シートにある」ことは原因になりにくい
入力規則の候補(元データ)が別シートにあること自体は、VBAの「追記/削除」ロジックが動かない直接原因になりにくいです。なぜなら、VBA側は基本的に「セルに入った値」を扱うだけで、候補の所在は参照しないからです。
ただし、別シート参照の入力規則は設定方法を誤ると「入力規則そのものが壊れている」状態になりやすく、結果としてTarget.Validationが想定どおり取得できないことがあります。ここは後半の「入力規則を安定させる(別シート参照の正攻法)」で対策します。
動かないときに多い原因
提示コードが動かないケースの多くは、候補リストの場所よりも「VBAコードの置き場所」や「構文・変数名の不整合」に起因します。特に、Worksheet_Changeはイベントなので、些細なミスでも一切動かなくなります。
| 症状 | よくある原因 | 対策 |
|---|---|---|
| ドロップダウンで選んでも追記されない | コードが標準モジュールに入っている/別シートのコードモジュールに入っている | 対象セルがあるワークシートのコードモジュールに貼り付ける(後述) |
| エラーで止まる/何も起きない | 変数名の不一致(例:MultipleSelectRangeとMultiSelectRange) | 変数名を統一し、Option Explicitで未宣言変数を検出する |
| 一度動いたが、その後Excel全体が反応しない | Application.EnableEvents = Falseのまま戻っていない | 必ず復帰する終了処理(Exitラベル+エラーハンドラ)を入れる |
| リスト以外のセル編集でエラー | Target.Validation.Typeを、入力規則がないセルに対して参照している | エラーを握りつぶす関数で「リストかどうか」を安全判定する |
| 貼り付けで意図しない動作 | 複数セル同時変更でUndoが破綻 | Target.CountLarge > 1は処理しない(安全側に倒す) |
| VBEで「コンパイル エラー」になる | Ifの閉じ忘れ(End If不足)/括弧やThenの書き方ミス | インデントを整え、IfのネストをすべてEnd Ifで閉じる。Option Explicitで事前検知する |
イベントは「一行でも構文が崩れると発火以前にコンパイルで止まる」ため、インデントを揃えてネストを見える化し、まずはエラーなくコンパイルできる状態にするのが近道です。
まずは入力規則(リスト)を安定させる(別シート参照の正攻法)
別シートに候補リストがある場合、入力規則の「元の値(Source)」に直接=Sheet2!$A$1:$A$100のような形で入れると、環境や操作手順によっては設定できなかったり、いつの間にか壊れたりします。安定運用するなら「名前付き範囲(名前定義)」を経由するのが最も確実です。
| やりたいこと | おすすめの設定 | ポイント |
|---|---|---|
| 別シートの範囲を入力規則リストにする | 名前付き範囲を作り、入力規則の元の値を=名前にする | 参照先が別シートでも壊れにくい |
| 候補が増減するので自動で追従させたい | 候補範囲をExcelテーブル化し、その列を参照する名前を作る | 行追加で候補が自動拡張し、運用が楽 |
| 候補に空白が混ざるのを避けたい | 候補列を「空白なし」に整備(または別列で整形) | 空白があると「空の選択肢」が出て操作ミスの原因 |
名前付き範囲の作り方(代表例)は次のとおりです。
- 候補リストがあるシートで範囲(例:A2:A200)を選択
- 数式バー左の「名前ボックス」に任意の名前(例:
dv_Category)を入力してEnter - 入力規則を設定したいセル範囲を選択 →「データ」→「データの入力規則」→「リスト」→「元の値」に
=dv_Category
こうしておくと、VBA側のロジックが「入力規則がリストかどうか」を判定する処理も安定します。
VBAを入れる場所が最重要(ここを間違えると動きません)
Worksheet_Changeは「特定のシートで、セルが変更されたとき」にだけ動くイベントです。したがって、コードは次の場所に入れる必要があります。
- 対象セルが存在するワークシート(例:Sheet1)のコードモジュール
手順の目安は以下です。
- Alt + F11 でVBE(VBAエディタ)を開く
- 左のプロジェクトで対象ブックを展開
- 「Microsoft Excel Objects」配下の該当シート(例:Sheet1(入力シート))をダブルクリック
- 右側のコードウィンドウに貼り付ける
また、シートを明示するためにRange("C2:C124")ではなくMe.Range("C2:C124")と書くと、「どのシートの範囲か」がブレず安全です。
複数選択(追記/再選択で削除)の完成版コード
以下は、入力規則(リスト)のセルを「カンマ区切りで追記」し、同じ項目をもう一度選ぶと削除する(トグル)実装です。貼り付けなどの複数セル変更は事故りやすいので対象外にし、入力規則がないセルでもエラーにならないようにしています。
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Const DELIM As String = ", " '区切り文字(必要に応じて変更)
Dim oldText As String
Dim newItem As String
Dim multiSelectRange As Range
Dim prevEvents As Boolean
prevEvents = Application.EnableEvents
On Error GoTo CleanUp
'貼り付け等で複数セルが一度に変わるとUndoが崩れるため対象外
If Target.CountLarge > 1 Then GoTo CleanUp
'複数選択を許可する範囲(必要に応じて変更)
Set multiSelectRange = Me.Range("C2:C124")
If Intersect(Target, multiSelectRange) Is Nothing Then GoTo CleanUp
'入力規則が「リスト」以外なら処理しない
If Not IsListValidation(Target) Then GoTo CleanUp
'ユーザーがDeleteで消した場合は、そのまま空にする(追記ロジックは走らせない)
If Len(Target.Value2) = 0 Then GoTo CleanUp
Application.EnableEvents = False
'今回選ばれた値(New)
newItem = Trim$(CStr(Target.Value2))
'直前の値(Old)を取得するためにUndo
Application.Undo
oldText = Trim$(CStr(Target.Value2))
'Oldが空なら、そのままNewを入れる
If Len(oldText) = 0 Then
Target.Value = newItem
GoTo CleanUp
End If
'Oldに含まれていなければ追記、含まれていれば削除(トグル)
If ListContainsItem(oldText, newItem) Then
Target.Value = RemoveItemFromList(oldText, newItem, DELIM)
Else
Target.Value = AppendItemToList(oldText, newItem, DELIM)
End If
CleanUp:
On Error Resume Next
Application.EnableEvents = prevEvents
End Sub
'セルに入力規則が存在しない場合でも落ちないように安全判定する
Private Function IsListValidation(ByVal cell As Range) As Boolean
On Error GoTo ErrHandler
IsListValidation = (cell.Validation.Type = 3) 'xlValidateList = 3
Exit Function
ErrHandler:
IsListValidation = False
End Function
'「追記」:既存の末尾に区切り文字+項目を追加
Private Function AppendItemToList(ByVal listText As String, ByVal item As String, ByVal delim As String) As String
If Len(listText) = 0 Then
AppendItemToList = item
Else
AppendItemToList = listText & delim & item
End If
End Function
'「含まれるか」:カンマ区切りのトークンとして完全一致で判定(部分一致事故を防ぐ)
Private Function ListContainsItem(ByVal listText As String, ByVal item As String) As Boolean
Dim arr() As String
Dim i As Long
Dim token As String
arr = Split(NormalizeComma(listText), ",")
For i = LBound(arr) To UBound(arr)
token = Trim$(arr(i))
If StrComp(token, item, vbTextCompare) = 0 Then
ListContainsItem = True
Exit Function
End If
Next i
End Function
'「削除」:一致した項目を取り除き、区切り文字で再結合
Private Function RemoveItemFromList(ByVal listText As String, ByVal item As String, ByVal delim As String) As String
Dim arr() As String
Dim i As Long
Dim token As String
Dim result As String
arr = Split(NormalizeComma(listText), ",")
For i = LBound(arr) To UBound(arr)
token = Trim$(arr(i))
If Len(token) > 0 Then
If StrComp(token, item, vbTextCompare) <> 0 Then
If Len(result) > 0 Then result = result & delim
result = result & token
End If
End If
Next i
RemoveItemFromList = result
End Function
'全角カンマや読点などの揺れを最低限吸収(既存データが混ざってもSplitできるようにする)
Private Function NormalizeComma(ByVal s As String) As String
s = Replace(s, ",", ",") '全角カンマ
s = Replace(s, "、", ",") '読点
NormalizeComma = s
End Function
コードの押さえどころ(重要ポイントだけ)
対象範囲は「必ず」シートを明示する
Set multiSelectRange = Me.Range("C2:C124")のMeは「このシート自身」を指します。標準モジュールや別シートにコードがあると、意図しないシートのRangeを参照してしまい、Intersect判定がずれて動作しません。シートモジュールに置き、Me.Rangeで指定するのが安全です。
Application.Undoが必要な理由
Worksheet_Changeが発火した時点では、セルにはすでに「新しい値(New)」が入っています。そこで、Application.Undoで直前の状態に戻し、戻ったセル値を「旧値(Old)」として読み取ります。最後に改めて目的の値をTarget.Valueへセットし直す、という流れです。
部分一致の事故を避ける
単純にInStrで含有判定すると、例えば「A」と「AA」のように似た値があるときに誤判定しやすくなります。完成版では、いったんSplitで配列化し、トークン単位で完全一致(大文字小文字は無視)判定しています。
EnableEventsは「必ず」元に戻す
Application.EnableEvents = Falseにしないと、コードがセル値を書き換えた瞬間に再度Worksheet_Changeが走り、無限ループになります。一方で、エラーや途中終了で戻し忘れると、Excel全体でイベントが発火しなくなり「マクロが壊れた」ように見えます。終了処理で確実に復帰する構造が重要です。
動作イメージ(追記/削除の挙動)
| 操作 | セルの表示例 | 内部で起きていること |
|---|---|---|
| 最初に「りんご」を選ぶ | りんご | Oldが空なのでNewをそのままセット |
| 次に「みかん」を選ぶ | りんご, みかん | Oldに含まれないので末尾に追記 |
| もう一度「りんご」を選ぶ | みかん | Oldに含まれるので削除(トグル) |
カスタマイズ例(現場で便利な調整)
区切り文字を変えたい
コード冒頭のDELIMを書き換えるだけです。日本語の一覧なら読点「、」にしたいケースもありますが、既存データとの互換性を考えるとカンマ+半角スペースが無難です。
| DELIMの例 | 見え方 | 向いているケース |
|---|---|---|
", " | A, B, C | 最も一般的。CSVにも近く扱いやすい |
"、" | A、B、C | 日本語文として自然に見せたい |
" / " | A / B / C | 値の中にカンマが入り得る場合の回避策 |
重複を許したくない(常にユニーク化したい)
完成版は「含まれていれば削除」という仕様なので、結果的に重複は発生しにくいですが、過去データに重複が混ざることはあります。削除関数は一致項目をすべて取り除くため、再選択で重複が一掃されます。さらに厳密にユニーク化したいなら、追記前に既存リストを整形してから処理する方法が有効です。
選択数に上限を設けたい
例えば「最大3つまで」に制限したい場合は、追記する前に現在の要素数を数え、上限を超えるなら処理を中断します。
'追記の直前に入れる例(最大3件)
If UBound(Split(NormalizeComma(oldText), ",")) + 1 >= 3 Then
'上限到達:新規追加しない(必要ならメッセージ表示など)
Target.Value = oldText
GoTo CleanUp
End If
並び順を常に整えたい(ソートしたい)
「入力者の選んだ順」を残したい場合はそのままでOKですが、検索・集計都合で並び順を固定したい場合もあります。並び替えまでやるとコードが長くなるので、運用上は次のどちらかが現実的です。
- 表示は選択順のままにして、集計用に別セルで正規化(PowerQueryや関数で整形)
- 選択肢が少ない場合は、VBAで配列をソートして再結合(追加実装)
トラブルシューティング(うまく動かない時の確認順)
| チェック項目 | 確認方法 | ありがちな落とし穴 |
|---|---|---|
| マクロ有効ブックか | 拡張子が.xlsmになっているか | .xlsxだとVBAを保存できない |
| マクロが有効になっているか | 起動時のセキュリティ警告で「コンテンツの有効化」を押したか | 社内ポリシーでブロックされていることもある |
| コードの場所が正しいか | 対象シートのコードモジュールに貼ったか | ThisWorkbookや標準モジュールではイベントが発火しない |
| 対象範囲が合っているか | Me.Range("C2:C124")が実際のセル範囲と一致しているか | 列や行がズレると一切処理されない |
| 入力規則がリストになっているか | セル→データの入力規則で「リスト」になっているか | コピー・貼り付けで入力規則が外れていることがある |
| 貼り付けで試していないか | まずはドロップダウン選択で試す | 複数セル貼り付けは意図通りにならない |
| 共同編集・Web版Excelではないか | デスクトップ版Excelで開いているか | Excel for the webはVBAが動作しない |
実運用でハマりやすい注意点
Application.Undoが効かないケースがある
Application.Undoは「直前の操作」を戻す仕組みなので、操作によっては期待通りに戻らないことがあります。代表例は次のとおりです。
- 複数セル同時貼り付け(Undo対象が広すぎる)
- 別マクロが直前に実行されてUndo履歴が変わっている
- 外部アドインや特殊な編集操作でUndo履歴が崩れる
このため、完成版では複数セル変更を対象外にしています。大量入力をしたい場合は、後処理で整形する運用(入力後に別マクロで分解・整形)も検討してください。
区切り文字がデータに含まれると破綻する
この方式は「セル内を区切り文字で分割して要素にする」ため、項目そのものにカンマが含まれると正しく分解できません。候補リストにカンマが含まれる可能性がある場合は、区切り文字を" / "などに変える、あるいは別列にID(数値)を保持して表示名は参照で出す、といった設計が安全です。
入力規則の候補の管理は「テーブル+名前定義」が強い
候補が増減する運用では、別シートの候補範囲をExcelテーブル(挿入→テーブル)にしておくと、行追加だけで範囲が伸びます。そのテーブル列を参照する名前付き範囲を入力規則に指定すれば、メンテナンスコストを大きく下げられます。
まとめ
Excelの入力規則(リスト)を複数選択にしたい場合、別シートに候補があること自体よりも、イベントコードの置き場所や構文ミス、EnableEventsの戻し忘れが原因で動かないケースがほとんどです。まずは「入力規則を名前付き範囲で安定化」し、「対象シートのコードモジュールに、事故りにくいWorksheet_Change」を入れることで、カンマ区切り追記と再選択削除(トグル)が実用レベルで動くようになります。

コメント