'*****************************************************************************************************
'    ___     _     _______  __      _   ____  _   _  _______  ___     _   _   _    ___     _     _.
'   //  \\  /\\      //    // \\   //  //    //  //    //    //  \\  //  //  //   //  \\  //|   //
'  //___// //__\    //    //__//  //  //    //__//    //    //   // //  //  //   //   // // |  //
' //      //   \\  //    //  \\  //  //    //  \\    //    //   // //  //  //   //   // //  | //
'//      //    // //    //   // //  //___ //    \\  //     \\__// //__//  //___ \\__// //   |//
'****************************************************************************************************
'        MODULE DE SCROLLING SUR CONTROLS ACTIVX DANS FEUILLE ET USERFORM SANS HOOKING!!!!!!
'Auteurs:
'@jurassic pork
'@patricktoulon
'version:1.6
'date version:13/03/2025
' Dans cet exercice on utilisera pas le hooking mouse
'on utilse le peekmessage avec condition sur l'object Iaccessible dans un do/loop avec doevents
'dans ce do/loop on surveille la sortie  de l'Iaccesible et on sort  si le curseur n'est plus sur le control
'dans cette version on utilise le m^$eme code pour les controls dans feuille et userform
'****************************************************************************************************
Option Explicit
Public bLooping As Boolean
Private Type POINTAPI: X As Long: Y As Long: End Type
#If VBA7 Then
    Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
    Private Declare PtrSafe Function PeekMessage Lib "user32" Alias "PeekMessageA" (lpMsg As Msg, ByVal hWnd As LongPtr, ByVal wMsgFilterMin As Long, ByVal wMsgFilterMax As Long, ByVal wRemoveMsg As Long) As Long
    Private Declare PtrSafe Function WaitMessage Lib "user32" () As Long
    Private Declare PtrSafe Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
    Private Type Msg: hWnd As LongPtr: message As Long: wParam As LongPtr: lParam As LongPtr: time As Long: pt As POINTAPI: End Type
    #If Win64 Then
        Private Const NULL_PTR = 0
        Private Declare PtrSafe Function AccessibleObjectFromPoint Lib "Oleacc" (ByVal arg1 As LongPtr, ppacc As IAccessible, pvarChild As Variant) As Long
    #Else
        Private Const NULL_PTR = 0&
        Private Declare PtrSafe Function AccessibleObjectFromPoint Lib "Oleacc" (ByVal lX As Long, ByVal lY As Long, ppacc As IAccessible, pvarChild As Variant) As Long
    #End If
Private Function HiWord(Param As LongPtr) As Integer
    Call CopyMemory(HiWord, ByVal VarPtr(Param) + 2&, 2&)
End Function
#Else
    Private Const NULL_PTR = 0&
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
    Private Declare Function PeekMessage Lib "user32" Alias "PeekMessageA" (lpMsg As Msg, ByVal hWnd As Long, ByVal wMsgFilterMin As Long, ByVal wMsgFilterMax As Long, ByVal wRemoveMsg As Long) As Long
    Private Declare Function WaitMessage Lib "user32" () As Long
    Private Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
    Private Type Msg: hWnd As Long: message As Long: wParam As Long: lParam As Long: time As Long: pt As POINTAPI: End Type
    Private Declare Function GetDpiForWindow Lib "user32" (ByVal hWnd As Long) As Long
Private Function HiWord(Param As Long) As Integer
    Call CopyMemory(HiWord, ByVal VarPtr(Param) + 2&, 2&)
End Function
#End If
Function IsScrollable(control) As Boolean
    Dim PosControl As IAccessible, pos As POINTAPI, ok As Boolean, role
    Select Case True
        Case TypeName(control) = "ListBox": role = 33
        Case TypeOf control Is ComboBox: role = 33
        Case TypeOf control Is Frame: role = 20
        Case TypeOf control Is Image: role = 40
        Case TypeOf control Is UserForm: role = 16
        Case TypeOf control Is ScrollBar: role = 3
    End Select
    GetCursorPos pos
    #If Win64 Then
        Dim lngPtr As LongPtr
        CopyMemory lngPtr, pos, LenB(pos)
        AccessibleObjectFromPoint lngPtr, PosControl, 0
    #Else
        AccessibleObjectFromPoint pos.X, pos.Y, PosControl, 0
    #End If
    On Error Resume Next
    'permet de continuer  à scroller quand on est dans le périmètre  de la combobox originale
    If PosControl.accRole = 46 And bLooping Then IsScrollable = True: Exit Function 'ne fonctionne  que dans le userform
    [c1] = PosControl.accRole
    ok = PosControl.accRole = role
    IsScrollable = ok
End Function
'ici on capte le message de la souris  et selon le messgae on appelle la sub scroller qui gère toute les sorte de control
Sub MouseWheelOut(control As Object, Optional pilote As Object = Nothing)
    Const WHEEL_DELTA = 120&, WM_MOUSEWHEEL = &H20A, PM_NOREMOVE = &H0&
    Dim tMsg As Msg, lDelta As Integer, criter As Boolean, A
    Dim PosControl As IAccessible
    criter = IsScrollable(control) 'on controle si on est bon pour scroller (permet de pouvoir  atteindre le dropbutton de la combo)
    If Not criter Or bLooping Then Exit Sub
    If Not TypeOf control Is UserForm Then If TypeName(control.Parent) = "Worksheet" Then ActiveSheet.ScrollArea = control.TopLeftCell.Offset(1).Address 'bloque le scrollarea de la feuille a la cellule la plus proche du control( EVITE LES PETITS SURSAUTS)
    Do
        criter = IsScrollable(control)
        If Not criter Then
            bLooping = False
            If Not TypeOf control Is UserForm Then If TypeName(control.Parent) = "Worksheet" Then ActiveSheet.ScrollArea = "" 'debloque le scrollarea de la feuille
            If Not TypeOf control Is UserForm Then If TypeName(control.Parent) = "Worksheet" Then control.TopLeftCell.Offset(1).Select 'la selection provoque le replis de la combo
            Exit Sub
        End If
        bLooping = True
        Call WaitMessage
        If PeekMessage(tMsg, NULL_PTR, WM_MOUSEWHEEL, WM_MOUSEWHEEL, PM_NOREMOVE) Then
            lDelta = HiWord(tMsg.wParam)
            scroller lDelta, control, pilote
        End If 'End of PeekMessage
        DoEvents
    Loop While criter
End Sub
Sub scroller(lDelta, control, pilote)
    If pilote Is Nothing Then
        If TypeOf control Is UserForm Then
            If lDelta > 0& Then
                control.ScrollTop = Application.Max(control.ScrollTop - 2, 0)
            Else
                control.ScrollTop = Application.Min(control.ScrollTop + 2, control.ScrollHeight)
            End If
        End If
        Select Case TypeName(control)
            Case "Frame"
                If lDelta > 0& Then
                    control.ScrollTop = Application.Max(control.ScrollTop - 8, 0)
                Else
                    control.ScrollTop = Application.Min(control.ScrollTop + 8, control.ScrollHeight)
                End If
            Case "ListBox", "ComboBox"
                If lDelta > 0& Then
                    On Error Resume Next
                    control.TopIndex = Application.Max(control.TopIndex - 1, 0)
                    On Error GoTo 0
                Else
                    On Error Resume Next
                    control.TopIndex = Application.Min(control.TopIndex + 1, control.ListCount - 1)
                    On Error GoTo 0
                End If
        End Select
    End If
    If Not pilote Is Nothing Then ' par control pilote
        Select Case TypeName(pilote)
            Case "ScrollBar"
               If lDelta > 0& Then
                    pilote.Value = Application.Max(pilote.Value - pilote.LargeChange, pilote.Min)
                Else
                    pilote.Value = Application.Min(pilote.Value + pilote.LargeChange, pilote.Max)
                End If
                'On peut ici ajouter des control piloté par un autre comme pour le scrollbar
        End Select
    End If
End Sub