Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. Code review: multiple checkboxes as QStyledItemDelegate
Forum Updated to NodeBB v4.3 + New Features

Code review: multiple checkboxes as QStyledItemDelegate

Scheduled Pinned Locked Moved General and Desktop
9 Posts 3 Posters 541 Views 2 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • M Offline
    M Offline
    Mark81
    wrote on last edited by Mark81
    #1

    I kindly ask to check my code below. It is a QStyledItemDelegate I wrote to achieve a specific goal.
    My table has an integer column that represents the binary sum of weekdays. I want to display 7 checkboxes to let the user to select the desired combination of weekdays.

    I thought it was a simple task, but I ended with a quite articulated code and I'm not sure if it's the best approach or there is another simpler. The good news is it works.

    I studied the stardelegate example code and the qcheckbox source code.

    #ifndef DELEGATEWEEKDAYS_H
    #define DELEGATEWEEKDAYS_H
    
    #include <QStyledItemDelegate>
    #include <QPainter>
    #include <QBoxLayout>
    #include <QCheckBox>
    #include <QDebug>
    #include <QApplication>
    #include <QMouseEvent>
    
    class WeekdaySelector
    {
    public:
        enum class EditMode { Editable, ReadOnly };
    
        explicit WeekdaySelector(int value = 0) : _wd(value)
        {
            _wdPress = -1;
        }
    
        void paint(QPainter *painter, const QRect &rect, const QPalette &palette, EditMode mode) const
        {
            QCheckBox dummy;
            int w = dummy.sizeHint().width();
    
            painter->save();
            for (int i = 0; i < 7; i++)
            {
                QStyleOptionButton cbOpt;
                cbOpt.rect = rect;
                bool isChecked = _wd & (1 << i);
    
                if (isChecked) cbOpt.state = QStyle::State_On;
                else cbOpt.state = QStyle::State_Off;
    
                if (_wdPress == i) cbOpt.state.setFlag(QStyle::State_Sunken);
    
                painter->translate(6, 0); // gap
                QApplication::style()->drawControl(QStyle::CE_CheckBox, &cbOpt, painter);
                painter->translate(w, 0); // width
            }
            painter->restore();
        }
    
        QSize sizeHint() const
        {
            QCheckBox dummy;
            QSize size = dummy.sizeHint();
            size.setWidth(size.width() * 7 + 5 * 6); // 7 checkbox + 6 gaps
            return size;
        }
    
        int weekdays() const { return _wd; }
        void setWeekdays(int wd) { _wd = wd; }
        void toggleWeekday(int pos) { _wd ^= (1 << pos); }
        int pressWeekday() { return _wdPress; }
        void setPressWeekday(int pos) { _wdPress = pos; }
    
    private:
        int _wd;
        int _wdPress;
    };
    Q_DECLARE_METATYPE(WeekdaySelector)
    
    class EditorWeekdays : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit EditorWeekdays(QWidget *parent = nullptr) : QWidget(parent)
        {
            setAutoFillBackground(true);
        }
    
        WeekdaySelector selector() { return _selector; }
        void setSelector(WeekdaySelector selector) { _selector = selector; }
    
        QSize sizeHint() const override
        {
            return _selector.sizeHint();
        }
    
    protected:
        void paintEvent(QPaintEvent *event) override
        {
            QPainter painter(this);
            _selector.paint(&painter, rect(), palette(), WeekdaySelector::EditMode::Editable);
        }
    
        void mousePressEvent(QMouseEvent *e) override
        {
            const int day = checkboxAtPosition(e->position().x());
            if (day != - 1)
            {
                _selector.setPressWeekday(day);
                update();
            }
            QWidget::mousePressEvent(e);
        }
    
        void mouseReleaseEvent(QMouseEvent *e) override
        {
            const int day = checkboxAtPosition(e->position().x());
            if (day != -1)
            {
                if (day == _selector.pressWeekday()) _selector.toggleWeekday(day);
                _selector.setPressWeekday(-1);
                update();
            }
            QWidget::mouseReleaseEvent(e);
        }
    
    private:
        WeekdaySelector _selector;
    
        int checkboxAtPosition(int x) const
        {
            QCheckBox dummy;
            int w = dummy.sizeHint().width();
    
            int x0 = 6;
            if (x < x0) return -1;
    
            for (int i = 0; i < 7; i++)
            {
                x0 += w;
                if (x < x0) return i;
    
                x0 += 6;
                if (x < x0) return -1;
            }
    
            return -1;
        }
    };
    
    class DelegateWeekdays : public QStyledItemDelegate
    {
        Q_OBJECT
    
    public:
        DelegateWeekdays(QObject *parent = nullptr) : QStyledItemDelegate(parent)
        {
        }
    
        QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override
        {
            EditorWeekdays *editor = new EditorWeekdays(parent);
            return editor;
        }
    
        void setEditorData(QWidget *editor, const QModelIndex &index) const override
        {
            WeekdaySelector selector(index.data().toInt());
            EditorWeekdays *e = qobject_cast<EditorWeekdays *>(editor);
            e->setSelector(selector);
    
            int value = index.model()->data(index, Qt::EditRole).toInt();
            selector.setWeekdays(value);
        }
    
        void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override
        {
            EditorWeekdays *e = qobject_cast<EditorWeekdays *>(editor);
            model->setData(index, QVariant::fromValue(e->selector().weekdays()));
        }
    
        QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
        {
            WeekdaySelector selector(index.data().toInt());
            return selector.sizeHint();
        }
    
        void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
        {
            WeekdaySelector selector(index.data().toInt());
            QStyleOptionViewItem opt = option;
            initStyleOption(&opt, index);
            selector.paint(painter, opt.rect, opt.palette, WeekdaySelector::EditMode::ReadOnly);
        }
    };
    
    #endif // DELEGATEWEEKDAYS_H
    

    As I said it works: it displays the value from the model and the editor allows me to change the weekdays and store the new value into the database.

    Questions:

    1. Was all that code necessary? I mean, instead of mimic the behavior of the checkboxes could I have used QCheckbox directly?
    2. Is it correct to create a dummy QCheckBox to retrieve its size?
    QCheckBox dummy;
    int w = dummy.sizeHint().width();
    
    1. I'm also trying to add the code to handle the keyboard (i.e. cursor or tab to change the focus and space to toggle the flag), but I'm not able to add the focus border, like this:

    d584891f-827e-4233-ba93-af1dad482d70-image.png

    according to the documentation when drawing a CE_CheckBox the state flag State_HasFocus should be available, but if I add in my paint() event:

    cbOpt.state.setFlag(QStyle::State_HasFocus);
    QApplication::style()->drawControl(QStyle::CE_CheckBox, &cbOpt, painter);
    

    nothing happens, the checkboxes are painted without focus:

    41978948-01cc-4e07-9d6f-4a8c65cc9873-image.png

    I read the wrong documentation?

    Christian EhrlicherC 1 Reply Last reply
    0
    • M Mark81 marked this topic as a regular topic on
    • M Mark81

      I kindly ask to check my code below. It is a QStyledItemDelegate I wrote to achieve a specific goal.
      My table has an integer column that represents the binary sum of weekdays. I want to display 7 checkboxes to let the user to select the desired combination of weekdays.

      I thought it was a simple task, but I ended with a quite articulated code and I'm not sure if it's the best approach or there is another simpler. The good news is it works.

      I studied the stardelegate example code and the qcheckbox source code.

      #ifndef DELEGATEWEEKDAYS_H
      #define DELEGATEWEEKDAYS_H
      
      #include <QStyledItemDelegate>
      #include <QPainter>
      #include <QBoxLayout>
      #include <QCheckBox>
      #include <QDebug>
      #include <QApplication>
      #include <QMouseEvent>
      
      class WeekdaySelector
      {
      public:
          enum class EditMode { Editable, ReadOnly };
      
          explicit WeekdaySelector(int value = 0) : _wd(value)
          {
              _wdPress = -1;
          }
      
          void paint(QPainter *painter, const QRect &rect, const QPalette &palette, EditMode mode) const
          {
              QCheckBox dummy;
              int w = dummy.sizeHint().width();
      
              painter->save();
              for (int i = 0; i < 7; i++)
              {
                  QStyleOptionButton cbOpt;
                  cbOpt.rect = rect;
                  bool isChecked = _wd & (1 << i);
      
                  if (isChecked) cbOpt.state = QStyle::State_On;
                  else cbOpt.state = QStyle::State_Off;
      
                  if (_wdPress == i) cbOpt.state.setFlag(QStyle::State_Sunken);
      
                  painter->translate(6, 0); // gap
                  QApplication::style()->drawControl(QStyle::CE_CheckBox, &cbOpt, painter);
                  painter->translate(w, 0); // width
              }
              painter->restore();
          }
      
          QSize sizeHint() const
          {
              QCheckBox dummy;
              QSize size = dummy.sizeHint();
              size.setWidth(size.width() * 7 + 5 * 6); // 7 checkbox + 6 gaps
              return size;
          }
      
          int weekdays() const { return _wd; }
          void setWeekdays(int wd) { _wd = wd; }
          void toggleWeekday(int pos) { _wd ^= (1 << pos); }
          int pressWeekday() { return _wdPress; }
          void setPressWeekday(int pos) { _wdPress = pos; }
      
      private:
          int _wd;
          int _wdPress;
      };
      Q_DECLARE_METATYPE(WeekdaySelector)
      
      class EditorWeekdays : public QWidget
      {
          Q_OBJECT
      
      public:
          explicit EditorWeekdays(QWidget *parent = nullptr) : QWidget(parent)
          {
              setAutoFillBackground(true);
          }
      
          WeekdaySelector selector() { return _selector; }
          void setSelector(WeekdaySelector selector) { _selector = selector; }
      
          QSize sizeHint() const override
          {
              return _selector.sizeHint();
          }
      
      protected:
          void paintEvent(QPaintEvent *event) override
          {
              QPainter painter(this);
              _selector.paint(&painter, rect(), palette(), WeekdaySelector::EditMode::Editable);
          }
      
          void mousePressEvent(QMouseEvent *e) override
          {
              const int day = checkboxAtPosition(e->position().x());
              if (day != - 1)
              {
                  _selector.setPressWeekday(day);
                  update();
              }
              QWidget::mousePressEvent(e);
          }
      
          void mouseReleaseEvent(QMouseEvent *e) override
          {
              const int day = checkboxAtPosition(e->position().x());
              if (day != -1)
              {
                  if (day == _selector.pressWeekday()) _selector.toggleWeekday(day);
                  _selector.setPressWeekday(-1);
                  update();
              }
              QWidget::mouseReleaseEvent(e);
          }
      
      private:
          WeekdaySelector _selector;
      
          int checkboxAtPosition(int x) const
          {
              QCheckBox dummy;
              int w = dummy.sizeHint().width();
      
              int x0 = 6;
              if (x < x0) return -1;
      
              for (int i = 0; i < 7; i++)
              {
                  x0 += w;
                  if (x < x0) return i;
      
                  x0 += 6;
                  if (x < x0) return -1;
              }
      
              return -1;
          }
      };
      
      class DelegateWeekdays : public QStyledItemDelegate
      {
          Q_OBJECT
      
      public:
          DelegateWeekdays(QObject *parent = nullptr) : QStyledItemDelegate(parent)
          {
          }
      
          QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override
          {
              EditorWeekdays *editor = new EditorWeekdays(parent);
              return editor;
          }
      
          void setEditorData(QWidget *editor, const QModelIndex &index) const override
          {
              WeekdaySelector selector(index.data().toInt());
              EditorWeekdays *e = qobject_cast<EditorWeekdays *>(editor);
              e->setSelector(selector);
      
              int value = index.model()->data(index, Qt::EditRole).toInt();
              selector.setWeekdays(value);
          }
      
          void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override
          {
              EditorWeekdays *e = qobject_cast<EditorWeekdays *>(editor);
              model->setData(index, QVariant::fromValue(e->selector().weekdays()));
          }
      
          QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
          {
              WeekdaySelector selector(index.data().toInt());
              return selector.sizeHint();
          }
      
          void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
          {
              WeekdaySelector selector(index.data().toInt());
              QStyleOptionViewItem opt = option;
              initStyleOption(&opt, index);
              selector.paint(painter, opt.rect, opt.palette, WeekdaySelector::EditMode::ReadOnly);
          }
      };
      
      #endif // DELEGATEWEEKDAYS_H
      

      As I said it works: it displays the value from the model and the editor allows me to change the weekdays and store the new value into the database.

      Questions:

      1. Was all that code necessary? I mean, instead of mimic the behavior of the checkboxes could I have used QCheckbox directly?
      2. Is it correct to create a dummy QCheckBox to retrieve its size?
      QCheckBox dummy;
      int w = dummy.sizeHint().width();
      
      1. I'm also trying to add the code to handle the keyboard (i.e. cursor or tab to change the focus and space to toggle the flag), but I'm not able to add the focus border, like this:

      d584891f-827e-4233-ba93-af1dad482d70-image.png

      according to the documentation when drawing a CE_CheckBox the state flag State_HasFocus should be available, but if I add in my paint() event:

      cbOpt.state.setFlag(QStyle::State_HasFocus);
      QApplication::style()->drawControl(QStyle::CE_CheckBox, &cbOpt, painter);
      

      nothing happens, the checkboxes are painted without focus:

      41978948-01cc-4e07-9d6f-4a8c65cc9873-image.png

      I read the wrong documentation?

      Christian EhrlicherC Offline
      Christian EhrlicherC Offline
      Christian Ehrlicher
      Lifetime Qt Champion
      wrote on last edited by
      #2

      @Mark81 said in Code review: multiple checkboxes as QStyledItemDelegate:

      cbOpt.state.setFlag(QStyle::State_HasFocus);

      Don't you think you should set the flag before calling the painting through the style?
      What style do you use? Fusion and Windows are honoring the focus flag through it's base class: https://code.qt.io/cgit/qt/qtbase.git/tree/src/widgets/styles/qcommonstyle.cpp#n1421

      Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
      Visit the Qt Academy at https://academy.qt.io/catalog

      M 1 Reply Last reply
      0
      • Christian EhrlicherC Christian Ehrlicher

        @Mark81 said in Code review: multiple checkboxes as QStyledItemDelegate:

        cbOpt.state.setFlag(QStyle::State_HasFocus);

        Don't you think you should set the flag before calling the painting through the style?
        What style do you use? Fusion and Windows are honoring the focus flag through it's base class: https://code.qt.io/cgit/qt/qtbase.git/tree/src/widgets/styles/qcommonstyle.cpp#n1421

        M Offline
        M Offline
        Mark81
        wrote on last edited by Mark81
        #3

        @Christian-Ehrlicher yep, that was a typo when I copied 'n pasted - corrected thanks.
        I'm using the default style under X11 (Ubuntu).

        QApplication a(argc, argv);
        qDebug() << a.style()->name();
        

        tells me it's fusion, indeed.

        The focus works on the QCheckBox control as from my screenshot, but setting the flag (in the proper order!) does not draw the focus border.

        1 Reply Last reply
        0
        • Christian EhrlicherC Offline
          Christian EhrlicherC Offline
          Christian Ehrlicher
          Lifetime Qt Champion
          wrote on last edited by
          #4

          Then debug to see why it is not painted. As you can see the style honors the flag.

          Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
          Visit the Qt Academy at https://academy.qt.io/catalog

          M 1 Reply Last reply
          0
          • Christian EhrlicherC Christian Ehrlicher

            Then debug to see why it is not painted. As you can see the style honors the flag.

            M Offline
            M Offline
            Mark81
            wrote on last edited by
            #5

            @Christian-Ehrlicher said in Code review: multiple checkboxes as QStyledItemDelegate:

            Then debug to see why it is not painted. As you can see the style honors the flag.

            How can I debug this kind of issue? This is beyond my knowledge.
            With the debugger I checked the flag is actually set, just before call the drawControl() function:

            b39665c5-c5b7-4289-89b9-828763ce22fc-image.png

            Since the error is of course on my side, what else can I verify?

            1 Reply Last reply
            0
            • SGaistS Offline
              SGaistS Offline
              SGaist
              Lifetime Qt Champion
              wrote on last edited by
              #6

              Hi,

              This stack overflow thread covers a similar issue.

              Basically, you have not initialized the button options with all the required values.

              Interested in AI ? www.idiap.ch
              Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

              M 1 Reply Last reply
              0
              • Christian EhrlicherC Offline
                Christian EhrlicherC Offline
                Christian Ehrlicher
                Lifetime Qt Champion
                wrote on last edited by
                #7

                Install the Qt debug libs and source code and step into the Qt source code

                Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
                Visit the Qt Academy at https://academy.qt.io/catalog

                M 1 Reply Last reply
                0
                • SGaistS SGaist

                  Hi,

                  This stack overflow thread covers a similar issue.

                  Basically, you have not initialized the button options with all the required values.

                  M Offline
                  M Offline
                  Mark81
                  wrote on last edited by Mark81
                  #8

                  @SGaist said in Code review: multiple checkboxes as QStyledItemDelegate:

                  This stack overflow thread covers a similar issue.
                  Basically, you have not initialized the button options with all the required values.

                  Where can I find in the docs what are all the required values to draw the focus rect?

                  I tried the following:

                  QStyleOptionButton opt;
                  opt.rect = rect;
                  bool isChecked = _wd & (1 << i);
                  
                  if (isChecked) opt.state.setFlag(QStyle::State_On);
                  else opt.state.setFlag(QStyle::State_Off);
                  if (_wdPress == i) opt.state.setFlag(QStyle::State_Sunken);
                   
                  opt.state.setFlag(QStyle::State_Enabled);
                  opt.state.setFlag(QStyle::State_HasFocus);
                  
                  painter->translate(6, 0); // gap
                  QApplication::style()->drawControl(QStyle::CE_CheckBox, &opt, painter);
                  painter->translate(w, 0); // width
                  

                  and also other combinations (with State_FocusAtBorder, State_MouseOver, State_Active, State_Selected, State_Editing, State_KeyboardFocusChange) but I was not able to draw the focus rect.

                  I'm downloading the source code as @Christian-Ehrlicher kindly suggested, but it seems odd I need to debug the Qt source code if the error is on my side.

                  1 Reply Last reply
                  0
                  • Christian EhrlicherC Christian Ehrlicher

                    Install the Qt debug libs and source code and step into the Qt source code

                    M Offline
                    M Offline
                    Mark81
                    wrote on last edited by Mark81
                    #9

                    @Christian-Ehrlicher I did. From what I understand it seems it run the code you highlighted before:

                    3500a556-2b28-431a-b11f-31cbba20e27d-image.png

                    but still no focus rect:

                    d2894d0c-722b-4369-950a-bcb7166cbe04-image.png

                    To further digging the issue I placed a QCheckBox on a form and I managed to make the focus rect appeared:

                    25d70160-d0ac-4dde-9716-fb563b6888d4-image.png

                    then I placed a breakpoint to inspect the properties of the state variable:

                    QStyle::State_Enabled | QStyle::State_On | QStyle::State_HasFocus | QStyle::State_Active | QStyle::State_KeyboardFocusChange            
                    

                    and as I've done before, I added them to my delegate:

                    if (isChecked) opt.state.setFlag(QStyle::State_On);
                    else opt.state.setFlag(QStyle::State_Off);
                    if (_wdPress == i) opt.state.setFlag(QStyle::State_Sunken);
                    
                    opt.state.setFlag(QStyle::State_Enabled);
                    opt.state.setFlag(QStyle::State_HasFocus);
                    opt.state.setFlag(QStyle::State_Active);
                    opt.state.setFlag(QStyle::State_KeyboardFocusChange);
                    

                    and now it works. But it works even removing the Enabled and Active state.
                    And I clearly tried these flags before, as I wrote above.

                    I have no time to reinstall Qt 6.8.1 to test again, but it seems something has changed in 6.8.2.

                    1 Reply Last reply
                    0

                    • Login

                    • Login or register to search.
                    • First post
                      Last post
                    0
                    • Categories
                    • Recent
                    • Tags
                    • Popular
                    • Users
                    • Groups
                    • Search
                    • Get Qt Extensions
                    • Unsolved