2

I am currently learning how to write a GUI in PyQt5 that enable user to draw lines and rectangles in a QGrapchisView Scene and sets a QWebEngineView Widget containing a folium map as background.

Issue: In order to synchronize QGrapchisView Scene and folium map pan and zoom, I use a Event Filter that passes GraphicsScene events to QWebEngineView Widget.

The QEvent.GraphicsSceneWheel event is passed as expected, but the QEvent.GraphicsSceneMouseRelease, QEvent.GraphicsSceneMousePress and QEvent.GraphicsSceneMouseMove are not pass to the QWebEngineView Widget.

Expected behavior: Selected GraphicsScene events are pass to the QWebEngineView Widget, and it enables to synchronize pan and zoom for both Widgets.

What I have tried so far:

import folium
import sys
from PyQt5 import QtGui, QtCore, QtWidgets, QtWebEngineWidgets
import io

class eventFilterClass(QtCore.QObject):
    def __init__(self, sender, receiver, gv):
        super(eventFilterClass, self).__init__()
        self.gv = gv
        self.m_sender = sender
        self.m_receiver = receiver
        self.m_sender.installEventFilter(self)

    def eventFilter(self, obj, event):
        if self.m_sender is obj:
            if event.type() == QtCore.QEvent.GraphicsSceneMousePress:
                if event.button() == QtCore.Qt.MiddleButton:
                    new_event = QtGui.QMouseEvent(int(2), self.gv.mapFromScene(event.scenePos()),
                                                  self.gv.mapFromScene(event.scenePos()), event.screenPos(), event.button(),
                                                  event.buttons(), event.modifiers(), event.source())
                    QtCore.QCoreApplication.postEvent(self.m_receiver.focusProxy(), new_event)
                    return True

            elif event.type() == QtCore.QEvent.GraphicsSceneMouseMove:
                if event.buttons() == QtCore.Qt.NoButton:
                    new_event = QtGui.QMouseEvent(int(5), self.gv.mapFromScene(event.scenePos()),self.gv.mapFromScene(event.scenePos()), event.screenPos(), event.button(), event.buttons(), event.modifiers(), event.source())
                    QtCore.QCoreApplication.postEvent(self.m_receiver.focusProxy(), new_event)
                    return True

            elif event.type() == QtCore.QEvent.GraphicsSceneMouseRelease:
                print('Release!')
                if event.button() == QtCore.Qt.MiddleButton:
                    new_event = QtGui.QMouseEvent(int(3), self.gv.mapFromScene(event.scenePos()),
                                                  self.gv.mapFromScene(event.scenePos()), event.screenPos(), event.button(),
                                                  event.buttons(), event.modifiers(), event.source())
                    QtCore.QCoreApplication.postEvent(self.m_receiver.focusProxy(), new_event)

            elif event.type() == QtCore.QEvent.GraphicsSceneWheel:
                new_event = QtGui.QWheelEvent(self.gv.mapFromScene(event.scenePos()), event.screenPos(), QtCore.QPoint(), QtCore.QPoint(0, event.delta()),event.buttons(), event.modifiers(), int(0), False, int(0))
                QtCore.QCoreApplication.postEvent(self.m_receiver.focusProxy(), new_event)
                return True
        return False

class GraphicsView(QtWidgets.QGraphicsView):
    def __init__(self, parent, scene=None):
        super(GraphicsView, self).__init__(scene, parent)
        "VARIABLES INICIALES"
        self.pos_init_class = None
        self.scale_factor = 1.25
        "ACTIVAR TRACKING DE POSICION DE MOUSE"
        self.setMouseTracking(True)
        "REMOVER BARRAS DE SCROLL"
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        "ASIGNAR ANCLA PARA HACER ZOOM SOBRE EL MISMO PUNTO"
        self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)

    def wheelEvent(self, event):
        if event.angleDelta().y() > 0:
            self.scale(self.scale_factor, self.scale_factor)
        else:
            self.scale(1 / self.scale_factor, 1 / self.scale_factor)
        super(GraphicsView, self).wheelEvent(event)

    def mousePressEvent(self, event):
        pos = self.mapToScene(event.pos())
        "PAN"
        if event.button() == QtCore.Qt.MiddleButton:
            self.pos_init_class = pos
            QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ClosedHandCursor)
            super(GraphicsView, self).mousePressEvent(event)
            return
        super(GraphicsView, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        pos = self.mapToScene(event.pos())
        "PAN"
        if self.pos_init_class:
            delta = self.pos_init_class - self.mapToScene(event.pos())
            r = self.mapToScene(self.viewport().rect()).boundingRect()
            self.setSceneRect(r.translated(delta))
            return
        super(GraphicsView, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        "PAN"
        if self.pos_init_class and event.button() == QtCore.Qt.MiddleButton:
            self.pos_init_class = None
            QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor)
        super(GraphicsView, self).mouseReleaseEvent(event)

class Ui_MainWindow(object):
    def __init__(self):
        super(Ui_MainWindow, self).__init__()

    def zoom_ext(self):
        "Zoom extent"
        x_range, y_range, h_range, w_range = self.graphicsView.scene().itemsBoundingRect().getRect()
        rect_scene = QtCore.QRectF(x_range, y_range, h_range, w_range)
        self.graphicsView.setSceneRect(rect_scene)
        unity = self.graphicsView.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
        self.graphicsView.scale(1 / unity.width(), 1 / unity.height())
        viewrect = self.graphicsView.viewport().rect()
        scenerect = self.graphicsView.transform().mapRect(rect_scene)
        factor = min(viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height())
        self.graphicsView.scale(factor, factor)

    def draw_lines(self):
        cancha = QtGui.QPolygonF([
            QtCore.QPointF(721383.8266, -9678885.4514),
            QtCore.QPointF(721404.5488, -9678961.6564),
            QtCore.QPointF(721453.4389, -9678948.7816),
            QtCore.QPointF(721433.20288, -9678871.92070)])
        self.graphicsView.scene().addPolygon(cancha, QtGui.QPen(QtCore.Qt.red, 2.5, QtCore.Qt.SolidLine))
        caseta = QtGui.QPolygonF([
            QtCore.QPointF(721455.8594, -9678925.4517),
            QtCore.QPointF(721492.5411, -9678915.2403),
            QtCore.QPointF(721498.3404, -9678937.9702),
            QtCore.QPointF(721461.7904, -9678947.5050)])
        self.graphicsView.scene().addPolygon(caseta, QtGui.QPen(QtCore.Qt.red, 2.5, QtCore.Qt.SolidLine))

    def setupUi(self, MainWindow):
        "CENTRAL WIDGET"
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        MainWindow.setCentralWidget(self.centralwidget)
        MainWindow.resize(1000, 1000)

        "WEB MAP"
        self.w = QtWebEngineWidgets.QWebEngineView(self.centralwidget)
        self.w.setGeometry(QtCore.QRect(0, 0, 1000, 1000))

        "QGRAPCHISVIEW SCENE"
        self.graphicsView = GraphicsView(parent=self.centralwidget)
        self.scene = QtWidgets.QGraphicsScene(parent=self.graphicsView)
        self.graphicsView.setScene(self.scene)
        self.graphicsView.setGeometry(QtCore.QRect(0, 0, 1000, 1000))
        self.graphicsView.setStyleSheet("background:transparent;")

        "Draw LINES"
        self.draw_lines()
        "ZOOM EXT"
        self.zoom_ext()

        "AGREAGR MAPA"
        self.m = folium.Map(tiles='http://www.google.cn/maps/vt?lyrs=s@189&gl=cn&x={x}&y={y}&z={z}', attr='Google Satellite', zoomSnap=0.0, wheelDebounceTime=-1,
                            wheelPxPerZoomLevel=105, prefer_canvas=True, control_scale=True, max_zoom=100000,
                            zoomControl=False)
        sw, ne = (-2.903683906544795, -79.00835706455769), (-2.9026234455284583, -79.00729860799157)
        self.m.fit_bounds([sw, ne], padding=(0, 0))
        data = io.BytesIO()
        self.m.save(data, close_file=False)
        self.w.setHtml(data.getvalue().decode())

        "APLICAR FILTER"
        self.efc = eventFilterClass(sender=self.graphicsView.scene(), receiver=self.w, gv=self.graphicsView)

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

Question:

Is there any other way of accomplishing this?

R. dV
  • 416
  • 1
  • 3
  • 15
MBV
  • 591
  • 3
  • 17
  • From what I understand you is that you want the mouse events of the upper widget (QGraphicsView) to be transmitted to the lower widget (QWebEngineView) as well, am I correct? – eyllanesc Jul 16 '20 at 19:20
  • Hi eyllanesc, yes that is correct. The goal is to have a map background that pans and zooms synchronically with the content of the QGrapchisView Scene. I believe that passing mouse events from the top widget to the bottom widget is a way to accomplish this, but I am open to any suggestions. – MBV Jul 16 '20 at 19:29

1 Answers1

3

It appears that the OP tries to create the events by taking custom parameters unnecessarily. In my solution I propose to just copy the parameters:

import io
import sys

from PyQt5 import QtGui, QtCore, QtWidgets, QtWebEngineWidgets

import folium


class ReplyEvents(QtCore.QObject):
    def __init__(self, sender, receiver, parent=None):
        super().__init__(parent)
        self._sender = sender
        self._receiver = receiver
        self._sender.installEventFilter(self)

    def eventFilter(self, obj, event):
        if obj is self._sender:
            new_event = None
            if event.type() in (
                QtCore.QEvent.MouseButtonPress,
                QtCore.QEvent.MouseButtonRelease,
                QtCore.QEvent.MouseButtonDblClick,
                QtCore.QEvent.MouseMove,
            ):
                new_event = QtGui.QMouseEvent(
                    event.type(),
                    event.pos(),
                    event.button(),
                    event.buttons(),
                    event.modifiers(),
                )

            elif event.type() == QtCore.QEvent.Wheel:
                new_event = QtGui.QWheelEvent(
                    event.pos(),
                    event.globalPosition(),
                    event.pixelDelta(),
                    event.angleDelta(),
                    event.buttons(),
                    event.modifiers(),
                    event.phase(),
                    event.inverted(),
                )
            if new_event:
                QtCore.QCoreApplication.postEvent(self._receiver, new_event)
        return super().eventFilter(obj, event)


class GraphicsView(QtWidgets.QGraphicsView):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.scale_factor = 1.25
        self.pos_init_class = QtCore.QPointF()

        scene = QtWidgets.QGraphicsScene(self)
        self.setScene(scene)

        self.setStyleSheet("background:transparent;")
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)

        self.setMouseTracking(True)

    def wheelEvent(self, event):
        if event.angleDelta().y() > 0:
            self.scale(self.scale_factor, self.scale_factor)
        else:
            self.scale(1 / self.scale_factor, 1 / self.scale_factor)
        super().wheelEvent(event)

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.MiddleButton:
            self.pos_init_class = self.mapToScene(event.pos())
            QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ClosedHandCursor)
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if not self.pos_init_class.isNull():
            delta = self.pos_init_class - self.mapToScene(event.pos())
            r = self.mapToScene(self.viewport().rect()).boundingRect()
            self.setSceneRect(r.translated(delta))
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if not self.pos_init_class.isNull() and event.button() == QtCore.Qt.MiddleButton:
            self.pos_init_class = QtCore.QPointF()
            QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor)
        super().mouseReleaseEvent(event)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.resize(1000, 1000)

        self.web_view = QtWebEngineWidgets.QWebEngineView(self)
        self.graphics_view = GraphicsView(self)

        geometry = QtCore.QRect(0, 0, 1000, 1000)

        self.web_view.setGeometry(geometry)
        self.graphics_view.setGeometry(geometry)

        self.draw_lines()
        self.zoom_ext()

        m = folium.Map(
            tiles="http://www.google.cn/maps/vt?lyrs=s@189&gl=cn&x={x}&y={y}&z={z}",
            attr="Google Satellite",
            zoomSnap=0.0,
            wheelDebounceTime=-1,
            wheelPxPerZoomLevel=105,
            prefer_canvas=True,
            control_scale=True,
            max_zoom=100000,
            zoomControl=False,
        )

        sw, ne = (
            (-2.903683906544795, -79.00835706455769),
            (-2.9026234455284583, -79.00729860799157),
        )
        m.fit_bounds([sw, ne], padding=(0, 0))
        data = io.BytesIO()
        m.save(data, close_file=False)
        self.web_view.setHtml(data.getvalue().decode())

        r = ReplyEvents(self.graphics_view.viewport(), self.web_view.focusProxy(), self)

    def draw_lines(self):
        cancha = QtGui.QPolygonF(
            [
                QtCore.QPointF(721383.8266, -9678885.4514),
                QtCore.QPointF(721404.5488, -9678961.6564),
                QtCore.QPointF(721453.4389, -9678948.7816),
                QtCore.QPointF(721433.20288, -9678871.92070),
            ]
        )
        self.graphics_view.scene().addPolygon(
            cancha, QtGui.QPen(QtCore.Qt.red, 2.5, QtCore.Qt.SolidLine)
        )
        caseta = QtGui.QPolygonF(
            [
                QtCore.QPointF(721455.8594, -9678925.4517),
                QtCore.QPointF(721492.5411, -9678915.2403),
                QtCore.QPointF(721498.3404, -9678937.9702),
                QtCore.QPointF(721461.7904, -9678947.5050),
            ]
        )
        self.graphics_view.scene().addPolygon(
            caseta, QtGui.QPen(QtCore.Qt.red, 2.5, QtCore.Qt.SolidLine)
        )

    def zoom_ext(self):
        "Zoom extent"
        x_range, y_range, h_range, w_range = (
            self.graphics_view.scene().itemsBoundingRect().getRect()
        )
        rect_scene = QtCore.QRectF(x_range, y_range, h_range, w_range)
        self.graphics_view.setSceneRect(rect_scene)
        unity = self.graphics_view.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
        self.graphics_view.scale(1 / unity.width(), 1 / unity.height())
        viewrect = self.graphics_view.viewport().rect()
        scenerect = self.graphics_view.transform().mapRect(rect_scene)
        factor = min(
            viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height()
        )
        self.graphics_view.scale(factor, factor)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • This solved the issue asked in this post, but I believe that this is not the solution I am looking for in the sense that mouse events(pan and zoom) do not retain canvas scale or position afterwards, thus the background map does not match the QGrapchisView Scene content scale and position so it does not accomplish the purpose of having a map reference to draw on. Anyways this was very helpful, Thank you for your time. – MBV Jul 16 '20 at 21:37
  • @MarceloBarrosVanegas You say: *This solved the issue asked in this post*, and what else do you want? well that is the objective of SO. If my answer resolved *the issue asked in this post* then you should mark it as correct – eyllanesc Jul 16 '20 at 21:40
  • Sorry, I had not noticed that I did not mark it as correct. I believed that passing mouse events from the QGraphicsView scene to the QWebEngineView was the way to accomplish a QGIS like canvas for the user to show or draw lines and rectangles in a map background as reference , but this solution does not maintained the same canvas space of the QGraphicsView Scene on the QWebEngineView (map position an scale to the map reference) after zoom or pan thus, as far is not a suitable solution for my ultimate goal. Thanks for your time. – MBV Jul 16 '20 at 22:00
  • @MarceloBarrosVanegas Why don't you use QML? So you can use Map and create the items. https://doc.qt.io/qt-5/qml-qtlocation-map.html – eyllanesc Jul 16 '20 at 22:06
  • Thank you eyllanesc, I will check this, I have not reach that part of the doc classes yet. Again thanks for your time. – MBV Jul 16 '20 at 23:00