如何在 X11 上监视所有 Windows(不只是一个)的鼠标移动事件

How do I Monitor Mouse Movement Events in All Windows (Not Just One) on X11

我正在尝试编写一个 X11 程序来监视桌面上的所有鼠标移动。每当人类用户移动鼠标,或机器人应用程序通过 XWarpPointer() 以编程方式移动鼠标时,程序应该能够收到通知。我知道应该可以通过 XSelectInput() 设置 PointerMotionMask 并监视 MotionNotify,但是我在从 all 接收鼠标事件时遇到问题 windows,不止一个。

最初,在下面的演示中,我只是尝试从根 window 接收指针运动事件。

#include <stdio.h>
#include <X11/Xlib.h>

int main(int argc, char **argv)
{
    Display *display;
    Window root_window;
    XEvent event;

    display = XOpenDisplay(0);
    root_window = XRootWindow(display, 0); 
    XSelectInput(display, root_window, PointerMotionMask);

    while (1) {
        XNextEvent(display, &event);
        switch(event.type) { 
            case MotionNotify:
                printf("x: %d y: %d\n", event.xmotion.x, event.xmotion.y );
                break;
        }
    }   
    return 0;
}

但它不会收到任何事件,除非鼠标指针位于空白桌面背景上。很明显,仅从根 window 接收事件是行不通的。然后我尝试了一个解决方法:首先,在根 window 上设置 SubstructureNotifyMask 以监视所有 CreateNotify 事件以捕获所有新创建的 windows,然后调用 XSelectInput() 以在这些 windows 上启用 PointerMotionMask

#include <stdio.h>
#include <X11/Xlib.h>

int main(int argc, char **argv)
{
    Display *display;
    Window root_window;
    XEvent event;

    display = XOpenDisplay(0);
    root_window = XRootWindow(display, 0); 
    XSelectInput(display, root_window, SubstructureNotifyMask);

    while (1) {
        XNextEvent(display, &event);
        switch(event.type) { 
            case CreateNotify:
                XSelectInput(display, event.xcreatewindow.window, PointerMotionMask);
                break;
            case MotionNotify:
                printf("x: %d y: %d\n", event.xmotion.x, event.xmotion.y);
                break;
        }
    }   
    return 0;
}

这种方法比较成功,我开始从新windows接收一些鼠标事件。不幸的是,它仍然无法在 window 内的所有部分工作 - 例如,它无法从终端仿真器的控制台区域接收鼠标事件,但可以在鼠标位于标题栏周围时接收事件。看来一个window可以创建更多子windows,所以鼠标事件不会被记录。

然后我尝试了另一种解决方法 - 在 CreateNotify 中同时设置 SubstructureNotifyMaskPointerMotionMask,所以当 window 创建 child window, SubstructureNotifyMask 确保将以递归方式接收更多 CreateNotify 事件,因此所有 child windows 也将获得 PointerMotionMask

#include <stdio.h>
#include <X11/Xlib.h>

int main(int argc, char **argv)
{
    Display *display;
    Window root_window;
    XEvent event;

    display = XOpenDisplay(0);
    root_window = XRootWindow(display, 0); 
    XSelectInput(display, root_window, SubstructureNotifyMask);

    while (1) {
        XNextEvent(display, &event);
        switch(event.type) { 
            case CreateNotify:
                XSelectInput(display, event.xcreatewindow.window, SubstructureNotifyMask | PointerMotionMask);
                break;
            case MotionNotify:
                printf("x: %d y: %d\n", event.xmotion.x, event.xmotion.y);
                break;
        }
    }   
    return 0;
}

它比第二个例子好一点,但不可靠:


那么,我如何在 X11 上监控所有 windows 中的鼠标移动事件?

经过一些研究,尤其是阅读了 Xeyes 的源代码(我总是觉得这个演示很愚蠢,但在这里帮助很大!),我发现:

  • 在所有 windows 和 subwindows 上调用 XSelectInput() 是徒劳的尝试,您必须在每个 window 上设置掩码和子 window 曾经创建过,这不是一个可靠的解决方案,因此不推荐。

  • 相反,通过 XQueryPointer() 直接从 X 服务器明确地持续拉出鼠标指针,而不是要求 X 服务器将 MotionEvent 推送给我们更好。

一个天真的解决方案是简单地通过 XtAppAddTimeOut() 设置一个计时器并定期调用 XQueryPointer(),它有效,而且确实 it was what Xeyes did in the past!但它不必要地浪费了 CPU 时间。如今,最佳实践是利用 XInputExtention 2.0。工作流程是:

  1. 初始化 XInput v2.0

  2. 通过XISetMask()XIEventMask()启用各种掩码以接收来自[=20=的XI_RawMotion事件(或XI_Motion,见下面的注释) ](或XIAllDevices)。

  3. 收到XI_RawMotion(或XI_Motion)事件后,调用XQueryPointer()

  4. XQueryPointer() returns:

    • 鼠标坐标相对于根 window。
    • 鼠标光标下的活动window,如果有的话。
  5. 如果我们想要相对于鼠标光标下的活动 window 的相对坐标,请执行 XTranslateCoordinates()

演示

这是一个演示(另存为mouse.c,用gcc mouse.c -o mouse -lX11 -lXi编译)。但是,它无法检测到 XWarpPointer(),请参阅下面的注释。

#include <stdio.h>
#include <assert.h>
#include <X11/Xlib.h>
#include <X11/extensions/XInput2.h>

int main(int argc, char **argv)
{
    Display *display;
    Window root_window;

    /* Initialize (FIXME: no error checking). */
    display = XOpenDisplay(0);
    root_window = XRootWindow(display, 0);

    /* check XInput */
    int xi_opcode, event, error;
    if (!XQueryExtension(display, "XInputExtension", &xi_opcode, &event, &error)) {
        fprintf(stderr, "Error: XInput extension is not supported!\n");
        return 1;
    }

    /* Check XInput 2.0 */
    int major = 2;
    int minor = 0;
    int retval = XIQueryVersion(display, &major, &minor);
    if (retval != Success) {
        fprintf(stderr, "Error: XInput 2.0 is not supported (ancient X11?)\n");
        return 1;
    }

    /*
     * Set mask to receive XI_RawMotion events. Because it's raw,
     * XWarpPointer() events are not included, you can use XI_Motion
     * instead.
     */
    unsigned char mask_bytes[(XI_LASTEVENT + 7) / 8] = {0};  /* must be zeroed! */
    XISetMask(mask_bytes, XI_RawMotion);

    /* Set mask to receive events from all master devices */
    XIEventMask evmasks[1];
    /* You can use XIAllDevices for XWarpPointer() */
    evmasks[0].deviceid = XIAllMasterDevices;
    evmasks[0].mask_len = sizeof(mask_bytes);
    evmasks[0].mask = mask_bytes;
    XISelectEvents(display, root_window, evmasks, 1);

    XEvent xevent;
    while (1) {
        XNextEvent(display, &xevent);

        if (xevent.xcookie.type != GenericEvent || xevent.xcookie.extension != xi_opcode) {
            /* not an XInput event */
            continue;
        }
        XGetEventData(display, &xevent.xcookie);
        if (xevent.xcookie.evtype != XI_RawMotion) {
            /*
             * Not an XI_RawMotion event (you may want to detect
             * XI_Motion as well, see comments above).
             */
            XFreeEventData(display, &xevent.xcookie);
            continue;
        }
        XFreeEventData(display, &xevent.xcookie);

        Window root_return, child_return;
        int root_x_return, root_y_return;
        int win_x_return, win_y_return;
        unsigned int mask_return;
        /*
         * We need:
         *     child_return - the active window under the cursor
         *     win_{x,y}_return - pointer coordinate with respect to root window
         */
        int retval = XQueryPointer(display, root_window, &root_return, &child_return,
                                   &root_x_return, &root_y_return,
                                   &win_x_return, &win_y_return,
                                   &mask_return);
        if (!retval) {
            /* pointer is not in the same screen, ignore */
            continue;
        }

        /* We used root window as its reference, so both should be the same */
        assert(root_x_return == win_x_return);
        assert(root_y_return == win_y_return);

        printf("root: x %d y %d\n", root_x_return, root_y_return);

        if (child_return) {
            int local_x, local_y;
            XTranslateCoordinates(display, root_window, child_return,
                                  root_x_return, root_y_return,
                                  &local_x, &local_y, &child_return);
            printf("local: x %d y %d\n\n", local_x, local_y);
        }
    }

    XCloseDisplay(display);

    return 0;
}

示例输出

root: x 631 y 334
local: x 140 y 251

root: x 628 y 338
local: x 137 y 255

root: x 619 y 343
local: x 128 y 260

XWarpPointer() 烦恼

如果 X.Org 1.10.4 之后的较新系统上的机器人应用程序通过 XWarpPointer() 移动指针,则上述演示将不起作用。这是故意的,请参阅 FreeDesktop 上的 Bug 30068

为了接收所有鼠标移动触发的鼠标事件,包括XWarpPointer(),将XI_RawMotion改为XI_Motion,将XIAllMasterDevices改为XIAllDevices .

参考资料

此演示缺少错误检查,可能包含错误。如有疑问,请查看以下权威参考资料。