从 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
,它是唯一的小部件——它在启动后获得焦点。
但是,我没有让它工作。我的其他想法:
对称发送QKeyPress
和QKeyRelease
在QKeyEvent
中提供 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
修改后的样本有两个QLineEdit
s(其中至少一个肯定不是焦点):
#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();
即使是没有焦点的QLineEdit
s,如果直接发布给他们,似乎也能正确处理关键事件。 (我不确定这种行为是否取决于 Windows 系统。在我的情况下,它是我在 cygwin 中测试的 X11。)
正如 OP 明确提到的 QPushButton
s,我再次调整了我的示例:
#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系统)
如何使用 width: auto 使选项卡的宽度随文本内部的变化而变化在 Qt5 中的 QTabBar::tab 中?
Qt - 使用 QTransform(或类似的),将内部 QRect 缩放到 QGraphics/从 QGraphics