调试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后重新编译库,问题得到解决。

 

 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐