Checkbox in a header cell in QTableView
Asked Answered
B

3

11

I want to have a simple column header with a checkbox that selects/ deselects all rows in a QTableView. Clicking the check box in the header causes either to select or deselect all rows.

enter image description here

When I want to add a check box in a table cell, I have to just return the check state for the Qt::CheckStateRole in the data(..) for the required model indices as below. This is working as expected.

QVariant MyModel::data( const QModelIndex & rIndex, int iRole) const
{
    ...

    if (iRole == Qt::Qt::CheckStateRole)
    {
        return checkstate;
    }

}

But when I want to add a checkbox in a header cell, the above method is not working. Hear is my sample code.

QVariant MyModel::headerData( int iSection, Qt::Orientation eOrientation, int iRole) const
{
    ...

    if (iRole == Qt::CheckStateRole)
    {
        return checkstate;
    }

}

The QTableView does not call headerData() function in my model with the Qt::CheckStateRole, as it does with data() function.

Why is this behavior? How can I insert a check box in a header cell by only modifying my custom table model?

(I do not want to create a custom QTableView or QHeaderView for this purpose)

Bluenose answered 4/2, 2014 at 16:27 Comment(0)
K
5

You cannot do it — Qt doesn't support check boxes in headers by default. You can read How can I insert a checkbox into the header of my view? for further information and its realization using a custom QHeaderView:

How can I insert a checkbox into the header of my view?

Currently there is no API to insert widgets in the header, but you can paint the checkbox yourself in order to insert it into the header.

What you could do is to subclass QHeaderView, reimplement paintSection() and then call [drawPrimitive()](http://doc.qt.io/qt-5/qstyle.html#drawPrimitivewith] PE_IndicatorCheckBox in the section where you want to have this checkbox.

You would also need to reimplement the mousePressEvent() to detect when the checkbox is clicked, in order to paint the checked and unchecked states.

The example below illustrates how this can be done:

#include <QtWidgets>

class MyHeader : public QHeaderView
{
public:
   MyHeader(Qt::Orientation orientation, QWidget * parent = nullptr) : QHeaderView(orientation, parent) {}

protected:
   void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const
   {
       painter->save();
       QHeaderView::paintSection(painter, rect, logicalIndex);
       painter->restore();
       if (logicalIndex == 0)
       {
           QStyleOptionButton option;
            option.rect = QRect(10,10,10,10);
            if (isOn)
                option.state = QStyle::State_On;
            else
                option.state = QStyle::State_Off;
            this->style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &option, painter);
       }

   }
   void mousePressEvent(QMouseEvent *event)
   {
       if (isOn)
           isOn = false;
       else
           isOn = true;
       this->update();
       QHeaderView::mousePressEvent(event);
   }
private:
    bool isOn;
};

int main(int argc, char **argv)
{
   QApplication app(argc, argv);
   QTableWidget table;
   table.setRowCount(4);
   table.setColumnCount(3);

   MyHeader *myHeader = new MyHeader(Qt::Horizontal, &table);
   table.setHorizontalHeader(myHeader);
   table.show();
   return app.exec();
}

To have all your other checkboxes checked, you could use the pressed() signal which should be fired from the mousePressEvent() and connect it to a custom slot where you set all your checkboxes checked or unchecked.

Kuomintang answered 4/2, 2014 at 16:39 Comment(0)
S
8

First: The check state should be stored in the model. All the tools are already there.

bool MyModel::setHeaderData(int index, Qt::Orientation orient, const QVariant& val, int role)
{
  if(Qt::Vertical != orient)
    return Base::setHeaderData(index, orient, val, role);

  storeCheckState(index, val);
  emit headerDataChanged(orient, index, index);
  return true;
}

QVariant MyModel::headerData(int index, Qt::Orientation o, int role) const
{
    if(Qt::Vertical != orient)
      return Base::headerData(index, o, role);

    switch(role)
    {
    ...
    case Qt::CheckStateRole:
      return fetchCheckState(index);
    }

  return Base::headerData(index, o, role);
}

Second: We toggle checked state simply by handling the click signal on the header.

connect(header, &QHeaderView::sectionClicked, receiver
        , [receiver](int sec)
{
  const auto index = logicalIndex(sec);
  model()->setHeaderData(index
                         , Qt::Vertical
                         , Qt::CheckState(model()->headerData(index, Qt::Vertical, Qt::CheckStateRole).toUInt()) != Qt::Checked ? Qt::Checked : Qt::Unchecked
                         , Qt::CheckStateRole);
});

Third: At this point we have fully functional checking behavior, only part missing is the visualization. The smartest way to go is to again use the model, taking advantage of the Qt::DecorationRole. Here is a dummy implementation:

QVariant MyModel::headerData(int index, Qt::Orientation o, int role) const
{
    if(Qt::Vertical != orient)
      return Base::headerData(index, o, role);

    switch(role)
    {
      case Qt::DecorationRole:
      {
        QPixmap p{12,12};
        p.fill(Qt::CheckState(headerData(index, o, Qt::CheckStateRole).toUInt()) ? Qt::green : Qt::red);
        return p;
       }
       break;
    ...
    }

  return Base::headerData(index, o, role);
}

Of course, one can draw a real checkbox there using the styled drawing.

Notice, this solution does not require sub-classing and custom widgets.

Also, the check state is decoupled from the view/UI. The only downside is fact the visuals are handled by the model, but this is optional - any way can be used to draw the check state, the one from the alternative answers included.

Selfdrive answered 6/8, 2016 at 16:41 Comment(0)
H
6

Here is a bit modified/fixed (checkbox was displayed as disabled and redrawing didn't work) code from the link in the accepted answer.

.h

class CheckBoxHeader : public QHeaderView
{
    Q_OBJECT

public:
    CheckBoxHeader(Qt::Orientation orientation, QWidget* parent = 0);

    bool isChecked() const { return isChecked_; }
    void setIsChecked(bool val);

signals:
    void checkBoxClicked(bool state);

protected:
    void paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const;

    void mousePressEvent(QMouseEvent* event);

private:
    bool isChecked_;

    void redrawCheckBox();
};

.cpp

#include "CheckBoxHeader.h"

CheckBoxHeader::CheckBoxHeader(Qt::Orientation orientation, QWidget* parent /*= 0*/)
    : QHeaderView(orientation, parent)
{
    isChecked_ = true;
}

void CheckBoxHeader::paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const
{
    painter->save();
    QHeaderView::paintSection(painter, rect, logicalIndex);  
    painter->restore();
    if (logicalIndex == 0)
    {
        QStyleOptionButton option;

        option.rect = QRect(1,3,20,20);

        option.state = QStyle::State_Enabled | QStyle::State_Active;

        if (isChecked_)
            option.state |= QStyle::State_On;
        else
            option.state |= QStyle::State_Off;
        option.state |= QStyle::State_Off;

        style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &option, painter);
    }
}

void CheckBoxHeader::mousePressEvent(QMouseEvent* event)
{
    setIsChecked(!isChecked());

    emit checkBoxClicked(isChecked());
}

void CheckBoxHeader::redrawCheckBox()
{
    viewport()->update();
}

void CheckBoxHeader::setIsChecked(bool val)
{
    if (isChecked_ != val)
    {
        isChecked_ = val;

        redrawCheckBox();
    }
}

usage

CheckBoxHeader* header = new CheckBoxHeader(Qt::Horizontal, &table);
table.setHorizontalHeader(header);
// handle signal if needed (to set checkboxes in all rows, etc.)
//connect(header, &CheckBoxHeader::checkBoxClicked, this, &MyForm::on_header_checkBoxClicked);
Homing answered 14/4, 2015 at 7:12 Comment(1)
This answer was ported to python with pyqt and then improved, see Adding checkBox as vertical header in QtableViewMidge
K
5

You cannot do it — Qt doesn't support check boxes in headers by default. You can read How can I insert a checkbox into the header of my view? for further information and its realization using a custom QHeaderView:

How can I insert a checkbox into the header of my view?

Currently there is no API to insert widgets in the header, but you can paint the checkbox yourself in order to insert it into the header.

What you could do is to subclass QHeaderView, reimplement paintSection() and then call [drawPrimitive()](http://doc.qt.io/qt-5/qstyle.html#drawPrimitivewith] PE_IndicatorCheckBox in the section where you want to have this checkbox.

You would also need to reimplement the mousePressEvent() to detect when the checkbox is clicked, in order to paint the checked and unchecked states.

The example below illustrates how this can be done:

#include <QtWidgets>

class MyHeader : public QHeaderView
{
public:
   MyHeader(Qt::Orientation orientation, QWidget * parent = nullptr) : QHeaderView(orientation, parent) {}

protected:
   void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const
   {
       painter->save();
       QHeaderView::paintSection(painter, rect, logicalIndex);
       painter->restore();
       if (logicalIndex == 0)
       {
           QStyleOptionButton option;
            option.rect = QRect(10,10,10,10);
            if (isOn)
                option.state = QStyle::State_On;
            else
                option.state = QStyle::State_Off;
            this->style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &option, painter);
       }

   }
   void mousePressEvent(QMouseEvent *event)
   {
       if (isOn)
           isOn = false;
       else
           isOn = true;
       this->update();
       QHeaderView::mousePressEvent(event);
   }
private:
    bool isOn;
};

int main(int argc, char **argv)
{
   QApplication app(argc, argv);
   QTableWidget table;
   table.setRowCount(4);
   table.setColumnCount(3);

   MyHeader *myHeader = new MyHeader(Qt::Horizontal, &table);
   table.setHorizontalHeader(myHeader);
   table.show();
   return app.exec();
}

To have all your other checkboxes checked, you could use the pressed() signal which should be fired from the mousePressEvent() and connect it to a custom slot where you set all your checkboxes checked or unchecked.

Kuomintang answered 4/2, 2014 at 16:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.