[PyQt] Multitple icons/pixmap in same cell

Maurizio Berti maurizio.berti at gmail.com
Sat Sep 7 01:55:23 BST 2019


Using multiple columns with a "shared" header cell is possible, but not
easy.

You'll need to create a subclass of QHeaderView for the horizontal header
and possibly set a custom proxy model (QIdentityProxyModel) overriding its
setModel, but that's not only. I think setSelectionModel requires some
care, and you'll have to implement a lot of things if you need correct
resizing of columns.
I would sincerely discourage this approach, at least if you're looking for
a good implementation in a similar scenario: since you're obviously
grouping all icons in the same item, the obvious solution is an item
delegate.

Looking at your image reference it seems that the icon on the sixth item is
right aligned, but it's unclear if that's because extra icons have some
reserved space or it's just a matter of alignment.

I've decided for a more "standard" approach which simply puts all icons
left aligned, but since you've cited tooltips I also added support for
those.
The idea is that you use a role for each icon you want to show, given that
a "status" icon is always visible, and any other icon is shown whenever
exists and its role is set to True.
I use QStyle and QStyleItemView to draw the base item rectangle, then find
the rectangle where an icon would be shown and use that data to paint all
icons.

from random import choice
from PyQt5 import QtCore, QtGui, QtWidgets

StateRole = QtCore.Qt.UserRole + 1
FolderRole = StateRole + 1
DriveRole = FolderRole + 1

class IconDelegate(QtWidgets.QStyledItemDelegate):
    lastIndex = None
    def __init__(self):
        super(IconDelegate, self).__init__()
        self.stateIcons = (QtGui.QIcon.fromTheme('offline'),
            QtGui.QIcon.fromTheme('online'))
        self.extraIconData = [
            (FolderRole, 'Folder', QtGui.QIcon.fromTheme('folder')),
            (DriveRole, 'Drive', QtGui.QIcon.fromTheme('drive-harddisk'))
        ]

    def sizeHint(self, option, index):
        hint = super(IconDelegate, self).sizeHint(option, index)
        option = QtWidgets.QStyleOptionViewItem(option)
        option.features |= option.HasDecoration
        widget = option.widget
        style = widget.style()
        iconRect = style.subElementRect(style.SE_ItemViewItemDecoration,
            option, widget)
        margin = iconRect.left() - option.rect.left()
        width = iconRect.width() * (len(self.extraIconData) + 1)
        width += margin * (len(self.extraIconData) + 2)
        hint.setWidth(width)
        return hint

    def paint(self, qp, option, index):
        # create a new QStyleOption (*never* use the one given in arguments)
        option = QtWidgets.QStyleOptionViewItem(option)

        widget = option.widget
        style = widget.style()

        # paint the base item (borders, gradients, selection colors, etc)
        style.drawControl(style.CE_ItemViewItem, option, qp, widget)

        # "lie" about the decoration, to get a valid icon rectangle (even
if we
        # don't have any "real" icon set for the item)
        option.features |= option.HasDecoration
        iconRect = style.subElementRect(style.SE_ItemViewItemDecoration,
            option, widget)
        iconSize = iconRect.size()
        margin = iconRect.left() - option.rect.left()

        qp.save()
        # ensure that we do not draw outside the item rectangle (and add
some
        # fancy margin on the right
        qp.setClipRect(option.rect.adjusted(0, 0, -margin, 0))

        # draw the main state icon, assuming all items have one
        qp.drawPixmap(iconRect,
            self.stateIcons[index.data(StateRole)].pixmap(iconSize))

        # cycle through roles, draw an icon if the index has that role set
to True
        left = delta = margin + iconRect.width()
        for role, name, icon in self.extraIconData:
            if index.data(role):
                qp.drawPixmap(iconRect.translated(left, 0),
                    icon.pixmap(iconSize))
                left += delta
        qp.restore()

    def helpEvent(self, event, view, option, index):
        if event.type() != QtCore.QEvent.ToolTip:
            return super(IconDelegate, self).helpEvent(event, view, option,
index)
        option = QtWidgets.QStyleOptionViewItem(option)
        widget = option.widget
        style = widget.style()
        option.features |= option.HasDecoration

        iconRect = style.subElementRect(style.SE_ItemViewItemDecoration,
            option, widget)
        iconRect.setTop(option.rect.y())
        iconRect.setHeight(option.rect.height())

        # similar to what we do in the paint() method
        if event.pos() in iconRect:
            # (*) clear any existing tooltip; a single space is better , as
            # sometimes it's not enough to use an empty string
            if index != self.lastIndex:
                QtWidgets.QToolTip.showText(QtCore.QPoint(), ' ')
            QtWidgets.QToolTip.showText(event.globalPos(),
                'State: {}'.format(('Off', 'On')[index.data(StateRole)]),
view)
        else:
            margin = iconRect.left() - option.rect.left()
            left = delta = margin + iconRect.width()
            for role, name, icon in self.extraIconData:
                if index.data(role):
                    # if the role is set to True check if the mouse is
within
                    # the icon rectangle
                    if event.pos() in iconRect.translated(left, 0):
                        # see above (*)
                        if index != self.lastIndex:
                            QtWidgets.QToolTip.showText(QtCore.QPoint(), '
')
                        QtWidgets.QToolTip.showText(event.globalPos(),
name, view)
                        break
                    # shift the left *only* if the role is True, otherwise
we
                    # can assume that that icon doesn't exist at all
                    left += delta
            else:
                # nothing interesting, hide the existing tooltip, if any
                QtWidgets.QToolTip.hideText()
        self.lastIndex = index
        return True


class Window(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)

        table = QtWidgets.QTableView()
        layout.addWidget(table)

        model = QtGui.QStandardItemModel()
        model.setHorizontalHeaderLabels(['Name', 'State'])
        for row in range(5):
            mainItem = QtGui.QStandardItem('Item {}'.format(row + 1))
            stateItem = QtGui.QStandardItem()
            stateItem.setData(choice([0, 1]), StateRole)
            stateItem.setData(choice([0, 1]), FolderRole)
            stateItem.setData(choice([0, 1]), DriveRole)
            model.appendRow([mainItem, stateItem])

        table.setModel(model)
        table.setItemDelegateForColumn(1, IconDelegate())
        table.resizeColumnsToContents()

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec_())


Il giorno ven 6 set 2019 alle ore 16:45 donoban <donoban at riseup.net> ha
scritto:

> Hi,
>
> I am trying to adapt a medium sized pyqt appication to model/view
> architecture (https://github.com/QubesOS/qubes-manager/pull/195)
>
> I have a problem when trying to imitate the 'State' column behavior. As
> you could see here:
>
> https://www.linuxjournal.com/sites/default/files/styles/850x500/public/nodeimage/story/12011f2.png
>
> This column can have multiple icons which show the current state of the
> VM. In the internal code it uses a custom widget with a layout and adds
> new widgets with a pixmap and tooltip.
>
> Does anybody know how to imitate this with model/view design? Could I
> split the 'State' column in multiple columns which share the same header
> cell?
>
> Regards.
> _______________________________________________
> PyQt mailing list    PyQt at riverbankcomputing.com
> https://www.riverbankcomputing.com/mailman/listinfo/pyqt
>


-- 
È difficile avere una convinzione precisa quando si parla delle ragioni del
cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20190907/2b917bc7/attachment-0001.html>


More information about the PyQt mailing list