Jaafar Tribak

Well-known Member
Joined
Dec 5, 2002
Messages
9,797
Office Version
  1. 2016
Platform
  1. Windows
Hi dear forum,

In the past few months, I have posted various codes which show how to run code upon wheelmouse-scrolling.

Basically, intercepting the mouse scroll event relies on four known techniques : Subclassing, Installing a windows Mouse Hook, using the SetWinEventHook API or using the PeekMessage\GetMessage API functions.

Unfortunately, each of these methods carry their own set of issues. Namely the following isues:

1- Subclassing a window within excel is prone to crashing and only works when applied to modal userforms.

2- Installing a Mouse hook or using the SetWinEventHook API is a better alternative but, it puts a noticeable burden on excel and most importantly, it will crash the entire application (GPF) if an unhandled error occurs or if the VBE is reset before uninstalling the mouse hook.

3- PeekMessage\GetMessage is probably the safest method as it won't crash excel but, it requires continiously running a loop in the background. Obviously, this makes this method less than ideal and should probably only be used in small temporary loops during code execution.

The above mentioned issues could be avoided if the code was running from within a dll loaded into excel but, I am looking at making this work purely with straight VBA without any dependencies.

Here, I present a workaround which seems to work just fine after running various tests (XL 2007,2010,2016 32bit and 64bit).

Basically, I am installing a low level windows mouse hook but, from a second hidden excel instance created on the fly.

By running the mouse hook from a remote process, there will be no risk of crashing excel should an error occur while the hook is installed... There are no heavy loops continiously running in the background either.

The hidden excel instance communicates with the current excel session back and forth and closes itself automatically when the current excel session is closed.

I have made the code into an addin so it can be flexibly used from all open workbooks .


WheelScroll.xla


MouseWheel Event Handler singnature:
VBA Code:
Private Sub OnMouseWheelScroll( _
    ByVal WheelDirection As Long, _
    ByVal Ctrl As Integer, _
    ByVal Shift As Integer, _
    ByRef Cancel As Boolean)

WheelDirection >0 Wheel Forward
WheelDirection <0 Wheel Backward

Ctrl =0 Ctrl Key is up while mousescrolling
Ctrl =-1 Ctrl Key is down while mousescrolling

Shift =0 Shift Key is up while mousescrolling
Shift =-1 Shift Key is down while mousescrolling

Cancel =TRUE to cancel mousescrolling

Note: The above event handler must be placed in the ThisWorkbook Module of the client workbook(s) (See next post in this thread for some usage examples)



Addin Code

1
- Code in a Standard Module of the Addin:

VBA Code:
Option Explicit

Private Type POINTAPI
    X As Long
    Y As Long
End Type

Private Type MOUSEHOOKSTRUCT
    Location As POINTAPI
    MouseData As Long
    Flags As Long
    time As Long
    ExtraInfo As Long
End Type
    
    
#If VBA7 Then
    Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr
    Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As Long
    Private Declare PtrSafe Function CLSIDFromString Lib "Ole32" (ByVal lpsz As LongPtr, pclsid As Any) As Long
    Private Declare PtrSafe Function GetActiveObject Lib "OleAut32" (rclsid As Any, ByVal pvReserved As LongPtr, ppunk As Any) As Long
    Private Declare PtrSafe Function RegisterActiveObject Lib "OleAut32" (ByVal pUnk As IUnknown, rclsid As Any, ByVal dwFlags As Long, pdwRegister As Long) As Long
    Private Declare PtrSafe Function SetProp Lib "user32" Alias "SetPropA" (ByVal hwnd As LongPtr, ByVal lpString As String, ByVal hData As LongPtr) As Long
    Private Declare PtrSafe Function GetProp Lib "user32" Alias "GetPropA" (ByVal hwnd As LongPtr, ByVal lpString As String) As LongPtr
    Private Declare PtrSafe Function RemoveProp Lib "user32" Alias "RemovePropA" (ByVal hwnd As LongPtr, ByVal lpString As String) As LongPtr
    Private Declare PtrSafe Function GetDesktopWindow Lib "user32" () As LongPtr
    Private Declare PtrSafe Function CoDisconnectObject Lib "ole32.dll" (ByVal pUnk As IUnknown, pvReserved As Long) As Long
    Private Declare PtrSafe Function RevokeActiveObject Lib "oleaut32.dll" (ByVal dwRegister As Long, ByVal pvReserved As Long) As Long
    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, lParam As Any) As LongPtr
    Private Declare PtrSafe Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As LongPtr
    Private Declare PtrSafe Function GetForegroundWindow Lib "user32" () As LongPtr
    Private Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As LongPtr, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
    Private Declare PtrSafe Function GetKeyState Lib "user32" (ByVal nVirtKey As Long) As Integer
    Private Declare PtrSafe Sub mouse_event Lib "user32" (ByVal dwFlags As Long, ByVal dx As Long, ByVal dy As Long, ByVal cButtons As Long, ByVal dwExtraInfo As LongPtr)
    Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
    Private Declare PtrSafe Function GetWindowThreadProcessId Lib "user32" (ByVal hwnd As LongPtr, lpdwProcessId As Long) As Long
    Private Declare PtrSafe Function GetCurrentProcessId Lib "kernel32" () As Long
    
    Private lMouseHook As LongPtr
#Else
    Private Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
    Private Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
    Private Declare Function CLSIDFromString Lib "Ole32" (ByVal lpsz As Long, pclsid As Any) As Long
    Private Declare Function GetActiveObject Lib "OleAut32" (rclsid As Any, ByVal pvReserved As Long, ppunk As Any) As Long
    Private Declare Function RegisterActiveObject Lib "OleAut32" (ByVal pUnk As IUnknown, rclsid As Any, ByVal dwFlags As Long, pdwRegister As Long) As Long
    Private Declare Function SetProp Lib "user32" Alias "SetPropA" (ByVal hwnd As Long, ByVal lpString As String, ByVal hData As Long) As Long
    Private Declare Function GetProp Lib "user32" Alias "GetPropA" (ByVal hwnd As Long, ByVal lpString As String) As Long
    Private Declare Function RemoveProp Lib "user32" Alias "RemovePropA" (ByVal hwnd As Long, ByVal lpString As String) As Long
    Private Declare Function GetDesktopWindow Lib "user32" () As Long
    Private Declare Function CoDisconnectObject Lib "ole32.dll" (ByVal pUnk As IUnknown, pvReserved As Long) As Long
    Private Declare Function RevokeActiveObject Lib "oleaut32.dll" (ByVal dwRegister As Long, ByVal pvReserved As Long) As Long
    Private Declare Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA" (ByVal idHook As Long, ByVal lpfn As Long, ByVal hmod As Long, ByVal dwThreadId As Long) As Long
    Private Declare Function UnhookWindowsHookEx Lib "user32" (ByVal hHook As Long) As Long
    Private Declare Function CallNextHookEx Lib "user32" (ByVal hHook As Long, ByVal ncode As Long, ByVal wParam As Long, lParam As Any) As Long
    Private Declare Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As Long
    Private Declare Function GetForegroundWindow Lib "user32" () As Long
    Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
    Private Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Long) As Integer
    Private Declare Sub mouse_event Lib "user32" (ByVal dwFlags As Long, ByVal dx As Long, ByVal dy As Long, ByVal cButtons As Long, ByVal dwExtraInfo As Long)
    Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
    Private Declare Function GetWindowThreadProcessId Lib "user32" (ByVal hwnd As Long, lpdwProcessId As Long) As Long
    Private Declare Function GetCurrentProcessId Lib "kernel32" () As Long
    
    Private lMouseHook As Long
#End If

Private bHookIsSet As Boolean
Private bCancelArgument As Boolean
Private lErr As Long
Private oAddin As Workbook
Private oActiveWb As Object
Private lWheelDirection As Long
Private iCtrlKey As Integer
Private iShiftKey As Integer


Public Sub StartMouseWheelHook()
    
    Dim ClassID(0 To 3) As Long
    Dim lOleId As Long
    Dim lRet As Long
    Dim oApp As Application

    On Error GoTo Xit
    
    If ThisWorkbook.ReadOnly Then Exit Sub
    
    If CBool(GetProp(GetDesktopWindow, "OleId")) Then Exit Sub
    
    Call SetProp(GetDesktopWindow, "PID", GetCurrentProcessId)

    lRet = CLSIDFromString(StrPtr("{88D97E8B-D351-4FF4-A8EB-BF18EDD35267}"), ClassID(0))
    lRet = RegisterActiveObject(ThisWorkbook, ClassID(0), 0, lOleId)
    
    If lRet = 0& Then
        Call SetProp(GetDesktopWindow, "OleId", lOleId)
        Set oApp = New Application
        With oApp
            .Workbooks.Open Filename:=ThisWorkbook.FullName, UpdateLinks:=False, ReadOnly:=True
            .Run "On_Open"
        End With
    End If
    
    Exit Sub
Xit:
    oApp.Quit
    Call ResetProps
    Call FinishMouseWheelHook

End Sub

Public Sub FinishMouseWheelHook()

    Dim pUnk As IUnknown
    Dim Wb As Workbook
    Dim ClassID(0 To 3) As Long
    Dim lRet As Long
    
    On Error GoTo Xit
    
    If ThisWorkbook.ReadOnly Then Exit Sub
    
    lRet = CoDisconnectObject(ThisWorkbook, 0)
    lRet = RevokeActiveObject(CLng(GetProp(GetDesktopWindow, "OleId")), 0)
    lRet = CLSIDFromString(StrPtr("{88D97E8B-D351-4FF4-A8EB-BF18EDD35268}"), ClassID(0))
    lRet = GetActiveObject(ClassID(0), 0, pUnk)
    
    If lRet = 0& Then
        Set Wb = pUnk
        Set pUnk = Nothing
        If Not Wb Is Nothing Then
            Wb.Parent.Run "On_Close"
            Set Wb = Nothing
        End If
        Call RemoveProp(GetDesktopWindow, "OleId")
    End If
    
    Exit Sub
Xit:
    Call ResetProps

End Sub

Public Sub Register(ByVal Wb As Workbook)

    Dim ClassID(0 To 3) As Long
    Dim lOleId As Long
    Dim lRet As Long
    
    If Wb.IsAddin = False Then
        lRet = RevokeActiveObject(CLng(GetProp(GetDesktopWindow, "CurrentWorkbook")), 0)
        lRet = CLSIDFromString(StrPtr("{88D97E8B-D351-4FF4-A8EB-BF18EDD35271}"), ClassID(0))
        lRet = RegisterActiveObject(Wb, ClassID(0), 0, lOleId)
        
        If lRet = 0& Then
            Call SetProp(GetDesktopWindow, "CurrentWorkbook", lOleId)
        End If
    End If

End Sub



'\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
'             All of the following routines are executed ONLY in the second excel instance !!!
'\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Private Sub On_Open()

    Dim ClassID(0 To 3) As Long
    Dim lOleId As Long
    Dim lRet As Long

    If ThisWorkbook.ReadOnly Then
        Set oAddin = GetAddin
        If oAddin Is Nothing Then
            ThisWorkbook.Saved = True: Application.Quit
        Else
            lRet = CLSIDFromString(StrPtr("{88D97E8B-D351-4FF4-A8EB-BF18EDD35268}"), ClassID(0))
            lRet = RegisterActiveObject(ThisWorkbook, ClassID(0), 0, lOleId)
            If lRet = 0& Then
                Call SetTimer(Application.hwnd, 0, 0, AddressOf CallSetHook)
            End If
        End If
    End If

End Sub

Private Sub On_Close()

    Call UnInstallMouseHook
    Call KillTimer(Application.hwnd, 0)
    Call ResetProps
    Set oAddin = Nothing
    ThisWorkbook.Saved = True
    DoEvents
    Application.OnTime Now, "CloseRemoteXL"

End Sub

Private Sub ResetProps()
    Call RemoveProp(GetDesktopWindow, "OleId")
    Call RemoveProp(GetDesktopWindow, "PID")
    Call RemoveProp(GetDesktopWindow, "CurrentWorkbook")
End Sub

Private Sub CloseRemoteXL()
    Application.Quit
End Sub

Private Sub CallSetHook()

    If Not bHookIsSet Then
        InstallMouseHook
    End If
    Call KillTimer(Application.hwnd, 0)

End Sub

Private Sub InstallMouseHook()

    Const WH_MOUSE_LL As Long = 14

    If Not bHookIsSet Then
        lMouseHook = SetWindowsHookEx(WH_MOUSE_LL, AddressOf MouseProc, GetModuleHandle(vbNullString), 0&)
        bHookIsSet = lMouseHook <> 0
    End If

End Sub

Private Sub UnInstallMouseHook()

    If bHookIsSet Then
        Call UnhookWindowsHookEx(lMouseHook)
        bHookIsSet = lMouseHook = 0
    End If

End Sub

#If Win64 Then
    Private Function MouseProc(ByVal ncode As Long, ByVal wParam As LongLong, ByRef lParam As MOUSEHOOKSTRUCT) As LongLong
#Else
    Private Function MouseProc(ByVal ncode As Long, ByVal wParam As Long, ByRef lParam As MOUSEHOOKSTRUCT) As Long
#End If

    Const HC_ACTION As Long = 0&
    Const WM_MOUSEWHEEL = &H20A
    Const VK_SHIFT = &H10
    Const VK_CONTROL = &H11
    
    Dim sBuff As String * 256, lRet As Long
    Dim lProcessId As Long

    On Error Resume Next
    
    If GetAddin Is Nothing Then
        Call On_Close
        Exit Function
    End If
    
    If ncode < HC_ACTION Then
        MouseProc = CallNextHookEx(lMouseHook, ncode, wParam, lParam)
        Exit Function
    End If

    lRet = GetClassName(GetForegroundWindow, sBuff, 256)
    If Left(sBuff, lRet) = "wndclass_desked_gsk" Then Exit Function
    
    Call GetWindowThreadProcessId(GetForegroundWindow, lProcessId)
    If GetProp(GetDesktopWindow, "PID") <> lProcessId Then Exit Function
    
    iCtrlKey = 0: iShiftKey = 0
    If wParam = WM_MOUSEWHEEL Then
        If GetKeyState(VK_CONTROL) And &H80 Then
            iCtrlKey = -1
        End If
        If GetKeyState(VK_SHIFT) And &H80 Then
            iShiftKey = -1
        End If
        lWheelDirection = Hiword(CLng(lParam.MouseData))
        Call KillTimer(Application.hwnd, 0)
        Call SetTimer(Application.hwnd, 0, 0&, AddressOf CallOnScroll)
        MouseProc = -1
        Exit Function
    End If
    
    MouseProc = CallNextHookEx(lMouseHook, ncode, wParam, ByVal lParam)

End Function

Private Sub CallOnScroll()
    
    Call KillTimer(Application.hwnd, 0)
    
    On Error Resume Next

    Set oActiveWb = GetActiveWorkBook
    
    With oActiveWb.Application
        bCancelArgument = False
        lErr = 0
        Call .Run(oActiveWb.CodeName & ".OnMouseWheelScroll", lWheelDirection, iCtrlKey, iShiftKey, bCancelArgument)
        lErr = Err.Number
        Call Sleep(50)
        Call SetTimer(Application.hwnd, 0, 0&, AddressOf DoWheelScroll)
    End With

End Sub

Private Sub DoWheelScroll()

    Const MOUSEEVENTF_WHEEL = &H800
    Const WHEEL_DELTA = 120

    Call KillTimer(Application.hwnd, 0)
    
    On Error Resume Next

    Call UnInstallMouseHook
        With oActiveWb.Application
            If .hwnd = GetForegroundWindow Then
                If bCancelArgument = False Or lErr Then
                    lErr = 0
                    Call mouse_event(MOUSEEVENTF_WHEEL, 0&, 0&, lWheelDirection, 0)
                End If
            End If
        End With
    Call InstallMouseHook

End Sub

Private Function GetAddin() As Workbook

    Dim pUnk As IUnknown
    Dim ClassID(0 To 3) As Long
    Dim lRet As Long

    lRet = CLSIDFromString(StrPtr("{88D97E8B-D351-4FF4-A8EB-BF18EDD35267}"), ClassID(0))
    lRet = GetActiveObject(ClassID(0), 0, pUnk)
    If lRet = 0& Then Set GetAddin = pUnk

End Function

Private Function GetActiveWorkBook() As Object

    Dim pUnk As IUnknown
    Dim ClassID(0 To 3) As Long
    Dim lRet As Long

    On Error Resume Next

    lRet = CLSIDFromString(StrPtr("{88D97E8B-D351-4FF4-A8EB-BF18EDD35271}"), ClassID(0))
    lRet = GetActiveObject(ClassID(0), 0, pUnk)
    If lRet = 0& Then Set GetActiveWorkBook = pUnk

End Function

Private Function Hiword(ByVal DWord As Long) As Integer
    Hiword = (DWord And &HFFFF0000) \ &H10000
End Function


2- Code in the ThisWorkbook Module of the Addin:

VBA Code:
Option Explicit

Private WithEvents XlApp As Application

Private Sub Workbook_Open()
    Set XlApp = Application
    Call StartMouseWheelHook
End Sub

Private Sub Workbook_BeforeClose(Cancel As Boolean)
    Call FinishMouseWheelHook
End Sub

Private Sub Workbook_AddinInstall()
    Call Register(ActiveWorkbook)
End Sub

Private Sub XlApp_NewWorkbook(ByVal Wb As Workbook)
    Call Register(Wb)
End Sub

Private Sub XlApp_WorkbookActivate(ByVal Wb As Workbook)
    Call Register(Wb)
End Sub

In the next post, I'll show some interesting uses of the addin.
 
Addin usage example 1: ifound it is working only on userfom,not work on worksheet,win10 、office365 64bit
Hi and welcome to the forum.

The addin will only capture mousewwheel on userforms.
 
Upvote 0

Excel Facts

Can a formula spear through sheets?
Use =SUM(January:December!E7) to sum E7 on all of the sheets from January through December
Hi and welcome to the forum.

The addin will only capture mousewwheel on userforms.
Addin usage example 1,Switches between worksheet tabs when wheelscrolling and holding the Shift key down,is working on userform? so ,is that means we have to insert a userform for switch tabs?
Is there a better way to swich tabs directly on worksheets?
Thank you for your reply!
 
Upvote 0
Addin usage example 1,Switches between worksheet tabs when wheelscrolling and holding the Shift key down,is working on userform? so ,is that means we have to insert a userform for switch tabs?
Is there a better way to swich tabs directly on worksheets?
Thank you for your reply!
Hi qiys,

I am sorry to have confused you. Actually the addin IS supposed to work, not just on userforms, but also on other objects as well such as worksheets etc.

I wrote this code quite some time ago so I had forgotten what exactly it was supposed to do until I saw the usage example 1 in post#2 🤭

For some reason, I tested it and, like you said, the mousewheel event doesn't work for switching worksheet tabs . It works only on userforms !

I will need to revise the addin code and see why it is not working as advertised.

Agai, sorry for the confusion and thanks for letting me know.
 
Upvote 0
Ok. I have figured out why the addin was only working on userforms: I somehow added an IF clause to exclude all objects other than userforms . I did this probably while answering RobK who was enquiring about wheel-mouse scrolling userfoms.

Addin Update: WheelScroll.xla

Anayway, the addin is now updated and should work as advertised in all examples shown in post#2

Again, thanks @qiys for pointing out the issue and feedback. (y)
 
Upvote 0
Ok. I have figured out why the addin was only working on userforms: I somehow added an IF clause to exclude all objects other than userforms . I did this probably while answering RobK who was enquiring about wheel-mouse scrolling userfoms.

Addin Update: WheelScroll.xla

Anayway, the addin is now updated and should work as advertised in all examples shown in post#2

Again, thanks @qiys for pointing out the issue and feedback. (y)
Oh, yes,its working right on worksheets! very good !
Thank you for your responsibility and patience. You are a great man!(y)(y)(y)
 
Upvote 0
Oh, yes,its working right on worksheets! very good !
Thank you for your responsibility and patience. You are a great man!(y)(y)(y)
Switches between worksheet tabs is working right, but, the mousewheel is no use on sheet! when mouse wheeling ,the scrollbar not move!
 
Upvote 0
Switches between worksheet tabs is working right, but, the mousewheel is no use on sheet! when mouse wheeling ,the scrollbar not move!

That is true, unfortunately.

When I wrote the code, I didn't anticipate this problem. Too bad.

I will have to rethink the whole thing so that it doesn't block default mousescrolling. I am not sure it will be possible.
 
Upvote 0
That is true, unfortunately.

When I wrote the code, I didn't anticipate this problem. Too bad.

I will have to rethink the whole thing so that it doesn't block default mousescrolling. I am not sure it will be possible.
dear,any result?
 
Upvote 0

Forum statistics

Threads
1,223,903
Messages
6,175,284
Members
452,630
Latest member
OdubiYouth

We've detected that you are using an adblocker.

We have a great community of people providing Excel help here, but the hosting costs are enormous. You can help keep this site running by allowing ads on MrExcel.com.
Allow Ads at MrExcel

Which adblocker are you using?

Disable AdBlock

Follow these easy steps to disable AdBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the icon in the browser’s toolbar.
2)Click on the "Pause on this site" option.
Go back

Disable AdBlock Plus

Follow these easy steps to disable AdBlock Plus

1)Click on the icon in the browser’s toolbar.
2)Click on the toggle to disable it for "mrexcel.com".
Go back

Disable uBlock Origin

Follow these easy steps to disable uBlock Origin

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back

Disable uBlock

Follow these easy steps to disable uBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back
Back
Top