IT Notes

Удаленное управление компьютером по сети: Передача событий клавиатуры (Linux/X11)

Предыдущие части:

  1. Удаленное управление компьютером по сети: Введение
  2. Удаленное управление компьютером по сети: Формирование видео-потока
  3. Удаленное управление компьютером по сети: Наблюдение
  4. Удаленное управление компьютером по сети: Передача событий клавиатуры (Win32)

Логика воспроизведения событий клавиатуры на Linux-системах в целом не сильно отличается от того, что мы уже реализовали под Windows. Но есть и отличия, о которых мы поговорим по мере необходимости.

Начнем с карты преобразования кодов клавиш из Qt::Key в специфичные системные коды X11. Займемся классом KeyMapper, добавив файл keymapper_x11.cpp (сокращенная версия):

#include "keymapper.h"

#include <X11/keysymdef.h>
#include <X11/Intrinsic.h>

KeyMapper::KeyMapper() {
    m_keyMap[ Qt::Key_Escape ] = XK_Escape;
    m_keyMap[ Qt::Key_Tab ] = XK_Tab;
    m_keyMap[ Qt::Key_Backtab ] = XK_ISO_Left_Tab;
    m_keyMap[ Qt::Key_Backspace ] = XK_BackSpace;
    m_keyMap[ Qt::Key_Return ] = XK_Return;
    m_keyMap[ Qt::Key_Insert ] = XK_Insert;
    m_keyMap[ Qt::Key_Delete ] = XK_Delete;
    m_keyMap[ Qt::Key_Pause ] = XK_Pause;
    m_keyMap[ Qt::Key_Print ] = XK_Print;
    m_keyMap[ Qt::Key_SysReq ] = 0x1005FF60;

    // И т.д.
}

uint KeyMapper::findNativeVirtualKeyCode( Qt::Key keyCode ) {
    uint code = 0;

    Display* display = XOpenDisplay( nullptr );
    if( display ) {
        if( m_keyMap.contains( keyCode ) ) {
            code = XKeysymToKeycode( display, m_keyMap[ keyCode ] );
        }
        XCloseDisplay( display );
    }

    return code;
}

Отличие этой от Windows-версии заключается в том, что в таблице Qt-кодам клавиш в соответствие ставится не прямой X11-код клавиши, а ее символьный вариант. Нам для заполнения таблицы достаточно специальных клавиш (Delete, Enter и т.д.), букв латинского алфавита A-Z и цифр 0-9, для которых в X11/keysymdef.h предусмотрены значения символьных кодов.

Для преобразования символьного кода клавиши в абсолютный используется функция XKeysymToKeycode(). Под абсолютным кодом клавиши имеется в виду код, однозначно присвоенный этой клавише, который не зависит от контекста (раскладки, клавиш-модификаторов и т.д.).

Обратите внимание, что для XKeysymToKeycode() в качестве одного из параметров требуется указатель на текущий дисплей. Его мы получаем с помощью функции XOpenDisplay().

Переходим к непосредственному воспроизведению действия нажатия/отпускания клавиши. Функция onKeyAction() в файле remotecontrolserver.cpp:

void onKeyAction( bool pressed, int keyCode, const QString& text ) {
    static KeyMapper keyMapper;
    static bool shiftPressed = false;

    if( keyCode == Qt::Key_Shift ) shiftPressed = pressed;

    Display* display = XOpenDisplay( nullptr );
    if( display == nullptr ) {
        return;
    }

    uint vKeyCode = 0;
    if(
        ( vKeyCode = keyMapper.findNativeVirtualKeyCode( Qt::Key( keyCode ) ) ) == 0 &&
        !text.isEmpty()
    ) {
        if( shiftPressed ) onKeyAction( false, Qt::Key_Shift, "" );

        QString uSym = QString().sprintf( "U%04X", text.at( 0 ).unicode() );
        auto keySym = XStringToKeysym( uSym.toStdString().c_str() );

        int min, max, numcodes;
        XDisplayKeycodes( display, &min, &max );
        if( KeySym* keySyms = XGetKeyboardMapping( display, min, max - min + 1, &numcodes ) ) {
            keySyms[ ( max - min - 1 ) * numcodes ] = keySym;
            XChangeKeyboardMapping( display, min, numcodes, keySyms, max - min );
            XFree( keySyms );
            XFlush( display );

            vKeyCode = XKeysymToKeycode( display, keySym );
        }

        if( shiftPressed ) onKeyAction( true, Qt::Key_Shift, "" );
    }

    XTestFakeKeyEvent( display, vKeyCode, pressed, 0 );

    XFlush( display );
    XCloseDisplay( display );
}

В простейшем случае код клавиши находится в таблице из keyMapper. Этот код мы передаем функции XTestFakeKeyEvent() по аналогии с тем, как делали ранее при воспроизведении событий мыши (функция XTestFakeButtonEvent()).

Чуть сложнее дела обстоят в ситуации, когда для Qt-кода ничего подходящего найти не удалось. В силу вступает "план Б": используем текст text, который был получен на клиенте в результате нажатия клавиши.

Основная особенность заключается в том, что мы явно контролируем то, нажат ли Shift. Это связано с тем, что в строке text уже может быть заглавная буква или специальный символ (например, для Shift + цифра). В результате, если явно не "отжимать" клавишу Shift, мы воспроизведем совсем не то, что хотели. Достигается это повторным вызовом функции onKeyAction():

if( shiftPressed ) onKeyAction( false, Qt::Key_Shift, "" );

// …

if( shiftPressed ) onKeyAction( true, Qt::Key_Shift, "" );

Сама имитация нажатия сводится к регистрации дополнительного кода клавиши в таблице символьных кодов клавиш на основании первого Unicode-символа строки text:

QString uSym = QString().sprintf( "U%04X", text.at( 0 ).unicode() );
auto keySym = XStringToKeysym( uSym.toStdString().c_str() );

int min, max, numcodes;
XDisplayKeycodes( display, &min, &max );
if( KeySym* keySyms = XGetKeyboardMapping( display, min, max - min + 1, &numcodes ) ) {
    keySyms[ ( max - min - 1 ) * numcodes ] = keySym;
    XChangeKeyboardMapping( display, min, numcodes, keySyms, max - min );
    XFree( keySyms );
    XFlush( display );

    vKeyCode = XKeysymToKeycode( display, keySym );
}

Функция XStringToKeysym() принимает строку char* в формате UXXXX, где последнее - код печатного символа в таблице Unicode (например, U0411 для Б), а возвращает числовое значение символьного кода клавиши.

Вся дальнейшая магия основана на использовании функций XGetKeyboardMapping() и XChangeKeyboardMapping(). После представленных манипуляций вызов XKeysymToKeycode() возвращает код клавиши, который можно воспроизводить с помощью знакомой нам функции XTestFakeKeyEvent().

Выводы

Как и для Windows-версии, реализация под Linux не лишена недостатков. Они выражаются практически в том же самом: возможны "залипания" клавиш, могут некорректно отрабатывать клавиши-модификаторы, проблемы с раскладками. Также наблюдаются возможные проблемы при быстрой печати символами Unicode. Это связано со смешиванием событий нажатия/отпускания клавиш и особенностями X11 API.

Однако вывод остается прежним: пользоваться таким приложением уже можно. Все дальнейшие улучшения в большей степени заключаются в исправлении ошибок и доработке/оптимизации алгоритмов, а не в добавлении новых функций. Таким образом, хоть мы и оставили множество допущений, на этом работа над проектом окончена.

Исходники

 Скачать исходники примера RemoteControlDemo

Также они доступны на github: https://github.com/itnotesblog/RemoteControlDemo под тэгом v.0.5.0.

Сборка проекта проверялась под Linux и Windows с Qt 4.8.4 и компиляторами gcc (x64) и msvc.2010 (32-bit) соответственно.

Похожие публикации

Комментарии

Здравствуйте

Крутая программа =) Как ее доработать просто надо на удаленной машине выключить приложение ?

Здравствуйте. Спасибо за комментарий =)

tysik:

Как ее доработать просто надо на удаленной машине выключить приложение ?

Не до конца понял, что имеется в виду. Если требуется полноценный доступ к удаленной машине, то рекомендую воспользоваться TeamViewer или другими аналогами, поскольку проект , описанный в статье, не рассчитан на серьезное использование (по многим причинам) и предназначен в первую очередь для учебных целей

Просто надо получить список приложений на удаленной машине если запущен пасьянс то надо принудительно закрыть программу . Как доработать программу ?

Понял. Т.е. нужен удаленный диспетчер задач? Опять же проще воспользоваться готовым решением, которых вполне хватает. Но если есть желание создать такое приложение самостоятельно, то нужно определиться с целевыми платформами, поскольку придется использовать специфические API конкретных ОС (как и в случае передачи событий мыши/клавиатуры). При этом идея реализации такого проекта полностью аналогична рассмотренной в моих статьях из серии "Удаленное управление". Только с управляющего компьютера отправляем команды на завершение процессов (вместо нажатий кнопок мыши или клавиатуры), а с целевого получаем список запущенных процессов (вместо видеопотока)

Понял буду думать . При Сборке возникла проблема клиету не передается изображение ошибка QPixmap::scaled: Pixmap is a null pixmap как ее устранить .

tysik:

При Сборке возникла проблема клиету не передается изображение ошибка QPixmap::scaled: Pixmap is a null pixmap как ее устранить .

Посмотрите на это обсуждение. Думаю, что должно помочь

не помогло картинки на клиенте нету захват мыши и клавиатуры работает.

А через DesktopRecorderDemo все работает? Какую ОС используете для запуска?

DesktopRecorderDemo все работает ОС Виндовс 7 ?

tysik:

DesktopRecorderDemo все работает ОС Виндовс 7 ?

Это на сервере? А на клиенте какая ОС и работает ли на нем DesktopRecorderDemo?