Raphael Huber
Raphael Huber

Reputation: 21

Pyautogui scroll fine tuning?

The pyautogui scroll amount value 1 is too small, 2 is to big for a specific task I want to do. Is there a way to scroll inbetween? I tried 1.5, but it didn't work.

I'm on OSX 10.13 and I can certainly scroll with more precision than what pyautogui is doing, when using the trackpad.

Upvotes: 2

Views: 6835

Answers (3)

Mama africa
Mama africa

Reputation: 77

I have just used

# Scroll up
pyautogui.hotkey('ctrl', 'up')

# Scroll down
pyautogui.hotkey('ctrl', 'down')

Upvotes: 0

hegash
hegash

Reputation: 863

This is an issue that has been annoying me, so I took a look at the pyautogui source code and was able to solve the problem. This will probably be quite a long answer; I'll try to explain every step in detail. Note that this only works for Mac. (scroll to the bottom if you want the answer, not the explanation)

First, here is the source code for the scroll function:

def _scroll(clicks, x=None, y=None):
    _vscroll(clicks, x, y)

def _vscroll(clicks, x=None, y=None):
    _moveTo(x, y)
    clicks = int(clicks)
    for _ in range(abs(clicks) // 10):
        scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
            None, # no source
            Quartz.kCGScrollEventUnitLine, # units
            1, # wheelCount (number of dimensions)
            10 if clicks >= 0 else -10) # vertical movement
        Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)

    scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
        None, # no source
        Quartz.kCGScrollEventUnitLine, # units
        1, # wheelCount (number of dimensions)
        clicks % 10 if clicks >= 0 else -1 * (-clicks % 10)) # vertical movement
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)

Let's break this down:

1.

def _scroll(clicks, x=None, y=None):
_vscroll(clicks, x, y)

This is just a wrapper for the _vscroll function, simple enough.

2.

The main thing to realise is that pyautogui, for Mac, uses Quartz Core Graphics, all it does is provide a simpler, more readable wrapper for the Quartz code.

With the scroll function, what it is doing is creating a scroll event:

scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent

And then posting it:

Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)

Ignore the details of the posting, we won't be changing any of that.

To me, it seems as if this code repeats itself, and I have no clue why any of the code after the for loop is included. I deleted this from my source code and everything works; If anyone knows why this code is included, please comment below and correct me.

3.

So we are left with the following code (ignoring the mouse moveTo, which has nothing to do with the scrolling itself):

clicks = int(clicks)
for _ in range(abs(clicks) // 10):
    scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
        None, # no source
        Quartz.kCGScrollEventUnitLine, # units
        1, # wheelCount (number of dimensions)
        10 if clicks >= 0 else -10) # vertical movement

    Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)

The format of a CGEventCreateScrollWheelEvent is the following:

Quartz.CGEventCreateScrollWheelEvent(source, units, wheelCount, scroll distance)

The source in this case is None, don't worry about that, and we are only dealing with 1 wheel, so wheelCount is 1.

What the source code is doing, therefore, is scrolling a distance of ±10 Quartz.kCGScrollEventUnitLine, which are your computers units for one 'scroll'. It repeats this in a for loop for however many times you specify because the system can bug if too many scroll units are sent at once.

Therefore, the minimum one can scroll on pyautogui is one iteration of this loop, which sends one computer unit. The problem is that these units are too big for fine scrolling.

SOLUTION

We need to change the minimum value we can send. Currently it is 1 Quartz.kCGScrollEventUnitLine, but we can change these to base units by replacing them with a zero. I also see no need to floor divide clicks (in range(abs(clicks) // 10)) and then send 10 scroll units.

We can change these two parts, and remove the unnecessary repetition:

def _scroll(clicks, x=None, y=None):
    _vscroll(clicks, x, y)

def _vscroll(clicks, x=None, y=None):
    _moveTo(x, y)
    clicks = int(clicks)
    for _ in range(abs(clicks)):  # <------------------------------------
        scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
            None, # no source
            0, # units  <------------------------------------------------
            1, # wheelCount (number of dimensions)
            1 if clicks >= 0 else -1) # vertical movement <--------------
        Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)

If you don't feel comfortable editing the source code itself, you can use these functions in your code directly, skipping out the need for pyautogui. Just have pyobjc installed (which you'll have anyway if you use pyautogui), remove _moveTo(x, y) and the keyword arguments, and use the following imports:

from Quartz.CoreGraphics import CGEventCreateScrollWheelEvent, CGEventPost, kCGHIDEventTap

I realise this answer is a bit late, but I came looking for answers to this problem and saw your question; When I solved the problem I thought I would share the knowledge.

Upvotes: 7

Anthony Markham
Anthony Markham

Reputation: 39

I really struggled with this one, so I thought I'd post my solution for Windows.

After a quick pip install pywin32, I got access to the necessary win32api & win32con, among others.

NOTE: The last time I checked, pywin32 was only supported for:

  • Python :: 2.7
  • Python :: 3.5
  • Python :: 3.6
  • Python :: 3.7
import time
import win32api
import win32con


def scroll(clicks=0, delta_x=0, delta_y=0, delay_between_ticks=0):
    """
    Source: https://learn.microsoft.com/en-gb/windows/win32/api/winuser/nf-winuser-mouse_event?redirectedfrom=MSDN

    void mouse_event(
      DWORD     dwFlags,
      DWORD     dx,
      DWORD     dy,
      DWORD     dwData,
      ULONG_PTR dwExtraInfo
    );

    If dwFlags contains MOUSEEVENTF_WHEEL,
    then dwData specifies the amount of wheel movement.
    A positive value indicates that the wheel was rotated forward, away from the user;
    A negative value indicates that the wheel was rotated backward, toward the user.
    One wheel click is defined as WHEEL_DELTA, which is 120.

    :param delay_between_ticks: 
    :param delta_y: 
    :param delta_x:
    :param clicks:
    :return:
    """

    if clicks > 0:
        increment = win32con.WHEEL_DELTA
    else:
        increment = win32con.WHEEL_DELTA * -1

    for _ in range(abs(clicks)):
        win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL, delta_x, delta_y, increment, 0)
        time.sleep(delay_between_ticks)

Then, after defining

click_point = x_position, y_position

and then using

pyautogui.moveTo(x=click_point[0], y=click_point[1], duration=0.25)

to make sure that my mouse is in the correct location. I just call the above scroll function:

scroll(-4, 0.1)

to scroll down 4 ticks with a 100ms delay between ticks.

Upvotes: 0

Related Questions