Center a QCheckBox or Decoration in an Itemview

From Qt Wiki
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

En Ar Bg De El Es Fa Fi Fr Hi Hu It Ja Kn Ko Ms Nl Pl Pt Ru Sq Th Tr Uk Zh

Overview

Currently Qt does not support centering a QCheckBox or the itemview decoration out-of-the box. There are two possibilities to achieve this behaviour: a custom item delegate derived from QStyledItemDelegate or a custom QProxyStyle.

Derive from QStyledItemDelegate

The proposed solution works when the checkbox or decoration should be aligned. As alignment indicator the Qt::TextAlignmentRole is 'misused' since no text is drawn for the cell.

class MyDelegate : public QStyledItemDelegate
{
public:
    using QStyledItemDelegate::QStyledItemDelegate;
    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        QStyleOptionViewItem opt = option;
        const QWidget *widget = option.widget;
        initStyleOption(&opt, index);
        QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
        style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, widget);
        if (opt.features & QStyleOptionViewItem::HasCheckIndicator) {
            switch (opt.checkState) {
            case Qt::Unchecked:
                opt.state |= QStyle::State_Off;
                break;
            case Qt::PartiallyChecked:
                opt.state |= QStyle::State_NoChange;
                break;
            case Qt::Checked:
                opt.state |= QStyle::State_On;
                break;
            }
            auto rect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &opt, widget);
            opt.rect = QStyle::alignedRect(opt.direction, index.data(Qt::TextAlignmentRole).value<Qt::Alignment>(), rect.size(), opt.rect);
            opt.state = opt.state & ~QStyle::State_HasFocus;
            style->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &opt, painter, widget);
        } else if (!opt.icon.isNull()) {
            // draw the icon
            QRect iconRect = style->subElementRect(QStyle::SE_ItemViewItemDecoration, &opt, widget);
            iconRect = QStyle::alignedRect(opt.direction, index.data(Qt::TextAlignmentRole).value<Qt::Alignment>(), iconRect.size(), opt.rect);
            QIcon::Mode mode = QIcon::Normal;
            if (!(opt.state & QStyle::State_Enabled))
                mode = QIcon::Disabled;
            else if (opt.state & QStyle::State_Selected)
                mode = QIcon::Selected;
            QIcon::State state = opt.state & QStyle::State_Open ? QIcon::On : QIcon::Off;
            opt.icon.paint(painter, iconRect, opt.decorationAlignment, mode, state);
        } else {
            QStyledItemDelegate::paint(painter, option, index);
        }
    }
protected:
    bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
    {
        Q_ASSERT(event);
        Q_ASSERT(model);
        // make sure that the item is checkable
        Qt::ItemFlags flags = model->flags(index);
        if (!(flags & Qt::ItemIsUserCheckable) || !(option.state & QStyle::State_Enabled) || !(flags & Qt::ItemIsEnabled))
            return false;
        // make sure that we have a check state
        QVariant value = index.data(Qt::CheckStateRole);
        if (!value.isValid())
            return false;
        const QWidget *widget = option.widget;
        QStyle *style = option.widget ? widget->style() : QApplication::style();
        // make sure that we have the right event type
        if ((event->type() == QEvent::MouseButtonRelease) || (event->type() == QEvent::MouseButtonDblClick) || (event->type() == QEvent::MouseButtonPress)) {
            QStyleOptionViewItem viewOpt(option);
            initStyleOption(&viewOpt, index);
            QRect checkRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &viewOpt, widget);
            checkRect = QStyle::alignedRect(viewOpt.direction, index.data(Qt::TextAlignmentRole).value<Qt::Alignment>(), checkRect.size(), viewOpt.rect);
            QMouseEvent *me = static_cast<QMouseEvent *>(event);
            if (me->button() != Qt::LeftButton || !checkRect.contains(me->pos()))
                return false;
            if ((event->type() == QEvent::MouseButtonPress) || (event->type() == QEvent::MouseButtonDblClick))
                return true;
        } else if (event->type() == QEvent::KeyPress) {
            if (static_cast<QKeyEvent *>(event)->key() != Qt::Key_Space && static_cast<QKeyEvent *>(event)->key() != Qt::Key_Select)
                return false;
        } else {
            return false;
        }
        Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
        if (flags & Qt::ItemIsUserTristate)
            state = ((Qt::CheckState)((state + 1) % 3));
        else
            state = (state == Qt::Checked) ? Qt::Unchecked : Qt::Checked;
        return model->setData(index, state, Qt::CheckStateRole);
    }
};
 

The delegate can be added directly to the desired view:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QStandardItemModel model;
    model.setColumnCount(1);
    model.setRowCount(2);

    QPixmap pix(20, 20);
    pix.fill(Qt::blue);

    auto checkableItem = new QStandardItem;
    checkableItem->setFlags(checkableItem->flags() | Qt::ItemIsUserCheckable);
    checkableItem->setTextAlignment(Qt::AlignCenter);
    checkableItem->setCheckState(Qt::Checked);
    model.setItem(0, 0, checkableItem);

    checkableItem = new QStandardItem;
    checkableItem->setFlags(checkableItem->flags() | Qt::ItemIsUserCheckable);
    checkableItem->setTextAlignment(Qt::AlignCenter);
    checkableItem->setIcon(pix);
    model.setItem(1, 0, checkableItem);

    QTableView tv;
    tv.setModel(&model);
    tv.setItemDelegate(new MyDelegate);
    // tv.setItemDelegateForColumn(0, new MyDelegate); // when only a single column should use the delegate
    tv.show();
    return a.exec();
}
 

Use a custom style derived from QProxyStyle

Another alternative is to create a custom QProxyStyle. The alignment of the checkbox and decoration are handled by custom roles (CheckAlignmentRole, DecorationAlignmentRole) in this case to not get in conflict with the text alignment and to have an indicator which cell should have the special checkbox handling.

enum { 
    CheckAlignmentRole = Qt::UserRole + Qt::CheckStateRole + Qt::TextAlignmentRole, 
    DecorationAlignmentRole = Qt::UserRole + Qt::DecorationRole + Qt::TextAlignmentRole 
};

class CenteredBoxProxyStyle : public QProxyStyle
{
public:
    using QProxyStyle::QProxyStyle;
    QRect subElementRect(QStyle::SubElement element, const QStyleOption *option, const QWidget *widget) const override
    {
        const QRect baseRes = QProxyStyle::subElementRect(element, option, widget);
        switch (element) {
        case SE_ItemViewItemCheckIndicator: {
            const QStyleOptionViewItem *const itemOpt = qstyleoption_cast<const QStyleOptionViewItem *>(option);
            const QVariant alignData = itemOpt ? itemOpt->index.data(CheckAlignmentRole) : QVariant();
            if (!alignData.isNull())
                return QStyle::alignedRect(itemOpt->direction, alignData.value<Qt::Alignment>(), baseRes.size(), itemOpt->rect);
            break;
        }
        case SE_ItemViewItemDecoration: {
            const QStyleOptionViewItem *const itemOpt = qstyleoption_cast<const QStyleOptionViewItem *>(option);
            const QVariant alignData = itemOpt ? itemOpt->index.data(DecorationAlignmentRole) : QVariant();
            if (!alignData.isNull())
                return QStyle::alignedRect(itemOpt->direction, alignData.value<Qt::Alignment>(), baseRes.size(), itemOpt->rect);
            break;
        }
        case SE_ItemViewItemFocusRect: {
            const QStyleOptionViewItem *const itemOpt = qstyleoption_cast<const QStyleOptionViewItem *>(option);
            const QVariant checkAlignData = itemOpt ? itemOpt->index.data(CheckAlignmentRole) : QVariant();
            const QVariant decorationAlignData = itemOpt ? itemOpt->index.data(DecorationAlignmentRole) : QVariant();
            if (!checkAlignData.isNull() || !decorationAlignData.isNull()) // when it is not null, then the focus rect should be drawn over the complete cell
                return option->rect;
            break;
        }
        default:
            break;
        }
        return baseRes;
    }
};
 

The proxy style can either set for the complete application or directly for the desired view:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    CenteredBoxProxyStyle style;
    // a.setStyle(&style); // when it should be set for the complete application

    QStandardItemModel model;
    model.setColumnCount(1);
    model.setRowCount(2);

    QPixmap pix(20, 20);
    pix.fill(Qt::blue);

    auto checkableItem = new QStandardItem;
    checkableItem->setFlags(checkableItem->flags() | Qt::ItemIsUserCheckable);
    checkableItem->setCheckState(Qt::Checked);
    checkableItem->setData(Qt::AlignCenter, CheckAlignmentRole);
    model.setItem(0, 0, checkableItem);

    checkableItem = new QStandardItem;
    checkableItem->setFlags(checkableItem->flags() | Qt::ItemIsUserCheckable);
    checkableItem->setData(Qt::AlignCenter, DecorationAlignmentRole);
    checkableItem->setIcon(pix);
    model.setItem(1, 0, checkableItem);

    QTableView tv;
    tv.setModel(&model);
    tv.setStyle(&style);
    tv.show();
    return a.exec();
}