第29回.ListBox・ComboBoxをマウスホイール対応させる
ユーザーフォームに配置した「ListBox」や「ComboBox」は、標準状態ではマウスホイールによるスクロール操作に対応していません。
大量のデータを選択するフォームでは、スクロールバーを直接ドラッグしなければならず、操作性の面で大きな課題となります。
「コンボボックスが閉じているのに画面外でスクロールが続いてしまう」「64bit環境で動作しない」「WindowsのDPI(画面拡大率)を変えると判定がズレる」
といった課題を抱えているものが少なくありません。
本記事では、これらの問題をクリアし、コンボボックスのドロップダウン展開状態まで厳密に判定する、【完全版】といえるVBAコードを公開・解説します。
本モジュールで解決できること
- ListBox対応
フォーム上のリストボックスにおいて、マウスホイールによるスムーズな上下スクロールを実現します。
- ComboBox対応
コンボボックスの選択項目切り替え、および展開時のドロップダウンリストのスクロールに対応します。
- ドロップダウン開閉判定
コンボボックスの展開状態をリアルタイムで検知し、開いている時・閉じている時で最適な操作対象を自動切り替えします。
- DPI対応(画面拡大率)
Windowsのディスプレイ設定(125%や150%などの拡大率)に関わらず、マウスの位置判定がズレません。
- 複雑なコンテナネスト(MultiPage / Frame)対応
MultiPageやFrameの中に配置されたコントロールであっても、親コンテナを遡って原点を自動計算するため正確に領域を割り出します。
- 32bit / 64bit完全対応
PtrSafe および LongPtr を適切に実装しているため、Officeのビット数を問わずそのまま動作します。
- 誤スクロール防止
リストやドロップダウンの枠外にマウスがある時は動作を遮断し、裏にある関係のない画面が勝手に動くのを防止します。
- 再入防止(再帰呼び出しガード)
ホイールの高速回転時に処理が重なってExcelがフリーズ・フック落ちするのを、実行中フラグにより完全に防ぎます。
機能概要
単にスクロールさせるだけでなく、「マウスカーソルが今どこにあるか」 を高精度に判定することで、直感的で誤作動のない操作感を実現しています。
コントロールごとのスクロール挙動とマウス位置判定
- ListBox(リストボックス)
- マウス位置: マウスカーソルが ListBox の枠内(表示領域)にある時のみスクロールが機能します。
- 挙動: ホイールの回転方向に合わせてリストが3行ずつ上下にスクロールします(SCROLL_ROWS 定数で変更可能)。枠外へ出ると即座にスクロールが停止します。
- ComboBox(コンボボックス)
- ドロップダウン非展開時(閉じている状態)
- マウス位置: マウスカーソルが ComboBox 本体の枠内にある時のみ機能します。
- 挙動: ホイールを回転させると、ドロップダウンを開かずに選択項目(ListIndex)が前後に切り替わります。枠外へ移動すると操作を受け付けなくなります。
- ドロップダウン展開時(リストが開いている状態)
- マウス位置: 画面上に展開されたドロップダウンリストの枠内にある時のみ機能します。
- 挙動: 展開されたリスト内をスムーズにスクロールできます。マウスカーソルをドロップダウンリストの枠外へ動かした瞬間にスクロールが停止するため、裏にある他のコントロールやフォームを誤ってスクロールさせてしまう心配がありません。
- ドロップダウン非展開時(閉じている状態)
技術的な特長(完成度を高める工夫)
- ドロップダウン領域のピンポイント特定(GW_OWNER 判定)
コンボボックス展開時にWindowsが一時生成するポップアップウィンドウの所有者(Owner)関係を GetWindow(hwnd, GW_OWNER) で追跡し、「今開いているドロップダウン領域」の絶対画面座標を正確に割り出します。ドロップダウンが閉じている時や枠外操作時の誤作動、裏画面の誤スクロールを完全に遮断します。 - DPIスケーリング・高DPI環境への完全対応(論理座標変換)
Windowsのディスプレイ設定(拡大/縮小 125%、150%など)に合わせて、VBA内部のポイント単位(pt)とAPIのピクセル単位(px)の変換係数を自動算出します。さらに、OSフックで取得した物理座標を PhysicalToLogicalPointForPerMonitorDPI API で即座に論理座標へ変換するため、高解像度モニターやマルチモニター環境でも判定座標が一切ズレません。 - MultiPage・Frame「深層ネスト」の階層再帰探索
コントロールが Frame や MultiPage 内に何重にもネスト配置されている場合でも、専用の逆引き関数による再帰探索で最上位フォームまで階層を遡ります。各Frameの枠線厚みやMultiPageのタブ見出し高さなどを正確に加算し、複雑なフォーム構造であってもコントロールの画面上の絶対領域を正しく特定します。 - 二重実行防止と自動解体の「二重安全ガード」
ホイールの高速回転時に処理が重ならないよう内部フラグ(IsProcessing)による再入防止を行っています。また、フック処理時に IsWindow API でフォームの生存確認を常時行い、フォーム閉鎖を検知した場合はその場で安全かつ自動的にフックを解除してExcelのクラッシュを防ぎます。 - 32bit / 64bit 完全対応
PtrSafe および LongPtr を適正に使用しており、Office 2010 以降のすべての環境(32bit / 64bit)でそのまま動作します。
ユーザーフォームのサンプルとVBA
コントロールの配置
- ListBox1(リストボックス)
- ComboBox1(コンボボックス)

UserFormのコード
マウスがコントロールの上に乗った(MouseMove)タイミングでフックを開始し、フォームを閉じる際や非アクティブ化時にフックを安全に解除します。
Option Explicit
Private Sub UserForm_Initialize()
Dim i As Long
For i = 1 To 100
Me.ComboBox1.AddItem "コンボ項目 " & i
Me.ComboBox2.AddItem "コンボ項目 " & i
Me.ComboBox3.AddItem "コンボ項目 " & i
Me.ListBox1.AddItem "リスト項目 " & i
Me.ListBox2.AddItem "リスト項目 " & i
Me.ListBox3.AddItem "リスト項目 " & i
Next i
End Sub
' ListBox 上にカーソルが乗ったらフック対象に設定
Private Sub ListBox1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call StartMouseHook(Me.ListBox1)
End Sub
Private Sub ListBox2_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call StartMouseHook(Me.ListBox2)
End Sub
Private Sub ListBox3_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call StartMouseHook(Me.ListBox3)
End Sub
' ComboBox 上にカーソルが乗ったらフック対象に設定
Private Sub ComboBox1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call StartMouseHook(Me.ComboBox1)
End Sub
Private Sub ComboBox2_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call StartMouseHook(Me.ComboBox2)
End Sub
Private Sub ComboBox3_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call StartMouseHook(Me.ComboBox3)
End Sub
' フォームが閉じられる直前にフックを終了
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
Call StopMouseHook
End Sub
' フォーム終了時にフックを終了
Private Sub UserForm_Terminate()
Call StopMouseHook
End Sub
※対象コントロールが多い場合は、イベントプロシージャーの共通化を検討してください。
第23回.イベントプロシージャーの共通化
標準モジュールのVBA
以下のコメントではモジュール名を指定していますが、実際にはモジュール名は任意です。
Option Explicit
'===============================================================================
' モジュール名 : ModMouseWheel
' 概要 : WH_MOUSE_LL(低レベルマウスフック)を使用した
' UserForm 上の ListBox / ComboBox マウスホイールスクロール制御
' 備考 : ・Office 2010 以降(32bit / 64bit)対応
' ・GW_OWNER 判定による ComboBox ドロップダウン領域の特定
' ・MultiPage (ClientLeft/ClientTop) や Frame (InsideWidth/Height)
' の内部原点自動算出による高精度な座標判定
' ・Page.Parent が MultiPage を返さない仕様に対応し、
' MultiPage を明示的に逆引きして原点計算に含めるよう対応
' ・TypeOf ページオブジェクト Is MSForms.UserForm の誤判定を回避するため
' 中間コンテナ型を肯定条件で列挙して階層追跡を実行
' ・ディスプレイ拡大率(DPI)が100%を超える環境向けに、
' マウスフック座標を PhysicalToLogicalPointForPerMonitorDPI で
' 論理座標に変換してから判定するよう対応
' ・MSForms.MultiPage / Page のプロパティ参照エラー(エラー438)回避のため
' MultiPage自身のLeft/Topに近似オフセット定数を加算する方式で原点を算出
' ・[安全対策] フォーム破棄時の自動解体ガード(IsWindow)および
' フックチェーン断絶防止ロジックを MouseProc に組み込み済み
'===============================================================================
'===============================================================================
' Windows API 宣言
'===============================================================================
' --- フック関連 API ---
' 低レベルフックプロシージャをフックチェーンに登録する関数
Private Declare PtrSafe Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA" ( _
ByVal idHook As Long, _
ByVal lpfn As LongPtr, _
ByVal hmod As LongPtr, _
ByVal dwThreadId As Long) As LongPtr
' 登録したフックプロシージャをフックチェーンから解除する関数
Private Declare PtrSafe Function UnhookWindowsHookEx Lib "user32" ( _
ByVal hHook As LongPtr) As Long
' フックチェーン内の次のフックプロシージャへメッセージを転送する関数
Private Declare PtrSafe Function CallNextHookEx Lib "user32" ( _
ByVal hHook As LongPtr, _
ByVal nCode As Long, _
ByVal wParam As LongPtr, _
ByVal lParam As LongPtr) As LongPtr
' --- メモリ・描画 (DPI計算) 関連 API ---
' メモリ領域間でデータをコピーする関数(構造体のデコンパイル用)
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
Destination As Any, _
Source As Any, _
ByVal Length As LongPtr)
' 指定されたウィンドウのデバイスコンテキスト(DC)を取得する関数
Private Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr
' デバイスコンテキスト(DC)を解放する関数
Private Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As LongPtr, ByVal hdc As LongPtr) As Long
' ディスプレイデバイスの各種機能を指す値(DPIなど)を取得する関数
Private Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hdc As LongPtr, ByVal nIndex As Long) As Long
' --- ウィンドウ・座標取得 関連 API ---
' クラス名およびウィンドウ名(キャプション)からウィンドウハンドルを取得する関数
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" ( _
ByVal lpClassName As String, _
ByVal lpWindowName As String) As LongPtr
' 現在アクティブなウィンドウのハンドルを取得する関数
Private Declare PtrSafe Function GetActiveWindow Lib "user32" () As LongPtr
' 指定されたウィンドウハンドルが実在・生存しているか確認する関数
Private Declare PtrSafe Function IsWindow Lib "user32" (ByVal hwnd As LongPtr) As Long
' 指定ウィンドウのクライアント領域座標をスクリーン座標系に変換する関数
Private Declare PtrSafe Function ClientToScreen Lib "user32" ( _
ByVal hwnd As LongPtr, _
ByRef lpPoint As POINTAPI) As Long
' ウィンドウの境界矩形領域(スクリーン座標系)を取得する関数
Private Declare PtrSafe Function GetWindowRect Lib "user32" ( _
ByVal hwnd As LongPtr, _
ByRef lpRect As RECT) As Long
' 指定されたウィンドウの表示状態(可視/不可視)を取得する関数
Private Declare PtrSafe Function IsWindowVisible Lib "user32" ( _
ByVal hwnd As LongPtr) As Long
' 指定されたウィンドウと特定のリレーション関係(親、所有者など)にあるウィンドウハンドルを取得する関数
Private Declare PtrSafe Function GetWindow Lib "user32" ( _
ByVal hwnd As LongPtr, _
ByVal wCmd As Long) As LongPtr
' 現在の呼び出し元スレッドのIDを取得する関数
Private Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long
' 指定されたスレッドIDに関連付けられている全ウィンドウを列挙する関数
Private Declare PtrSafe Function EnumThreadWindows Lib "user32" ( _
ByVal dwThreadId As Long, _
ByVal lpfn As LongPtr, _
ByVal lParam As LongPtr) As Long
' --- DPI座標変換 API ---
' フックから得られる物理ピクセル座標を、ウィンドウのDPI仮想化後の論理座標へ変換する関数(高DPI環境用)
Private Declare PtrSafe Function PhysicalToLogicalPointForPerMonitorDPI Lib "user32" ( _
ByVal hwnd As LongPtr, _
ByRef lpPoint As POINTAPI) As Long
'===============================================================================
' 定数定義
'===============================================================================
' --- フック識別定数 ---
Private Const WH_MOUSE_LL As Long = 14 ' 低レベルマウスフックのフックタイプ識別子
Private Const WM_MOUSEWHEEL As Long = &H20A ' マウスホイールの回転を感知するメッセージID
Private Const HC_ACTION As Long = 0 ' メッセージが正常に処理対象であることを示すフックコード
' --- DPI取得用定数 ---
Private Const LOGPIXELSX As Long = 88 ' 画面の横方向の論理インチ当たりピクセル数(DPI)を取得するインデックス
Private Const LOGPIXELSY As Long = 90 ' 画面の縦方向の論理インチ当たりピクセル数(DPI)を取得するインデックス
' --- ウィンドウ検索・スクロール調整用定数 ---
Private Const GW_OWNER As Long = 4 ' GetWindow関数でオーナーウィンドウ(所有者)を取得するためのコマンド値
Private Const SCROLL_ROWS As Long = 3 ' 1回のホイール回転でスクロールさせる行数(ListBox用)
'===============================================================================
' 構造体定義
'===============================================================================
' 画面上のX, Y座標(ピクセル単位)を格納する構造体
Private Type POINTAPI
X As Long ' X座標(水平位置)
Y As Long ' Y座標(垂直位置)
End Type
' 矩形領域の境界座標(ピクセル単位)を格納する構造体
Private Type RECT
Left As Long ' 左端の座標
Top As Long ' 上端の座標
Right As Long ' 右端の座標
Bottom As Long ' 下端の座標
End Type
' 低レベルマウスフック(WH_MOUSE_LL)から渡される詳細情報を格納する構造体
Private Type MSLLHOOKSTRUCT
pt As POINTAPI ' マウスカーソルの物理画面座標
mouseData As Long ' ホイール回転方向・移動量データ(上位16ビット)
flags As Long ' イベント発生フラグ
time As Long ' タイムスタンプ
dwExtraInfo As LongPtr ' 拡張情報ポインタ
End Type
'===============================================================================
' モジュール内変数定義
'===============================================================================
Private hHook As LongPtr ' 設定したフックのハンドル
Private TargetControl As Object ' スクロール制御対象のコントロール(ListBox / ComboBox)
Private IsProcessing As Boolean ' 重複処理(再帰呼び出し)を防止するための処理中フラグ
Private PointsToPixelX As Double ' ポイント(pt)からピクセル(px)への変換倍率(X方向)
Private PointsToPixelY As Double ' ポイント(pt)からピクセル(px)への変換倍率(Y方向)
Private FoundDropdownHwnd As LongPtr ' 列挙処理時に検出されたドロップダウンリストのHWND
Private TargetFormHwnd As LongPtr ' 対象コントロールを保持する親UserFormのHWND
'===============================================================================
' 公開プロシージャ(外部からの開始・停止制御)
'===============================================================================
'-------------------------------------------------------------------------------
' プロシージャ名 : StartMouseHook
' 概要 : 低レベルマウスフックを開始し、指定コントロールのスクロール制御を無効化・横取りします。
' 引数 : ctrl (Object) - スクロール対象とするListBoxまたはComboBox
' 戻り値 : なし
'-------------------------------------------------------------------------------
Public Sub StartMouseHook(ByRef ctrl As Object)
' 制御対象のコントロールをセット
Set TargetControl = ctrl
' DPIスケール情報が未初期化の場合は取得・計算を実行
If PointsToPixelX = 0 Then InitDpiScale
' 親フォームの HWND をフック開始時に事前に特定・キャッシュ
Dim parentForm As Object
Set parentForm = GetParentForm(ctrl)
If Not parentForm Is Nothing Then
' フォームのキャプション文字列からウィンドウハンドルを取得
If Len(parentForm.Caption) > 0 Then
TargetFormHwnd = FindWindow("ThunderDFrame", parentForm.Caption)
If TargetFormHwnd = 0 Then TargetFormHwnd = FindWindow("ThunderXFrame", parentForm.Caption)
End If
' キャプションから取得できなかった場合は現在アクティブなウィンドウを採用
If TargetFormHwnd = 0 Then TargetFormHwnd = GetActiveWindow()
End If
' フックが未登録の場合のみOSにフックプロシージャを登録
If hHook = 0 Then
hHook = SetWindowsHookEx(WH_MOUSE_LL, AddressOf MouseProc, 0&, 0&)
End If
End Sub
'-------------------------------------------------------------------------------
' プロシージャ名 : StopMouseHook
' 概要 : 低レベルマウスフックを解除し、リソースおよび保持変数をクリアします。
' 引数 : なし
' 戻り値 : なし
'-------------------------------------------------------------------------------
Public Sub StopMouseHook()
' フックが登録されている場合は解除を実施
If hHook <> 0 Then
UnhookWindowsHookEx hHook
hHook = 0
End If
' モジュール内保持変数を初期化
Set TargetControl = Nothing
TargetFormHwnd = 0
IsProcessing = False
End Sub
'===============================================================================
' フックコールバック & スクロール振り分け処理
'===============================================================================
'-------------------------------------------------------------------------------
' プロシージャ名 : MouseProc
' 概要 : Windowsメッセージを監視し、対象コントロール上でのホイール操作を検知してスクロールを実行します。
' 引数 : nCode (Long) - フックコード(HC_ACTION かどうか)
' wParam (LongPtr) - WindowsメッセージID (WM_MOUSEWHEEL 等)
' lParam (LongPtr) - MSLLHOOKSTRUCT構造体へのポインタ
' 戻り値 : LongPtr - 次のフックチェーンの戻り値
'-------------------------------------------------------------------------------
Public Function MouseProc(ByVal nCode As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr
' 万が一の例外発生時にフックチェーンが断絶しないよう、初期値として次のフック結果を確保
Dim nextHookResult As LongPtr
nextHookResult = CallNextHookEx(hHook, nCode, wParam, lParam)
On Error GoTo SafeExit
' ホイール操作メッセージの検知チェック
If nCode = HC_ACTION And wParam = WM_MOUSEWHEEL Then
' 対象コントロールが存在しない、または処理中の場合はスルー
If TargetControl Is Nothing Or IsProcessing Then
MouseProc = nextHookResult
Exit Function
End If
' 安全ガード: 親フォームのウィンドウが破棄・閉鎖されている場合は自動解体
If TargetFormHwnd <> 0 Then
If IsWindow(TargetFormHwnd) = 0 Then
StopMouseHook
MouseProc = nextHookResult
Exit Function
End If
End If
' lParam からフック構造体データをコピーして取得
Dim hs As MSLLHOOKSTRUCT
CopyMemory hs, ByVal lParam, LenB(hs)
' フックから渡された物理ピクセル座標を論理座標系へ変換(高DPI対策)
Dim ptLogical As POINTAPI
ptLogical = hs.pt
If TargetFormHwnd <> 0 Then
PhysicalToLogicalPointForPerMonitorDPI TargetFormHwnd, ptLogical
End If
' マウスカーソルが対象コントロール(またはそのドロップダウン)の上にあるか判定
If IsCursorOverControl(TargetControl, ptLogical) Then
IsProcessing = True ' 二重処理防止フラグをオン
' mouseDataの上位16ビットから回転方向と移動量を割り出し
Dim delta As Long
delta = CLng(hs.mouseData / &H10000)
' コントロールの型に応じたスクロール処理の実行
If TypeOf TargetControl Is MSForms.ListBox Then
ScrollListBox TargetControl, delta
ElseIf TypeOf TargetControl Is MSForms.ComboBox Then
ScrollComboSelection TargetControl, delta
End If
IsProcessing = False ' 処理完了に伴いフラグを解除
End If
End If
SafeExit:
MouseProc = nextHookResult
End Function
'===============================================================================
' 位置判定・ウィンドウ特定ヘルパー関数
'===============================================================================
'-------------------------------------------------------------------------------
' プロシージャ名 : IsCursorOverControl
' 概要 : 指定したマウス座標が対象コントロール(またはドロップダウンリスト)内にあるか判定します。
' 引数 : ctrl (Object) - 判定対象のコントロール
' mousePt (POINTAPI) - 判定する論理画面座標
' 戻り値 : Boolean - コントロール範囲内の場合は True、範囲外なら False
'-------------------------------------------------------------------------------
Private Function IsCursorOverControl(ByRef ctrl As Object, ByRef mousePt As POINTAPI) As Boolean
On Error GoTo ErrHandler
' 親フォームのHWNDが取得できていない場合は判定不能
If TargetFormHwnd = 0 Then
Exit Function
End If
' --------------------------------------------------------------------------
' パターン 1: ComboBox のドロップダウンリストが開いている場合の判定
' --------------------------------------------------------------------------
If TypeOf ctrl Is MSForms.ComboBox Then
Dim hwndComboList As LongPtr
hwndComboList = GetCurrentDropdownHwnd()
' ドロップダウンウィンドウが検知できた場合、その領域内かをチェック
If hwndComboList <> 0 Then
Dim rcList As RECT
GetWindowRect hwndComboList, rcList
If (mousePt.X >= rcList.Left And mousePt.X <= rcList.Right) And _
(mousePt.Y >= rcList.Top And mousePt.Y <= rcList.Bottom) Then
IsCursorOverControl = True
Else
IsCursorOverControl = False
End If
Exit Function
End If
End If
' --------------------------------------------------------------------------
' パターン 2: 通常時(閉じたComboBox、または ListBox)の本体枠内判定
' --------------------------------------------------------------------------
Dim rcCtrl As RECT
If GetControlScreenRect(ctrl, rcCtrl) Then
' 端数計算による僅かな誤差を吸い込むマージン設定(8px)
Const MARGIN_PX As Long = 8
' コントロールの表示範囲(マージン拡張含む)に座標が含まれるか評価
If (mousePt.X >= (rcCtrl.Left - MARGIN_PX) And mousePt.X <= (rcCtrl.Right + MARGIN_PX)) And _
(mousePt.Y >= (rcCtrl.Top - MARGIN_PX) And mousePt.Y <= (rcCtrl.Bottom + MARGIN_PX)) Then
IsCursorOverControl = True
Exit Function
End If
End If
IsCursorOverControl = False
Exit Function
ErrHandler:
IsCursorOverControl = False
End Function
'-------------------------------------------------------------------------------
' プロシージャ名 : GetCurrentDropdownHwnd
' 概要 : 現在開いている ComboBox のドロップダウンリストウィンドウの HWND を取得します。
' 引数 : なし
' 戻り値 : LongPtr - 検出されたドロップダウンリストの HWND(見つからない場合は 0)
'-------------------------------------------------------------------------------
Private Function GetCurrentDropdownHwnd() As LongPtr
FoundDropdownHwnd = 0
If TargetFormHwnd = 0 Then Exit Function
Dim threadId As Long
threadId = GetCurrentThreadId()
' 現在のGUIスレッドに属する全ウィンドウをスキャン
EnumThreadWindows threadId, AddressOf EnumThreadWndProc, 0&
GetCurrentDropdownHwnd = FoundDropdownHwnd
End Function
'-------------------------------------------------------------------------------
' プロシージャ名 : EnumThreadWndProc
' 概要 : EnumThreadWindows コールバック。親フォームに所有されているポップアップリストを特定します。
' 引数 : hwnd (LongPtr) - 検証中のウィンドウハンドル
' lParam (LongPtr) - ユーザー定義パラメータ
' 戻り値 : Long - 列挙を継続する場合は 1、打ち切る場合は 0
'-------------------------------------------------------------------------------
Public Function EnumThreadWndProc(ByVal hwnd As LongPtr, ByVal lParam As LongPtr) As Long
' 可視状態のウィンドウのみをチェック対象とする
If IsWindowVisible(hwnd) <> 0 Then
' 親フォーム自身ではなく、かつオーナーが親フォームであるウィンドウを探す
If hwnd <> TargetFormHwnd Then
If GetWindow(hwnd, GW_OWNER) = TargetFormHwnd Then
Dim rc As RECT
GetWindowRect hwnd, rc
' 面積を持った有効なポップアップリスト領域であれば検出確定
If (rc.Right - rc.Left > 0) And (rc.Bottom - rc.Top > 0) Then
FoundDropdownHwnd = hwnd
EnumThreadWndProc = 0 ' 目的のHWNDが見つかったため列挙終了
Exit Function
End If
End If
End If
End If
EnumThreadWndProc = 1 ' 次のウィンドウ列挙へ継続
End Function
'-------------------------------------------------------------------------------
' プロシージャ名 : GetControlScreenRect
' 概要 : コントロールのフォーム内オフセットを算出し、画面上のスクリーン領域(ピクセル単位)を取得します。
' 引数 : ctrl (Object) - 対象コントロール
' rcOut (RECT) - 算出されたスクリーン矩形領域の格納用構造体
' 戻り値 : Boolean - 算出に成功した場合は True、失敗した場合は False
'-------------------------------------------------------------------------------
Private Function GetControlScreenRect(ByRef ctrl As Object, ByRef rcOut As RECT) As Boolean
If TargetFormHwnd = 0 Then Exit Function
' フォーム左上原点からの相対位置(pt)を取得
Dim ctrlLeftPt As Double, ctrlTopPt As Double
If Not GetControlOffset(ctrl, ctrlLeftPt, ctrlTopPt) Then
GetControlScreenRect = False
Exit Function
End If
' フォームのクライアント領域の左上座標(スクリーン座標px)を取得
Dim ptClient As POINTAPI
ptClient.X = 0
ptClient.Y = 0
ClientToScreen TargetFormHwnd, ptClient
' ポイント単位をピクセル単位に変換して最終的なスクリーン座標を確定
Dim leftPx As Long, topPx As Long, widthPx As Long, heightPx As Long
leftPx = ptClient.X + CLng(ctrlLeftPt * PointsToPixelX)
topPx = ptClient.Y + CLng(ctrlTopPt * PointsToPixelY)
widthPx = CLng(ctrl.Width * PointsToPixelX)
heightPx = CLng(ctrl.Height * PointsToPixelY)
' 戻り値の構造体を設定
rcOut.Left = leftPx
rcOut.Top = topPx
rcOut.Right = leftPx + widthPx
rcOut.Bottom = topPx + heightPx
GetControlScreenRect = True
End Function
'-------------------------------------------------------------------------------
' プロシージャ名 : GetControlOffset
' 概要 : コントロールがネスト配置(Frame/MultiPage等)されている場合でも、フォーム原点からの総オフセット位置(pt)を階層追跡して算出します。
' 引数 : ctrl (Object) - 対象コントロール
' outLeft (Double) - 算出された左端からの累積オフセット(pt)
' outTop (Double) - 算出された上端からの累積オフセット(pt)
' 戻り値 : Boolean - 累積計算が正常終了した場合は True、途中で非表示項目等があった場合は False
'-------------------------------------------------------------------------------
Private Function GetControlOffset(ByRef ctrl As Object, ByRef outLeft As Double, ByRef outTop As Double) As Boolean
Dim tempObj As Object
outLeft = 0
outTop = 0
Set tempObj = ctrl
GetControlOffset = True
' 階層を上位(Parent)へたどるループ処理
' Page / Frame / 始点コントロール自身である間、繰り返し親方向へ遡る
Do While (tempObj Is ctrl) Or TypeOf tempObj Is MSForms.Page Or TypeOf tempObj Is MSForms.Frame
' 非表示(Visible = False)となっている要素が含まれる場合は無効とする
On Error Resume Next
If Not tempObj.Visible Then
GetControlOffset = False
Exit Function
End If
On Error GoTo 0
' --- 1. MultiPage 内の Page オブジェクトの処理 ---
If TypeOf tempObj Is MSForms.Page Then
Dim mp As MSForms.MultiPage
Dim pg As MSForms.Page
Set pg = tempObj
' Pageから親MultiPageを再帰逆引き検索
Set mp = FindOwnerMultiPage(pg)
If mp Is Nothing Then
GetControlOffset = False
Exit Function
End If
' 現在アクティブなタブでない場合は処理対象外とする
If mp.Value <> pg.Index Then
GetControlOffset = False
Exit Function
End If
' MultiPageの枠線およびタブ見出し高さを考慮したオフセットを加算
Const MP_TAB_HEADER_HEIGHT As Double = 18 ' タブ見出し帯の近似高さ(pt)
Const MP_BORDER_MARGIN As Double = 2 ' 枠線部分の近似厚み(pt)
outLeft = outLeft + mp.Left + MP_BORDER_MARGIN
outTop = outTop + mp.Top + MP_TAB_HEADER_HEIGHT
Set tempObj = mp.Parent
If tempObj Is Nothing Then Exit Function
' --- 2. Frame オブジェクトの処理 ---
ElseIf TypeOf tempObj Is MSForms.Frame Then
Dim fr As MSForms.Frame
Set fr = tempObj
' Frameの枠線・内部領域(InsideWidth/Height)の差分をオフセットとして補正加算
outLeft = outLeft + fr.Left + ((fr.Width - fr.InsideWidth) / 2)
outTop = outTop + fr.Top + (fr.Height - fr.InsideHeight - 2)
Set tempObj = tempObj.Parent
If tempObj Is Nothing Then Exit Function
' --- 3. その他の通常コントロール(最初に渡されたctrl自身など) ---
Else
outLeft = outLeft + tempObj.Left
outTop = outTop + tempObj.Top
Set tempObj = tempObj.Parent
If tempObj Is Nothing Then Exit Function
End If
Loop
End Function
'-------------------------------------------------------------------------------
' プロシージャ名 : FindOwnerMultiPage
' 概要 : 指定した Page オブジェクトを保持している親 MultiPage を逆引き取得します。
' 引数 : pg (MSForms.Page) - 所有者を検索したい Page オブジェクト
' 戻り値 : MSForms.MultiPage - 検出された MultiPage(見つからない場合は Nothing)
'-------------------------------------------------------------------------------
Private Function FindOwnerMultiPage(ByRef pg As MSForms.Page) As MSForms.MultiPage
Dim topForm As Object
Set topForm = GetParentForm(pg)
If topForm Is Nothing Then Exit Function
' 最上位フォームからコントロール構造を再帰スキャンして探索
Set FindOwnerMultiPage = SearchMultiPageRecursive(topForm, pg)
End Function
'-------------------------------------------------------------------------------
' プロシージャ名 : SearchMultiPageRecursive
' 概要 : 容器オブジェクト内のコントロール群を再帰的にスキャンし、指定 Page を所有する MultiPage を探します。
' 引数 : container (Object) - 検索対象のコンテナ(UserForm / Frame / Page)
' pg (MSForms.Page) - 探している Page オブジェクト
' 戻り値 : MSForms.MultiPage - 発見された MultiPage
'-------------------------------------------------------------------------------
Private Function SearchMultiPageRecursive(ByRef container As Object, ByRef pg As MSForms.Page) As MSForms.MultiPage
Dim c As Object
Dim i As Long
Dim found As MSForms.MultiPage
For Each c In container.Controls
' MultiPageが配置されているかチェック
If TypeOf c Is MSForms.MultiPage Then
For i = 0 To c.Pages.Count - 1
' 探しているPageオブジェクトと一致した場合
If c.Pages(i) Is pg Then
Set SearchMultiPageRecursive = c
Exit Function
End If
' MultiPageがネスト配置(マルチページ内にさらにマルチページ)されている場合の再帰探索
Set found = SearchMultiPageRecursive(c.Pages(i), pg)
If Not found Is Nothing Then
Set SearchMultiPageRecursive = found
Exit Function
End If
Next i
' Frame内のネスト探索
ElseIf TypeOf c Is MSForms.Frame Then
Set found = SearchMultiPageRecursive(c, pg)
If Not found Is Nothing Then
Set SearchMultiPageRecursive = found
Exit Function
End If
End If
Next c
End Function
'-------------------------------------------------------------------------------
' プロシージャ名 : GetParentForm
' 概要 : コントロール階層を上位にたどり、最上位にある UserForm を取得します。
' 引数 : ctrl (Object) - 起点となるコントロール
' 戻り値 : Object - 検出された UserForm オブジェクト
'-------------------------------------------------------------------------------
Private Function GetParentForm(ByRef ctrl As Object) As Object
Dim tempObj As Object
Set tempObj = ctrl
' コンテナ型(Page / Frame / MultiPage)および起点コントロール自身をたどる
Do While (tempObj Is ctrl) Or TypeOf tempObj Is MSForms.Page _
Or TypeOf tempObj Is MSForms.Frame Or TypeOf tempObj Is MSForms.MultiPage
If tempObj.Parent Is Nothing Then Exit Function
Set tempObj = tempObj.Parent
Loop
' ループ脱出時の最上位オブジェクト(UserForm)を返却
Set GetParentForm = tempObj
End Function
'-------------------------------------------------------------------------------
' プロシージャ名 : InitDpiScale
' 概要 : 画面の DPI 情報(1ポイント当たりのピクセル数)を取得し、スケール変数を設定します。
' 引数 : なし
' 戻り値 : なし
'-------------------------------------------------------------------------------
Private Sub InitDpiScale()
On Error Resume Next
Dim hdc As LongPtr
' 全画面のデバイスコンテキスト(DC)を取得
hdc = GetDC(0&)
If hdc <> 0 Then
' 各軸方向の DPI 値からポイント→ピクセル換算比を計算 (96DPI = 1.333... px/pt)
PointsToPixelX = GetDeviceCaps(hdc, LOGPIXELSX) / 72#
PointsToPixelY = GetDeviceCaps(hdc, LOGPIXELSY) / 72#
ReleaseDC 0&, hdc
End If
' APIからの取得に失敗した場合の標準フォールバック設定(100% DPI相当: 96/72)
If PointsToPixelX = 0 Then PointsToPixelX = 1.333333
If PointsToPixelY = 0 Then PointsToPixelY = 1.333333
End Sub
'===============================================================================
' 実際のスクロール移動処理
'===============================================================================
'-------------------------------------------------------------------------------
' プロシージャ名 : ScrollListBox
' 概要 : ListBox に対するスクロール制御を実行(TopIndex を変更)します。
' 引数 : lst (MSForms.ListBox) - 移動対象の ListBox
' delta (Long) - ホイール回転量・方向(正数で上スクロール、負数で下スクロール)
' 戻り値 : なし
'-------------------------------------------------------------------------------
Private Sub ScrollListBox(ByRef lst As MSForms.ListBox, ByVal delta As Long)
On Error Resume Next
With lst
' 上スクロール処理(回転量がプラス)
If delta > 0 Then
If .TopIndex >= SCROLL_ROWS Then
.TopIndex = .TopIndex - SCROLL_ROWS
Else
.TopIndex = 0
End If
' 下スクロール処理(回転量がマイナス)
ElseIf delta < 0 Then
If .ListCount > 0 Then
If .TopIndex + SCROLL_ROWS < .ListCount Then
.TopIndex = .TopIndex + SCROLL_ROWS
Else
.TopIndex = .ListCount - 1
End If
End If
End If
End With
End Sub
'-------------------------------------------------------------------------------
' プロシージャ名 : ScrollComboSelection
' 概要 : ComboBox に対する選択項目移動(ListIndex のインクリメント/デクリメント)を実行します。
' 引数 : cb (MSForms.ComboBox) - 移動対象の ComboBox
' delta (Long) - ホイール回転量・方向(正数で前項目、負数で次項目)
' 戻り値 : なし
'-------------------------------------------------------------------------------
Private Sub ScrollComboSelection(ByRef cb As MSForms.ComboBox, ByVal delta As Long)
On Error Resume Next
With cb
' 上スクロール(前の要素を選択)
If delta > 0 Then
If .ListIndex > 0 Then .ListIndex = .ListIndex - 1
' 下スクロール(次の要素を選択)
ElseIf delta < 0 Then
If .ListIndex < .ListCount - 1 Then .ListIndex = .ListIndex + 1
End If
End With
End Sub
VBAコードの詳細解説
主要なプロシージャごとにその役割と仕組みを解説します。
仕組み: StartMouseHook では、監視対象のコントロールを保持した上で SetWindowsHookEx APIを呼び出し、OS全体のマウスイベントを監視するフックプロシージャ(MouseProc)を登録します。二重フックを防ぐため未登録時(hHook = 0)のみ実行します。フォーム終了時や非アクティブ化時には StopMouseHook から UnhookWindowsHookEx を呼び出し、リソースを確実に解放します。
仕組み: ホイール操作(WM_MOUSEWHEEL)を検知した際、CopyMemory APIを使ってマウスの画面座標と回転方向を取り出します。取得した物理ピクセル座標は PhysicalToLogicalPointForPerMonitorDPI により論理座標系へと変換し、高DPI環境での判定ズレを防ぎます。また、フォーム破棄時に IsWindow で生存確認を行って安全に自動解体する仕組みや、二重呼び出し防止フラグ(IsProcessing)を備え、フリーズやフック落ちを防ぎます。
仕組み: ComboBox の判定時、まずドロップダウンリストが開いているかを調べます。リスト展開時は「展開されたドロップダウンリストの枠内」、閉じている時は「ComboBox 本体の枠内」というように、コンボボックスの状態に応じて当たり判定の対象領域を自動的に切り替え、僅かな誤差を吸収するマージン処理を含めて評価します。
仕組み: ドロップダウンはUserFormの子ウィンドウではなく、一時的に生成される別ウィンドウです。そのため通常の座標判定では検出できません。本モジュールでは EnumThreadWindows でスレッド内のウィンドウを順次チェックし、EnumThreadWndProc 内で GetWindow(hwnd, GW_OWNER) を使用して「親フォームが所有者(Owner)となっているポップアップ」のみを抽出することで、そのウィンドウだけを正確に特定しています。ドロップダウンが閉じている時はHWNDが「0」と判定されるため、画面外での誤動作や誤スクロールを完全に排除できます。
仕組み: InitDpiScale で画面のDPI解像度から換算係数を算出します。コントロールが Frame や MultiPage 内にネスト配置されている場合、GetControlOffset が親要素を遡り、Frameの枠線厚みやMultiPageのタブ見出し高さ(FindOwnerMultiPage や SearchMultiPageRecursive による再帰逆引き探索含む)を加算して絶対原点を算出します。これにDPI倍率を掛け合わせることで、どれほど複雑なフォーム構造や画面拡大率であっても寸分違わぬ領域判定を実現します。
仕組み: ホイールの回転方向に応じて移動方向を決定します。ListBox では TopIndex を調整して指定行数(定数 SCROLL_ROWS)分スクロールさせ、ComboBox では ListIndex を前後に変更することで、直感的な操作感を実現しています。
使用しているWindows APIの解説
- SetWindowsHookEx
役割: 指定したフックプロシージャをOSのフックチェーン(イベント監視ライン)に登録します。
用途: 低レベルマウスフック(WH_MOUSE_LL)を設置し、マウスホイールの回転通知をリアルタイムで受け取る仕組みを構築します。 - UnhookWindowsHookEx
役割: SetWindowsHookEx で登録したフックを解除します。
用途: フォームが閉じた際や非アクティブ化時に呼び出し、不要になった監視処理を終了してリソースを安全に解放します。 - CallNextHookEx
役割: 捕捉したフックイベントを、次のフック処理(OSや他のアプリ)へ通過させます。
用途: 自分の処理が終わった後、OSのマウス操作全般に影響を与えないようイベントを適切に次へ受け渡します。 - CopyMemory (RtlMoveMemory)
役割: メモリブロックのデータを別の変数(構造体)へ直接コピーします。
用途: フックAPIからポインタ(メモリ領域のアドレス)として渡されるマウスイベント情報を、VBAで読み取れる構造体(MSLLHOOKSTRUCT)へデータ展開します。
- FindWindow
役割: クラス名やウィンドウキャプション(タイトル)から、最前面にあるウィンドウのハンドル(HWND)を取得します。
用途: 対象の UserForm のキャプションから、フォーム本体のウィンドウハンドルを特定します。 - GetCurrentThreadId
役割: 現在実行されているスレッドのIDを取得します。
用途: 自プロセス(Excel/VBA)が生成したウィンドウのみを検索対象に絞り込むため、現在のスレッドIDを取得します。 - EnumThreadWindows
役割: 指定したスレッドが所有するすべてのウィンドウを順次列挙し、コールバック関数へ渡します。
用途: ComboBox が開いた際に一時生成されるドロップダウンリストのウィンドウを探し出します。 - GetWindow
役割: 指定したウィンドウと特定の関係(親・子・所有者など)にある別のウィンドウハンドルを取得します。
用途: 列挙されたウィンドウから GW_OWNER(所有者)フラグを用いて「UserFormが所有しているポップアップか」を判定し、ドロップダウンリスト領域をピンポイント特定します。 - IsWindowVisible
役割: 指定したウィンドウが現在画面上に表示(可視状態)されているかを判定します。
用途: 非表示状態のゴーストウィンドウや非アクティブなリストを排除し、現在展開されているドロップダウンのみを判定対象とします。
- ClientToScreen
役割: ウィンドウ内のクライアント領域座標(フォーム原点 0,0)を、画面全体の絶対座標(ピクセル)に変換します。
用途: UserForm が画面のどこに配置されているかを絶対ピクセル座標で割り出します。 - GetWindowRect
役割: 指定したウィンドウ全体の画面上の外形矩形(Left, Top, Right, Bottom)を取得します。
用途: 特定した ComboBox ドロップダウンリストの描画エリア(四角形)をピクセル単位で取得します。 - GetDC / ReleaseDC
役割: 画面(ディスプレイ)のデバイスコンテキスト(DC)を取得および解放します。
用途: 画面のグラフィック設定情報にアクセスするための準備・後始末を行います。 - GetDeviceCaps
役割: 指定したデバイスの各種能力値(解像度など)を取得します。
用途: 画面の物理DPI(1インチあたりのピクセル数)を取得し、Windowsの「画面拡大率(125%や150%など)」に応じた1ポイントあたりの正確なピクセル換算係数を算出します。 - PhysicalToLogicalPointForPerMonitorDPI
役割: OSフックで取得された物理ピクセル座標を、高DPIモニターや仮想化されたウィンドウの「論理ピクセル座標」へ変換します。
用途: Windowsの「高DPI設定(125%や150%など)」が有効なモニター上でマウスフック位置とフォーム位置の間に発生するズレを完全解消します。
同じテーマ「ユーザーフォーム入門」の記事
第19回.数値専用のテキストボックス
第20回.テキストボックスの各種イベント
第21回.ユーザーフォームの各種イベント
第22回.コントロールの動的作成
第23回.イベントプロシージャーの共通化
第24回.イベントプロシージャーの共通化(Enter,Exit)
第25回.簡易音楽プレーヤーの作成
第26回.プログレスバーを自作する
第27回.インクリメンタルサーチの実装
第28回.テンキーのスクリーンキーボード作成
第29回.ListBox・ComboBoxをマウスホイール対応させる
新着記事NEW ・・・新着記事一覧を見る
ITエンジニアのための経理・会計・簿記入門|エクセル雑感(2026-09-12)
「Python in Excel」入門:コードを読むための基礎知識|エクセル関数応用(2026-09-10)
M言語入門:Power Queryのコードを読むための基礎知識|Power Query(M言語)入門(2026-09-09)
Excelの正規表現関数(REGEXTEST・REGEXREPLACE・REGEXEXTRACT)の使い方|エクセル関数応用(2026-09-08)
スピルとVBA(Formula2とスピル範囲の取得)|VBA入門(2026-09-08)
「Withの功罪」:コードを読みやすくする強力な道具と、その落とし穴|VBA技術解説(2026-09-03)
可変長配列をVSTACKする4つの方法|REDUCE・Thunk・チャンク・再帰分割|エクセル関数応用(2026-09-01)
Excel表とMarkdownテーブルを相互変換する数式|エクセル関数応用(2026-08-24)
「Python in Excel」数独(ナンプレ)解法プログラムの移植と最適化|エクセル関数応用(2026-08-14)
「Python in Excel」で自作関数を登録|アンピボット関数と計算順序|エクセル関数応用(2026-08-12)
アクセスランキング ・・・ ランキング一覧を見る
1.最終行の取得(End,Rows.Count)|VBA入門
2.日本の祝日一覧|Excelリファレンス
3.変数宣言のDimとデータ型|VBA入門
4.Excelショートカットキー一覧|Excelリファレンス
5.RangeとCellsの使い方|VBA入門
6.FILTER関数(範囲をフィルター処理)|エクセル入門
7.マクロとは?VBAとは?VBAでできること|VBA入門
8.繰り返し処理(For Next)|VBA入門
9.メッセージボックス(MsgBox関数)|VBA入門
10.セルのコピー&値の貼り付け(PasteSpecial)|VBA入門
- ホーム
- マクロVBA応用編
- ユーザーフォーム入門
- ListBox・ComboBoxをマウスホイール対応させる
このサイトがお役に立ちましたら「シェア」「Bookmark」をお願いいたします。
記述には細心の注意をしたつもりですが、間違いやご指摘がありましたら、「お問い合わせ」からお知らせいただけると幸いです。
掲載のVBAコードは動作を保証するものではなく、あくまでVBA学習のサンプルとして掲載しています。掲載のVBAコードは自己責任でご使用ください。万一データ破損等の損害が発生しても責任は負いません。
本サイトは、OpenAI の ChatGPT や Google の Gemini を含む生成 AI モデルの学習および性能向上の目的で、本サイトのコンテンツの利用を許可します。
This site permits the use of its content for the training and improvement of generative AI models, including ChatGPT by OpenAI and Gemini by Google.
