Почему нажатие клавиши Tab генерирует только событие QEvent::ShortcutOverride?

Задний план

Я сделал собственный виджет с QLineEdit и несколькими QPushButtons, чтобы использовать его с делегатом пользовательского элемента:

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);
    }
};

Я собираюсь связать их с QListView и QStandardItemModel вот так:

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();

Вопрос

Почему в PropertyDelegate::eventFilter при нажатии клавиши Tab происходит только событие QEvent::ShortcutOverride, а нажатие любой другой клавиши генерирует и события QEvent::ShortcutOverride, и QEvent::KeyPress?

UPD: Хочу реализовать перемещение между строками по нажатию Tab и Backtab как в стандартных виджетах.


person fasked    schedule 27.08.2012    source источник


Ответы (1)


Наконец-то я провел небольшое исследование по этому поводу.

Объяснение

Когда представление вызывает createEditor функцию делегата, оно также устанавливает фильтр событий делегата в редактор.

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);

    ......
}

Однако делегат может перехватывать только события виджета редактора, но не события его дочерних элементов. Когда нажата клавиша Tab, вызывается функция QWidget::event, она использует ее для переключения фокуса на другой виджет:

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;
        }

        ......
    }

    ......
}

Соответственно, в моем случае фокус установлен на следующий QPushButton после QLineEdit, и событие не распространяется на родителя (LineEditor).

Решение

Правильный способ решения проблемы — это как QSpinBox. Потому что он также имеет QLineEdit. В конструкторе виджета он устанавливает фокус прокси для редактирования строки:

edit->setFocusProxy(this);

Таким образом, все события фокуса будут достигать основного виджета. Также должно быть установлено свойство focusPolicy, потому что по умолчанию оно равно NoFocus:

setFocusPolicy(Qt::WheelFocus);

Все, что нам нужно сделать в этот момент, это распространить необходимые события на QLineEdit из главного виджета следующим образом:

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);
}

Этого должно быть достаточно.

Сложный

Как было сказано выше, делегат не может перехватывать события потомков редактора. Поэтому, чтобы сделать поведение редактора «родным», я должен дублировать события от дочерних элементов к редактору.

LineEditor устанавливает фильтр событий в QLineEdit в конструкторе:

edit->installEventFilter(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);
}

Также можно использовать qApp->notify(this, event) для QKeyEvent вместо QApplication::postEvent, потому что мы все равно фильтруем эти события. Но это невозможно для QFocusEvent, потому что notify перенаправит событие, и оно не достигнет дочернего элемента.

Обратите внимание, что стандартный (QItemDelegate или QStyledItemDelegate) делегат сам по себе будет заботиться о ситуации, когда фокус изменяется внутри:

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();
            }

    ......
person fasked    schedule 15.06.2013