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

比尔盖*_*尔盖子 7 c linux x11 user-interface

我正在尝试编写一个 X11 程序来监视桌面上的所有鼠标移动。每当人类用户移动鼠标或通过XWarpPointer()机器人应用程序以编程方式移动鼠标时,该程序应该能够接收通知。我知道通过设置PointerMotionMaskviaXSelectInput()和 monitor应该可以实现MotionNotify,但是我在从所有窗口接收鼠标事件时遇到了麻烦,而不仅仅是一个窗口。

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

但它不会接收任何事件,除非鼠标指针位于空桌面背景上。很明显,仅仅从根窗口接收事件是行不通的。然后我尝试了一个解决方法:首先,SubstructureNotifyMask在根窗口上设置以监视所有CreateNotify事件以捕获所有新创建的窗口,然后调用XSelectInput()以启用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;
}
Run Code Online (Sandbox Code Playgroud)

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

然后我尝试了另一种解决方法 - 设置SubstructureNotifyMaskPointerMotionMaskin CreateNotify,因此当窗口创建子窗口时,SubstructureNotifyMask确保CreateNotify以递归方式接收更多事件,因此所有子窗口也将获得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;
}
Run Code Online (Sandbox Code Playgroud)

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

  • X 是完全异步的,是否有可能在我们有机会之前创建了子窗口XSelectInput()

  • 有时它只是报告BadWindow错误并崩溃。

  • X 事件处理变得混乱——如果程序已经处理了很多不同的 X 事件,那么SubstructureNotifyMask递归启用会使许多不相关的事件传递给其他处理程序,并且添加额外的代码来区分想要的和不需要的事件是一件痛苦的事情。


那么,如何在 X11 上的所有窗口中监视鼠标移动事件?

比尔盖*_*尔盖子 6

在做了一些研究之后,特别是阅读了 Xeyes 的源代码(我一直认为演示很愚蠢,但它在这里很有帮助!),我发现:

  • 调用XSelectInput()所有窗口和子窗口是徒劳的尝试,您必须在创建的每个窗口和子窗口上设置掩码,这不是一个强大的解决方案,也不推荐。

  • 相反,最好通过 持续从 X 服务器显式拉动鼠标指针XQueryPointer(),而不是要求 X 服务器将 MotionEvent 推送给我们。

一个简单的解决方案是简单地设置一个计时器XtAppAddTimeOut()XQueryPointer()定期调用它,确实有效,而且实际上,这就是 Xeyes 过去所做的!但它不必要地浪费了 CPU 时间。如今,最佳实践是利用 XInputExtention 2.0。工作流程是:

  1. 初始化 XInput v2.0

  2. 通过XISetMask()和启用各种掩码XIEventMask()以从(或)接收XI_RawMotion事件(或XI_Motion,请参阅下面的注释)。XIAllMasterDevicesXIAllDevices

  3. 当接收到XI_RawMotion(或XI_Motion)事件时,调用XQueryPointer()

  4. XQueryPointer() 返回:

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

演示

这是一个演示(另存为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;
}
Run Code Online (Sandbox Code Playgroud)

样本输出

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
Run Code Online (Sandbox Code Playgroud)

XWarpPointer() 问题

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

为了接收由所有鼠标移动触发的鼠标事件,包括XWarpPointer()、更改XI_RawMotionXI_Motion和更改XIAllMasterDevicesXIAllDevices

参考

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

  • 优秀的答案和出色的工作,坚持为自己回答这个问题并与他人分享答案。 (2认同)