Qt5 isAutoRepeat() 函数底层源码分析
调试Qt程序过程中发现一个isAutoRepeat()返回不正常的问题,此方法的作用是用于判断当前按键是否重复按下。现在问题是当键盘一直按着某个按键时,在event处理回调函数中,此方法返回值会发生随机性抖动。决定研究一下这个方法的源码调用过程。1 源码包qt-everywhere-opensource-src-5.5.1 系统ubuntu14.04 虚拟机2 底层事件处理插...
调试Qt程序过程中发现一个isAutoRepeat()返回不正常的问题,此方法的作用是用于判断当前按键是否重复按下。现在问题是当键盘一直按着某个按键时,在event处理回调函数中,此方法返回值会发生随机性抖动。决定研究一下这个方法的源码调用过程。
1 源码包 qt-everywhere-opensource-src-5.5.1 系统ubuntu14.04 虚拟机
2 底层事件处理插件方式:xcb,库版本1.10-2, 库名称libxcb.1.1.0.so,因为用到了X window
3. Qt源码调用关系图
4. 详细调用过程
1) libxcb事件上报。在函数xcb_wait_for_event中,如下:
xcb_generic_event_t *xcb_wait_for_event(xcb_connection_t *c)
{
xcb_generic_event_t *ret;
if(c->has_error)
return 0;
pthread_mutex_lock(&c->iolock);
/* get_event returns 0 on empty list. */
while(!(ret = get_event(c)))
if(!_xcb_conn_wait(c, &c->in.event_cond, 0, 0))
break;
_xcb_in_wake_up_next_reader(c);
pthread_mutex_unlock(&c->iolock);
return ret;
}
先是通过get_event函数查询当前event_list链表中是否有可读事件,如果有,则调出while循环,如果没有,调用_xcb_conn_wait函数去读底层socket设备节点尝试获取,如果是轮询模式,使用的是poll函数,如果是阻塞模式,使用的是select函数。
2)xcb_wait_for_event函数返回后,如果有事件,则调用addEvent函数暂时加入到m_events链表中,然后调用库libxcb.so中的poll_for_next_event函数,如下:
static xcb_generic_event_t *poll_for_next_event(xcb_connection_t *c, int queued)
{
xcb_generic_event_t *ret = 0;
if(!c->has_error)
{
pthread_mutex_lock(&c->iolock);
/* FIXME: follow X meets Z architecture changes. */
ret = get_event(c);
if(!ret && !queued && c->in.reading == 0 && _xcb_in_read(c)) /* _xcb_in_read shuts down the connection on error */
ret = get_event(c);
pthread_mutex_unlock(&c->iolock);
}
return ret;
}
这里同样调用get_event获知当前是否有事件发生,如果没有,则调用_xcb_in_read尝试重新去读一下底层设备节点,然后再一次调用get_event查询一下。此函数返回后,如果有事件发生,同时调用addEvent添加到m_events中,并继续调用,直到本次没有事件可查询为止才继续执行。经过以上的过程分析,其实此函数功能和xcb_wait_for_event作用差不多,都是读取事件,但此函数有一个作用,就是判断按键是否重复按下,类似去抖的意思,后面isAutoRepeat分析会讲到。
3)发送信号 eventPending通知上层来处理事件,信号绑定如下:
connect(this, SIGNAL(eventPending()), m_connection, SLOT(processXcbEvents()), Qt::QueuedConnection);
信号绑定在QXcbEventReader::start()函数中执行。
4) 接着就是事件的处理函数processXcbEvents
void QXcbConnection::processXcbEvents()
{
int connection_error = xcb_connection_has_error(xcb_connection());
if (connection_error) {
qWarning("The X11 connection broke (error %d). Did the X11 server die?", connection_error);
exit(1);
}
QXcbEventArray *eventqueue = m_reader->lock();
for(int i = 0; i < eventqueue->size(); ++i) {
xcb_generic_event_t *event = eventqueue->at(i);
if (!event)
continue;
QScopedPointer<xcb_generic_event_t, QScopedPointerPodDeleter> eventGuard(event);
(*eventqueue)[i] = 0;
uint response_type = event->response_type & ~0x80;
if (!response_type) {
handleXcbError((xcb_generic_error_t *)event);
} else {
if (response_type == XCB_MOTION_NOTIFY) {
// compress multiple motion notify events in a row
// to avoid swamping the event queue
xcb_generic_event_t *next = eventqueue->value(i+1, 0);
if (next && (next->response_type & ~0x80) == XCB_MOTION_NOTIFY)
continue;
}
if (response_type == XCB_CONFIGURE_NOTIFY) {
// compress multiple configure notify events for the same window
bool found = false;
for (int j = i; j < eventqueue->size(); ++j) {
xcb_generic_event_t *other = eventqueue->at(j);
if (other && (other->response_type & ~0x80) == XCB_CONFIGURE_NOTIFY
&& ((xcb_configure_notify_event_t *)other)->event == ((xcb_configure_notify_event_t *)event)->event)
{
found = true;
break;
}
}
if (found)
continue;
}
bool accepted = false;
if (clipboard()->processIncr())
clipboard()->incrTransactionPeeker(event, accepted);
if (accepted)
continue;
QVector<PeekFunc>::iterator it = m_peekFuncs.begin();
while (it != m_peekFuncs.end()) {
// These callbacks return true if the event is what they were
// waiting for, remove them from the list in that case.
if ((*it)(this, event))
it = m_peekFuncs.erase(it);
else
++it;
}
m_reader->unlock();
handleXcbEvent(event);
m_reader->lock();
}
}
eventqueue->clear();
m_reader->unlock();
// Indicate with a null event that the event the callbacks are waiting for
// is not in the queue currently.
Q_FOREACH (PeekFunc f, m_peekFuncs)
f(this, 0);
m_peekFuncs.clear();
xcb_flush(xcb_connection());
}
此函数先将事件从m_events链表中取出,并简单判断一下事件类型,然后对每一个事件调用handleXcbEvent处理,并将事件通过 (*eventqueue)[i] = 0;移除该事件。
void QXcbConnection::handleXcbEvent(xcb_generic_event_t *event)
{
#ifdef Q_XCB_DEBUG
{
QMutexLocker locker(&m_callLogMutex);
int i = 0;
for (; i < m_callLog.size(); ++i)
if (m_callLog.at(i).sequence >= event->sequence)
break;
m_callLog.remove(0, i);
}
#endif
long result = 0;
QAbstractEventDispatcher* dispatcher = QAbstractEventDispatcher::instance();
bool handled = dispatcher && dispatcher->filterNativeEvent(m_nativeInterface->genericEventFilterType(), event, &result);
uint response_type = event->response_type & ~0x80;
if (!handled) {
switch (response_type) {
case XCB_EXPOSE:
HANDLE_PLATFORM_WINDOW_EVENT(xcb_expose_event_t, window, handleExposeEvent);
//......
//ignore something
//......
case XCB_KEY_PRESS:
m_keyboard->updateXKBStateFromCore(((xcb_key_press_event_t *)event)->state);
HANDLE_KEYBOARD_EVENT(xcb_key_press_event_t, handleKeyPressEvent);
case XCB_KEY_RELEASE:
m_keyboard->updateXKBStateFromCore(((xcb_key_release_event_t *)event)->state);
HANDLE_KEYBOARD_EVENT(xcb_key_release_event_t, handleKeyReleaseEvent);
case XCB_MAPPING_NOTIFY:
m_keyboard->handleMappingNotifyEvent((xcb_mapping_notify_event_t *)event);
break;
//......
//ignore something
//......
default:
handled = false;
break;
}
}
//......
//ignore something
//......
函数比较大,这里省略了一些代码,其实主要过程就是判断按键事件类型,按下还是弹起,然后调用后续键盘的处理函数handleKeyEvent,如下:
void QXcbKeyboard::handleKeyEvent(xcb_window_t sourceWindow, QEvent::Type type, xcb_keycode_t code,
quint16 state, xcb_timestamp_t time)
{
Q_XCB_NOOP(connection());
if (!m_config)
return;
QXcbWindow *source = connection()->platformWindowFromId(sourceWindow);
QXcbWindow *targetWindow = connection()->focusWindow() ? connection()->focusWindow() : source;
if (!targetWindow || !source)
return;
if (type == QEvent::KeyPress)
targetWindow->updateNetWmUserTime(time);
xcb_keysym_t sym = xkb_state_key_get_one_sym(xkb_state, code);
QPlatformInputContext *inputContext = QGuiApplicationPrivate::platformIntegration()->inputContext();
QMetaMethod method;
if (inputContext) {
int methodIndex = inputContext->metaObject()->indexOfMethod("x11FilterEvent(uint,uint,uint,bool)");
if (methodIndex != -1)
method = inputContext->metaObject()->method(methodIndex);
}
if (method.isValid()) {
bool retval = false;
method.invoke(inputContext, Qt::DirectConnection,
Q_RETURN_ARG(bool, retval),
Q_ARG(uint, sym),
Q_ARG(uint, code),
Q_ARG(uint, state),
Q_ARG(bool, type == QEvent::KeyPress));
if (retval)
return;
}
QString string = lookupString(xkb_state, code);
// control modifier is set we should prefer latin character, this is
// used for standard shortcuts in checks like "key == QKeySequence::Copy",
// users can still see the actual X11 keysym with QKeyEvent::nativeVirtualKey
Qt::KeyboardModifiers modifiers = translateModifiers(state);
xcb_keysym_t translatedSym = XKB_KEY_NoSymbol;
if (modifiers & Qt::ControlModifier && !isLatin(sym))
translatedSym = lookupLatinKeysym(code);
if (translatedSym == XKB_KEY_NoSymbol)
translatedSym = sym;
int qtcode = keysymToQtKey(translatedSym, modifiers, string);
bool isAutoRepeat = false;
if (type == QEvent::KeyPress) {
if (m_autorepeat_code == code) {
isAutoRepeat = true;
m_autorepeat_code = 0;
}
} else {
// look ahead for auto-repeat
KeyChecker checker(source->xcb_window(), code, time);
xcb_generic_event_t *event = connection()->checkEvent(checker);
if (event) {
isAutoRepeat = true;
free(event);
}
m_autorepeat_code = isAutoRepeat ? code : 0;
}
bool filtered = false;
if (inputContext) {
QKeyEvent event(type, qtcode, modifiers, code, sym, state, string, isAutoRepeat, string.length());
event.setTimestamp(time);
filtered = inputContext->filterEvent(&event);
}
QWindow *window = targetWindow->window();
if (!filtered) {
if (type == QEvent::KeyPress && qtcode == Qt::Key_Menu) {
const QPoint globalPos = window->screen()->handle()->cursor()->pos();
const QPoint pos = window->mapFromGlobal(globalPos);
QWindowSystemInterface::handleContextMenuEvent(window, false, pos, globalPos, modifiers);
}
QWindowSystemInterface::handleExtendedKeyEvent(window, time, type, qtcode, modifiers,code, sym, state, string, isAutoRepeat);
}
if (isAutoRepeat && type == QEvent::KeyRelease) {
// since we removed it from the event queue using checkEvent we need to send the key press here
filtered = false;
if (method.isValid()) {
method.invoke(inputContext, Qt::DirectConnection,
Q_RETURN_ARG(bool, filtered),
Q_ARG(uint, sym),
Q_ARG(uint, code),
Q_ARG(uint, state),
Q_ARG(bool, true));
}
if (!filtered && inputContext) {
QKeyEvent event(QEvent::KeyPress, qtcode, modifiers, code, sym, state, string, isAutoRepeat, string.length());
event.setTimestamp(time);
filtered = inputContext->filterEvent(&event);
}
if (!filtered)
QWindowSystemInterface::handleExtendedKeyEvent(window, time, QEvent::KeyPress, qtcode, modifiers,code, sym, state, string, isAutoRepeat);
}
}
此函数先获取键盘事件,然后判断是否重复按下,最后调用通用Qt系统接口QWindowSystemInterface将键盘事件发送给应用对象,也就是应用中的event处理函数,通过回调方式通知用户去处理。那么这里怎么判断重复按下的呢?从libxcb上来的事件序列都是press,release,press,release....的类型,想要判断是否重复按下,需要通过调用函数checkEvent,该函数如下:
xcb_generic_event_t *QXcbConnection::checkEvent(T &checker)
{
QXcbEventArray *eventqueue = m_reader->lock();
for (int i = 0; i < eventqueue->size(); ++i) {
xcb_generic_event_t *event = eventqueue->at(i);
if (checker.checkEvent(event)) {
(*eventqueue)[i] = 0;
m_reader->unlock();
return event;
}
}
m_reader->unlock();
return 0;
}
通过分析,该函数从m_events判断事件是否有效,有效则返回为真,然后移除该事件。从过程4)中已经知道,当前需要处理的事件,也即eventqueue->at(0)其实是为空的,这样就可以知道这个函数其实是判断第二个事件是否存在,结合上述handleKeyEvent函数的isAutoRepeat判断分支:
bool isAutoRepeat = false;
if (type == QEvent::KeyPress) {
if (m_autorepeat_code == code) {
isAutoRepeat = true;
m_autorepeat_code = 0;
}
} else {
// look ahead for auto-repeat
KeyChecker checker(source->xcb_window(), code, time);
xcb_generic_event_t *event = connection()->checkEvent(checker);
if (event) {
isAutoRepeat = true;
free(event);
}
m_autorepeat_code = isAutoRepeat ? code : 0;
}
可以知道,当前处理事件为按键抬起时,才会去调用checkEvent函数,其实就是判断抬起事件后面是否还有其他按键事件,如果有,则说明按键还在按着,不然不会导致事件“粘连”的效果。至此,isAutoRepeat函数的判断过程分析结束。
5)回到问题,isAutoRepeat()返回值随机抖动,就是在一直按键按下的情况下是会有时候返回true,有时候返回false,通过以上分析以及搭环境跟踪调试,发现原因是libxcb库和Qt的xcb_wait_for_event和poll_for_next_event函数之间没有同步好,导致checkEvent函数没准确判断是否重复按下。修改一下libxcb后重新编译库,问题得到解决。
更多推荐
所有评论(0)