Reputation: 21
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
Reputation: 77
I have just used
# Scroll up
pyautogui.hotkey('ctrl', 'up')
# Scroll down
pyautogui.hotkey('ctrl', 'down')
Upvotes: 0
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
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:
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