Qt Forum

    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    • Unsolved

    Update: Forum Guidelines & Code of Conduct


    Qt World Summit: Early-Bird Tickets

    Unsolved Complex widget as an item delegate

    General and Desktop
    delegate buttons clickable
    2
    7
    262
    Loading More Posts
    • 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
      voltron last edited by

      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 Reply Quote 0
      • Chris Kawa
        Chris Kawa Moderators last edited by

        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 Reply Quote 2
        • V
          voltron last edited by

          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.

          Chris Kawa 1 Reply Last reply Reply Quote 0
          • Chris Kawa
            Chris Kawa Moderators @voltron last edited by

            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 Reply Quote 0
            • V
              voltron last edited by

              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 Reply Quote 0
              • Chris Kawa
                Chris Kawa Moderators last edited by

                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 Reply Quote 0
                • V
                  voltron last edited by

                  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 Reply Quote 0
                  • First post
                    Last post