4

I have a delegate MyDelegate which is used for QListWidget. The delegate is derived from QStyledItemDelegate. One of the goals of MyDelegate is to place a checkbox button on each row of ListWidget. It is done within the paint() event of MyDelegate:

void MyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QStyledItemDelegate::paint(painter, option, index);
    // ... drawing other delegate elements

    QStyleOptionButton checkbox;
    // setting up checkbox's size and position

    // now draw the checkbox
    QApplication::style()->drawControl(QStyle::CE_CheckBox, &checkbox, painter);
}

At first I thought the checkbox would automatically change its state on click, since I specified QStyle::CE_CheckBox. But it is not the case. Looks like I have to specify the checkbox visual behavior manually.

Data-wise, When user clicks on that checkbox, certain signal is emitted and the scene data is changed. I perform this action in editorEvent():

bool MyDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)  
{
    if (event->type() == QEvent::MouseButtonRelease) {
        if (/* within checkbox area */)
            emit this->clickedCheckbox(index);
    }
}  

The backend part works. However, I cannot figure out how to make the checkbox button to change its visual state from checked to unchecked, and backwards.

I realized that I can change the checkbox state manually by doing something like this from paint():

checkbox.state = someCondition? (QStyle::State_Enabled | QStyle::State_On) :
                           (QStyle::State_Enabled | QStyle::State_Off) ;

QStyle::State_On/Off does the trick of manual checkbox state change.

But I do not know how to set up that someCondition and where I should set it up. I tried to introduce it as a private bool variable which would be set in editorEvent() when the checkbox area gets a click, however, it does not produce the desired behavior: it sets all the other checkboxes of ListWidget to the same visual state. So, it behaved like some global condition for all the checkboxes.

I feel like, to accomplish my task, I have to re-implement the button and make it to change the checkbox state on click. But I'm lost on this way and not sure how to approach the problem. From the QStyleOptionButton API I do not see a clicked() or any other method I could use.

So, the question is: how do I make checkbox to behave like a checkbox, visually? If I need to re-implement a checkbox, then what class do I inherit?

vicrucann
  • 1,703
  • 1
  • 24
  • 34

3 Answers3

6

You can set some value that describes your checkbox state in MyDelegate::editorEvent and then use it to paint a proper checkbox:

const int CHECK_ROLE = Qt::UserRole + 1;

bool MyDelegate::editorEvent(QEvent *event,
                            QAbstractItemModel *model,
                            const QStyleOptionViewItem &option,
                            const QModelIndex &index)
{
    if (event->type() == QEvent::MouseButtonRelease)
    {
        bool value = index.data(CHECK_ROLE).toBool();

        // invert checkbox state
        model->setData(index, !value, CHECK_ROLE);

        return true;
    }

    return QStyledItemDelegate::editorEvent(event, model, option, index);
}

void MyDelegate::paint(QPainter *painter, 
                      const QStyleOptionViewItem &option, 
                      const QModelIndex &index) const
{
    QStyleOptionButton cbOpt;
    cbOpt.rect = option.rect;

    bool isChecked = index.data(CHECK_ROLE).toBool();
    if (isChecked)
    {
        cbOpt.state |= QStyle::State_On;
    }
    else
    {
        cbOpt.state |= QStyle::State_Off;
    }

    QApplication::style()->drawControl(QStyle::CE_CheckBox, &cbOpt, painter);
}
hank
  • 9,553
  • 3
  • 35
  • 50
  • 2
    Perfect, it works. For clarification: how did you come up with the `CHECK_ROLE` variable and why do you assign it to exactly `Qt::UserRole + 1`. Is that some common practice? – vicrucann Apr 22 '16 at 14:48
  • 2
    `QAbstractItemModel` derived models can store data of different roles. Some of these roles are predefined, like `Qt::DisplayRole`. `Qt::UserRole` is the first role which you can use for your special purposes. So, actually `CHECK_ROLE` could be equal to that value. See http://doc.qt.io/qt-4.8/qt.html#ItemDataRole-enum – hank Apr 22 '16 at 15:13
4

I added the following:

  • data(Qt:CheckStateRole) is toggled.
  • enable/disable the item.
  • center the box in the cell.
  • only toggle checkmark with left mouse button, and when checkmark is clicked (not the entire cell).

Here's the code:

// -------------------------------- //
class GFQtCheckboxItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
    const int CHECK_ROLE = Qt::CheckStateRole;

    // dont't let the default QStyledItemDelegate create the true/false combobox
    QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE
    {
        (void)parent;
        (void)option;
        (void)index;
        return nullptr;
    }


    QRect GetCheckboxRect(const QStyleOptionViewItem &option)const
    {
        QStyleOptionButton opt_button;
        opt_button.QStyleOption::operator=(option);
        QRect sz = QApplication::style()->subElementRect(QStyle::SE_ViewItemCheckIndicator, &opt_button);
        QRect r = option.rect;
        // center 'sz' within 'r'
        int dx = (r.width() - sz.width())/2;
        int dy = (r.height()- sz.height())/2;
        r.setTopLeft(r.topLeft() + QPoint(dx,dy));
        r.setWidth(sz.width());
        r.setHeight(sz.height());

        return r;
    }


    // click event
    bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
    {
        if (event->type() == QEvent::MouseButtonRelease)
        {
            QMouseEvent* pME = static_cast<QMouseEvent*>(event);
            if(pME->button() == Qt::LeftButton)
            {
                QRect ro = GetCheckboxRect(option);
                QPoint pte = pME->pos();
                if(ro.contains(pte) )
                {
                    bool value = index.data( CHECK_ROLE).toBool();

                    // invert checkbox state
                    model->setData(index, !value, CHECK_ROLE);

                    return true;
                }
            }
        }

        return QStyledItemDelegate::editorEvent(event, model, option, index);
    }

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
    {
        QStyleOptionButton cbOpt;
        cbOpt.rect = GetCheckboxRect(option);

        bool isChecked = index.data(CHECK_ROLE ).toBool();
        if (isChecked)
        {
            cbOpt.state |= QStyle::State_On;
        }
        else
        {
            cbOpt.state |= QStyle::State_Off;
        }

        QVariant enabled = index.data(Qt::ItemIsEnabled);
        if(enabled.isNull() || enabled.toBool() )
        {
            cbOpt.state |= QStyle::State_Enabled;
        }

        QApplication::style()->drawControl(QStyle::CE_CheckBox, &cbOpt, painter);
    }
};

And for completeness, here's how to apply it:

m_p_QTableView->setItemDelegateForColumn(iTableColumnIndex, new GFQtCheckboxItemDelegate() );
...
bool yesno ...;
pGrid->setData(index, yesno, Qt::CheckStateRole);

KungPhoo
  • 516
  • 4
  • 18
0

This is much easier if you change the QListWidget to a QListModel and a QListView.

Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
{
    return QAbstractListModel::flags(index) | Qt::ItemIsUserCheckable;
}

QVariant ListModel::data(const QModelIndex &index, int role) const
{
    if (role == Qt::EditRole) {
        return m_checkStateForRow[index.row()];
    }
}

bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role == Qt::CheckStateRole) {
        m_checkStateForRow[index.row()] = value.toInt();
        emit dataChanged(index, index, {role});
        return true;
    }
    return false;
}
user240515
  • 3,056
  • 1
  • 27
  • 34