Center a QCheckBox or Decoration in an Itemview

From Qt Wiki
Revision as of 16:39, 29 November 2020 by Christian Ehrlicher (talk | contribs) (Initial version)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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 behavior. If no user interaction is needed (e.g. the checkbox is should not be user-checkable) the easiest solution is to provide a custom item delegate derived from QStyledItemDelegate. If you need user interaction a custom QProxyStyle is needed.

Derive from QStyledItemDelegate

When deriving from QStyledItemDelegate no user-interaction is needed since the hittest area for the checkbox can not be modified with an item delegate. 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 MyStyle : 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, Qt::AlignVCenter | Qt::AlignHCenter, 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, Qt::AlignVCenter | Qt::AlignHCenter, 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);
    }
  }
};

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);

  auto checkableItem = new QStandardItem;
  checkableItem->setCheckState(Qt::Checked);
  checkableItem->setTextAlignment(Qt::AlignHCenter);
  model.setItem(0, 0, checkableItem);

  checkableItem = new QStandardItem;
  checkableItem->setIcon(QPixmap("qticon64.png"));
  checkableItem->setTextAlignment(Qt::AlignHCenter);
  model.setItem(1, 0, checkableItem);

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

Use a custom style derived from QProxyStyle

If you need user interaction for your QCheckBox you need to create a custom QProxyStyle so the hittest for the checkbox is done correct. The alignment of the QCheckBox is provided through a custom role (CheckAlignmentRole) 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. The style does not work for the decoration but it should be easy to implement it (replace SE_ItemViewItemCheckIndicator with SE_ItemViewItemDecoration and paint the return the decoration rect instead)

enum {
  CheckAlignmentRole = Qt::UserRole + Qt::CheckStateRole + 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);
    if (element == SE_ItemViewItemCheckIndicator) {
      const QStyleOptionViewItem* const itemOpt = qstyleoption_cast<const QStyleOptionViewItem*>(option);
      const QVariant alignData = itemOpt ? itemOpt->index.data(CheckAlignmentRole) : QVariant();
      if (alignData.isNull())
        return baseRes;
      const QRect itemRect = option->rect;
      Q_ASSERT(itemRect.width() > baseRes.width() && itemRect.height() > baseRes.height());
      const int alignFlag = alignData.toInt();
      int x = 0;
      if (alignFlag & Qt::AlignLeft)
        x = baseRes.x();
      else if (alignFlag & Qt::AlignRight)
        x = itemRect.x() + itemRect.width() - (baseRes.x() - itemRect.x())- baseRes.width();
      else if (alignFlag & Qt::AlignHCenter)
        x = itemRect.x() + (itemRect.width() / 2) - (baseRes.width() / 2);

      return QRect(QPoint(x, baseRes.y()), baseRes.size());
    } else if (element == SE_ItemViewItemFocusRect) {
      const QStyleOptionViewItem* const itemOpt = qstyleoption_cast<const QStyleOptionViewItem*>(option);
      const QVariant alignData = itemOpt ? itemOpt->index.data(CheckAlignmentRole) : QVariant();
      if (!alignData.isNull()) // when it is no null, then it's a checkbox cell and the focus rect should be drawn over the complete cell
        return option->rect;
    }
    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);
  // a.setStyle(new CenteredBoxProxyStyle); // when it should be set for the complete application

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

  auto checkableItem = new QStandardItem;
  checkableItem->setCheckState(Qt::Checked);
  checkableItem->setData(Qt::AlignHCenter, CheckAlignmentRole);
  model.setItem(0, 0, checkableItem);

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