Why pressing of “Tab” key emits only QEvent::Short

2019-02-07 03:17发布

问题:

Background

Hi guys. I've made a custom widget with QLineEdit and several QPushButtons to use it with custom item delegate:

class LineEditor : public QWidget
{
public:
    explicit LineEditor(QWidget *parent = 0) : QWidget(parent) {
        setLayout(new QHBoxLayout);

        layout()->setContentsMargins(0, 0, 0, 0);
        layout()->setSpacing(0);

        QLineEdit *edit = new QLineEdit(this);
        layout()->addWidget(edit);
        layout()->addWidget(new QPushButton(this));
        layout()->addWidget(new QPushButton(this));

        setFocusProxy(edit);
    }
};

class PropertyDelegate : public QItemDelegate
{
public:
    QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
        return new LineEditor(parent);
    }

    bool eventFilter(QObject *object, QEvent *event) {
        if (event->type() == QEvent::KeyPress) {
            qDebug() << "KeyPress";
        }
        if (event->type() == QEvent::ShortcutOverride) {
            qDebug() << "ShortcutOverride";
        }

        return QItemDelegate::eventFilter(object, event);
    }
};

I'm going to bind them with QListView and QStandardItemModel like this:

QStandardItemModel *model = new QStandardItemModel;
model->appendRow(new QStandardItem("1"));
model->appendRow(new QStandardItem("2"));
model->appendRow(new QStandardItem("3"));

QListView w;
w.setItemDelegate(new PropertyDelegate);
w.setModel(model);
w.show();

Question

Why in the PropertyDelegate::eventFilter when Tab key is pressed there is only QEvent::ShortcutOverride event, but pressing of any other key emits both QEvent::ShortcutOverride and QEvent::KeyPress events?

UPD: I want to implement the moving between lines by pressing Tab and Backtab like with standard widgets.

回答1:

Well, finally I've done some research about that.

Explanation

When a view calls createEditor function of delegate it also installs the delegate event filter to editor.

QWidget *QAbstractItemViewPrivate::editor(const QModelIndex &index,
                                          const QStyleOptionViewItem &options)
{
    Q_Q(QAbstractItemView);
    QWidget *w = editorForIndex(index).widget.data();
    if (!w) {
        QAbstractItemDelegate *delegate = delegateForIndex(index);
        if (!delegate)
            return 0;
        w = delegate->createEditor(viewport, options, index);
        if (w) {
            w->installEventFilter(delegate);

    ......
}

However the delegate can catch only events of editor widget, but not events of its children. When the Tab key is pressed the QWidget::event function is called, it uses it to change focus to another widget:

bool QWidget::event(QEvent *event)
{
    ......

    switch (event->type()) {
        ......
        case QEvent::KeyPress: {
        QKeyEvent *k = (QKeyEvent *)event;
        bool res = false;
        if (!(k->modifiers() & (Qt::ControlModifier | Qt::AltModifier))) {  //### Add MetaModifier?
            if (k->key() == Qt::Key_Backtab
                || (k->key() == Qt::Key_Tab && (k->modifiers() & Qt::ShiftModifier)))
                res = focusNextPrevChild(false);
            else if (k->key() == Qt::Key_Tab)
                res = focusNextPrevChild(true);
            if (res)
                break;
        }

        ......
    }

    ......
}

Accordingly in my case the focus is set to next QPushButton after QLineEdit and event isn't propagated to the parent (LineEditor).

Solving

The right way to solve the problem is like QSpinBox does it. Because it is also has QLineEdit. In constructor of widget it sets focus proxy for line edit:

edit->setFocusProxy(this);

So all of focus events will reach the main widget. Also the focusPolicy property must be set because it's NoFocus by default:

setFocusPolicy(Qt::WheelFocus);

All we need to do in this moment it is to propagate necessary events to QLineEdit from main widget like this:

bool LineEditor::event(QEvent *e)
{
    switch(e->type())
    {
    case QEvent::ShortcutOverride:
        if(m_lineEdit->event(e))
            return true;
        break;

    case QEvent::InputMethod:
        return m_lineEdit->event(e);

    default:
        break;
    }

    return QWidget::event(e);
}

void LineEditor::keyPressEvent(QKeyEvent *e)
{
    m_lineEdit->event(e);
}

void LineEditor::mousePressEvent(QMouseEvent *e)
{
    if(e->button() != Qt::LeftButton)
        return;

    e->ignore();
}

void LineEditor::mouseReleaseEvent(QMouseEvent *e)
{
    e->accept();
}

void LineEditor::focusInEvent(QFocusEvent *e)
{
    m_lineEdit->event(e);
    QWidget::focusInEvent(e);
}

void LineEditor::focusOutEvent(QFocusEvent *e)
{
    m_lineEdit->event(e);
    QWidget::focusOutEvent(e);
}

This should be enough.

Tricky

As it's said above the delegate can't catch events of editor's children. So to make editor's behavior like "native" I have to duplicate events from children to editor.

LineEditor installs event filter to QLineEdit in constructor:

edit->installEventFilter(this);

Implementation of filter looks like this:

bool LineEditor::eventFilter(QObject *object, QEvent *event)
{
    if(event->type() == QEvent::KeyPress)
    {
        QKeyEvent* keyEvent = static_cast<QKeyEvent *>(event);
        if(keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab)
        {
            QApplication::postEvent(this, new QKeyEvent(keyEvent->type(), keyEvent->key(), keyEvent->modifiers()));
            // Filter this event because the editor will be closed anyway
            return true;
        }
    }
    else if(event->type() == QEvent::FocusOut)
    {
        QFocusEvent* focusEvent = static_cast<QFocusEvent *>(event);
        QApplication::postEvent(this, new QFocusEvent(focusEvent->type(), focusEvent->reason()));

        // Don't filter because focus can be changed internally in editor
        return false;
    }

    return QWidget::eventFilter(object, event);
}

It's also possible to use qApp->notify(this, event) for QKeyEvent instead of QApplication::postEvent because we filter this events anyway. But it's not possible for QFocusEvent because notify will redirect the event and it will not reach a child.

Note that the standard (QItemDelegate or QStyledItemDelegate) delegate will care about situation when focus is changed internally by itself:

if (event->type() == QEvent::FocusOut || (event->type() == QEvent::Hide && editor->isWindow())) {
        //the Hide event will take care of he editors that are in fact complete dialogs
        if (!editor->isActiveWindow() || (QApplication::focusWidget() != editor)) {
            QWidget *w = QApplication::focusWidget();
            while (w) { // don't worry about focus changes internally in the editor
                if (w == editor)
                    return false;
                w = w->parentWidget();
            }

    ......