Эмуляция нажатия мультимедийных клавиш в Windows, Linux и Mac OS X


    В перечислении Qt::Key для события QKeyEvent определены 15 типов клавиш управления мультимедиа (см. таблицу в конце статьи). Все они могут быть использованы в фильтре событий (installEventFilter) для обработки нажатия клавиш на мультимедийной клавиатуре (позволяющей управлять аудиоустройством и воспроизведением).
    В статье рассмотрена обратная задача — отправка в систему команд управления мультимедиа путем эмуляции нажатия соответствующих клавиш в Windows, Linux и MacOSX (операционные системы упорядочены по времени, потраченному на поиск решения). Представленный в статье материал может быть отправной точкой к дальнейшему изучению вопроса кроссплатформенной отправки событий в цикл обработки системных сообщений.

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

    Так как QEvent позволяет отправлять сообщение только конкретному объекту «внутри» приложения, то стандартными средствами Qt эмулировать нажатия клавиш не получится. Для этого будем использовать системные вызовы Api (в случае с Windows) или соответствующие библиотеки (X Window System в Linux и ряд фреймворков в Mac OS X).
    Для удобства описания почти всю реализацию отправки сообщений разместим в функции sendKeyEventToSystem(Qt::Key qtKey), которой передается код клавиши из перечисления Qt::Key. Данная функция и будет вызываться из слотов, например:
    void playPauseToogle() {  //слот переключения между воспроизведением и паузой
    	postKeyEventToSystem(Qt::Key_MediaTogglePlayPause);
    }
    
    Платформенно-зависимый код будем отделять директивами #ifdef OS_TYPE и #endif (еще часть кода на Objective-C вынесем в отдельный файл macx.mm, но об этом позже).

    Эмуляция нажатия клавиатуры в Windows


    В данной операционной системе за отправку сообщений отвечает функция SendInput. Она позволяет отправлять сообщения с кодами, полный перечень которых представлен на странице MSDN Virtual-Key Codes.
    Для использования этой функции необходимо подключить заголовочный файл <Windows.h>.
    #ifdef Q_OS_WIN32
        #define WINVER 0x0500
        #include <Windows.h>
    #endif
    

    Примеров использования данной функции в интернете много и ее применение не должно вызвать проблем, поэтому сразу привожу код (в части Windows):
    sendKeyEventToSystem(Qt::Key qtKey) { //функция эмуляции нажатия клавиши. qtKey - тип клавиши
    #ifdef Q_OS_WIN32
        INPUT ip; //устройство ввода
        ip.type = INPUT_KEYBOARD;
        ip.ki.wScan = 0;
        ip.ki.time = 0;
        ip.ki.dwExtraInfo = 0;
    //в зависимости от типа клавиши
        switch (qtKey) {
        case Qt::Key_MediaPrevious:
            ip.ki.wVk = VK_MEDIA_PREV_TRACK; //предыдущий трек
            break;
        case Qt::Key_MediaTogglePlayPause:
            ip.ki.wVk = VK_MEDIA_PLAY_PAUSE; //для переключения режима воспроизведения
            break;
        case Qt::Key_MediaNext:
            ip.ki.wVk = VK_MEDIA_NEXT_TRACK; //следующий трек
            break;
        default:
            return;
            break;
        }
    	//посылаем событие нажатия клавиши
        ip.ki.dwFlags = 0;
        SendInput(1, &ip, sizeof(INPUT));
        //а затем отжатия клавиши
        ip.ki.dwFlags = KEYEVENTF_KEYUP;
        SendInput(1, &ip, sizeof(INPUT));
    #endif
    }
    
    Здесь и далее в примерах будут использоваться всего 3 клавиши. В конце статьи приведена таблица соответствия кодов.

    Эмуляция нажатия клавиатуры в Linux


    Для эмуляции клавиш в Linux, на мой взгляд, проще всего использовать библиотеку разработчиков libXtst (X11 Record extension library).
    Для ее получения из пакетов необходимо выполнить команду:
    sudo apt-get install libxtst-dev
    
    Также будет необходимо подключить библиотеку в файле проекта:
    unix:!macx:LIBS += -lXtst -lX11
    

    В начале файла подключим необходимые заголовочные файлы и определим ряд констант, соответствующих кодам мультимедийных клавиш (дело в том, что в файле X11/keysymdef.h коды для мультимедийных клавиш отсутствуют).
    #ifdef Q_OS_LINUX
        #include <X11/Xlib.h>
        #include <X11/extensions/XTest.h>
        #define XF86AudioLowerVolume   0x1008ff11
        #define XF86AudioMute                0x1008ff12 
        #define XF86AudioRaiseVolume   0x1008ff13
        #define XF86AudioPlay               0x1008ff14
        #define XF86AudioStop               0x1008ff15
        #define XF86AudioPrev                0x1008ff16
        #define XF86AudioNext                0x1008ff17
        #define XF86AudioPause            0x1008ff31
    #endif
    

    Код эмуляции в части Linux:
    #ifdef Q_OS_LINUX
        unsigned int key;
        unsigned int keycode;
        switch (qtKey) {
        case Qt::Key_MediaPrevious:
            key = XF86AudioPrev;
            break;
        case Qt::Key_MediaTogglePlayPause:
            key = XF86AudioPlay;
            break;
        case Qt::Key_MediaNext:
            key = XF86AudioNext;
            break;
        default:
            return;
            break;
        }
        // подключаемся к X
        Display *display;
        display = XOpenDisplay(NULL);
        // получаем код клавиши
        keycode = XKeysymToKeycode(display, key);
        // эмулируем нажатие клавиши
        XTestFakeKeyEvent(display, keycode, 1, 0);
        // эмулируем отжатие клавиши
        XTestFakeKeyEvent(display, keycode, 0, 0);
        // очищаем буфер X
        XFlush(display);
        // отключаемся от X
        XCloseDisplay(display);
    #endif
    

    Эмуляция нажатия клавиатуры в Mac OS X


    Попытки найти решение для MacOS X не приносили плодов (скупые примеры были написаны на Objective-C), до тех пор, пока в гугле не натолкнулся на статью с хабра Интеграция приложений Qt в среду Mac OS X (с использованием Cocoa и Objective-C++). Мне уже ранее попадалась англоязычная статья, в которой описывалось, как изолировать код C++ для использования в Objective-C приложении. Мне же нужно было совершенно противоположное — изолировать Objective-C код (который выполнял нужные мне функции, но в то же время на который ругался компиллятор). Все оказалось достаточно просто:
    1. создал файл macx.mm и разместил в нем Objective-С код (при этом в файле проекта автоматически появилась строка
    OBJECTIVE_SOURCES += macx.mm 

    2. создал файл macx.h и разместил в нем объявление функции из macx.mm (добавив #include «macx.h» в macx.mm).
    3. в файле проекта добавил подключение необходимых фреймворков, в частности:
    macx:LIBS += -framework ApplicationServices -framework IOKit

    4. Внутри макроса условной компиляции для Mac OS X добавил необходимые хидеры и macx.h.
    5. В уже знакомой вам switch-case структуре вставил вызовы новоиспеченной функции.

    Таким образом в начале файла у нас появилась конструкция для Mac OS X:
    #ifdef Q_OS_MAC
        #include <ApplicationServices/ApplicationServices.h> //оставил для типа UInt8
        #include <IOKit/hidsystem/ev_keymap.h> //коды клавиш
        #include "mac.h" //определение функции для вызова Objective-C кода
    #endif
    

    В функцию sendKeyEventToSystem добавляется следующий код:
    #ifdef Q_OS_MAC
        switch (qtKey) {
        case Qt::Key_MediaPrevious:
            HIDPostAuxKey( NX_KEYTYPE_PREVIOUS );
            break;
        case Qt::Key_MediaTogglePlayPause:
            HIDPostAuxKey( NX_KEYTYPE_PLAY );
            break;
        case Qt::Key_MediaNext:
            HIDPostAuxKey( NX_KEYTYPE_NEXT );
            break;
        default:
            return;
            break;
        }
    #endif
    


    Cодержимое файла mac.mm:
    #import <Cocoa/Cocoa.h>
    #import <IOKit/hidsystem/IOHIDLib.h>
    #import <IOKit/hidsystem/ev_keymap.h>
    #include "macx.h"
    
    static io_connect_t get_event_driver(void)
    {
        static  mach_port_t sEventDrvrRef = 0;
        mach_port_t masterPort, service, iter;
        kern_return_t    kr;
        if (!sEventDrvrRef)
        {
            // Get master device port
            kr = IOMasterPort( bootstrap_port, &masterPort );
            check( KERN_SUCCESS == kr);
    
            kr = IOServiceGetMatchingServices( masterPort, IOServiceMatching( kIOHIDSystemClass ), &iter );
            check( KERN_SUCCESS == kr);
    
            service = IOIteratorNext( iter );
            check( service );
    
            kr = IOServiceOpen( service, mach_task_self(),
                                kIOHIDParamConnectType, &sEventDrvrRef );
            check( KERN_SUCCESS == kr );
    
            IOObjectRelease( service );
            IOObjectRelease( iter );
        }
        return sEventDrvrRef;
    }
    
    void HIDPostAuxKey(const UInt8 auxKeyCode )
    {
      NXEventData   event;
      kern_return_t kr;
      IOGPoint      loc = { 0, 0 };
    
      // Сообщение о нажатии клавиши
      UInt32      evtInfo = auxKeyCode << 16 | NX_KEYDOWN << 8;
      bzero(&event, sizeof(NXEventData));
      event.compound.subType = NX_SUBTYPE_AUX_CONTROL_BUTTONS;
      event.compound.misc.L[0] = evtInfo;
      kr = IOHIDPostEvent( get_event_driver(), NX_SYSDEFINED, loc, &event, kNXEventDataVersion, 0, FALSE );
      check( KERN_SUCCESS == kr );
    
      // Сообщение о отжатии клавиши
      evtInfo = auxKeyCode << 16 | NX_KEYUP << 8;
      bzero(&event, sizeof(NXEventData));
      event.compound.subType = NX_SUBTYPE_AUX_CONTROL_BUTTONS;
      event.compound.misc.L[0] = evtInfo;
      kr = IOHIDPostEvent( get_event_driver(), NX_SYSDEFINED, loc, &event, kNXEventDataVersion, 0, FALSE );
      check( KERN_SUCCESS == kr );
    }
    

    Заключение:

    Для меня было немного странно, что до сих пор не существует открыто доступной кроссплатформенной библиотеки, позволяющей выполнять отправку сообщений (в том числе события клавиатуры, мыши и д.р.). Во всяком случае мне такую библиотеку так и не получилось найти. Представленный код далек от совершенства (представляю, как разрастется switch-case последовательность при добавлении новых клавиш). Но тем не менее пусть это будет небольшой вклад в общую копилку базы знаний о написании кросплатформенных приложений.
    В ходе работы над статьей было замечено, что VirtualBox перехватывает нажатия мультимедийных клавиш (проверялось на Ubuntu — c «железа» все работало). Данного недостатка лишена WMWare (проверялось на Mac OS X).

    Приложение: Перечень мультимедийных клавиш и их определений (с помощью #define).

    Qt::Key Windows Linux Mac OS X
    Qt::Key_VolumeDown VK_VOLUME_DOWN XF86AudioLowerVolume NX_KEYTYPE_SOUND_DOWN
    Qt::Key_VolumeMute VK_VOLUME_MUTE XF86AudioMute NX_KEYTYPE_MUTE
    Qt::Key_VolumeUp VK_VOLUME_UP XF86AudioRaiseVolume NX_KEYTYPE_SOUND_UP
    Qt::Key_BassBoost
    Qt::Key_BassUp
    Qt::Key_BassDown
    Qt::Key_TrebleUp
    Qt::Key_TrebleDown
    Qt::Key_MediaPlay VK_MEDIA_PLAY_PAUSE XF86AudioPlay NX_KEYTYPE_PLAY
    Qt::Key_MediaStop VK_MEDIA_STOP XF86AudioStop
    Qt::Key_MediaPrevious VK_MEDIA_PREV_TRACK XF86AudioPrev NX_KEYTYPE_PREVIOUS
    Qt::Key_MediaNext VK_MEDIA_NEXT_TRACK XF86AudioNext NX_KEYTYPE_NEXT
    Qt::Key_MediaRecord
    Qt::Key_MediaPause XF86AudioPause
    Qt::Key_MediaTogglePlayPause VK_MEDIA_PLAY_PAUSE XF86AudioPlay NX_KEYTYPE_PLAY

    Ссылки по теме:
    1. Существующие подходы к решению проблемы: C++ (Qt) cross-platform library for simulating keyboard input, sendkeys, send kestrokes, etc
    2. Описание функции SendInput и перечень кодов клавиатуры на сайте MSDN.
    3. Simulating Mediakey Presses in C & X11 — в статье описывается, как в Linux с помощью утилиты «xev» (в Ubuntu: «sudo apt-get install x11-utils») можно узнать коды клавиш, а также в дополнение статья с archlinux.org.
    4. Эмуляция мультимедийных клавиш в MacOS X на языке программирования Python.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 0

    Only users with full accounts can post comments. Log in, please.