Как отслеживать события движения мыши во всех Windows (а не только в одном) на X11 - PullRequest
4 голосов
/ 18 июня 2020

Я пытаюсь написать программу X11 для отслеживания всех движений мыши на рабочем столе. Программа должна иметь возможность получать уведомление всякий раз, когда мышь перемещается пользователем-человеком или перемещается программно через XWarpPointer() приложением roboti c. Я знаю, что это должно быть возможно, установив PointerMotionMask через XSelectInput() и монитор MotionNotify, но у меня проблемы с получением событий мыши от всех windows, а не только одного.

Первоначально я просто пытался получить события движения указателя из окна root в следующей демонстрации.

#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;
}

Но он не получает никаких событий, если только указатель мыши находится на пустом фоне рабочего стола. Понятно, что просто получать события из окна root не получится. Затем я попробовал обходной путь: сначала установите SubstructureNotifyMask в окне root, чтобы отслеживать все CreateNotify события, чтобы перехватить все вновь созданные windows, затем вызовите XSelectInput(), чтобы включить PointerMotionMask на этих windows .

#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. К сожалению, он по-прежнему не работает во всех частях окна - например, он не может получать события мыши из области консоли в эмуляторах терминала, но может получать события, когда мышь находится вокруг строки заголовка. Похоже, что в окне может быть создано больше под windows, поэтому события мыши не будут записываться.

Затем я попробовал другой обходной путь - установите как SubstructureNotifyMask, так и PointerMotionMask в CreateNotify, поэтому когда окно создает дочернее окно, SubstructureNotifyMask гарантирует, что больше CreateNotify событий будет получено рекурсивным образом, поэтому все дочерние 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;
}

Это работает немного лучше, чем второй пример, но он ненадежен:

  • X полностью асинхронен, возможно ли, что дочернее окно было создано до того, как у нас появилась возможность XSelectInput()?

  • Иногда он просто сообщает об ошибке BadWindow и вылетает.

  • Обработка X-событий становится беспорядочной - если программа уже обрабатывает множество различных X-событий, включение SubstructureNotifyMask рекурсивно приведет к доставке многих несвязанных событий другим обработчикам, и добавить дополнительный код для различения желаемых и нежелательных событий.


Итак, как мне отслеживать события движения мыши во всех windows на X11?

1 Ответ

3 голосов
/ 19 июня 2020

Проведя небольшое исследование, особенно прочитав исходный код Xeyes (мне всегда казалось, что демо - глупо, но здесь очень помогает!), Я обнаружил:

  • Calling XSelectInput() на всех windows и под windows - тщетная попытка, вам нужно установить маску для каждого отдельного окна и дочернего окна, когда-либо созданного, это не надежное решение и не рекомендуется.

  • Вместо этого лучше просто постоянно перетаскивать указатель мыши с X-сервера явно через XQueryPointer(), а не просить X-сервер сделать sh MotionEvent нам.

Одно наивное решение - просто установить таймер с помощью XtAppAddTimeOut() и периодически вызывать XQueryPointer(), это работает, и действительно, это было то, что Xeyes делал в прошлом ! Но это неоправданно тратит время процессора. В настоящее время лучше всего использовать XInputExtention 2.0. Рабочий процесс:

  1. Инициализация XInput v2.0

  2. Включение различных масок через XISetMask() и XIEventMask() для получения XI_RawMotion событий (или XI_Motion, см. примечания ниже) от XIAllMasterDevices (или XIAllDevices).

  3. При получении события XI_RawMotion (или XI_Motion) позвоните XQueryPointer().

  4. XQueryPointer() возвращает:

    • Координаты мыши относительно окна root.
    • Активное окно под курсором мыши, если он есть.
  5. Выполните 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() Проблемы

Демонстрация выше не работает если указатель перемещается через XWarpPointer() приложением roboti c в более новых системах после X.Org 1.10.4. Это сделано намеренно, см. Ошибка 30068 на FreeDesktop.

Чтобы получать события мыши, запускаемые всеми движениями мыши, включая XWarpPointer(), измените XI_RawMotion на XI_Motion и измените От XIAllMasterDevices до XIAllDevices.

Ссылки

В этой демонстрации отсутствует проверка ошибок, и она может содержать ошибки. В случае сомнений, пожалуйста, проверьте следующие авторитетные источники.

...