1

I have created an SVG image of a map (trains) that needs to be interactive and when a section of the map is clicked, the colour will change and a method to control things (via a raspberry Pi) will be executed.

For connecting a button to a slot is simple and at a guess I tried:

self.path.clicked.connect(self.some_func)

Is is possible to click an SVG element and have it execute a method based on its given id like when a button is clicked?

Here is an MVCE:

test.svg

<svg height="100" width="100">
    <circle id="elem1" cx="50" cy="50" r="40"/>
</svg> 

svg_widget.py

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtXml import *
from PyQt5.QtSvg import *
import sys

class svgWidget(QSvgWidget):
    def __init__(self, parent=None):
        super(QSvgWidget, self).__init__(parent)

        self.display = QTextEdit()
        self.doc = QDomDocument("doc")

        fName = QFile("test.svg")

        if not fName.open(QIODevice.ReadOnly):
            print("Cannot open the File")
            exit(-1)

        if not self.doc.setContent(fName):
            print("Cannot parse the content")
            fName.close()
            exit(-1)
        fName.close()

        roots = self.doc.elementsByTagName("svg")

        if roots.size() < 1:
            print("Cannot find root")
            exit(-1)

        self.root = roots.at(0).toElement()
        self.path = self.root.firstChild().toElement()

        self.load(self.doc.toByteArray())

        # self.path.clicked.connect(self.some_func)

        self.layout = QVBoxLayout()
        self.setLayout(self.layout)

        QTimer.singleShot(2000, lambda: self.some_func()) 

    def some_func(self):
        print('setting fill')
        self.path.setAttribute("fill", "#44dd75")
        print('setting stroke')
        self.path.setAttribute("stroke", "#FF0000")
        print('setting stroke-width')
        self.path.setAttribute("stroke-width", "3")

        self.load(self.doc.toByteArray())

        self.execute_method()


    def execute_method(self):
        print('method executed')

if __name__ == "__main__":  
    app = QApplication(sys.argv)
    svgWidget = svgWidget()    
    svgWidget.show()                                                                                                                                                        
    sys.exit(app.exec_()) 
johnashu
  • 2,167
  • 4
  • 19
  • 44
  • 2
    No, there is nothing like that. You will have to implement everything yourself from scatch if want to use SVG. You would be much better off creating the map using Qt's [Graphics View Framework](https://doc.qt.io/qt-5/graphicsview.html). – ekhumoro Nov 28 '19 at 15:07
  • 1
    Actually you could extract the individual named elements from an SVG document (with QSvgRenderer) and then draw them independently (as clickable/selectable items, optionally in a QGraphicsView or similar). You don't really need to parse the SVG manually (as XML) either. Or, get the bounds of each element and create "clickable regions" of the SVG graphics item manually (by listening for mouse events for example). Would your SVG source support something like this (IOW, does it have named elements)? – Maxim Paperno Nov 29 '19 at 07:18
  • yes, each element in svg can use an `id="elem_name"` . And yes, that is exactly what I was hoping for, to me able to assign element ids to mouse click and hover events.. I have updated the svg to refelct this. – johnashu Nov 29 '19 at 08:29

1 Answers1

4

I found a solution via a friend of mine.

It was found on here on Stackoverflow (He thinks).

If anyone knows the origin, please edit and add the link for credit.

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_())
johnashu
  • 2,167
  • 4
  • 19
  • 44
  • 1
    This seems good. If you don't want to use the build-in selection modes, in the SvgItem init save the `bounds` and in mouse event handler(s) you can check if `self.bounds.contains(event.pos())`. And emit a clicked signal, for example. Note that if you want to emit on mouse release, you'll need to `event.accept()` the mouse press event in order to get the release one delivered. And check for `bounds.contains(event.pos())` in each one. – Maxim Paperno Nov 29 '19 at 09:28
  • Example was taken from [this question](https://stackoverflow.com/questions/53288926/qgraphicssvgitem-event-propagation-interactive-svg-viewer). – Adrien Aug 22 '22 at 12:37