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. Complex widget as an item delegate
QtWS25 Last Chance

Complex widget as an item delegate

Scheduled Pinned Locked Moved Unsolved General and Desktop
delegatebuttonsclickable
7 Posts 2 Posters 2.0k Views
  • 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.
  • V Offline
    V Offline
    voltron
    wrote on 25 Nov 2022, 17:30 last edited by
    #1

    I want to show a complex widget consisting from two labels and a button for each item in the QListView, something like in the mockup below
    82ba07e8-2ed7-47e2-999b-b22e791f0de4-image.png
    There is no need to edit items, but button should be clickable.

    I subclassed QStyledItemDelegate and managed to show singleline texts and button. But now I need to change button style (hovered, pressed, normal) and handle clicks, also it would be nice to somehow hower active item.

    As I understand, to adjust button style I need to track mouse position inside the item and depending on it repaint delegate with different button style. Similarly with handling clicks. So I'm wondering is there any other way to use existing widget as a delegate?

    Using setItemWidget(…) is not an option, as the number of items in the view can be big.

    1 Reply Last reply
    0
    • C Offline
      C Offline
      Chris Kawa
      Lifetime Qt Champion
      wrote on 25 Nov 2022, 20:29 last edited by
      #2

      You could theoretically track the mouse and set/remove item widget of the single item that is currently under the cursor. It would take care of painting various states and clicking and you would paint the other items with a delegate, but it's a burden to keep both the widget and the delegate look exactly the same and I would say the same amount of work, if not more.

      I think overriding editorEvent and handling move/press/release event is not that hard and the right call here. Should be just a couple of lines.

      1 Reply Last reply
      2
      • V Offline
        V Offline
        voltron
        wrote on 26 Nov 2022, 14:10 last edited by
        #3

        Thanks, @Chris-Kawa. I tried to follow your suggestion and now my delegate can handle mouse clicks on the button. However, I'm stuck with changing button state, maybe you can guide me in the right direction?

        Here is my code

        class MyDelegate(QStyledItemDelegate):
            def __init__(self):
                super(MyDelegate, self).__init__()
                self.btnRect = None
        
            def sizeHint(self, option, index):
                fm = QFontMetrics(option.font)
                return QSize(150, fm.height() * 4 + fm.leading())
        
            def paint(self, painter, option, index):
                data = index.data(Qt.UserRole + 1)
        
                nameFont = QFont(option.font)
                nameFont.setWeight(QFont.Weight.Bold)
                fm = QFontMetrics(nameFont)
                padding = fm.lineSpacing() // 2
        
                nameRect = QRect(option.rect)
                nameRect.setLeft(nameRect.left() + padding)
                nameRect.setTop(nameRect.top() + padding)
                nameRect.setRight(nameRect.right() - padding)
                nameRect.setHeight(fm.lineSpacing())
        
                button = QStyleOptionButton()
                button.text = "Add"
                button.state = QStyle.State_Enabled
                textWidth = fm.width(button.text)
        
                self.btnRect = QRect(option.rect)
                self.btnRect.setLeft(nameRect.right() - textWidth * 2)
                self.btnRect.setTop(nameRect.bottom() + padding)
                self.btnRect.setRight(nameRect.right() - padding)
                self.btnRect.setHeight(fm.lineSpacing() * 2)
                button.rect = self.btnRect
        
                borderRect = QRect(option.rect.marginsRemoved(QMargins(4, 4, 4, 4)))
        
                painter.save()
        
                pen = painter.pen()
                if option.state & QStyle.State_MouseOver:
                    pp = QPen(option.palette.highlight().color())
                    pp.setWidth(2)
                    painter.setPen(pp)
                painter.drawRect(borderRect)
                painter.setPen(pen)
        
                # draw contents
                painter.setFont(nameFont)
                elided_text = fm.elidedText(data.title, Qt.ElideRight, nameRect.width())
                painter.drawText(nameRect, Qt.AlignLeading, elided_text)
        
                QApplication.style().drawControl(QStyle.CE_PushButton, button, painter)
        
                painter.restore()
        
            def editorEvent(self, event, model, option, index):
                if self.btnRect.contains(event.pos()):
                    if (event.type() == QEvent.MouseButtonPress):
                        # change button style to QStyle.State_Sunken
                        pass
                    elif (event.type() == QEvent.MouseButtonRelease):
                        # change button style to QStyle.State_Raised
        
                        self.do_something()
                    else:
                         # change button style to QStyle.State_HasFocus?
                         pass
                else:
                    # set button state  to normal QStyle.State_Enabled
                    pass
        
                return super(MyDelegate, self).editorEvent(event, model, option, index)
        
            def do_something(self):
                # do something when button clicked
                pass
        

        It is not clear to me what is the best way to pass button state to the paint() method. I tried to assing it to the member variable, but it does not work this way reliably, button state changes only sporadically.

        C 1 Reply Last reply 26 Nov 2022, 15:10
        0
        • V voltron
          26 Nov 2022, 14:10

          Thanks, @Chris-Kawa. I tried to follow your suggestion and now my delegate can handle mouse clicks on the button. However, I'm stuck with changing button state, maybe you can guide me in the right direction?

          Here is my code

          class MyDelegate(QStyledItemDelegate):
              def __init__(self):
                  super(MyDelegate, self).__init__()
                  self.btnRect = None
          
              def sizeHint(self, option, index):
                  fm = QFontMetrics(option.font)
                  return QSize(150, fm.height() * 4 + fm.leading())
          
              def paint(self, painter, option, index):
                  data = index.data(Qt.UserRole + 1)
          
                  nameFont = QFont(option.font)
                  nameFont.setWeight(QFont.Weight.Bold)
                  fm = QFontMetrics(nameFont)
                  padding = fm.lineSpacing() // 2
          
                  nameRect = QRect(option.rect)
                  nameRect.setLeft(nameRect.left() + padding)
                  nameRect.setTop(nameRect.top() + padding)
                  nameRect.setRight(nameRect.right() - padding)
                  nameRect.setHeight(fm.lineSpacing())
          
                  button = QStyleOptionButton()
                  button.text = "Add"
                  button.state = QStyle.State_Enabled
                  textWidth = fm.width(button.text)
          
                  self.btnRect = QRect(option.rect)
                  self.btnRect.setLeft(nameRect.right() - textWidth * 2)
                  self.btnRect.setTop(nameRect.bottom() + padding)
                  self.btnRect.setRight(nameRect.right() - padding)
                  self.btnRect.setHeight(fm.lineSpacing() * 2)
                  button.rect = self.btnRect
          
                  borderRect = QRect(option.rect.marginsRemoved(QMargins(4, 4, 4, 4)))
          
                  painter.save()
          
                  pen = painter.pen()
                  if option.state & QStyle.State_MouseOver:
                      pp = QPen(option.palette.highlight().color())
                      pp.setWidth(2)
                      painter.setPen(pp)
                  painter.drawRect(borderRect)
                  painter.setPen(pen)
          
                  # draw contents
                  painter.setFont(nameFont)
                  elided_text = fm.elidedText(data.title, Qt.ElideRight, nameRect.width())
                  painter.drawText(nameRect, Qt.AlignLeading, elided_text)
          
                  QApplication.style().drawControl(QStyle.CE_PushButton, button, painter)
          
                  painter.restore()
          
              def editorEvent(self, event, model, option, index):
                  if self.btnRect.contains(event.pos()):
                      if (event.type() == QEvent.MouseButtonPress):
                          # change button style to QStyle.State_Sunken
                          pass
                      elif (event.type() == QEvent.MouseButtonRelease):
                          # change button style to QStyle.State_Raised
          
                          self.do_something()
                      else:
                           # change button style to QStyle.State_HasFocus?
                           pass
                  else:
                      # set button state  to normal QStyle.State_Enabled
                      pass
          
                  return super(MyDelegate, self).editorEvent(event, model, option, index)
          
              def do_something(self):
                  # do something when button clicked
                  pass
          

          It is not clear to me what is the best way to pass button state to the paint() method. I tried to assing it to the member variable, but it does not work this way reliably, button state changes only sporadically.

          C Offline
          C Offline
          Chris Kawa
          Lifetime Qt Champion
          wrote on 26 Nov 2022, 15:10 last edited by
          #4

          I tried to assign it to the member variable, but it does not work this way reliably

          If you want to store the state of a button you have to do it separately for each index. You can do that for example through a custom user role like Qt.UserRole + 2. You would then read that out in the paint method.

          Another way is to store the state of the mouse buttons and cursor position to a member variable of the delegate and in the paint determine what the state of particular index button is based on that information.

          In any case make absolutely sure first that given event causes repaint at all. If not you'll have to trigger an update on the widget so that paint is called.

          1 Reply Last reply
          0
          • V Offline
            V Offline
            voltron
            wrote on 27 Nov 2022, 08:59 last edited by
            #5

            Thanks, this helps a lot! Setting button style in a custom user role in the editorEvent partially does the trick. In the editorEvent() I do

            if (event.type() == QEvent.MouseButtonPress):
                model.setData(QStyle.State_Sunken, Qt.UserRole + 2)
            

            and in the paint()

            button = QStyleOptionButton()
            button.state = index.data(Qt.UserRole + 2)
            

            The problem is that the first click on the item does not change button state. Only every second click makes changes in the sunken/raised state. Also hovering cursor over the button does not make it look active (having a focus).

            I tried to debug it and it looks like the first MouseButtonPress event is not propagated to the delegate. Probably the same happens with the mouse hovering. Any hits what can be missed/wrong?

            1 Reply Last reply
            0
            • C Offline
              C Offline
              Chris Kawa
              Lifetime Qt Champion
              wrote on 27 Nov 2022, 14:33 last edited by
              #6

              Do you have mouse tracking enabled on your view widget? Without that widgets get move events only when a button is pressed.

              As for the first button press - it works for me ok. Something must be swallowing it in your code.

              1 Reply Last reply
              0
              • V Offline
                V Offline
                voltron
                wrote on 28 Nov 2022, 06:34 last edited by
                #7

                Yes, I have mouse tracking enabled.

                Maybe my description of the issue was not very clear, sorry. When I click for the first time on the button, it does not change its style to QStyle.State_Sunken and then back to QStyle.State_Raised on release, this happens only on the every second click. Also button does not change its style on mouse hover.

                Here is a minimal example

                import sys
                
                from PyQt5.QtGui import *
                from PyQt5.QtCore import *
                from PyQt5.QtWidgets import *
                
                
                class MyDelegate(QStyledItemDelegate):
                    def __init__(self):
                        super(MyDelegate, self).__init__()
                        self.buttonRect = None
                
                    def sizeHint(self, option, index):
                        fm = QFontMetrics(option.font)
                        return QSize(150, fm.height() * 5 + fm.leading())
                
                    def paint(self, painter, option, index):
                        name = index.data(Qt.DisplayRole)
                        description = index.data(Qt.UserRole + 1)
                        btnStyle = QStyle.State(index.data(Qt.UserRole + 2))
                
                        nameFont = QFont(option.font)
                        nameFont.setWeight(QFont.Weight.Bold)
                        fm = QFontMetrics(nameFont)
                        padding = fm.lineSpacing() // 2
                
                        nameRect = QRect(option.rect)
                        nameRect.setLeft(nameRect.left() + padding)
                        nameRect.setTop(nameRect.top() + padding)
                        nameRect.setRight(nameRect.right() - padding)
                        nameRect.setHeight(fm.lineSpacing())
                
                        descrRect = QRect(option.rect)
                        descrRect.setLeft(descrRect.left() + padding)
                        descrRect.setTop(nameRect.bottom())
                        descrRect.setRight(descrRect.right() - padding)
                        descrRect.setHeight(fm.lineSpacing())
                
                        btnText = "Add"
                        textWidth = fm.width(btnText)
                
                        self.btnRect = QRect(option.rect)
                        self.btnRect.setLeft(descrRect.right() - textWidth * 2)
                        self.btnRect.setTop(descrRect.bottom() + padding)
                        self.btnRect.setRight(descrRect.right() - padding)
                        self.btnRect.setHeight(fm.lineSpacing() * 2)
                
                        borderRect = QRect(option.rect.marginsRemoved(QMargins(4, 4, 4, 4)))
                
                        painter.save()
                
                        pen = painter.pen()
                        if option.state & QStyle.State_MouseOver:
                            pp = QPen(option.palette.highlight().color())
                            pp.setWidth(2)
                            painter.setPen(pp)
                        painter.drawRect(borderRect)
                        painter.setPen(pen)
                
                        painter.setFont(nameFont)
                        elided_text = fm.elidedText(name, Qt.ElideRight, nameRect.width())
                        painter.drawText(nameRect, Qt.AlignLeading, elided_text)
                        painter.setFont(option.font)
                        fm = QFontMetrics(QFont(option.font))
                        elided_text = fm.elidedText(description, Qt.ElideRight, descrRect.width())
                        painter.drawText(descrRect, Qt.AlignLeading, elided_text)
                
                        button = QStyleOptionButton()
                        button.text = btnText
                        button.state = btnStyle
                        button.rect = self.btnRect
                        QApplication.style().drawControl(QStyle.CE_PushButton, button, painter)
                
                        painter.restore()
                
                    def editorEvent(self, event, model, option, index):
                        if self.btnRect.contains(event.pos()):
                            if event.type() == QEvent.MouseButtonPress:
                                model.setData(index, QStyle.State_Sunken, Qt.UserRole + 2)
                            elif event.type() == QEvent.MouseButtonRelease:
                                model.setData(index, QStyle.State_Raised, Qt.UserRole + 2)
                                self.do_something()
                            else:
                                 model.setData(index, QStyle.State_Enabled | QStyle.State_HasFocus, Qt.UserRole + 2)
                        else:
                            model.setData(index, QStyle.State_Enabled, Qt.UserRole + 2)
                
                        return super(MyDelegate, self).editorEvent(event, model, option, index)
                
                    def do_something(self):
                        print("button clicked")
                
                
                if __name__ == "__main__":
                    app = QApplication(sys.argv)
                
                    model = QStandardItemModel()
                    for i in range(10):
                        item = QStandardItem()
                        item.setData(f"Item {i}", Qt.DisplayRole)
                        item.setData("Item description, can be very long", Qt.UserRole + 1)
                        item.setData(QStyle.State_Enabled, Qt.UserRole + 2)
                        model.appendRow(item)
                
                    listView = QListView()
                    listView.setMouseTracking(True)
                    listView.setItemDelegate(MyDelegate())
                    listView.setModel(model)
                    listView.show()
                
                    app.exec()
                
                1 Reply Last reply
                0

                1/7

                25 Nov 2022, 17:30

                • Login

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