Adding Button to QTableView

Last week, I have been trying to add a button to a column in a QT TableView, So after 3 days, I finally was able to make it, I thought I would share it saving somebody’s else time.

So here is the idea, we are going to make our own Delegate which is derived from QStyledItemDelegate.

The new Delegate will perform these functionalities:

1- It will have a dummy button , this button will be used for rendering a fake button and paint it in the cells. the QStyleOptionViewItem.rect will be used to set the Geometry of the button every time, and QPixmap::grabWidget(btn) will capture the fake image, then it will be painted using painter->drawPixmap() on the cell, the Left Top corner point will be grabbed from the QStyleOptionViewItem parameter on paint function. A condition is used to make sure that we are working in the correct column by checking the headerData() value stored on the Qt::UserRole, otherwise, we revert to the normal paint() function of the parent class QStyledItemDelegate (this way, other cells painting are not affected).

2- The delegate by default will create an editor when a cell enters the edit mode. This is done through QStyledItemDelegate::createEditor() function. We will reimplement it so that when a user enters the edit mode in our column, the delegate will create a new button (memroy issue!) and show it to the user. This way the highlighting effect of the button and the down effect will be visible as we have a real button now. (this is a better stage than having only a solid fake button)

3- After some trials, I found that the data behind the cell is not preserved, So we have to re-implement the QStyledItemDelegate::setEditorData() and QStyledItemDelegate::setModelData(), so that the value of the column are not lost. I found that we can save the QVariant data safely on the created button

4- Using Signals and Slots, we can get the full effect of the button (hover, button presses) by making our button cell go into edit mode whenever the mouse enters a cell. We will connect the entered() Signal from the QAbstractItemView (parent of our QTableView) to our own slot. In this slot, we will make our button cell goes into the edit mode, and provide some logic so it close the edit mode.

5- All in the above, we have to check each time on weather we are on a Button cell or not to avoid affecting the functionality and appearance of other cells. This is done by assuming that the user will set the model’s Header data to int 1 for the UserRole Section, That is: myModel->setHeaderData(1, Qt::Horizontal, 1, Qt::UserRole);

So, here is the header:

#ifndef BUTTONCOLUMNDELEGATE_H
#define BUTTONCOLUMNDELEGATE_H

#include <QStyledItemDelegate>
#include <QWidget>
#include <QPushButton>
#include <QTableView>

class ButtonColumnDelegate : public QStyledItemDelegate
{
    Q_OBJECT

public:

    explicit ButtonColumnDelegate(QObject *parent = 0);
    ~ButtonColumnDelegate();

    QWidget * createEditor(QWidget *parent,
            const QStyleOptionViewItem &option,
            const QModelIndex &index) const;

    void setEditorData(QWidget *editor,
                                     const QModelIndex &index) const;

    void setModelData(QWidget *editor, QAbstractItemModel *model,
                                    const QModelIndex &index) const;

    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const;

    void updateEditorGeometry(QWidget *editor,
         const QStyleOptionViewItem &option, const QModelIndex &index) const;

public slots:
    void cellEntered(const QModelIndex &index);

private:
    QTableView *myWidget;
    QPushButton *btn;
    bool isOneCellInEditMode;
    QPersistentModelIndex currentEditedCellIndex;
};

#endif // BUTTONCOLUMNDELEGATE_H

And here is the implementation ButtonColumnDelegate.cpp

#include "ButtonColumnDelegate.h"
#include <QPainter>
#include <QPushButton>
#include <QStylePainter>
#include <QDebug>
#include <QTableView>

ButtonColumnDelegate::ButtonColumnDelegate(QObject *parent) :
    QStyledItemDelegate(parent)
{
    if(QTableView *tableView = qobject_cast<QTableView *>(parent))
    {
        myWidget = tableView;
        btn = new QPushButton("...", myWidget);
        btn->hide();
        myWidget->setMouseTracking(true);
        connect(myWidget, SIGNAL(entered(QModelIndex)),
                              this, SLOT(cellEntered(QModelIndex)));
        isOneCellInEditMode = false;
    }
}

ButtonColumnDelegate::~ButtonColumnDelegate()
{

}
//createEditor
QWidget * ButtonColumnDelegate::createEditor(QWidget *parent,
        const QStyleOptionViewItem &option,
        const QModelIndex &index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::UserRole).toInt() == 1) {
        QPushButton * btn = new QPushButton(parent);
        btn->setText(index.data().toString());
        return btn;
    } else {
        return QStyledItemDelegate::createEditor(parent, option, index);
    }
}

//setEditorData
void ButtonColumnDelegate::setEditorData(QWidget *editor,
                                 const QModelIndex &index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::UserRole).toInt() == 1) {
        QPushButton * btn = qobject_cast<QPushButton *>(editor);
        btn->setProperty("data_value", index.data());
    } else {
        QStyledItemDelegate::setEditorData(editor, index);
    }
}

//setModelData
void ButtonColumnDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
                                const QModelIndex &index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::UserRole).toInt() == 1) {
        QPushButton *btn = qobject_cast<QPushButton *>(editor);
        model->setData(index, btn->property("data_value"));
    } else {
        QStyledItemDelegate::setModelData(editor, model, index);
    }
}
//paint
void ButtonColumnDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    if( index.model()->headerData(index.column(), Qt::Horizontal, Qt::UserRole).toInt() == 1 )
    {
        btn->setGeometry(option.rect);
        btn->setText(index.data().toString());
        if (option.state == QStyle::State_Selected)
                     painter->fillRect(option.rect, option.palette.highlight());
        QPixmap map = QPixmap::grabWidget(btn);
        painter->drawPixmap(option.rect.x(),option.rect.y(),map);
    } else {
        QStyledItemDelegate::paint(painter,option, index);
    }
}
//updateGeometry
void ButtonColumnDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    editor->setGeometry(option.rect);
}

//cellEntered
void ButtonColumnDelegate::cellEntered(const QModelIndex &index)
{
    if(index.model()->headerData(index.column(), Qt::Horizontal, Qt::UserRole) == 1)
    {
        if(isOneCellInEditMode)
        {
            myWidget->closePersistentEditor(currentEditedCellIndex);
        }
        myWidget->openPersistentEditor(index);
        isOneCellInEditMode = true;
        currentEditedCellIndex = index;
    } else {
        if(isOneCellInEditMode)
        {
            isOneCellInEditMode = false;
            myWidget->closePersistentEditor(currentEditedCellIndex);
        }
    }
}


To use the delegate in one of QTableView and a Model, you can use:

myModel= new QSqlRelationalTableModel(this);
myModel->setTable("my_sql_table");
myModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
//the header data is used what is used by our delegate to make the columns appear as a button:
myModel->setHeaderData(myModel->fieldIndex("sql_column"), Qt::Horizontal, 1, Qt::UserRole);
//We will make a new instance of our delegate and use it with our table
ButtonColumnDelegate *delegate = new ButtonColumnDelegate(ui->tableView);
ui->tableView->setItemDelegate(delegate);
About these ads
This entry was posted in Uncategorized. Bookmark the permalink.

3 Responses to Adding Button to QTableView

  1. Oggie says:

    Hi,
    I have a table with the result of a select query and I would like insert an update and delete button for every row in that table. I have read your tutorial and following it I did what you say but I can’t insert those buttons.

    Here is my code,
    if (model->query().isSelect())
    {
    //the header data is used what is used by our delegate to make the columns appear as a button:
    model->insertColumns(0, 1);
    model->setHeaderData(0, Qt::Horizontal, “Update”);

    //We will make a new instance of our delegate and use it with our table
    ButtonColumnDelegate *update = new ButtonColumnDelegate(table);
    update->tr(“Update”);

    for (int i = 0; i query().size(); ++i) {
    model->setData(model->index(i, 0), QVariant(“Update”));
    }

    table->setItemDelegate(update);
    }

    table->setModel(model);

    I know this should must insert the string “Update” in the first column of every row but even that fails.

    How can I do what I need?

    Thanks a lot.

  2. Jason B. says:

    Thanks for this example, it worked great for me. There was one small issue I had though. Whenever I moved the mouse over the buttons in the table, the current table index would sometimes change. The behavior seemed almost random, but after running a debugger, I found that the button was grabbing focus when the editor was created on mouse enter and QTableView was changing its index based on the focus of its editors.

    I was able to fix the problem by calling btn->setFocusPolicy(Qt::NoFocus) in the createEditor() function to disallow the button from getting focus.

  3. Luke says:

    I’m confused what your button is actually doing though once it is clicked. Is that logic in the view? I want to open a new dialog window when the button is clicked. How would you do that.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s