IT Notes

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

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

  1. Удаленное управление компьютером по сети: Введение
  2. Удаленное управление компьютером по сети: Формирование видео-потока
  3. Удаленное управление компьютером по сети: Наблюдение

Реализацию удаленного управления компьютером по сети начнем с контроля мышью. Задача состоит из трех частей:

  1. Научиться отслеживать события мыши на клиенте;
  2. Обеспечить передачу событий мыши по сети;
  3. Воспроизвести полученные события мыши на стороне сервера.

Рассмотрим каждый из этих пунктов отдельно.

Отслеживание событий мыши в Qt

Любой виджет в Qt может реализовать обработчики событий мыши. Нам понадобятся следующие четыре:

// Событие перемещения мыши
void mouseMoveEvent( QMouseEvent* e );

// Событие нажатия кнопки мыши
void mousePressEvent( QMouseEvent* e );

// Событие отпускания кнопки мыши
void mouseReleaseEvent( QMouseEvent* e );

// Событие прокручивания колесика мыши
void wheelEvent( QWheelEvent* e );

Единственная сложность на этом шаге: организовать преобразование координат мыши из "клиентских" в "реальные" для сервера. При передаче кадра сервер передает не только байты изображения, но и "реальное" разрешение экрана. Сохраним его в виде поля нашего класса RemoteControlClientWidget:

void RemoteControlClientWidget::onFrameAvailable( const QByteArray& imgData, const QSize& realSize ) {
    m_realFrameSize = realSize;

    // Дальше без изменений
}

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

QPoint RemoteControlClientWidget::toRealPos( const QPoint& mousePos ) {
    auto realPos = ui->viewLbl->mapFromParent( mousePos );

    if( ui->viewLbl->pixmap() ) {
        auto viewRect = ui->viewLbl->rect();
        auto imgRect =  ui->viewLbl->pixmap()->rect();

        auto xOffset = ( viewRect.width() - imgRect.width() ) / 2;
        auto yOffset = ( viewRect.height() - imgRect.height() ) / 2;

        realPos = QPoint( realPos.x() - xOffset,  realPos.y() - yOffset );

        if( realPos.x() < 0 )  realPos.setX( 0 );
        if( realPos.y() < 0 )  realPos.setY( 0 );

        if( imgRect.width() != 0 ) {
            auto ratio = m_realFrameSize.width() / static_cast< double >( imgRect.width() );
            realPos.setX( realPos.x() * ratio );
            realPos.setY( realPos.y() * ratio );

            if( m_realFrameSize.width() < realPos.x() )  realPos.setX( m_realFrameSize.width() );
            if( m_realFrameSize.height() < realPos.y() )  realPos.setY( m_realFrameSize.height() );
        }
    }

    return realPos;
}

Функция вышла довольно простой. Обратим внимание лишь на ее первую строку:

auto realPos = ui->viewLbl->mapFromParent( mousePos );

Так мы переходим от системы координат всего виджета к системе координат ui->viewLbl, на котором выводятся кадры видео-потока.

Чтобы перемещения мыши отслеживались, необходимо не забыть включить эту возможность явно. Добавим следующий вызов в конструктор:

setMouseTracking( true );
ui->viewLbl->setMouseTracking( true );

Теперь осталось лишь добавить реализацию обработчиков событий мыши:

void RemoteControlClientWidget::mouseMoveEvent( QMouseEvent* e ) {
    if( m_connected ) {
        auto pos = toRealPos( e->pos() );
        m_mouseMoved = ( m_mousePos != pos );
        m_mousePos = pos;
    }
}

void RemoteControlClientWidget::mousePressEvent( QMouseEvent* e ) {
    if( m_connected ) {
        m_mousePos = toRealPos( e->pos() );
        m_peer.call( MOUSE_PRESS_SIG, m_mousePos, e->button() );
    }
}

void RemoteControlClientWidget::mouseReleaseEvent( QMouseEvent* e ) {
    if( m_connected ) {
        m_mousePos = toRealPos( e->pos() );
        m_peer.call( MOUSE_RELEASE_SIG, m_mousePos, e->button() );
    }
}

void RemoteControlClientWidget::wheelEvent( QWheelEvent* e ) {
    if( m_connected ) {
        m_peer.call( MOUSE_WHEEL_SIG, e->delta() );
    }
}

На этом месте мы плавно переходим к следующему пункту реализации.

Передача событий мыши по сети

Начнем с событий нажатия/отпускания кнопок мыши, а также события прокручивания колесика. Мы хотим обеспечить максимально быстрый отклик сервера, поэтому передаем их через наше Qxt-соединения сразу же.

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

С событием перемещения курсора мыши дела обстоят чуть сложнее. Нет нужды перенаправлять каждое движение мыши, поскольку за секунду их может быть весьма не мало. Достаточно фиксировать последнюю позицию курсора, и отправлять ее на сервер только время от времени по таймеру.

Для этого в RemoteControlClientWidget мы завели поле m_mousePos типа QPoint, в котором и фиксируем последнюю координату. Осталось подготовить обработчик события таймера:

void RemoteControlClientWidget::onMouseMoveTimeOut() {
    if( m_mouseMoved ) {
        m_peer.call( MOUSE_MOVE_SIG, m_mousePos );
        m_mouseMoved = false;
    }
}

Дополнительной оптимизацией служит то, что если позиция мыши не меняется, то и на сервер ничего не отправляется.

Воспроизведение событий мыши на стороне сервера

Qt легко позволяет перемещать курсор мыши. С обработки этого действия и начнем:

void RemoteControlServer::onMouseMoveRequest( quint64, const QPoint& pos ) {
    QCursor::setPos( pos );
}

Теперь с клиента уже можно двигать курсором. Но останавливаться рано - осталось еще много других событий. Но Qt нам уже не поможет. Придется пользоваться специфическими API конкретных платформ.

Эмуляция событий мыши в Linux

При решении поставленной задачи в Linux удобно воспользоваться возможностями библиотеки XTest:

void onMouseAction( const QPoint& pos, Bool pressed, uint mouseBtn ) {
    QCursor::setPos( pos );

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

    uint btn = 0;
    switch( mouseBtn ) {
    case Qt::LeftButton:
        btn = Button1;
        break;
    case Qt::RightButton:
        btn = Button3;
        break;
    case Qt::MiddleButton:
        btn = Button2;
        break;
    }

    XTestFakeButtonEvent( display, btn, pressed, CurrentTime );

    XFlush( display );
    XCloseDisplay( display );
}

void RemoteControlServer::onMousePressRequest( quint64, const QPoint& pos, int mouseBtn ) {
    onMouseAction( pos, True, mouseBtn );
}

void RemoteControlServer::onMouseReleaseRequest( quint64, const QPoint& pos, int mouseBtn ) {
    onMouseAction( pos, False, mouseBtn );
}

void RemoteControlServer::onMouseWheelRequest( quint64, int delta ) {
    Display* display = XOpenDisplay( nullptr );
    if( display == nullptr ) {
        return;
    }

    auto btn = ( 0 <= delta ) ? Button4 : Button5;

    XTestFakeButtonEvent( display, btn, True, 0 );
    XTestFakeButtonEvent( display, btn, False, 0 );

    XFlush( display );
    XCloseDisplay( display );
}

Особенность Linux-кода в том, что XTest не позволяет указать фактическую величину дельта-смещения колесика мыши. К тому же, движение вперед-назад интерпретируются как события для разных кнопок (Button4 и Button5).

Эмуляция событий мыши в Windows

С помощью функции SendInput() из Win32 API задача решается не сложнее, чем в Linux:

void onMouseAction( const QPoint& pos, int flags ) {
    QCursor::setPos( pos );

    INPUT input;
    input.type = INPUT_MOUSE;
    input.mi.mouseData = 0;
    input.mi.dwFlags = flags;
    SendInput( 1, &input, sizeof( input ) );
}

void RemoteControlServer::onMousePressRequest( quint64, const QPoint& pos, int mouseBtn ) {
    int flags = MOUSEEVENTF_ABSOLUTE;
    switch ( mouseBtn ) {
    case Qt::LeftButton:
        flags |= MOUSEEVENTF_LEFTDOWN;
        break;
    case Qt::RightButton:
        flags |= MOUSEEVENTF_RIGHTDOWN;
        break;
    case Qt::MiddleButton:
        flags |= MOUSEEVENTF_MIDDLEDOWN;
        break;
    default:
        return;
    }
    onMouseAction( pos, flags );
}

void RemoteControlServer::onMouseReleaseRequest( quint64, const QPoint& pos, int mouseBtn ) {
    int flags = MOUSEEVENTF_ABSOLUTE;
    switch ( mouseBtn ) {
    case Qt::LeftButton:
        flags |= MOUSEEVENTF_LEFTUP;
        break;
    case Qt::RightButton:
        flags |= MOUSEEVENTF_RIGHTUP;
        break;
    case Qt::MiddleButton:
        flags |= MOUSEEVENTF_MIDDLEUP;
        break;
    default:
        return;
    }
    onMouseAction( pos, flags );
}

void RemoteControlServer::onMouseWheelRequest( quint64, int delta ) {
    INPUT input;
    input.type = INPUT_MOUSE;
    input.mi.mouseData = delta;
    input.mi.dwFlags = MOUSEEVENTF_WHEEL;
    SendInput( 1, &input, sizeof( input ) );
}

Выводы

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

Исходники

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

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

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

Понравилась статья?
Не забудь поделиться ей с друзьями!

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

Комментарии

RSS RSS-рассылка

Популярное

Дешевый хостинг