NJMR
NJMR

Reputation: 1926

Facing issue with paint method for QPushButton when used QStyledItemDelegate

I have implemented PushButtonDelegate for a column in QTableView since we are using QSortFilterProxyModel to filter the table view. I am setting this delegate to one of the columns as shown below...

table_->setItemDelegateForColumn(11, new PushButtonDelegate(table_));

The PushButtonDelegate is derived from QStyledItemDelegate and I have overridden createEditor, setEditorData, setModelData, updateEditorGeometry, and paint methods as shown below...

PushButtonDelegate::PushButtonDelegate(QObject* parent) :QStyledItemDelegate(parent)
{
    if (const auto table_view = qobject_cast<QTableView*>(parent))
    {
        my_widget_ = table_view;
        btn_ = new QPushButton(button_text_, my_widget_);
        btn_->hide();
        //my_widget_->setMouseTracking(true);
        connect(my_widget_, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(CellEntered(QModelIndex)));
        is_one_cell_in_edit_mode_ = false;
    }
}
QWidget* PushButtonDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::DisplayRole).toString() == constants::kUse)
    {
        const auto btn = new QPushButton(parent);
        const auto value = index.data().toString();
        btn->setCheckable(true);
        btn->setChecked(value == "Yes" ? true : false);
        btn->setFocusPolicy(Qt::NoFocus);
        connect(btn, SIGNAL(clicked(bool)), this, SLOT(UpdateUseButton(bool)));

        return btn;
    }
    else
    {
        return QStyledItemDelegate::createEditor(parent, option, index);
    }
}

void PushButtonDelegate::UpdateUseButton(bool checked) const
{
    if (const auto selection_button = dynamic_cast<QPushButton*>(sender()))
    {
        const QString value = selection_button->text();
        selection_button->setText(value == "Yes" ? "No" : "Yes");
        selection_button->setChecked(value == "Yes" ? false : true);
        selection_button->setCheckable(true);
    }
}

void PushButtonDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::DisplayRole).toString() == constants::kUse)
    {
        const auto value = index.model()->data(index, Qt::EditRole).toString();
        const auto pb = dynamic_cast<QPushButton*>(editor);
        pb->setChecked(value == "Yes" ? true : false);
        pb->setText(value);
        pb->setCheckable(true);
    }
    else
    {
        QStyledItemDelegate::setEditorData(editor, index);
    }
}

void PushButtonDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::DisplayRole).toString() == constants::kUse)
    {
        const auto pb = dynamic_cast<QPushButton*>(editor);
        const auto value = pb->text();
        pb->setChecked(value == "Yes" ? true : false);
        pb->setCheckable(true);
        const auto ptr = dynamic_cast<QSortFilterProxyModel*>(model);
        ptr->setData(index, value, Qt::EditRole);
    }
    else
    {
        QStyledItemDelegate::setModelData(editor, model, index);
    }
}

void PushButtonDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::DisplayRole).toString() == constants::kUse)
    {
        painter->save();
        btn_->setGeometry(option.rect);
        const auto value = index.data().toString();
        btn_->setCheckable(true);
        btn_->setText(value);
        btn_->setChecked(value == "Yes" ? true : false);

        if (value == "Yes")
            painter->fillRect(option.rect, option.palette.highlight());
        else
            painter->fillRect(option.rect, option.palette.shadow());

        const QPixmap map = btn_->grab();
        painter->drawPixmap(option.rect.x(), option.rect.y(), map);

        painter->restore();
    }
    else
    {
        QStyledItemDelegate::paint(painter, option, index);
    }
}

void PushButtonDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    editor->setGeometry(option.rect);
}

I am facing the following issues...

  1. I have to click on the button 3 times to change the text from "No" to "Yes".
  2. The highlighting of the buttons is not proper as shown (Button should be highlighted when the text is yes)...

enter image description here

Need help...

Upvotes: 3

Views: 463

Answers (1)

linuxfever
linuxfever

Reputation: 3823

The reason for having to click a cell 3 times to change the value is because you need a double click to edit (which creates the editor) and another click to call UpdateUseButton. You don't need to do it this way, you can instead perform the click when you create your editor. In fact, you don't even need the UpdateUseButton as you can make the necessary changes in setModelData. Add the following slot to your PushButtonDelegate:

void commitAndCloseEditor();

and then implement the class like this:

QWidget* PushButtonDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::DisplayRole).toString() == constants::kUse)
    {
        const auto btn = new QPushButton(parent);
        const auto value = index.data().toString();
        btn->setCheckable(true);
        btn->setChecked(value == "Yes" ? true : false);
        btn->setFocusPolicy(Qt::NoFocus);
        
        // You don't need this, you can update your data in setModelData instead
        // connect(btn, SIGNAL(clicked(bool)), this, SLOT(UpdateUseButton(bool)));
        
        // the click will commit the data and close the editor
        // we connect it with a queued connection so that the slot is called after this function exits
        connect(btn, &QPushButton::clicked, this, &PushButtonDelegate::commitAndCloseEditor, Qt::QueuedConnection);
        emit btn->clicked(false); // perform a click programatically to that the commitAndCloseEditor will be called after we exit this function 
        return btn;
    }
    else
    {
        return QStyledItemDelegate::createEditor(parent, option, index);
    }
}

void PushButtonDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::DisplayRole).toString() == constants::kUse)
    {
        //nothing to do when you are in your delegate column
    }
    else
    {
        QStyledItemDelegate::setEditorData(editor, index);
    }
}

void PushButtonDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::DisplayRole).toString() == constants::kUse)
    {
        // set your model data here
        model->setData(index, (index.data().toString() == "Yes") ? "No" : "Yes");
    }
    else
    {
        QStyledItemDelegate::setModelData(editor, model, index);
    }
}

void PushButtonDelegate::commitAndCloseEditor()
{
    auto* editor = qobject_cast<QWidget*>(sender());
    emit commitData(editor);
    emit closeEditor(editor);
}

Notice that I've used a Qt::QueuedConnnection for connecting the clicked signal. This ensures that the commitAndCloseEditor slot will be called after both createEditor and setEditorData have exited (easy to check by using print statements in the beginning of each function). If you remove the queued connection, then the commitAndCloseEditor will be called before createEditor returns the actual editor, hence the slot will do nothing as the editor is not open yet.

Now, for the paint method... it would seem that btn_->setChecked is the reason for not properly showing the background color in the buttons. I'm not sure why it behaves like that, but a workaround would be to use stylesheets like so:

void PushButtonDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    if (index.model()->headerData(index.column(), Qt::Horizontal, Qt::DisplayRole).toString() == constants::kUse)
    {
        painter->save();
        btn_->setGeometry(option.rect);
        const auto value = index.data().toString();
        
        btn_->setText(value);
        
        QString stylesheet = QString("background-color: %1").arg((value == "Yes") ? "green" : "red"  );
        btn_->setStyleSheet(stylesheet);

        const QPixmap map = btn_->grab();
        painter->drawPixmap(option.rect.x(), option.rect.y(), map);

        painter->restore();
    }
    else
    {
        QStyledItemDelegate::paint(painter, option, index);
    }
}

Toggling the value should now work with a double click and the result (on my Windows box) looks like this:

outcome

Upvotes: 2

Related Questions