如何在Mac OS X中向窗口发送鼠标点击事件

6

有没有办法在Mac OSX上向一个隐藏的(不在前台显示)窗口发送鼠标点击事件?我正在尝试使用pyobjcpyautogui,对这种情况还很陌生。如果您有任何关键字或想法,请告诉我。谢谢!


尝试使用 https://pypi.python.org/pypi/pynput ,也许它能够解决问题。 - ShivaGuntuku
谢谢,但是看了之后,似乎仍然是控制和监控“真实”的鼠标和键盘,而不是在Mac OS X中向指定的窗口发送鼠标单击事件。@ShivaGuntuku - KAs
为了GUI自动化的目的,可以尝试在脚本中结合pynput和automa。不管你尝试的目的是什么,这可能是一个示例想法。请访问http://www.getautoma.com/docs获取更多信息。 - ShivaGuntuku
我打算制作一个后台机器人,可以帮我完成一些基本的鼠标点击和键盘输入操作,在此同时,我可以在前台做自己的事情。如果使用 GUI 自动化,我认为鼠标和键盘将被占用,无法同时进行其他操作。但还是谢谢!如果您知道与我的场景相关的任何内容,请告诉我:) @ShivaGuntuku - KAs
嘿 @KAs,你找到一个可行的解决方案了吗? - return true
1个回答

15

编辑:根据更多的调查,最后更新在结尾处。但简而言之,在OSX中通常是不可能做到这一点的。

免责声明:这并不是一个真正的答案。我本来要发布大致相同的问题,但是后来我找到了这个问题。为了不重复提问,我想通过一些评论来分享我已经发现/尝试过的东西。虽然我是SO上的新手,但我还没有足够的声誉来留下评论,所以我在这里发表评论作为“答案”。所以它并不是一个真正的答案,但希望能帮助你或其他人更接近答案。

类似答案:

据我所知,在OSX中仅向一个应用程序发送鼠标事件的“正确”方法是使用Core Graphics CGEventPostToPSN函数。

但是获取“PSN”(进程序列号)并不是非常直观,人们以前用于执行此操作的所有方法均已弃用。有一个替代功能被称为CGEventPostToPid,它使用标准*nix进程ID来指定目标应用程序。

我已成功使用此函数将键盘事件发布到后台应用程序,但没有鼠标事件。

例如,这将通过PID将字符发送到您指定的任何应用程序:

pid = 1234  # get/input a real pid from somewhere for the target app.
type_a_key_down_event = Quartz.CGEventCreateKeyboardEvent(objc.NULL, 0, True)
type_a_key_up_event = Quartz.CGEventCreateKeyboardEvent(objc.NULL, 0, False)

Quartz.CGEventPostToPid(pid, type_a_key_down_event)
Quartz.CGEventPostToPid(pid, type_a_key_up_event)

(键盘事件创建文档:https://developer.apple.com/reference/coregraphics/1456564-cgeventcreatekeyboardevent?language=objc)

然而,这并不会向目标应用程序发送点击事件:

pid = 1234  # get/input a real pid from somewhere for the target app.
point = Quartz.CGPoint()
point.x = 100  # get a target x from somewhere
point.y = 100  # likewise, your target y from somewhere
left_mouse_down_event = Quartz.CGEventCreateMouseEvent(objc.NULL, Quartz.kCGEventLeftMouseDown, point, Quartz.kCGMouseButtonLeft)
left_mouse_up_event = Quartz.CGEventCreateMouseEvent(objc.NULL, Quartz.kCGEventLeftMouseUp, point, Quartz.kCGMouseButtonLeft)

Quartz.CGEventPostToPid(pid, left_mouse_down_event)
Quartz.CGEventPostToPid(pid, left_mouse_up_event)

一些SO的答案建议您在鼠标事件中使用CGEventPost

Quartz.CGEventPost(Quartz.kCGHIDEventTap, left_mouse_down_event)
Quartz.CGEventPost(Quartz.kCGHIDEventTap, left_mouse_up_event)

我认为pyautogui的操作是这样的,因为它能够成功地在请求的坐标处单击鼠标,但它是通过移动和点击全局鼠标来实现的。 运行该代码将把鼠标移动到屏幕上的某个点,并在该点上进行单击,然后将鼠标留在那里。 这不是我想要的,也不认为这是你想要的。

我想要的是CGEventPostToPSN或其更现代化的版本CGEventPostToPid所描述的行为:发布一个输入事件到目标应用程序,无论它是否在前台,它都不会窃取焦点或更改实际鼠标位置。

我最接近的是根据Simulating mouse-down event on another window from CGWindowListCreate中的一些代码进行调整。

它建议使用NSEvent而不是CGEvent。 我已经尝试过objective-C和pyobjc,但效果有限。 我尝试过的objective-C版本对我没用,所以我不会贴出来。 pyobjc版本允许我从终端单击iTerm或反之亦然,但我无法使单击穿透其他应用程序。 我尝试了Chrome,Console和一些其他应用程序,但都没有成功。

下面是部分可工作的pyobjc代码:

#! /usr/bin/env python
"""
CLI for sending mouse clicks to OSX apps.

Author: toejough
License: MIT
"""


# [ Imports ]
import Quartz
import fire
import AppKit


# [ API ]
def click(*, x, y, window_id, pid):
    """Click at x, y in the app with the pid."""
    point = Quartz.CGPoint()
    point.x = x
    point.y = y

    event_types = (
        AppKit.NSEventTypeMouseMoved,
        AppKit.NSEventTypeLeftMouseDown,
        AppKit.NSEventTypeLeftMouseUp,
    )

    for this_event_type in event_types:
        event = _create_ns_mouse_event(this_event_type, point=point, window_id=window_id)
        Quartz.CGEventPostToPid(pid, event)


# [ Internal ]
def _create_ns_mouse_event(event_type, *, point, window_id=None):
    """Create a mouse event."""
    create_ns_mouse_event = AppKit.NSEvent.mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure_
    ns_event = create_ns_mouse_event(
        event_type,  # Event type
        point,  # Window-specific coordinate
        0,  # Flags
        AppKit.NSProcessInfo.processInfo().systemUptime(),  # time of the event since system boot
        window_id,  # window ID the event is targeted to
        None,  # display graphics context.
        0,  # event number
        1,  # the number of mouse clicks associated with the event.
        0  # pressure applied to the input device, from 0.0 to 1.0
    )
    return ns_event.CGEvent()


# [ Script ]
if __name__ == "__main__":
    fire.Fire(click)

尝试使用标志、事件号、时间等进行操作,但没有成功。无论我如何更改这些内容,我最多只能在iTerm与Terminal之间发送点击。不确定终端应用程序的特殊性是什么,导致其他应用程序不能正常工作。

您可以从https://developer.apple.com/reference/coregraphics/quartz_window_services?language=objc中记录的CGWindowListCopyWindowInfo和相关函数中获取window_id

如果有人有更好的答案和真正的方案适用于所有应用程序,请发布并告知我们。或者,如果您对为什么点击无法正常工作有解释/线索,请发布。

我收集到的唯一可能有趣的证据是,每次运行上述脚本时,我的系统日志中都会出现以下内容:

4/21/17 4:16:08.284 PM launchservicesd[80]: SecTaskLoadEntitlements failed error=22
4/21/17 4:16:08.288 PM launchservicesd[80]: SecTaskLoadEntitlements failed error=22
4/21/17 4:16:08.295 PM tccd[21292]: SecTaskLoadEntitlements failed error=22

可能是一个错误的线索 - 我在成功情况下(将鼠标点击发送到不同的终端应用程序)和失败情况下(将鼠标点击发送到非终端应用程序)都会出现这些错误。

希望这可以帮助您或其他读者更接近真正的解决方案 - 如果是这样,请回复!

更新:我认为事件之所以对终端应用程序有效而对一般应用程序无效,是由于 osx 内置的反焦点跟随鼠标范式。您可以发送事件,但除非应用程序处于活动状态(或允许 FFM),否则操作系统会阻止它。

允许 FFM 的应用程序(如 iTerm 和终端)可以接收上述 ns_event 变量,这就是我能够使它们工作的原因。只要首先聚焦于其他应用程序,其他应用程序也会接收到鼠标事件(不移动全局鼠标指针!)。

您可以通过将睡眠插入上述程序或在终端中,手动将目标应用程序置于前台,并查看鼠标事件是否经过来自己验证。

据我所知,使鼠标事件通常工作的唯一方法是绕过操作系统的 FFM 过滤器,我不知道有什么方法可以做到这一点。如果有人找到了方法,请告诉我!

有关 FFM 和 OSX 的更多信息 在这里


你有取得任何进展吗? - user1768741
不是,虽然我很快会在High Sierra上重新访问这个问题! - toejough
尝试了High Sierra,没有成功。请参阅上面的编辑帖子以获得更多信息。 - toejough
嘿,最近有什么新进展吗? - return true
不好意思。我已经有一段时间没有在处理需要控制鼠标/键盘的项目了。 - toejough

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接