从 Qt 应用程序内部模拟 TAB 或 SPACE 按键

Posted

技术标签:

【中文标题】从 Qt 应用程序内部模拟 TAB 或 SPACE 按键【英文标题】:Simulate TAB or SPACE keypress from the inside of a Qt application 【发布时间】:2018-09-13 12:18:33 【问题描述】:

目的是从第二个应用程序中选择和单击 Qt 应用程序的小部件(就像用户可以使用 TAB 和 SPACE 键击一样)。两个应用程序都在同一个网络上,并通过 QTcpSocket “交谈”,但这不是问题。 因此,2nb 应用程序可以看作是一个 3 个按钮面板(TAB、Shift TAB 和 SPACE),它应该控制第一个应用程序。第一个应用程序也可以像任何其他 Qt 应用程序一样直接使用。 从第一个应用程序内部,收到一条足够的消息后,我尝试发送一个按键事件,但没有任何反应:

void Appli1::onAppli2_TabClickedMessage()

    QKeyEvent keyEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier);
    QCoreApplication::sendEvent((QObject*)appli1.MainWindow(), &keyEvent);

我想这不是正确的做法。我验证了我在调试器中执行了这个插槽,但什么也没发生,就像事件进入无限虚空一样......

感谢任何帮助(方法或想法)

【问题讨论】:

您拥有的代码应该可以工作。唯一看起来很奇怪的是这个演员(QObject*) appli1.MainWindow(),它不应该被需要。此外,您通常不应该在 C++ 中使用 C 风格的强制转换。 我刚刚查看了QCoreApplication::sendEvent()。您的样本看起来正确。你认为MainWindow() 会如何处理这个关键事件?尝试将“可打印”密钥发送到例如QLineEdit。但是,如果没有焦点,它对按键有何反应? (我从未尝试过。)可能是,从QWidget 派生一个带有重载keyPressEvent() 的小部件以将其用作接收器。因此,您可以轻松地确认您发送的关键事件到达。 你不能像那样伪造输入,尤其是对于一个众所周知的输入处理不可靠的 Qt 应用程序。尝试使用 UI 自动化,毕竟这就是它的用途。 @Jaa-c 谢谢...你是对的,它可以工作:-) 我构建了一个小应用程序并且它工作...我只需要插入一个 appli->processEvents()使其可见。主题关闭 为什么不将此作为自我回答发送?顺便提一句。我通过切换到postEvent 解决了您的问题。 【参考方案1】:

IInspectable 提到 OP 的正确方法是使用 UIAutomation。 (当然,这是假设 OP 是在 MS Windows 上开发的,但对于其他 OS / Windows 系统肯定有类似的解决方案。)


出于好奇,我玩了一下。

我的接收器是QLineEdit,它是唯一的小部件——它在启动后获得焦点。

但是,我没有让它工作。我的其他想法:

对称发送QKeyPressQKeyReleaseQKeyEvent 中提供 OP 忽略的 QString 参数

但这没有帮助。

最后,我用postEvent() 替换了sendEvent(),因为我有点担心直接发送事件(除了应用程序事件循环)。令人惊讶的是,这行得通。

testQSendEvent.cc:

#include <QtWidgets>

int main(int argc, char **argv)

  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  QLineEdit qEdt;
  qEdt.show();
  QTimer qTimer;
  qTimer.setInterval(1000/*ms*/);
  qTimer.start();
  int i = 0;
  QObject::connect(&qTimer, &QTimer::timeout,
    [&]() 
      qDebug() << "About to send key event for" << i;
#if 0 // 1st attempt
       QKeyEvent keyEvent(QEvent::KeyPress, Qt::Key_0 + i, Qt::NoModifier,
          QString(QChar('0' + i)));
        QApplication::sendEvent(&qEdt, &keyEvent);
      
       QKeyEvent keyEvent(QEvent::KeyRelease, Qt::Key_0 + i, Qt::NoModifier,
          QString(QChar('0' + i)));
        QApplication::sendEvent(&qEdt, &keyEvent);
      
#else // 2nd attempt
      QApplication::postEvent(&qEdt,
        new QKeyEvent(QEvent::KeyPress, Qt::Key_0 + i, Qt::NoModifier,
          QString(QChar('0' + i))));
      QApplication::postEvent(&qEdt,
        new QKeyEvent(QEvent::KeyRelease, Qt::Key_0 + i, Qt::NoModifier,
          QString(QChar('0' + i))));
#endif // 0
      if (++i >= 10) i = 0;
    );
  return app.exec();

testQSendEvent.pro:

SOURCES = testQSendEvent.cc

QT = widgets

在cygwin64中编译测试:

$ qmake-qt5 testQSendEvent.pro

$ make

$ ./testQSendEvent
Qt Version: 5.9.4
About to send key event for 0
About to send key event for 1
About to send key event for 2
About to send key event for 3
About to send key event for 4
About to send key event for 5
About to send key event for 6
About to send key event for 7
About to send key event for 8
About to send key event for 9
About to send key event for 0
About to send key event for 1
About to send key event for 2
About to send key event for 3
About to send key event for 4
About to send key event for 5


修改后的样本有两个QLineEdits(其中至少一个肯定不是焦点):

#include <QtWidgets>

int main(int argc, char **argv)

  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  QWidget qWin;
  QVBoxLayout qVBox;
  QLineEdit qEdt1, qEdt2;
  qVBox.addWidget(&qEdt1);
  qVBox.addWidget(&qEdt2);
  qWin.setLayout(&qVBox);
  qWin.show();
  QTimer qTimer;
  qTimer.setInterval(1000/*ms*/);
  qTimer.start();
  int i = 0;
  QObject::connect(&qTimer, &QTimer::timeout,
    [&]() 
      QApplication::postEvent(&qEdt1,
        new QKeyEvent(QEvent::KeyPress, Qt::Key_0 + i, Qt::NoModifier,
        QString(QChar('0' + i))));
      QApplication::postEvent(&qEdt1,
        new QKeyEvent(QEvent::KeyRelease, Qt::Key_0 + i, Qt::NoModifier,
        QString(QChar('0' + i))));
      QApplication::postEvent(&qEdt2,
        new QKeyEvent(QEvent::KeyPress, Qt::Key_A + i, Qt::NoModifier,
        QString(QChar('A' + i))));
      QApplication::postEvent(&qEdt2,
        new QKeyEvent(QEvent::KeyRelease, Qt::Key_A + i, Qt::NoModifier,
        QString(QChar('A' + i))));
      if (++i >= 10) i = 0;
    );
  return app.exec();

即使是没有焦点的QLineEdits,如果直接发布给他们,似乎也能正确处理关键事件。 (我不确定这种行为是否取决于 Windows 系统。在我的情况下,它是我在 cygwin 中测试的 X11。)


正如 OP 明确提到的 QPushButtons,我再次调整了我的示例:

#include <QtWidgets>

int main(int argc, char **argv)

  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  QWidget qWin;
  QVBoxLayout qVBox;
  QPushButton qBtn1("Button 1");
  QPushButton qBtn2("Button 2");
  qVBox.addWidget(&qBtn1);
  qVBox.addWidget(&qBtn2);
  qWin.setLayout(&qVBox);
  qWin.show();
  QTimer qTimer;
  qTimer.setInterval(1000/*ms*/);
  qTimer.start();
  int i = 0;
  QObject::connect(&qTimer, &QTimer::timeout,
    [&]() 
      switch (i) 
        case 0:
          QApplication::postEvent(&qBtn1,
            new QKeyEvent(QEvent::KeyPress, Qt::Key_Space, Qt::NoModifier,
              QString(QChar(' '))));
          break;
        case 1:
          QApplication::postEvent(&qBtn1,
            new QKeyEvent(QEvent::KeyRelease, Qt::Key_Space, Qt::NoModifier,
              QString(QChar(' '))));
          break;
        case 2:
#if 0 // EXCLUDED
          QApplication::postEvent(&qBtn1,
            new QKeyEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier,
              QString(QChar('\t'))));
          QApplication::postEvent(&qBtn1,
            new QKeyEvent(QEvent::KeyRelease, Qt::Key_Tab, Qt::NoModifier,
              QString(QChar('\t'))));
#endif // 0
          break;
        case 3:
          QApplication::postEvent(&qBtn2,
            new QKeyEvent(QEvent::KeyPress, Qt::Key_Space, Qt::NoModifier,
              QString(QChar(' '))));
          break;
        case 4:
          QApplication::postEvent(&qBtn2,
            new QKeyEvent(QEvent::KeyRelease, Qt::Key_Space, Qt::NoModifier,
              QString(QChar(' '))));
          break;
        case 5:
#if 0 // EXCLUDED
          QApplication::postEvent(&qBtn2,
            new QKeyEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier,
              QString(QChar('\t'))));
          QApplication::postEvent(&qBtn2,
            new QKeyEvent(QEvent::KeyRelease, Qt::Key_Tab, Qt::NoModifier,
              QString(QChar('\t'))));
#endif // 0
          break;
      
      if (++i > 5) i = 0;
    );
  QObject::connect(&qBtn1, &QPushButton::clicked,
    [&](bool)  qDebug() << "Button 1 clicked"; );
  QObject::connect(&qBtn2, &QPushButton::clicked,
    [&](bool)  qDebug() << "Button 2 clicked"; );
  return app.exec();

甚至发出了QPushButton::clicked 信号:

$ ./testQSendEvent
Qt Version: 5.9.4
Button 1 clicked
Button 2 clicked
Button 1 clicked
Button 2 clicked
Button 1 clicked
Button 2 clicked
Button 1 clicked

我试过改变焦点和不改变焦点。 (在我EXCLUDED Tab 事件之前,我可以看到焦点发生了变化。)但是,以及在QLineEdit 中,Space 键事件在@987654354 中处理@ 独立于焦点。

最后一个示例代码版本,我在 VS2013 中使用 Qt 5.9.2(用于原生 WinAPI)再次编译。它的行为完全类似于我在 cygwin 中使用 g++ 和 Qt 5.9.4 for X11 编译的那个。 IE。发送 Space 键事件到QPushButton 改变了它的视觉外观,QPushButton::clicked 信号被正确发出。


我“意外地”观察到应该正确设置QKeyEvent 的第四个QString 参数。我发帖的时候发现了这个

 QKeyEvent(QEvent::KeyPress, Qt::Key_A + i, Qt::NoModifier,
   QString(QChar('0' + i)));

接收QLineEdit插入的数字显然忽略了Qt::Key_A + i

【讨论】:

我有几个小部件,我使用 TAB 按键事件从一个小部件移动到下一个小部件(并且 shift TAB 以相反的顺序进行)但是 ... SPACE 按键不会激活按钮 :-( 这可能与QPushButton 是否具有焦点有关。默认按钮也可能起作用。 @gnogno35 我再次扩展了我的答案——这次是按钮。它对我有用。您使用的是什么操作系统和窗口系统? 正如我已经指出的,这不是您自动化 UI 的方式。使用UI Automation,这是它的预期目的之一。 @IInspectable 我注意到了您的评论(甚至想在我的回答中提到它,但忘记了)。这更像是玩弄探索 Qt 如何做和不做的结果。顺便提一句。 gnogno35 确认UIAutomation 可能会成为她/他的首选。【参考方案2】:

模拟按键似乎没有必要。接收器处理三个命令:

如果主窗口处于非活动状态,则将其激活,如果没有小部件具有焦点,则第一个获得它的小部件显式获得焦点,并忽略该命令。

NEXT_FOCUS:将焦点移至焦点链中的下一项(与 Tab 一样)

PREVIOUS_FOCUS:将焦点移至焦点链中的前一项(就像 Shift-Tab 一样)

CLICK:调用焦点小部件的 click() 方法 - Qt 的所有按钮都支持它,因为它在它们的基础 QAbstractButton 类中。

因此,像下面这样的东西应该可以解决问题,并且您不需要模拟键盘 - 特别是根本不能保证您提到的特定键具有您期望的效果 - 行为是特定于平台的,甚至可能是特定于主题的。例如。在 OS X 上,本机行为是空格键无效,因为按钮不可聚焦。

enum Command 
   NEXT_FOCUS,      // focus next clickable widget
   PREVIOUS_FOCUS,  // focus previous clickable widget
   FOCUS,           // focus current clickable widget, or next one if none
   CLICK            // like FOCUS, but also clicks the widget
;

bool executeCommand(QWidget *window, Command command) 
   Q_ASSERT(window && window->isWindow());
   using Method = bool (QWidget::*)(bool);
   struct Helper : QWidget 
      static Method get_focusNextPrevChild()  return &Helper::focusNextPrevChild; 
   ;
   static Method const focusNextPrevChild = Helper::get_focusNextPrevChild();

   if (!window->isActiveWindow()) 
      window->activateWindow();
      command = FOCUS;
   

   for (QWidget *focused = nullptr;;) 
      if (command == NEXT_FOCUS) 
         if (!(window->*focusNextPrevChild)(true)) return false;
       else if (command == PREVIOUS_FOCUS) 
         if (!(window->*focusNextPrevChild)(false)) return false;
      
      auto *newFocused = window->focusWidget();
      if (!newFocused) return false;
      if (focused && focused == newFocused)
         return false;  // no eligible widgets found
      else if (!focused)
         focused = newFocused;
      auto *newMetaObject = newFocused->metaObject();
      int const clickIdx = newMetaObject->indexOfMethod("click()");
      if (clickIdx >= 0)
         return command == FOCUS || newMetaObject->method(clickIdx).invoke(focused);
      if (command != NEXT_FOCUS && command != PREVIOUS_FOCUS)
         command = NEXT_FOCUS;  // iterate over widgets till a clickable one is found
   

【讨论】:

非常有趣的方法......我会试一试。空格键行为可以解决:-) @ Kuba Ober Tested : 我添加了一个可见与否的测试(真正的 HMI 由几个面板组成,其中只有一个是可见的)。对“可点击”的测试应该没问题,但不存在......也许使用 whatThis 属性可以解决。无论如何,主要问题是焦点按钮周围没有“装饰”。不知道为什么... @ Kuba Ober ...一旦明确关注一个小部件,它就可以工作

以上是关于从 Qt 应用程序内部模拟 TAB 或 SPACE 按键的主要内容,如果未能解决你的问题,请参考以下文章

QT模拟win系统键盘输出,模拟组合快捷键输出(仅限windows系统)

QT模拟win系统键盘输出,模拟组合快捷键输出(仅限windows系统)

Qt Key Event 的行为与键盘不同

如何使用 width: auto 使选项卡的宽度随文本内部的变化而变化在 Qt5 中的 QTabBar::tab 中?

Qt - 使用 QTransform(或类似的),将内部 QRect 缩放到 QGraphics/从 QGraphics

将事件发布到 Qt GUI 线程以模拟按键