LINUX.ORG.RU

Qt и глобальный перехват событий иксов

 ,


1

5

Я тут работаю над самодельной экранной клавиатурой и для некоторой логики мне требуется следить за всеми нажатиям и отпусканиями клавиш - как виртуальными, так и хардварными. Само моё приложение написано на Qt.

Делаю так:

Display *display = QX11Info::display();
XSelectInput(display, DefaultRootWindow(display), KeyPressMask | KeyReleaseMask);
QApplication::instance()->installNativeEventFilter(this);
...
bool KeyboardWidget::nativeEventFilter(const QByteArray &eventType, void *message, long *) {
	xcb_generic_event_t *ev = static_cast<xcb_generic_event_t*>(message);
	switch (ev->response_type & ~0x80) {
		case XCB_KEY_PRESS: {
			xcb_key_press_event_t *key_press_event = reinterpret_cast<xcb_key_press_event_t*>(ev);
			keyEventReceived(key_press_event->detail, true);
			break;
		}
		case XCB_KEY_RELEASE: {
			xcb_key_release_event_t *key_release_event = reinterpret_cast<xcb_key_release_event_t*>(ev);
			keyEventReceived(key_release_event->detail, false);
			break;
		}
	}
	return false;
}

Пока окно приложения имело фокус, приём клавиатурных событий исправно работал (ещё бы он не работал), но как только я сделал так, чтобы окно фокус не получало (setWindowFlags(Qt::WindowStaysOnTopHint | Qt::ToolTip | Qt::FramelessWindowHint)), всё работать перестало. То есть по факту события глобально не ловятся. Что делать?

★★★★★

Ответ на: комментарий от RazrFalcon

Там будут рассказывать как повесить обработчик на какую-нибудь одну комбинацию клавиш. А я хочу получать нажатия ВСЕХ клавиш, но при этом не изменять их так, чтобы эти события дошли до законных получателей.

KivApple ★★★★★ ()

Единственный нормально работающий способ, который я в свое время использовал - это создание «виртуального устройства ввода» с помощью модуля ядра uinput. С его помощью ты сможешь как получать события ввода, так и эмулировать нажатия клавиш. Единственная оговорка - твое виртуальное uinput устройство должно появиться в системе до запуска иксов. Короче говоря, тебе потребуется создать демон, выполняющий всю нужную работу и организовать связь с ним из твоей программы.

m0rph ★★★★★ ()
Ответ на: комментарий от Dron

Попробовал такое:

XGrabKey(display, XKeysymToKeycode(display, XK_BackSpace), AnyModifier, DefaultRootWindow(display), True, GrabModeAsync, GrabModeAsync);

и такое:

XGrabKey(display, AnyKey, AnyModifier, DefaultRootWindow(display), True, GrabModeAsync, GrabModeAsync);

Первый вариант позволяет мне отлавливать Backspace как если бы эти события были адресованы моему приложению, однако в итоге я перехватываю нажатия - настоящему получателю они уже не доходят. А мне нужно лишь определять нажатые клавиши, но оставлять нормальное поведение.

Второй вариант ничего не делает, однако всё становится яснее, если сделать небольшую тестовую программу:

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

int main() {
    Display *d = XOpenDisplay(0);
    Window root = DefaultRootWindow(d);

    int rv = XGrabKey(d, AnyKey, AnyModifier, root, 1, GrabModeAsync, GrabModeAsync);
    printf("XGrabKey returned %d\n", rv);

    XEvent evt;
    while(1) {
        XNextEvent(d, &evt);
        printf("Got event %d\n", evt.type);
    }
}

При запуске получаем ошибку:

XGrabKey returned 1
X Error of failed request:  BadAccess (attempt to access private resource denied)
  Major opcode of failed request:  33 (X_GrabKey)
  Serial number of failed request:  7
  Current serial number in output stream:  7

То есть XGrabKey вроде как умеет то, что нужно, но не совсем.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от Dron

Есть вот такое вот решение:

bool KeyboardWidget::nativeEventFilter(const QByteArray &eventType, void *message, long *) {
	xcb_generic_event_t *ev = static_cast<xcb_generic_event_t*>(message);
	switch (ev->response_type & ~0x80) {
		case XCB_KEY_PRESS: {
			xcb_key_press_event_t *key_press_event = reinterpret_cast<xcb_key_press_event_t*>(ev);
			keyEventReceived(key_press_event->detail, true);
			break;
		}
		case XCB_KEY_RELEASE: {
			xcb_key_release_event_t *key_release_event = reinterpret_cast<xcb_key_release_event_t*>(ev);
			keyEventReceived(key_release_event->detail, false);
			break;
		}
	case XCB_FOCUS_OUT:
		focusChanged();
		break;
	}
	return false;
}

void KeyboardWidget::focusChanged() {
	Display *display = QX11Info::display();
	Window currentWindow;
	int rev;
	XGetInputFocus(display, &currentWindow, &rev);
	if ((currentWindow == PointerRoot) || (currentWindow == None)) {
		currentWindow = DefaultRootWindow(display);
	}
	XSelectInput(display, currentWindow, KeyPressMask | KeyReleaseMask | FocusChangeMask);
}

...

QApplication::instance()->installNativeEventFilter(this);
focusChanged();	

Такой вариант работает и ловит клавиши, которые при этом таки доставляются нужному приложению, но... только если указатель мыши находится над окном моего приложения. Если данное условие не выполняется, то никаких событий моё приложение не получает.

UPD: Не совсем так. Для Google Chrome всё ок, для Qt Creator ловит только если указатель над окном моего приложения.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от KivApple

может тогда глянуть как в onboard всё работает гуй там на питоне, а внутри модули на си, onboard/osk вроде в сырцах, я не могу проверить сейчас

Dron ★★★★★ ()
Ответ на: комментарий от Dron

Нашёл вот что: http://stackoverflow.com/questions/32449666/my-xlib-code-can-listen-to-keyboa...

Суть такова: если приложение использует XInput2, то я не могу получить нормально его события с помощью одного лишь XSelectInput.

Теоретически возможное решение:

bool KeyboardWidget::nativeEventFilter(const QByteArray &eventType, void *message, long *) {
	xcb_generic_event_t *ev = static_cast<xcb_generic_event_t*>(message);
	XGenericEventCookie *cookie;
	if (m_displayHasXInput && XGetEventData(QX11Info::display(), cookie)) {
		if ((cookie->type == GenericEvent) && (cookie->extension == m_xi_opcode)) {
			XIDeviceEvent *devev = static_cast<XIDeviceEvent*>(cookie->data);
			switch (devev->evtype) {
				case XI_KeyPress:
					keyEventReceived(devev->detail, true);
					break;
				case XI_KeyRelease:
					keyEventReceived(devev->detail, false);
					break;
				case XI_FocusOut:
					focusChanged();
					break;
			}
		}
	} else {
		switch (ev->response_type & ~0x80) {
			case XCB_KEY_PRESS: {
				xcb_key_press_event_t *key_press_event = reinterpret_cast<xcb_key_press_event_t*>(ev);
				keyEventReceived(key_press_event->detail, true);
				break;
			}
			case XCB_KEY_RELEASE: {
				xcb_key_release_event_t *key_release_event = reinterpret_cast<xcb_key_release_event_t*>(ev);
				keyEventReceived(key_release_event->detail, false);
				break;
			}
			case XCB_FOCUS_OUT:
				focusChanged();
				break;
		}
	}
	return false;
}

void KeyboardWidget::focusChanged() {
	Display *display = QX11Info::display();
	Window currentWindow;
	int rev;
	XGetInputFocus(display, &currentWindow, &rev);
	if ((currentWindow == PointerRoot) || (currentWindow == None)) {
		currentWindow = DefaultRootWindow(display);
	}
	if (m_displayHasXInput) {
		XIEventMask evmask;
		evmask.deviceid = XIAllMasterDevices;
		evmask.mask_len = XIMaskLen(XI_LASTEVENT);
		evmask.mask = (unsigned char*)malloc(evmask.mask_len);
		memset(evmask.mask, 0, evmask.mask_len);
		XISetMask(evmask.mask, XI_KeyPress);
		XISetMask(evmask.mask, XI_KeyRelease);
		XISetMask(evmask.mask, XI_FocusOut);
		int prev_evmask_count;
		XIEventMask *prev_evmask = XIGetSelectedEvents(display, currentWindow, &prev_evmask_count);
		if (prev_evmask) {
			for (int i = 0; i < prev_evmask_count; ++i) {
				if (prev_evmask[i].deviceid != XIAllMasterDevices) continue;
				for (int j = 0; j < qMin(evmask.mask_len, prev_evmask[i].mask_len); ++j) {
					evmask.mask[j] |= prev_evmask[i].mask[j];
				}
			}
			XFree(prev_evmask);
		}
		XISelectEvents(display, currentWindow, &evmask, 1);
	}
	XSelectInput(display, currentWindow, KeyPressMask | KeyReleaseMask | FocusChangeMask);
}

...

QApplication::instance()->installNativeEventFilter(this);
{
	int ev, err;
	m_displayHasXInput = XQueryExtension(QX11Info::display(), "XInputExtension", &m_xi_opcode, &ev, &err);
}
focusChanged();

Однако возникает проблема, что XGenericEventCookie *cookie должен быть инициализирован каким-то значением. Например, в этом примере https://github.com/esjeon/xinput2-touch/blob/master/event.c делают так:

XGenericEventCookie *cookie = &ev.xcookie;

Но ведь Qt-то выдаёт мне xcb-событие, а не напрямую иксовое, так что я так сделать не могу. Пробовал сам выделять память под куку, но в итоге ничего хорошего не вышло.

В Google Chrome теперь вообще события не идут, в Qt Creator не идут только если навести курсор на моё окно. То есть получается, что события таки перехватываются, однако их надо как-то обрабатывать (иначе они могут и не дойти), а XGetEventData не срабатывает из-за плохой куки.

KivApple ★★★★★ ()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от Dron

Нашёл вот эту утилиту: https://github.com/wavexx/screenkey

Скачал, запустил - она вполне нормально отображает нажатия, как в Google Chrome, так и в Qt Creator. Буду изучать её исходники.

KivApple ★★★★★ ()
Ответ на: комментарий от Dron

Короче я победил. Использую XRecord для получения событий клавиатуры - https://github.com/KivApple/qvkbd/blob/master/src/x11eventlistener.cpp (пока там только базовый функционал, в смысле этой штукой можно печатать).

Мне, правда, не очень нравится, что я открываю аж 3 соединения к иксам. Можно уменьшить их количество до двух, но нужно ответить на один вопрос: если я сделаю XSynchronize(QX11Info::display(), True), то ничего плохого не случится (например, в плане производительности)? Если всё нормально, то можно использовать для создания контекста XRecord qt-шное соединение с иксами.

KivApple ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.