Как я могу имитировать щелчки мыши, отправляя события в систему событий Qt?

Я хотел бы провести элементарный тест автоматизации моего приложения Qt. Он записывает события мыши и записывает их в файл (например, mousepress(300, 400)). При запуске автоматизации он считывает координаты из файла, отправляет соответствующие события мыши и выполняет сравнение пикселей с ранее сохраненным скриншотом.

В настоящее время у меня есть оверлейный виджет, который охватывает приложение и имеет прозрачные события мыши. Все, что он делает, это отслеживает координаты. При обратном считывании данных это наложение рисует прямоугольник в месте нажатия мыши. Мне нужна помощь при отправке mousePressEvents в систему событий Qt. Он рисует точки в правильном месте, но никогда не делает физический щелчок. Есть ли способ сделать это с помощью Qt или мне придется использовать Window SendInput()?

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

Widget::Widget( QWidget *parent )
: QFrame( parent )
, mPoint(QPoint(0,0))
{
   setWindowFlags(Qt::WindowStaysOnTopHint);
   setStyleSheet("background-color: rgba(0, 0,255, 2%);");
   setAttribute(Qt::WA_TransparentForMouseEvents, true);
   setGeometry(parent->geometry());
   ...
}

void Widget::run()
{
   QFile file( "coordinates.txt", NULL );
   if(!file.open( QIODevice::ReadOnly ))
       return;

   QTextStream in(&file);
   int i = 0;
   while (!in.atEnd())
   {
       QString line = in.readLine();
       if(line.startsWith("mousepress"))
       {
          int startIndex = line.indexOf('(');
          int endIndex = line.indexOf(')');

          QString coord = line.mid(startIndex+1, line.size() - startIndex - 2);
          QStringList nbr = coord.split(',');
          mPoint = QPoint(nbr[0].toInt(), nbr[1].toInt());
          QWidget *receiver  = QApplication::widgetAt(mPoint);
          QMouseEvent *event = new QMouseEvent(QEvent::MouseButtonPress, mPoint, Qt::LeftButton, Qt::LeftButton,  Qt::NoModifier);
          QCoreApplication::postEvent(receiver, event); // same result with sendEvent() 
          QCoreApplication::processEvents();
          update();
          // wait till the event finished, then continue with next point
      }
   }
}


void Widget::paintEvent(QPaintEvent *event)
{
  QPainter p( this );
  QPen pen;
  pen.setBrush(Qt::NoBrush);

  if(!mPoint.isNull())
  {
    pen.setColor(Qt::red);
    pen.setWidth( 2 );
    p.setPen(pen);

    p.drawRoundRect(mPoint.x(), mPoint.y(), 10, 10, 25, 25);
    p.drawText(mPoint, QString::number(mPoint.x()) + ", " + QString::number(mPoint.y()));
  }
}

[Отредактировано]

Я последовал совету ddriver, и он работает после нескольких изменений: я сохраняю глобальные и локальные позиции в файле для отправки в QMouseEvent.

Как я могу быть уверен, что щелчок мышью завершен, прежде чем делать снимок экрана и сравнивать его с сохраненным изображением?

void Widget::DoStep()
{
  if(!mInStream.atEnd())
  {
      QString line = mInStream.readLine();
      if(line.startsWith("MouseButtonPress"))
      {
          QPoint pos = parseGlobalPos();
          QPoint localPos = parseLocalPos();
          QWidget *receiver  = QApplication::widgetAt(pos);

          QMouseEvent *event = new QMouseEvent(QEvent::MouseButtonPress,localPos, pos, Qt::LeftButton, Qt::LeftButton,  Qt::NoModifier);
          QApplication::postEvent(receiver, event);
          QMouseEvent *eventRelease = new QMouseEvent(QEvent::MouseButtonRelease, localPos, pos, Qt::LeftButton, Qt::LeftButton,  Qt::NoModifier);
          QApplication::postEvent(receiver, eventRelease);

          // after successful click, take screenshot and compare them
      } 
   }

  if (!mInStream.atEnd())
      QMetaObject::invokeMethod(this, "DoStep", Qt::QueuedConnection);
  else
      file.close();
}

person user2246120    schedule 03.03.2016    source источник
comment
Это может не решить эту проблему полностью, однако вы отправляете событие MouseButtonPress, но вам не хватает соответствующего MouseButtonRelease. Кроме того, я предлагаю не зацикливаться и не ждать завершения события, а вместо этого использовать одиночный QTimer для каждого события и позволить основному четному циклу продолжать работать, не прибегая к вызову processEvents.   -  person TheDarkKnight    schedule 03.03.2016
comment
спасибо, я попытался опубликовать дополнительное событие MouseButtonRelease, но это не сработало.   -  person user2246120    schedule 03.03.2016
comment
Я не совсем уверен, что понимаю вашу идею QTimer. QTimer::singleShot(0, приемник, SlotSendEvent()); Невозможно передать координаты в слот. Что вы имели в виду?   -  person user2246120    schedule 03.03.2016
comment
Это если вы создаете объект QTimer и подключаетесь к лямбда-функции. Еще лучше, если вы используете Qt 5.4, вы можете сделать это с помощью статический вызов: QTimer::singleShot(1000, [=]() { PostEvent(evt); } );   -  person TheDarkKnight    schedule 04.03.2016
comment
Я получил тот же результат и никакого фактического нажатия мыши в приложении. Любые другие идеи?   -  person user2246120    schedule 06.03.2016


Ответы (2)


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

Решение состоит в том, чтобы не использовать блокирующий цикл while, а реализовать цикл, управляемый системой событий. Затем вы обрабатываете одну строку в каждом цикле цикла событий, пока не закончите работу.

Переместите файл, поток и все такое за пределы функции, сделайте их членами класса, чтобы они сохранялись. Затем в run() вы просто настраиваете ввод для чтения и перемещаете все материалы для публикации событий в новую функцию, например doStep(), а в run() после настройки у вас есть:

QMetaObject::invokeMethod(this, "doStep", Qt::QueuedConnection);

В doStep(), помимо событий, в конце вы также выполняете планирование:

if (!in.atEnd()) QMetaObject::invokeMethod(this, "doStep", Qt::QueuedConnection);
else // we are done, close file, cleanup, whatever

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

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

person dtech    schedule 09.03.2016
comment
это сработало: но как я могу быть уверен, что щелчок мыши завершен, прежде чем делать снимок экрана и сравнивать его с сохраненным изображением? см. мой отредактированный вопрос - person user2246120; 11.03.2016
comment
@ user2246120 - вы можете выполнить еще один вызов в очереди - doStep() запланирует takeScreenshot(), а takeScreenshot() запланирует следующее doStep(). Таким образом, все события будут обрабатываться перед каждым запланированным вызовом. - person dtech; 11.03.2016
comment
Другой вариант — запланировать создание снимка экрана из обработчика события клика получателя. Непонятно, что именно вы подразумеваете под завершением клика. Я предполагаю, что это рисунок положения щелчка. Так что вы можете поместить его и после кода рисования. - person dtech; 11.03.2016
comment
Я не совсем понимаю последовательность событий: могу ли я быть уверен, что объект-получатель обрабатывает событие, которое было отправлено через: сначала QApplication::postEvent(receiver, eventRelease), до обработки QMetaObject::invokeMethod(this, "screenshot", Qt::QueuedConnection);? Если они оба отправляются в одну и ту же очередь и обрабатываются по порядку, тогда да: это работает! - person user2246120; 11.03.2016
comment
@user2246120 user2246120 - дело в том, что когда вы используете соединения в очереди, вызов ставится в очередь для следующего цикла цикла событий, тогда как входные события отправляются немедленно. Таким образом, событие выпуска будет обработано немедленно, в то время как событие вызова будет поставлено в очередь для следующего цикла цикла событий, поэтому оно не может быть обработано до события выпуска. И если очередь идет шаг-›скриншот-›шаг, то они будут выполняться также в правильном порядке, так как они всегда будут запланированы на следующий цикл событийного цикла, а их работа выполняется блокирующим образом, поэтому завершиться до этого. - person dtech; 11.03.2016
comment
ваше объяснение имеет смысл. Проблема в том, что некоторые клики приводят к зависанию пользовательского интерфейса на секунду или около того. например нажимаем на кнопку-›обработка->открывается панель инструментов. В этом случае снимок экрана уже сделан до того, как панель инструментов опустится. Я мог бы исправить это, добавив QTimer::singleShot(2000, this, SLOT(SlotTakeScreenshot()));, но я хотел бы понять, почему это происходит. - person user2246120; 12.03.2016
comment
@user2246120 user2246120 - без кода и без информации о том, что происходит при нажатии, я тоже не могу понять, почему это происходит. Я не экстрасенс, знаете ли ;) Если вам подходит таймер, дерзайте. - person dtech; 12.03.2016

Попробуйте отправить событие в область просмотра QGraphicsView:

qApp->sendEvent(view->viewport(), &mousePressEvent);
person viddik13    schedule 03.03.2016
comment
Это не отвечает и даже не пытается ответить на вопрос. - person dtech; 10.03.2016