ItemClipsToShape QGraphicsSVGItem

587 views Asked by At

The following flag does not do what it is meant to and clip the shape. Instead it creates a rectangular selection.

self.setFlag(self.ItemClipsToShape)

enter image description hereenter image description here

How can I make the selection clip to the shape?

test.svg

<svg viewBox='0 0 108 95' 
xmlns='http://www.w3.org/2000/svg'>
<g transform='scale(0.1)'>
        <path id="p2" fill='blue' stroke='red' d='M249,699v43h211v-43h-64l-2,3l-2,4l-4,3c0,0-1,2-2,2h-4c-2,0-3,0-4,1c-1,1-3,1-3,
                        2l-3,4c0,1-1,2-2,2h-4c0,0-2,1-3,0l-3-1c-1,0-3-1-3-2c-1-1,0-2-1-3l-1-3c-1-1-2-1-3-1c-1,0-4,
                        0-4-1c0-2,0-3-1-4v-3v-3z'/>
    <path id="p3" fill='blue' d='M385,593c0,9-6,15-13,15c-7,0-13-6-13-15c0-8,12-39,14-39c1,0,12,31,12,39'/>
</g>
</svg>

svg_mouse.py

from PyQt5 import QtWidgets
from PyQt5.QtSvg import QGraphicsSvgItem, QSvgRenderer

class SvgItem(QGraphicsSvgItem):

    def __init__(self, id, renderer, parent=None):
        super().__init__(parent)
        self.id = id
        self.setSharedRenderer(renderer)
        self.setElementId(id)
        bounds = renderer.boundsOnElement(id)
        self.setPos(bounds.topLeft())
        #self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) #horrible selection-box

    def mousePressEvent(self, event: 'QtWidgets.QGraphicsSceneMouseEvent'):
        print('svg item: ' + self.id + ' - mousePressEvent()')
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event: 'QtWidgets.QGraphicsSceneMouseEvent'):
        print('svg item: ' + self.id + ' - mouseReleaseEvent()')
        super().mouseReleaseEvent(event)

class SvgViewer(QtWidgets.QGraphicsView):
    def __init__(self, parent):
        super().__init__(parent)
        self._scene = QtWidgets.QGraphicsScene(self)
        self._renderer = QSvgRenderer()
        self.setScene(self._scene)

    def set_svg(self, data):
        self.resetTransform()
        self._scene.clear()
        self._renderer.load(data)
        item1 = SvgItem('p2', self._renderer)
        self._scene.addItem(item1)
        item2 = SvgItem('p3', self._renderer)
        self._scene.addItem(item2)

    def mousePressEvent(self, event: 'QtWidgets.QGraphicsSceneMouseEvent'):
        print('SvgViewer - mousePressEvent()')
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event: 'QtWidgets.QGraphicsSceneMouseEvent'):
        print('SvgViewer - mouseReleaseEvent()')
        super().mouseReleaseEvent(event)

class Window(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.viewer = SvgViewer(self)
        vb_layout = QtWidgets.QVBoxLayout(self)
        vb_layout.addWidget(self.viewer)
        img = b'''
                <svg viewBox='0 0 108 95' xmlns='http://www.w3.org/2000/svg'>
                    <g transform='scale(0.1)'>
                        <a href=""><path id="p2" d='M249,699v43h211v-43h-64l-2,3l-2,4l-4,3c0,0-1,2-2,2h-4c-2,0-3,0-4,1c-1,1-3,1-3,
                            2l-3,4c0,1-1,2-2,2h-4c0,0-2,1-3,0l-3-1c-1,0-3-1-3-2c-1-1,0-2-1-3l-1-3c-1-1-2-1-3-1c-1,0-4,
                            0-4-1c0-2,0-3-1-4v-3v-3z'/></a>
                        <path id="p3" d='M385,593c0,9-6,15-13,15c-7,0-13-6-13-15c0-8,12-39,14-39c1,0,12,31,12,39'/>
                    </g>
                </svg>'''
        self.viewer.set_svg(img)


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.setGeometry(500, 300, 600, 400)
    window.show()
    sys.exit(app.exec_())
1

There are 1 answers

9
musicamante On BEST ANSWER

I think that QGraphicsSvgItem uses the bounds rectangle for its shape.

A possible hacky but effective solution is to do what QGraphicsPixmapItem does, that is to get the mask and use the alpha region for to return the shape() of the element.

Note that I'm not setting the ItemClipsToShape flags, because there's no need for it.
I also implemented the correct selection outline, based on the Qt implementation.

clipping and selection

class SvgItem(QGraphicsSvgItem):

    def __init__(self, id, renderer, parent=None):
        super().__init__(parent)
        self.id = id
        self.setSharedRenderer(renderer)
        self.setElementId(id)
        bounds = renderer.boundsOnElement(id)
        self.setPos(bounds.topLeft())
        self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable)


        # create an image based on the item size
        img = QtGui.QImage(bounds.size().toSize(), QtGui.QImage.Format_ARGB32)
        # clear its buffer (this is important!)
        img.fill(QtCore.Qt.transparent)
        # create a qpainter and ask the renderer to render it
        qp = QtGui.QPainter(img)
        renderer.render(qp, id)
        qp.end()

        # create the mask by adding a QRegion based on it
        mask = img.createAlphaMask()
        shape = QtGui.QPainterPath()
        shape.addRegion(QtGui.QRegion(QtGui.QBitmap.fromImage(mask)))
        # a QBitmap based region can be unnecessarily complex, let's
        # simplify it
        self._shape = shape.simplified()

    def shape(self):
        return self._shape

    def paint(self, qp, option, widget):
        # keep track of the selected state and call the base painting
        # implementation without it
        selected = option.state & QtWidgets.QStyle.State_Selected
        option.state &= ~QtWidgets.QStyle.State_Selected
        super().paint(qp, option, widget)

        if selected:
            # draw the selection based on the shape, using the right
            # amount of contrast with the background
            fgcolor = option.palette.windowText().color()
            bgcolor = QtGui.QColor(
                0 if fgcolor.red() > 127 else 255, 
                0 if fgcolor.green() > 127 else 255, 
                0 if fgcolor.blue() > 127 else 255, 
            )

            qp.setPen(QtGui.QPen(bgcolor, 0, QtCore.Qt.SolidLine))
            qp.setBrush(QtCore.Qt.NoBrush)
            qp.drawPath(self._shape)

            qp.setPen(QtGui.QPen(option.palette.windowText(), 0, QtCore.Qt.DashLine))
            qp.setBrush(QtCore.Qt.NoBrush)
            qp.drawPath(self._shape)