1

Problems

Assume a Windows 7 machine, especially a laptop.

  1. No mouse driver allows holding down the right mouse button and moving the mouse to scroll, and yet retain the right mouse button functionality. This is useful if you do not have a middle button.
  2. Mouse drivers do not work for some applications, and some like UltraNav feel very jerky for most applications where they work.
  3. The UltraNav driver for the ThinkPad does not allow the middle button to function as a middle-click at the same time as allowing scrolling by holding it down and moving the mouse.
  4. The UltraNav driver also does not allow click-and-drag at the same time as scrolling, which is extremely useful in editing documents.
  5. Accelerated scrolling is impossible on a wheel-mouse and poorly done by the UltraNav driver.
  6. With the default mouse driver the fastest possible pointer speed might still be much too slow and tiring especially for ThinkPad trackpoints.

What I desired was an AutoHotkey script to solve all these problems, but nothing I have found online have come close to even solving a few of them. After two years I have developed a complete solution below. Of course some techniques that I used were adapted from various posts on the internet, but often each post had only one or two of the methods, and so they do not work for many applications.

Comments are welcome! However, please understand that I may not have the time to implement suggestions or troubleshoot if it does not work for you. Thanks!

  • I use AutoHotkey v1.1.10.01, but this script should work in later versions too. – user21820 Feb 19 '17 at 18:41
1

I designed my own AutoHotkey script that uses a low level mouse hook to solve all the above problems. The code is too long so see the other answer for the code.

Important instructions

Use the 32-bit version of AutoHotkey, even if you are using 64-bit Windows, otherwise it just will not work in some places! Run the script on login with highest possible privileges for the user, using Task Scheduler, otherwise it will not work for an admin user in applications running with full admin rights. If you have UltraNav installed, select "Smooth" for the middle mouse button, otherwise it will interfere with the script for some unknown reason and scrolling will not work.

On rare occasions something goes wrong with the mouse hook, especially if you try to scroll while an application is changing its GUI. I suspect it is because I am not running it at real-time priority but I think it's too risky because if it gets stuck then there may be no way to stop it. Currently it is set to run at high priority. If anything goes wrong, try the following hotkeys in order.

LCtrl-RCtrl: Reload. RCtrl-LCtrl: Disable mouse hook. (If your mouse now works, Reload.) RCtrl-Esc: Disable the whole script. (You'll need to run it again.)

Features

  1. By default scrolling is performed when the middle mouse button is held down and the mouse is moved. One can set rightbuttonscroll:=1 to use the right mouse button instead (especially for laptops which don't have a middle mouse button), in which case to get the original right-click-drag functionality one might have to press an additional Ctrl/Shift/Alt before that, depending on the specific application. (In most applications at least one of these combinations does the same as the original right-click.)
  2. If one has no middle mouse button, one can also set leftrighttomiddle:=1, and then pressing both the left and right mouse buttons within clicklimit (milliseconds) generates the middle button press, and releasing both generates the middle button release. To keep the generated middle button down it is enough to keep just one of the two real buttons pressed.
  3. Scrolling functions independently of other mouse-buttons, so for example one can scroll any time while holding down the left mouse button during a selection operation, as long as the application supports it. (Some applications do not allow scrolling while selecting for some strange reason.)
  4. By default, after pressing and releasing the scroll-button (whether middle or right mouse button) within clicklimit generates the original scroll-button click. If the scroll-button is held longer than that, scrolling is enabled and releasing the scroll-button will not generate any click. To enable scrolling even during that initial delay, set scrollbeforeclick:=1. With this setting, clicking is possible even if slight scrolling has already been done, as long as it does not exceed scrolllimit.
  5. Scrolling is slightly sticky, meaning that if one accidentally releases the scroll-button during scrolling for less than resetdelay, then it is treated as if it did not occur and scrolling continues. In particular this means that accidentally clicking at the end of scrolling will not generate the original scroll-button click.
  6. Clicking is also slightly sticky, meaning that if one presses a mouse button within resetdelay after releasing a mouse button, then it is assumed that one
  7. Some applications are very sluggish in handling scrolling and so we cannot pump too many commands to them to perform scrolling. interval is the interval between batches of scrolling-related commands sent to the application. In each interval all scrolling actions by the user are combined into one. Decreasing the interval makes scrolling more responsive to the user but may choke poorly designed applications. If fine control is desired one can always set a default value at the start of gettarget and adjust it depending on the application.
  8. Some applications really take too long to process scrolling commands, and so if a command is not processed within timelimit, all the remaining commands in the batch for that interval are aborted.
  9. One can fine-tune the scrolling by editing scrollamount to change the scroll speed given the speed of the mouse or editing scrolladjust to change the way scrolling is performed given the accumulated scroll amounts. By default scrolling is snapped to the vertical or horizontal if the scroll amount in that direction is more than 5 times that in the other direction.
  10. One can also fine-tune the mouse speed and acceleration. Press LCtrl-Ralt-D to show the current mouse speed, LCtrl-Ralt-S to decrease the speed and LCtrl-Ralt-F to increase the speed. It will change the pointer speed that is in the Mouse settings. Edit moveadjust to change the way pointer movements are converted into mouse movements. By default, low speeds remain the same and high speeds are multiplied by 3, and there is a smooth transition in between, and also every mouse movement is spread over the current one and the next one, because at high pointer speeds the hardware or default mouse driver produces choppy output. Make sure moveadjust runs fast!

Methods

There are 11 different methods of scrolling that I have discovered in various applications. Some applications only respond to one of these, which is quite annoying. The methods are described in the comments in the code itself, but there is no reliable way to determine the correct method to use, and so one has to manually customize gettarget to choose the method for each application that does not work with the default wheel messages. Try the methods in the order they occur in scroll. For each method, you might have to experiment with changing the targets of the messages, namely ctrl,window,parent as appropriate.

To toggle debugging, press LCtrl-AppMenu. It shows the raw movements of the mouse the adjusted amounts, for both normal movement and scrolling. If you hold down the scroll-button without moving the mouse, it shows the ancestry of both the target under the mouse and the control chosen by gettarget. I sometimes had to also use Spy++ to see the control hierarchy and guess where to send the messages to.

Comments

The 64-bit AutoHotkey causes my script to fail for Programmer's Notepad, while the 32-bit AutoHotkey works perfectly, and I don't know why. Other applications seem to have no such problem. If it's a bug in my script, I'd be glad to know!

  • This is awesome! There seems to be a problem with multi-monitors - when I run it with 2 monitors (win 10), the mouse is 'trapped' in the top left corner, while on a single monitor it works fine. Do you have a source repo for this (seems like something kept a little more nicely in github or the like); If you're not interested, would you be happy for me to put it in a repo? – askvictor Feb 19 '17 at 0:39
  • On further inspection, it seems not so much with multiple monitors, but an external monitor to the laptop - If I use this script with the external monitor either as the only monitor, or extended, then the mouse gets trapped in the top right. If I use the script with only the laptop monitor, or with displays duplicated, it works fine. – askvictor Feb 19 '17 at 2:30
  • @askvictor: I don't have a source repository, since it was just a personal project. I would definitely be happy if you put my code up under the same license as the one it is under on SE. But before that let's fix the weird bug. Could you toggle dbg by pressing LCtrl+AppMenu, or changing that shortcut (near the top) or just setting dbg:=true temporarily and tell me what it says in the message box when you move around? – user21820 Feb 19 '17 at 2:36
  • I've created a gist at gist.github.com/askvictor/c4703ec76900542415e74a61057889a9 - but if you'd rather set one up yourself (and me fork) let me know and I'll delete it. I think this discussion might be better on github than here? In any case, the debug message box shows small positive (usually between 0 and 20) numbers for both dimensions of the origin, and small negative (usually between -10 and 0) for both dimensions of both move co-ords. – askvictor Feb 19 '17 at 2:50
  • @askvictor: Thanks for setting it up! I don't have a GitHub account but we can discuss in a chat-room when the option comes up. By the way, someone else using my script has no problem both on the laptop or on an external monitor connected to the laptop (but never tried multiple monitors), so it's strange. The first Move number is the raw value, and the second is the user-adjusted value (for smooth acceleration). If the first is always negative it explains why the mouse drifts to the top left. But that may be a problem with either AutoHotkey's mousegetpos or Windows' WM_MOUSEMOVE message. – user21820 Feb 19 '17 at 2:54
1

The code is too long so it is split into two answers, and see the third answer for the instructions.

Code Part 1

#commentflag // ; Change to C++ comment style

// Settings //

global rightbuttonscroll:=0 // set to 1 iff the right button is to be used for scrolling instead of the middle button
global scrollbeforeclick:=1 // set to 1 iff scrolling is allowed before the original button click otherwise scrolling is delayed for (clicklimit)
global leftrighttomiddle:=0 // set to 1 iff pressing both left+right buttons together is to be used to generate the middle button press
global scrolllimit:=3.5     // maximum scroll allowed between scroll button press and release for the original button click to be generated
global clicklimit:=210      // maximum time allowed between scroll button press and release for the original button click to be generated
global resetdelay:=210      // minimum time required between scroll button release and press for the original button click to be allowed
global interval:=35         // minimum interval between scrolling updates
global timelimit:=70        // maximum time allowed for continuing the scrolling update in each direction
global dbg:=false

// Initialization //

#singleinstance,force
#keyhistory 0
#usehook on
process priority,,H
setkeydelay 1
setcontroldelay -1
coordmode mouse,screen

global buttondown:=( rightbuttonscroll==1 ? 0x204 : 0x207 )
global buttonup:=( rightbuttonscroll==1 ? 0x205 : 0x208 )
global buttonoriginal:=( rightbuttonscroll==1 ? "RButton" : "MButton" )

global speed
global handling:=0
global scrolling:=0
global scrolldrag:=0
global scrollsticky:=0
global clicksticky:=0
global leftphysical:=0
global rightphysical:=0
global lefttopress:=0
global righttopress:=0
global middlepressed:=0

global mx,my
global dx,dy
global lx:=0
global ly:=0
global sx:=0
global sy:=0
global totalx:=0
global totaly:=0
global ctrl,window,parent
global methodx
global methody
global scrollbarx
global scrollbary

global max16bit:=32767
global sbinfo
varsetcapacity(sbinfo,28)
numput(28,sbinfo,0)
numput(23,sbinfo,4)

global mousehook:=dllcall("SetWindowsHookEx","int",14,"uint",RegisterCallback("handlemouse","fast"),"uint",0,"uint",0)

message("AutoHotkey loaded")
return

// Reload & Debug & Exit & Disable Mousehook //

LCtrl & RCtrl::reload
LCtrl & AppsKey::dbg:=!dbg
RCtrl & Esc::exitapp
RCtrl & LCtrl::dllcall("UnhookWindowsHookEx","uint",mousehook)

// Message //

messageoff:
    tooltip
return

message(text)
{
    tooltip % text  //,a_screenwidth-strlen(text)*7,a_screenheight-42
    settimer messageoff,-1000
}

// Mouse Movement //

moveadjust(byref x,byref y)
{
    movespread(x,y)
    z2:=x**2+y**2
    r:=(70+z2*4)/(70+z2)
    x:=rtoz(x*r)
    y:=rtoz(y*r)
}
movespread(byref x,byref y)
{
    nx:=rtoz(x/2)
    ny:=rtoz(y/2)
    x-=nx
    y-=ny
    x+=lx
    y+=ly
    lx:=nx
    ly:=ny
    settimer movereset,-210
}
movereset:
    lx:=0
    ly:=0
return

getspeed()
{
    DllCall("SystemParametersInfo","Int",112,"Int",0,"UIntP",speed,"Int",0)
}
setspeed()
{
    DllCall("SystemParametersInfo","Int",113,"Int",0,"UInt",speed,"Int",2)
}
showspeed()
{
    message("Mouse speed = " . speed)
}

<^>!d::
    getspeed()
    showspeed()
return
<^>!s::
    getspeed()
    if( speed>1 )
    {
        speed:=speed-1
    }
    setspeed()
    showspeed()
return
<^>!f::
    getspeed()
    if( speed<20 )
    {
        speed:=speed+1
    }
    setspeed()
    showspeed()
return

// Mouse Scrolling //

/*
    Usage
        Combination : Function
            ScrollButton-Release : ScrollButton-Click  (where the intervening time and scroll do not exceed the limits set)
            ScrollButton-Modifiers-Move : Modifiers-Scroll  (where Modifiers can be any combination of Shift and Ctrl and Alt)
        If rightbuttonscroll = 1 :
            LButton-RButton : MButton  (where the intervening time does not exceed the limit set)
            RButton-LButton : MButton  (where the intervening time does not exceed the limit set)
    Methods
        Send wheel messages (0x20a/0x20e) to the control
            The wheel amount is only 16-bit which is at most only 32767/120 wheel notches
            Some applications do not handle the wheel amount correctly so you may need to send integer multiples of wheel notches or many wheel messages
            Microsoft Word apparently assumes that wheel messages are always posted and behaves incorrectly unless PostMessage is used
        Send scrollbars' thumb position to the control
            This works for many scrollable controls but not all
        Send scroll messages (0x115/0x114) to the control unless its scrollbar thumb position is already at the end
            Many applications respond rather slowly to scroll messages
        Send scroll messages to the scrollbars
            This works for some applications and should be slightly faster than scroll messages to the parent
            Many applications respond rather slowly so wheel messages are preferable if they work
        Send scroll messages to the scrollbars' parents
            This is supposed to be a standard way to control scrollbars that are separate from the control
            Many applications respond rather slowly so wheel messages are preferable if they work
        Send keys (Up/Down/Left/Right/PgUp/PgDn) to the scrollbars
            This is equivalent to clicking the arrows or the empty space on either side of the thumb track
            Some applications respond slowly so keys should not be sent too fast
        Send keys to the control
            Only for rare cases that do not respond to anything else
        Call Office function SmallScroll
            This function only exists for Office applications and even then it is broken in some like Powerpoint
    Customization
        scrollamount(x)
            Can assume that x is non-negative
            Must return non-negative output
        scrolladjust(x,y)
            Can modify scroll amounts (x,y)
        gettarget()
            Can assume that (mx,my) is the current mouse position
            Must set ctrl to the handle of the control that is to be scrolled
            Must set parent to getparent(ctrl) if scrolltoscrollbarparent() or keystoscrollbar() is used
*/

scrollamount(x)
{
    return x**1.5/8
}

scrolladjust(byref x,byref y)
{
    ax:=abs(x)
    ay:=abs(y)
    z:=sqrt(x**2+y**2)
    if( ax>ay*5 )
    {
        x:=( x>0 ? z : -z )
        y:=0
    }
    if( ay>ax*5 )
    {
        y:=( y>0 ? z : -z )
        x:=0
    }
}

gettarget()
{
    gosub messageoff
    ctrl0:=getctrlat(mx,my)
    window:=getwindow(ctrl0)
    class:=getclass(window)
    title:=gettitle(window)
    ctrl:=ctrl0
    loop
    {
        ctrlname:=getnameaschild(ctrl)
        ctrlclass:=getclass(ctrl)
        parent:=getparent(ctrl)
        parentname:=getnameatroot(parent)
        parentclass:=getclass(parent)
        if( ctrl!=window and ( regexmatch(ctrlclass,"^(Button|T?ComboBox|CtrlNotifySink|SysLink)$")==1 or parentclass=="ComboBox" ) )
        {
            ctrl:=parent
            continue
        }
        break
    }
    gp:=getparent(parent)
    ggp:=getparent(gp)
    gpname:=getnameaschild(gp)
    ggpname:=getnameaschild(ggp)
    methodx:="wheel"        // needed for: Firefox , Gimp , ...
    methody:="wheel"        // needed for: Firefox , File Chooser , Explorer , Word , Outlook , IE , ...
    scrollbarx:=""
    scrollbary:=""
    if( ctrlclass=="ComboLBox" )            // Standard Combo Boxes dropdown list
    {
        methodx:="thumbpos"
        methody:="thumbpos"
    }
    if( ctrlclass=="ListBox" )          // Standard List Boxes
    {
        methodx:="thumbpos"
        methody:="thumbpos"
    }
    if( ctrlclass=="ScrollBar" )            // Standard ScrollBar controls
    {
        methodx:="scrolltoscrollbarparent"  // "scroll" works for most places but not some ( Character Map , ... )
        methody:="scrolltoscrollbarparent"  // "scroll" works for most places but not some ( Character Map , ... )
        scrollbarx:="scrollbar1"
        scrollbary:="scrollbar1"
    }
    if( class=="OpusApp" )          // Microsoft Word
    {
        methodx:="office"
        methody:="postwheel"
        if( gpname=="_WwB1" )
        {
            ctrl:=getdescendant(window,"_WwG1")
        }
    }
    if( class=="XLMAIN" )           // Microsoft Excel
    {
        methodx:="office"
        if( gpname=="EXCEL71" )
        {
            ctrl:=gp
        }
    }
    if( class=="PPTFrameClass" or class=="PP12FrameClass" )         // Microsoft Powerpoint
    {
        methody:="wheelsingle"
        ctrlnameatroot:=getnameatroot(ctrl)
        if( ctrlnameatroot=="NetUIHWND3" or ctrlnameatroot=="NetUIHWND4" )
        {
            ctrl:=getdescendant(window,"paneClassDC1")
            ctrlname:="paneClassDC1"
            parent:=getparent(ctrl)
        }
        if( ctrlname=="paneClassDC1" )
        {
            methodx:="scrolltoscrollbarparent"
            methody:="scrolltoscrollbarparent"  // Powerpoint scroll up is broken when there are 9 slides at 100% zoom in normal mode
            scrollbarx:="NUIScrollbar2"
            scrollbary:="NUIScrollbar1"
            //methody:="office" // Powerpoint does not update the view pane immediately and so it is disorienting
        }
        if( ctrlnameatroot=="NetUIHWND5" )
        {
            ctrl:=getdescendant(window,"paneClassDC2")
        }
    }
    if( class=="rctrl_renwnd32" )           // Microsoft Outlook
    {
        methodx:="office"
        gpclass:=getclass(gp)
        ctrlnameatroot:=getnameatroot(ctrl)
        if( ctrlclass=="SUPERGRID" )
        {
            methodx:="scroll"
            methody:="scroll"
        }
        if( gpclass=="SUPERGRID" )
        {
            methodx:="scroll"
            methody:="scroll"
            ctrl:=gp
        }
        if( gpname=="_WwB1" )
        {
            ctrl:=getdescendant(window,"_WwG1")
        }
    }
    if( ctrlclass=="OUTEXVLB" )         // Microsoft Outlook { Address Book , Group membership , ... }
    {
        methodx:="thumbpos"
        if( regexmatch(title,"Global Address List")==0 )
        {
            methody:="scroll"
        }
    }
    if( title=="Symbol" and regexmatch(class,"bosa_sdm_(msword|Microsoft Office Word 12.0|XL9|Mso96)")==1 )         // Microsoft Office Insert Symbol
    {
        controlget v,visible,,ScrollBar1,ahk_id %ctrl%
        if( v==0 )
        {
            ctrl:=getdescendant(window,"Edit1")
            methody:="thumbpos"
        }
    }
    if( class=="wndclass_desked_gsk" )          // Microsoft Visual Basic
    {
        if( ctrlclass=="VbaWindow" )
        {
            methodx:="scrolltoscrollbarparent"
            parent:=ctrl
            scrollbarx:="scrollbar2"
        }
    }
    if( regexmatch(ctrlclass,"^RichEdit20W(PT)?$")==1 )         // Windows Text Areas
    {
        methodx:="wheel"
        methody:="scroll"
    }
    if( class=="AcrobatSDIWindow" )         // Adobe Reader
    {
        if( regexmatch(parentname,"AVL_AVView")==1 )
        {
            ctrl:=getdescendant(parent,"AVL_AVView4")
            if( ctrl=="" )
            {
                ctrl:=getdescendant(parent,"AVL_AVView1")
            }
            methodx:="scrolltoscrollbarparent"
            scrollbarx:="scrollbar1"
            methody:="wheel"
        }
    }
    if( ggpname=="SHELLDLL_DefView1" )          // Windows Explorer Scrollbars
    {
        ctrl:=gp
        ctrlname:=gpname
        parentname:=ggpname
    }
    if( ctrlname=="DirectUIHWND1" )
    {
        if( parentname=="SHELLDLL_DefView1" )           // Windows Explorer (including Standard File Choosers)
        {
            methodx:="scrolltoscrollbar"    // "scrolltoscrollbarparent" also works
            scrollbarx:="scrollbar1"
            controlget v,visible,,ScrollBar2,ahk_id %ctrl%
            methody:=( v==1 ? "wheel" : "" )
        }
        if( class=="CabinetWClass" )
        {
            if( parentname=="XBabyHost1" )          // Control Panel
            {
                methody:="scrolltoscrollbar"    // "scrolltoscrollbarparent" also works
                scrollbary:="scrollbar1"
                if( title=="Personalization" )
                {
                    scrollbary:="scrollbar3"
                }
            }
        }
    }
    if( ctrlclass=="CharGridWClass" )           // Character Map
    {
        methody:="scrolltoscrollbarparent"
        scrollbary:="scrollbar1"
    }
    if( class=="ConsoleWindowClass" )           // Console Window
    {
        methodx:="thumbpos"
        methody:="thumbpos"
    }
    if( class=="WordPadClass" )         // WordPad
    {
        methodx:="scroll"
        methody:="scroll"   // WordPad scroll down is broken when scrolling horizontally at the same time
    }
    if( class=="ATL:006AD5B8" )         // Programmer's Notepad
    {
        methodx:="scroll"
    }
    if( class=="SWT_Window0" or ctrlclass=="Internet Explorer_Server" )         // Eclipse
    {
        methodx:="scroll"
    }
    if( ctrlclass=="TSynEdit" )         // TSynEdit ; Dev C++ , ...
    {
        methodx:="thumbpos"
        methody:="thumbpos"
    }
    if( ctrlclass=="TListView" )            // TlistView ; Dev C++ , ...
    {
        methodx:="scroll"
    }
    if( ctrlclass=="TPSSynEdit" )           // TPSSynEdit ; PSPad , ...
    {
        methodx:="thumbpos"
    }
    if( class=="QWidget" or class=="Qt5QWindowIcon" )
    {
        if( regexmatch(title,"LyX")==1 )            // Lyx
        {
            methodx:=""
                // prevents horizontal scrolling from becoming vertical scrolling in the edit pane
                // but disables horizontal scrolling everywhere else
        }
        if( regexmatch(title,"TeXworks$")>0 )           // TeXWorks
        {
            methodx:="wheelint"
            methody:="wheelint"
        }
    }
    if( class=="gdkWindowToplevel" )            // Gimp
    {
        methody:="wheelsingle"  // Gimp performs horizontal scrolling when the mouse is scrolled over the horizontal scrollbar
    }
    if( class=="SunAwtDialog" )         // Java AWT Dialogs ; GeoGebra , Logisim , ...
    {
        methody:="wheelint"
    }
    if( regexmatch(ctrlname,"IupCanvas")==1 )           // IupCanvas
    {
        methodx:="scroll"
    }
    if( class=="SunAwtFrame" )
    {
        if( regexmatch(title,"GeoGebra|.*\.ggb$")==1 )          // GeoGebra
        {
            methody:="wheelint"
        }
        if( regexmatch(title,"Logisim")==1 )            // Logisim
        {
            methodx:="keys" // performs scrolling only if the drawing area has the focus
        }
    }
    if( class=="MSPaintApp" )           // MSPaint
    {
        if( parentname=="MSPaintView1" )
        {
            methodx:="thumbpos"
            methody:="thumbpos"
        }
    }
    if( class=="ATL:643E3490" )         // Real World Paint
    {
        if( ctrlclass=="RWViewImageEdit" )
        {
            methodx:="scroll"
            methody:="scroll"
        }
    }
    if( ctrlclass=="DSUI:PagesView" )           // PDF-XChange Viewer (also as a browser plugin)
    {
        methodx:="scroll"
        methody:="wheelint"
    }
    if( ctrlclass=="PuTTY" )            // PuTTY
    {
        methody:="scroll"
    }
if( dbg )
{
p:=getparent(ctrl)
gp:=getparent(p)
ggp:=getparent(gp)
message( "Root class = " class 
    . "`nRoot title = " title 
    . "`nTarget = [" ctrl0 "]" 
        . "`n`t(as child) " getnameaschild(ctrl0) 
        . "`n`t(at root)  " getnameatroot(ctrl0) 
    . "`nControl = [" ctrl "]" 
        . "`n`t(at child) " getnameaschild(ctrl) 
        . "`n`t(as root)  " getnameatroot(ctrl) 
    . "`nControl ancestors = " 
        . "`n`t < [" p "] " getnameatroot(p) 
        . "`n`t < [" gp "] " getnameatroot(gp) 
        . "`n`t < [" ggp "] " getnameatroot(ggp) 
    . "`nMethod = (" methodx "," methody ")" 
    . "`nScrollbars = (" scrollbarx "," scrollbary ")" )
}
}

scroll:
    critical on
    if( getwindow(getctrlat(mx,my))!=window )
    {
        scrolling:=0
    }
    if( scrolling==0 )
    {
        return
    }
    settimer scroll,-%interval%
    if( scrollbeforeclick!=1 and scrolldrag==0 )
    {
        sx:=0
        sy:=0
        return
    }
    tx:=sx
    ty:=sy
    sx-=tx
    sy-=ty
    totalx+=tx
    totaly+=ty
    if( totalx**2+totaly**2>scrolllimit )
    {
        scrolldrag:=1
    }
    scrolladjust(tx,ty)
    rx:=0
    ry:=0
    comobjerror(false)
    if( tx!=0 )
    {
        if( methodx=="wheel" )
        {
            sendwheel("h",tx)
        }
        else if( methodx=="postwheel" )
        {
            postwheel("h",tx)
        }
        else
        {
            txi:=rtoz(tx)
            rx:=tx-txi
            if( txi!=0 )
            {
                if( methodx=="wheelint" )
                {
                    sendwheel("h",txi)
                }
                else if( methodx=="wheelsingle" )
                {
                    sendwheelsingle("h",txi)
                }
                else if( methodx=="thumbpos" )
                {
                    sendthumbpos("h",txi)
                }
                else if( methodx=="scroll" )
                {
                    sendscroll("h",txi)
                }
                else if( methodx=="scrolltoscrollbar" )
                {
                    sendscrolltoscrollbar(scrollbarx,txi)
                }
                else if( methodx=="scrolltoscrollbarparent" )
                {
                    sendscrolltoscrollbarparent(scrollbarx,"h",txi)
                }
                else if( methodx=="keys" )
                {
                    sendkeys("h",txi)
                }
                else if( methodx=="keystoscrollbar" )
                {
                    sendkeystoscrollbar(scrollbarx,txi)
                }
                else if( methodx=="office" )
                {
                    Acc_ObjectFromWindow(ctrl,-16).SmallScroll(0,0,(txi>0?txi:0),(txi<0?-txi:0))
                }
            }
        }
    }
    if( ty!=0 )
    {
        if( methody=="wheel" )
        {
            sendwheel("v",-ty)
        }
        else if( methody=="postwheel" )
        {
            postwheel("v",-ty)
        }
        else
        {
            tyi:=rtoz(ty)
            ry:=ty-tyi
            if( tyi!=0 )
            {
                if( methody=="wheelint" )
                {
                    sendwheel("v",-tyi)
                }
                else if( methody=="wheelsingle" )
                {
                    sendwheelsingle("v",-tyi)
                }
                else if( methody=="thumbpos" )
                {
                    sendthumbpos("v",tyi)
                }
                else if( methody=="scroll" )
                {
                    sendscroll("v",tyi)
                }
                else if( methody=="scrolltoscrollbar" )
                {
                    sendscrolltoscrollbar(scrollbary,tyi)
                }
                else if( methody=="scrolltoscrollbarparent" )
                {
                    sendscrolltoscrollbarparent(scrollbary,"v",tyi)
                }
                else if( methody=="keys" )
                {
                    sendkeys("v",tyi)
                }
                else if( methody=="keystoscrollbar" )
                {
                    sendkeystoscrollbar(scrollbary,tyi)
                }
                else if( methody=="office" )
                {
                    Acc_ObjectFromWindow(ctrl,-16).SmallScroll((tyi>0?tyi:0),(tyi<0?-tyi:0))
                }
            }
        }
    }
    comobjerror(true)
    sx:=rx
    sy:=ry
return

sendwheel(dir,amount)
{
    t:=a_tickcount
    msg:=( dir=="v" ? 0x20a : 0x20e )
    flags:=getkeystate("Ctrl")<<3|getkeystate("Shift")<<2
    amount*=120
    while( amount>max16bit )
    {
        sendmessage msg,max16bit<<16|flags,mx|my<<16,,ahk_id %ctrl%,,,,timelimit
        amount-=max16bit
        if( a_tickcount-t>=timelimit )
        {
            return
        }
    }
    while( amount<-max16bit )
    {
        sendmessage msg,-max16bit<<16|flags,mx|my<<16,,ahk_id %ctrl%,,,,timelimit
        amount+=max16bit
        if( a_tickcount-t>=timelimit )
        {
            return
        }
    }
    sendmessage msg,round(amount)<<16|flags,mx|my<<16,,ahk_id %ctrl%,,,,timelimit
}
postwheel(dir,amount)
{
    msg:=( dir=="v" ? 0x20a : 0x20e )
    flags:=getkeystate("Ctrl")<<3|getkeystate("Shift")<<2
    amount*=120
    while( amount>max16bit )
    {
        postmessage msg,max16bit<<16|flags,mx|my<<16,,ahk_id %ctrl%
        amount-=max16bit
    }
    while( amount<-max16bit )
    {
        postmessage msg,-max16bit<<16|flags,mx|my<<16,,ahk_id %ctrl%
        amount+=max16bit
    }
    postmessage msg,round(amount)<<16|flags,mx|my<<16,,ahk_id %ctrl%
}
sendwheelsingle(dir,amount)
{
    t:=a_tickcount
    msg:=( dir=="v" ? 0x20a : 0x20e )
    flags:=getkeystate("Ctrl")<<3|getkeystate("Shift")<<2
    loop % abs(amount)
    {
        sendmessage msg,(amount<0?-120:120)<<16|flags,mx|my<<16,,ahk_id %ctrl%,,,,timelimit
        if( a_tickcount-t>=timelimit )
        {
            return
        }
    }
}
sendthumbpos(dir,amount)
{
    msg:=( dir=="v" ? 0x115 : 0x114 )
    sb:=dllcall("GetScrollInfo","uint",ctrl,"int",(dir=="v"?1:0),"uint",&sbinfo)
    if( sb )
    {
        sbmin:=numget(sbinfo,8,"int")
        sbmax:=numget(sbinfo,12,"int")
        sbpos:=numget(sbinfo,20,"int")
        if( amount>max16bit )
        {
            amount=max16bit
        }
        if( amount<-max16bit )
        {
            amount=-max16bit
        }
        pos:=sbpos+amount
        if( pos<sbmin )
        {
            pos:=sbmin
        }
        if( pos>sbmax )
        {
            pos:=sbmax
        }
        sendmessage msg,pos<<16|4,,,ahk_id %ctrl%,,,,timelimit
    }
}
sendscroll(dir,amount)
{
    t:=a_tickcount
    msg:=( dir=="v" ? 0x115 : 0x114 )
    flag:=( amount<0 ? 0 : 1 )
    loop % abs(amount)
    {
        sb:=dllcall("GetScrollInfo","uint",ctrl,"int",(dir=="v"?1:0),"uint",&sbinfo)
        if( sb )
        {
            sbmin:=numget(sbinfo,8,"int")
            sbmax:=numget(sbinfo,12,"int")
            sbpage:=numget(sbinfo,16,"uint")
            sbpos:=numget(sbinfo,20,"int")
            if( ( sbpos==sbmin and amount<0 ) or ( sbpos+sbpage==sbmax+1 and amount>0 ) )
            {
                return
            }
        }
        sendmessage msg,flag,,,ahk_id %ctrl%,,,,timelimit
        if( a_tickcount-t>=timelimit )
        {
            return
        }
    }
}
sendscrolltoscrollbar(name,amount)
{
    t:=a_tickcount
    flag:=( amount<0 ? 0 : 1 )
    loop % abs(amount)
    {
        sendmessage 0x115,flag,,%name%,ahk_id %ctrl%,,,,timelimit
        if( a_tickcount-t>=timelimit )
        {
            return
        }
    }
}
sendscrolltoscrollbarparent(name,dir,amount)
{
    sb:=getdescendant(parent,name)
    sbp:=getparent(sb)
    t:=a_tickcount
    msg:=( dir=="v" ? 0x115 : 0x114 )
    flag:=( amount<0 ? 0 : 1 )
    loop % abs(amount)
    {
        sendmessage msg,flag,sb,,ahk_id %sbp%,,,,timelimit
        if( a_tickcount-t>=timelimit )
        {
            return
        }
    }
}
sendkeys(dir,amount)
{
    t:=a_tickcount
    key:=( dir=="v" ? ( amount<0 ? "{Up}" : "{Down}" ) : ( amount<0 ? "{Left}" : "{Right}" ) )
    loop % abs(amount)
    {
        controlsend, ,%key%,ahk_id %ctrl%
        if( a_tickcount-t>=timelimit )
        {
            return
        }
    }
}
sendkeystoscrollbar(name,amount)
{
    t:=a_tickcount
    key:=( amount<0 ? "{Up}" : "{Down}" )
    controlget e,enabled,,%name%,ahk_id %parent%
    if( e==1 )
    {
        loop % abs(amount)
        {
            controlsend %name%,%key%,ahk_id %parent%
            if( a_tickcount-t>=timelimit )
            {
                break
            }
        }
    }
}
1

The code is too long so it is split into two answers, and see the third answer for the instructions.

Code Part 2

scrollbuttonreset:
    scrollsticky:=0
return

scrollbuttoncannotclick:
    scrolldrag:=1
    righttopress:=0
return

clickreset:
    clicksticky:=0
return

scrollbuttondown:
    critical on
    mousegetpos mx,my
    gettarget()
    sx:=0
    sy:=0
    totalx:=0
    totaly:=0
    settimer scroll,-%interval%
    settimer scrollbuttonreset,off
return

scrollbuttonup:
    critical on
    if( scrolldrag==0 and scrollsticky==0 )
    {
        handling++
        sendevent {Blind}{%buttonoriginal% down}
        sendevent {Blind}{%buttonoriginal% up}
        handling--
        scrolling:=0
    }
    scrolldrag:=0
    righttopress:=0
    settimer scrollbuttondown,off
    settimer scrollbuttoncannotclick,off
    scrollsticky:=1
    settimer scrollbuttonreset,-%resetdelay%
return

leftdown:
    critical on
    lefttopress:=0
    handling++
    sendevent {Blind}{LButton down}
    handling--
return

rightdown:
    critical on
    righttopress:=0
    handling++
    sendevent {Blind}{RButton down}
    handling--
return

middledown:
    critical on
    scrolling:=0
    scrolldrag:=0
    middlepressed:=1
    settimer scrollbuttondown,off
    settimer scrollbuttoncannotclick,off
    handling++
    sendevent {Blind}{MButton down}
    handling--
return

leftup:
    critical on
    handling++
    sendevent {Blind}{LButton up}
    handling--
    clicksticky:=1
    settimer clickreset,-%resetdelay%
return

rightup:
    critical on
    handling++
    sendevent {Blind}{RButton up}
    handling--
    clicksticky:=1
    settimer clickreset,-%resetdelay%
return

middleup:
    critical on
    middlepressed:=0
    handling++
    sendevent {Blind}{MButton up}
    handling--
    clicksticky:=1
    settimer clickreset,-%resetdelay%
return

leftclick:
    critical on
    gosub leftdown
    gosub leftup
return

rightclick:
    critical on
    gosub rightdown
    gosub rightup
return

// Mouse handler //

handlemouse(nCode,wParam,lParam)
{
    critical on
    if( a_ispaused==1 )
    {
        exitapp // something goes wrong if it remains paused
    }
    o:=0
    if( handling==0 && nCode>=0 )
    {
        if( wParam==0x201 )
        {
            leftphysical:=1
        }
        else if( wParam==0x204 )
        {
            rightphysical:=1
        }
        else if( wParam==0x202 )
        {
            leftphysical:=0
        }
        else if( wParam==0x205 )
        {
            rightphysical:=0
        }
        if( wParam==0x200 )
        {
            // Handle mouse move //
            mousegetpos mx,my
            x:=numget(lParam+0,0,"int") // the "+0" is necessary!
            y:=numget(lParam+0,4,"int") // the "+0" is necessary!
            dx:=x-mx
            dy:=y-my
            if( scrolling==0 )
            {
                // Click immediately on mouse drag //
                if( leftrighttomiddle==1 )
                {
                    if( lefttopress==1 )
                    {
                        settimer leftdown,-0
                    }
                    if( righttopress==1 )
                    {
                        settimer rightdown,-0
                    }
                }
                // Adjust mouse movement //
                moveadjust(dx,dy)
                handling++
                mousemove dx,dy,0,R
                handling--
if( dbg )
{
message( "Origin = " mx " " my "`nMove = " x-mx " " y-my "`n         -> " dx " " dy )
}
                // Release mouse buttons if out of sync with physical state //
                if( getkeystate("LButton")>leftphysical )
                {
                    settimer leftup,-0
                }
                if( getkeystate("RButton")>rightphysical )
                {
                    settimer rightup,-0
                }
                o:=1
            }
            else
            {
                // Handle mouse move when scrolling //
                if( dx!=0 or dy!=0 )
                {
                    sx+=( dx>0 ? scrollamount(dx) : -scrollamount(-dx) )
                    sy+=( dy>0 ? scrollamount(dy) : -scrollamount(-dy) )
if( dbg )
{
message( "Origin = " mx " " my "`nMove = " dx " " dy "`n         -> " round(sx,2) " " round(sy,2) )
}
                }
                o:=1
            }
        }
        else if( scrolling==0 )
        {
            if( wParam==buttondown and middlepressed==0 and lefttopress==0 )
            {
                // Handle scroll button down //
                if( getkeystate("Ctrl")==0 and getkeystate("Shift")==0 and getkeystate("Alt")==0 )
                {
                    // Handle scroll start //
                    scrolling:=1
                    scrolldrag:=0
                    if( buttondown==0x204 )
                    {
                        righttopress:=1
                    }
                    settimer scrollbuttondown,-0
                    settimer scrollbuttoncannotclick,-%clicklimit%
                    o:=1
                }
            }
        }
        else if( scrolling==1 )
        {
            if( wParam==buttonup )
            {
                // Handle scroll button up //
                scrolling:=0
                settimer scrollbuttonup,-0
                o:=1
            }
        }
        if( leftrighttomiddle==1 and o==0 )
        {
            if( middlepressed==1 )
            {
                // Release middle button only when both left and right buttons are released //
                if( leftphysical==0 and rightphysical==0 )
                {
                    settimer middleup,-0
                }
                o:=1
            }
            else if( scrolldrag==0 )
            {
                // Process left+right=middle //
                if( wParam==0x201 and scrollsticky==0 )
                {
                    // Handle left button down //
                    if( righttopress==0 )
                    {
                        if( clicksticky==1 )
                        {
                            settimer leftdown,-0
                        }
                        else
                        {
                            lefttopress:=1
                            settimer leftdown,-%clicklimit%
                        }
                    }
                    else
                    {
                        righttopress:=0
                        settimer rightdown,off
                        settimer middledown,-0
                    }
                    o:=1
                }
                else if( wParam==0x204 and scrollsticky==0 )
                {
                    // Handle right button down //
                    if( lefttopress==0 )
                    {
                        if( clicksticky==1 )
                        {
                            settimer rightdown,-0
                        }
                        else
                        {
                            righttopress:=1
                            settimer rightdown,-%clicklimit%
                        }
                    }
                    else
                    {
                        lefttopress:=0
                        settimer leftdown,off
                        settimer middledown,-0
                    }
                    o:=1
                }
                else if( wParam==0x202 )
                {
                    // Handle left button up //
                    if( lefttopress==1 )
                    {
                        settimer leftdown,off
                        settimer leftclick,-0
                    }
                    else
                    {
                        settimer leftup,-0
                    }
                    o:=1
                }
                else if( wParam==0x205 )
                {
                    // Handle right button up //
                    if( righttopress==1 )
                    {
                        settimer rightdown,off
                        settimer rightclick,-0
                    }
                    else
                    {
                        settimer rightup,-0
                    }
                    o:=1
                }
            }
        }
    }
    // Pass on any other mouse events //
    if( o==0 )
    {
        o:=dllcall("CallNextHookEx","uint",mousehook,"int",nCode,"uint",wParam,"uint",lParam)
    }
    return o
}

// Utilities //

rtoz(r)
{
    return ( r>0 ? floor(r) : ceil(r) )
}
getparent(handle)
{
    return dllcall("GetParent","uint",handle)
}
getancestor(handle,steps)
{
    if( steps==0 )
    {
        return handle
    }
    if( steps>0 )
    {
        return getancestor(getparent(handle),steps-1)
    }
    return ""
}
getname(root,handle)
{
    local CH,CN,S,P
    WinGet, CH, ControlListHwnd, ahk_id %root%
    WinGet, CN, ControlList, ahk_id %root%
    setformat integerfast,h
    handle+=0
    handle.=""
    setformat integerfast,d
    LF:= "`n",  CH:= LF CH LF, CN:= LF CN LF,  S:= SubStr( CH, 1, InStr( CH, LF handle LF ) )
    StringReplace, S, S,`n,`n, UseErrorLevel
    StringGetPos, P, CN, `n, L%ErrorLevel%
    Return SubStr( CN, P+2, InStr( CN, LF, 0, P+2 ) -P-2 )
}
getdescendant(handle,name)
{
    local CH,CN,S,P
    WinGet, CH, ControlListHwnd, ahk_id %handle%
    WinGet, CN, ControlList, ahk_id %handle%
    setformat integerfast,h
    handle+=0
    handle.=""
    setformat integerfast,d
    LF:= "`n",  CH:= LF CH LF, CN:= LF CN LF,  S:= SubStr( CN, 1, InStr( CN, LF name LF ) )
    StringReplace, S, S,`n,`n, UseErrorLevel
    StringGetPos, P, CH, `n, L%ErrorLevel%
    Return SubStr( CH, P+2, InStr( CH, LF, 0, P+2 ) -P-2 )*1
}
getnameatroot(handle)
{
    return getname(dllcall("GetAncestor","uint",handle,"uint",2),handle)
}
getnameaschild(handle)
{
    return getname(getparent(handle),handle)
}
getclass(handle)
{
    local class
    wingetclass class,ahk_id %handle%
    return class
}
gettitle(handle)
{
    local title
    wingettitle title,ahk_id %handle%
    return title
}
getposition(handle,byref left,byref top,byref right,byref bottom)
{
    local rect
    varsetcapacity(rect,16)
    dllcall("GetWindowRect","uint",handle,"uint",&rect)
    left:=numget(rect,0,"int")
    top:=numget(rect,4,"int")
    right:=numget(rect,8,"int")
    bottom:=numget(rect,12,"int")
}
getctrlat2(x,y,first,current)
{
    /*
        Pushes the following invisible container controls to the back because they are in front of their contents for no reason
            SysTabControl32 : The usual class that contains tabbed panes ( Mouse properties , ... )
            Static : A class occasionally used to contain tabbed panes ( Programmer's Notepad Options > Fonts and Colours > Advanced , ... )
            Button : A typical class used to contain a List Box ( Outlook Contact > Properties > General > Members , ... )
        Executes WindowFromPoint again to access the contents of such container controls
    */
    local handle,class,style
    class:=getclass(current)
    winget style,style,ahk_id %current%
    if( class=="SysTabControl32" or class=="Static" or ( class=="Button" and (style&0x7)==0x7 ) )
    {
        dllcall("SetWindowPos","uint",current,"uint",1,"int",0,"int",0,"int",0,"int",0,"uint",0x3)  // push it to the back where it belongs
        handle:=dllcall("WindowFromPoint","int",x,"int",y)
        //handle:=DllCall( "WindowFromPoint", "int64", (y << 32) | (x & 0xFFFFFFFF), "Ptr") // for negative 64-bit
        if( handle==first )
        {
            return first
        }
        return getctrlat2(x,y,first,handle)
    }
    return current
}
getctrlat(x,y)
{
    local handle
    handle:=dllcall("WindowFromPoint","int",x,"int",y)
    //handle:=DllCall( "WindowFromPoint", "int64", (y << 32) | (x & 0xFFFFFFFF), "Ptr") // for negative 64-bit
    return getctrlat2(x,y,handle,handle)
}
getwindow(handle)
{
    return dllcall("GetAncestor","uint",handle,"uint",2)
}

Acc_Init()
{
    Static h
    If Not h
        h:=DllCall("LoadLibrary","Str","oleacc","Ptr")
}
Acc_ObjectFromWindow(hWnd, idObject = -4)
{
    local o
    Acc_Init()
    o:=DllCall("oleacc\AccessibleObjectFromWindow"
        , "Ptr", hWnd
        , "UInt", idObject&=0xFFFFFFFF
        , "Ptr", -VarSetCapacity(IID,16)+NumPut(idObject==0xFFFFFFF0?0x46000000000000C0:0x719B3800AA000C81
            ,NumPut(idObject==0xFFFFFFF0?0x0000000000020400:0x11CF3C3D618736E0,IID,"Int64")
            ,"Int64") 
        ,"Ptr*", pacc)
    if( o==0 )
        Return ComObjEnwrap(9,pacc,1)
}

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.