bsdo64
bsdo64

Reputation: 225

Mouse hit detection with QPainterPath in QGraphicsItem

I implementing custom chart. But I stucked with mouse hitting detection with QPainterPath.

I tried with graphicsitem's shape(), boundingRect(). but that only checks rough shape of boundary.

I want to check mouse hit system with exact position on QPainterPath path instance. But seems to no api like that functionality.

My app's QGraphicsScene is set with same coordinate with QGraphicsView in view's resizeEvent().

scene: MyScene = self.scene()
scene.setSceneRect(self.rect().x(), self.rect().y(),
                   self.rect().width(), self.rect().height())

At the same time, my plot QGraphicsItem scales by QTransform.

plot: QGraphicsItem = scene.plot
trans = QTransform()
data = plot.df['data']
data = data - data.min()
data_max = data.max()
data_min = data.min()
trans.scale(self.width() / len(data),
            self.height() / (data_max - data_min))
plot.trans = trans
plot.setTransform(trans)

And in the MyScene, add rect item mouse_rec. So, I check mouse_rec and plot item's path with mouse_rec.collidesWithPath(path)

It just works only with original path.

Here is all code. Just copy and paste, you could run it.

Red plot is original path and yellow plot is scaled path. Mouse hit check is only works with red plot...

import numpy
import pandas

from PyQt5 import QtGui
from PyQt5.QtCore import Qt, QRectF, QRect
from PyQt5.QtGui import QRadialGradient, QGradient, QPen, QPainterPath, QTransform, QPainter, QColor
from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QGraphicsSceneMouseEvent, QGraphicsItem, \
    QStyleOptionGraphicsItem, QWidget, QGraphicsRectItem


class MyItem(QGraphicsItem):
    def __init__(self, df, parent=None):
        QGraphicsItem.__init__(self, parent)
        self.num = 1
        self.df = df
        self.path = QPainterPath()
        self.trans = QTransform()
        self.cached = False
        self.printed = False
        self.setZValue(0)

    def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionGraphicsItem', widget: QWidget = ...):
        data = self.df['data']
        data = data - data.min()
        data_max = data.max()
        data_min = data.min()

        if not self.cached:
            for i in range(data.size - 1):
                self.path.moveTo(i, data[i])
                self.path.lineTo(i+1, data[i+1])

            self.cached = True

        pen = QPen(Qt.white)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.drawRect(0, 0, data.size, data_max - data_min)

        pen.setColor(Qt.yellow)
        painter.setPen(pen)
        painter.drawPath(self.path)

        if not self.printed:
            rec_item = self.scene().addPath(self.path, QPen(Qt.red))
            rec_item.setZValue(-10)
            self.printed = True

    def boundingRect(self):
        data = self.df['data']
        data_max = data.max()
        data_min = data.min()

        return QRectF(0, 0, data.size, data_max - data_min)


class MyScene(QGraphicsScene):
    def __init__(self, data, parent=None):
        QGraphicsScene.__init__(self, parent)
        self.data = data
        self.mouse_rect = QGraphicsRectItem()
        self.plot: MyItem(data) = None
        self.bounding_rect = QGraphicsRectItem()
        self.setBackgroundBrush(QColor('#14161f'))

        self.addItem(self.bounding_rect)
        self.printed = False

    def mouseMoveEvent(self, event: 'QGraphicsSceneMouseEvent'):
        print()

        print("rec rect : ", self.mouse_rect.rect())
        print("Scene rect : ", self.sceneRect())
        print("ItemBounding rect : ", self.itemsBoundingRect())
        print("transform : ", self.plot.transform().m11(), ", ", self.plot.transform().m22())
        item = self.itemAt(event.scenePos(), self.plot.transform())

        if item and isinstance(item, MyItem):
            print()
            print('collides path : ', self.mouse_rect.collidesWithPath(item.path))
            print('collides item : ', self.mouse_rect.collidesWithItem(item))

        super().mouseMoveEvent(event)

    def print_bound(self, rect):
        self.bounding_rect.setPen(QPen(Qt.green))
        self.bounding_rect.setRect(rect.x() + 5, rect.y() + 5,
                                   rect.width() - 10, rect.height() - 10)


class MyView(QGraphicsView):
    def __init__(self, data, parent=None):
        QGraphicsView.__init__(self, parent)
        self.data = data
        self.setMouseTracking(True)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    def wheelEvent(self, event: QtGui.QWheelEvent):
        print("pixel / Data : {}".format(self.width() / len(self.data)))

    def resizeEvent(self, event: QtGui.QResizeEvent):
        scene: MyScene = self.scene()
        scene.setSceneRect(self.rect().x(), self.rect().y(),
                           self.rect().width(), self.rect().height())

        scene.print_bound(self.rect())

        plot: QGraphicsItem = scene.plot
        trans = QTransform()
        data = plot.df['data']
        data = data - data.min()
        data_max = data.max()
        data_min = data.min()
        trans.scale(self.width() / len(data),
                    self.height() / (data_max - data_min))
        plot.trans = trans
        plot.setTransform(trans)

    def mouseMoveEvent(self, event: QtGui.QMouseEvent):
        mouse_rect: QGraphicsRectItem = self.scene().mouse_rect
        mouse_rect.setRect(event.pos().x() - 2, event.pos().y() - 2, 4, 4)

        super().mouseMoveEvent(event)


if __name__ == '__main__':
    df = pandas.DataFrame({'data': numpy.random.randint(0, 20, 50)})

    app = QApplication([])
    scene = MyScene(df)
    view = MyView(df)
    view.setScene(scene)

    rec = QGraphicsRectItem(-2, -2, 4, 4)
    rec.setPen(Qt.white)
    scene.mouse_rect = rec
    scene.addItem(rec)

    plot = MyItem(df)
    scene.addItem(plot)
    scene.plot = plot

    view.show()

    app.exec_()

Any idea checking mouse point with path ?? I first tried custom math function calculating [point <-> line] distance, but that need much time and making lagging app..

I will make not only line plot but also bar, area, points, candlestick plot.. Is there any idea to solve this problem?

Upvotes: 0

Views: 800

Answers (1)

eyllanesc
eyllanesc

Reputation: 244132

You have to convert the position of the path with respect to the item that is scaled to the position relative to the scene using mapToScene():

if item and isinstance(item, MyItem):
    print('collides path : ', self.mouse_rect.collidesWithPath(item.mapToScene(item.path)))
    print('collides item : ', self.mouse_rect.collidesWithItem(item))

Upvotes: 2

Related Questions