Search code examples
qtqwidgetqtablewidgetqpushbutton

QTableWidget setCellWidget(QWidget*) Inconsistant Behavior with Cell Selection and Focus


I've made a subclassed QTableWidget and wanted to make some cells have QPushButtons as cell widgets since I've made a pretty heavily styled button using QPropertyAnimations and what not and really wanted to embed the widget in the cell. Well I've used the setCellWidget(QWidget* widget) function that is a part of the QTableWidget and it was almost perfect. Since I do some subclassed QStyledItemDelegate class drawing on my table's cell items, I draw some border lines that seemed to be conflicting a bit with the size of the cell widget.

Previously someone asked how to center a QCheckBox in a QTableWidget's cell so it's not offset on the left hand side. The answer to that question was essentially this:

  1. Create a QWidget
  2. Create the QWidget you want to center
  3. Create a QH/VLayout and set the top level QWidget's layout to be this
  4. Add the sub widget that you want to center to this layout
  5. On the QTableWidget use the setCellWidget function and set it for the row and column with the top level QWidget and voila, you have a centered QWidget in the cell that you can manipulate and align however you want

Great, that visually worked... However, I noticed some nasty side effects of this. The arrow key navigation seems to break and I can no longer press the Enter key or Space key to "click/press" the QPushButton that I embedded in the managing QWidget for that particular cell. It seems that changing the focus messes something up internally on the QTableWidget when trying to move away from the cell with the arrows keys on the keyboard.

I read online that some people said to disable the setTabKeyNavigation(bool) on the table to fix some similar navigation issues after setting focus... This did not do anything in my case. I've made a minimal compilable example that show cases the behavior

TableWidget.h:

#ifndef TABLEWIDGET_H
#define TABLEWIDGET_H

#include "button.h"
#include "widget.h"

#include <QTableWidget>
#include <QDebug>

class TableWidget : public QTableWidget{
    Q_OBJECT

public:
    TableWidget(QWidget* parent = nullptr);
    Widget *createWidget();
    Button *createButton();
    void setTableCell(QWidget *selecteditem);
};

#endif // TABLEWIDGET_H

TableWidget.cpp:

#include "tablewidget.h"

TableWidget::TableWidget(QWidget* parent) : QTableWidget(parent){
    setFixedSize(750, 500);

    setColumnCount(5);

    for(int i = 0; i < 5; ++i){
        insertRow(rowCount());
        for(int j = 0; j < columnCount(); ++j){
            QTableWidgetItem* item = new QTableWidgetItem;
            item->setFlags(item->flags() ^ Qt::ItemIsEditable);
            setItem(j, i, item);
        }
    }

    setCellWidget(0, 0, createButton());
    setCellWidget(2, 0, createWidget());
}

Button* TableWidget::createButton(){
    Button* button = new Button;
    connect(button, &Button::focusReceived, this, [this, button](){ setTableCell(button); }, Qt::DirectConnection);
    return button;
}

Widget* TableWidget::createWidget(){
    Button* button = new Button;
    connect(button, &Button::focusReceived, this, [this, button](){ setTableCell(button); }, Qt::DirectConnection);

    return new Widget(button);
}

//Helper to make keyboard focus more intuitive for cell widgets versus regular items
void TableWidget::setTableCell(QWidget* selecteditem){
    //Find the sender in the table
    for(int row = 0; row < rowCount(); ++row){
        for(int col = 0; col < columnCount(); ++col){
            if(cellWidget(row, col) == selecteditem){
                qDebug() << "TableWidget::setTableCell";
                setCurrentCell(row, col);
                setCurrentItem(this->item(row, col));
                setCurrentIndex(this->indexFromItem(this->item(row, col)));
                return;
            }
        }
    }
}

Button.h:

#ifndef BUTTON_H
#define BUTTON_H

#include <QPushButton>
#include <QFocusEvent>
#include <QDebug>

class Button : public QPushButton{
    Q_OBJECT
public:
    Button(QWidget *parent = nullptr);

    void focusIn(QFocusEvent *event);
signals:
    void focusReceived();
public slots:
    bool event(QEvent* e);
};

#endif // BUTTON_H

Button.cpp:

#include "button.h"

Button::Button(QWidget* parent) : QPushButton(parent){
    setStyleSheet(QString("background-color: solid rgba(255, 0, 0, 75);"));
}

bool Button::event(QEvent* event){
    switch(event->type()){
        case QEvent::FocusIn:
            focusIn(static_cast<QFocusEvent*>(event));
            return true;
            break;
        default:
            break;
    }

    return QWidget::event(event);
}

void Button::focusIn(QFocusEvent* event){
    qDebug() << "Button::focusIn";
    emit focusReceived();
    QPushButton::focusInEvent(event);
}

Widget.h:

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QFocusEvent>
#include <QHBoxLayout>
#include <QPointer>
#include "button.h"

class Widget : public QWidget{
    Q_OBJECT
public:
    Widget(Button* button, QWidget *parent = nullptr);

public slots:
    bool event(QEvent* event);
    void focusIn(QFocusEvent *event);

signals:
    void focusReceived();

protected:
    QPointer<QHBoxLayout> m_hLayout;
    QPointer<Button>      m_button;
};

#endif // WIDGET_H

Widget.cpp:

#include "widget.h"

Widget::Widget(Button* button, QWidget* parent) : QWidget(parent){
    m_button = button;
    setStyleSheet(QString("background-color: solid rgba(0, 0, 0, 0);"));
    m_hLayout = new QHBoxLayout;
    setLayout(m_hLayout);
    m_hLayout->addWidget(m_button);
}

bool Widget::event(QEvent* event){
    switch(event->type()){
        case QEvent::FocusIn:
            focusIn(static_cast<QFocusEvent*>(event));
            return true;
            break;
        default:
            break;
    }
    return QWidget::event(event);
}

void Widget::focusIn(QFocusEvent* event){
    qDebug() << "Widget::focusIn";
    emit focusReceived();
    QWidget::focusInEvent(event);
}

So, when navigating the TableWidget you would expect to be able to see the highlighted cells to move with the cursor and "temporarily" give soft Focus to the widget for keyboard events. This happens ONLY on the Button object. The Button object will correctly print that in the focusIn function that it went off when the cell selected contains it. However, you would expect the same behavior to occur for the Widget too since its added exactly the same way with exactly the same code, just with a Button embedded inside of its QHBoxLayout. Once you navigate to the Button or the Widget the keyboard navigation for the TableWidget seems to break and key presses don't forward from the Widget to its child Button even if I were to set the setFocusProxy on the Widget to its m_button field which is the exact same type Button that can correctly get the KeyEvent. I'm not quite sure if this is a bug or if I've mangled some behavior.


Solution

  • Well, it turns out this behavior is caused specifically by the QAbstractButton::keyPressEvent(QKeyEvent *e) function. So, when the buttons are added to the cell widget of the TableWidget, I believe the table explicitly becomes their parent widget. This makes sense for memory management and what not, okay.

    So, the QAbstractButtons have a case statement for the Qt::Key_Down and Qt::Key_Up keys and they essentially check if their parent widget is of the type QAbstractItemView*. If they do, they call the QAbstractButtonPrivate::move(int key) function. This will determine after querying a list of all buttons a part of the button's buttonGroup (something that I'm not sure we have any control over), which button should be next. This apparently in a QAbstractItemView, which a QTableWidget inherits from, orders them in column major order such then if there's a button in the next row or the next column, the column will instead be chosen over the next row if the Qt::Key_Down is caught. This explains the behavior I was receiving. Why they make this the base behavior beats me, because I find it very strange that I can't alter this behavior since it's a part of the QAbstractButtonPrivate class which we all obviously have no control over. Luckily it's virtual and we can override the behavior.

    So the solution is to implement the Button::keyPressEvent(QPressEvent *e) and override in each case of Qt::Key_Down and Qt::Key_Up to emit a signal that will be captured by the Table and return. Do not break and call the base class implementation for either of these cases. When emitting, emit an identifier, much like the QAbstractButtonPrivate::move(int key) function does to the table and set the selected cell/current item in a slot your add to the Table to be the next item/widget to the right/left or up/down as you would expect with the normal key behavior of QTableWidgetItems. This seemed to work and I could finally get focus of the buttons embedded in the Widget with correct selection behavior.