沿QPainterPath获取点击点

时间:2018-12-27 19:38:09

标签: python pyside qgraphicsview qgraphicsscene qpainterpath

如何获得代表沿QPainterPath单击的点的百分比。例如,说我有一条线,如下图所示,用户单击了QPainterPath,由红点表示。我想记录该点沿路径下降的百分比。在这种情况下,由于该点位于75%左右,它将打印0.75。

这些是已知变量:

# QPainterPath
path = QPainterPath()
path.moveTo( QPointF(10.00, -10.00) )
path.cubicTo(
    QPointF(114.19, -10.00),
    QPointF(145.80, -150.00),
    QPointF(250.00, -150.00)
)

# User Clicked Point
QPointF(187.00, -130.00)

enter image description here

已更新!

我的目标是使用户能够单击路径并插入点。下面是我到目前为止的代码。您会在视频中看到在点之间添加点时似乎失败。只需单击路径即可插入一个点。

观看视频链接的错误:

https://youtu.be/nlWyZUIa7II

import sys
from PySide.QtGui import *
from PySide.QtCore import *
import random, math


class MyGraphicsView(QGraphicsView):
    def __init__(self):
        super(MyGraphicsView, self).__init__()
        self.setDragMode(QGraphicsView.RubberBandDrag)
        self.setCacheMode(QGraphicsView.CacheBackground)
        self.setHorizontalScrollBarPolicy( Qt.ScrollBarAlwaysOff )
        self.setVerticalScrollBarPolicy( Qt.ScrollBarAlwaysOff )


    def mousePressEvent(self,  event):
        item = self.itemAt(event.pos())
        if event.button() == Qt.LeftButton and isinstance(item, ConnectionItem):
            percentage = self.percentageByPoint(item.shape(), self.mapToScene(event.pos()))
            item.addKnotByPercent(percentage)
            event.accept()
        elif event.button() == Qt.MiddleButton:
            super(MyGraphicsView, self).mousePressEvent(event)


    # connection methods
    def percentageByPoint(self, path, point, precision=0.5, width=3.0):
        percentage = -1.0
        if path.contains(point):
            t = 0.0
            d = []
            while t <=100.0: 
                d.append(QVector2D(point - path.pointAtPercent(t/100.0)).length())
                t += precision
            percentage = d.index(min(d))*precision
        return percentage


class MyGraphicsScene(QGraphicsScene):
    def __init__(self,  parent):
        super(MyGraphicsScene,  self).__init__()
        self.setBackgroundBrush(QBrush(QColor(50,50,50)))


class KnotItem(QGraphicsEllipseItem):
    def __init__(self, parent=None,):
        super(self.__class__, self).__init__(parent)
        self.setAcceptHoverEvents(True)
        self.setFlag(self.ItemSendsScenePositionChanges, True)
        self.setFlag(self.ItemIsSelectable, True) # false
        self.setFlag(self.ItemIsMovable, True) # false
        self.setRect(-6, -6, 12, 12)

    # Overrides
    def paint(self, painter, option, widget=None):
        painter.save()
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setPen(QPen(QColor(30,30,30), 2, Qt.SolidLine))
        painter.setBrush(QBrush(QColor(255,30,30)))
        painter.drawEllipse(self.rect())    
        painter.restore()


    def itemChange(self, change, value):
        if change == self.ItemScenePositionHasChanged:
            if self.parentItem():
                self.parentItem().update()
        return super(self.__class__, self).itemChange(change, value)


    def boundingRect(self):
        rect = self.rect()
        rect.adjust(-1,-1,1,1)
        return rect


class ConnectionItem(QGraphicsPathItem):
    def __init__(self, startPoint, endPoint, parent=None):
        super(ConnectionItem,  self).__init__()
        self._hover = False
        self.setAcceptHoverEvents(True)
        self.setFlag( QGraphicsItem.ItemIsSelectable )
        self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
        self.setZValue(-100)

        self.startPoint = startPoint
        self.endPoint = endPoint
        self.knots = []
        self.update()


    def getBezierPath(self, points=[], curving=1.0):
        # Calculate Bezier Line
        path = QPainterPath()
        curving = 1.0 # range 0-1

        if len(points) < 2:
            return path

        path.moveTo(points[0])

        for i in range(len(points)-1):
            startPoint = points[i]
            endPoint = points[i+1]

            # use distance as mult, closer the nodes less the bezier
            dist = math.hypot(endPoint.x() - startPoint.x(), endPoint.y() - startPoint.y())

            # multiply distance by 0.375 
            offset = dist * 0.375 * curving
            ctrlPt1 = startPoint + QPointF(offset,0);
            ctrlPt2 = endPoint + QPointF(-offset,0);

            # print startPoint, ctrlPt1, ctrlPt2, endPoint
            path.cubicTo(ctrlPt1, ctrlPt2, endPoint)

        return path


    def drawPath(self, pos=None):
        # Calculate Bezier Line
        points = [self.startPoint]
        for k in self.knots:
            points.append(k.scenePos())
        points.append(self.endPoint)
        path = self.getBezierPath(points)
        self.setPath(path)


    def update(self):
        super(self.__class__, self).update()
        self.drawPath()


    def paint(self, painter, option, widget):
        painter.setRenderHints( QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing, True )
        pen = QPen(QColor(170,170,170), 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        if self.isSelected():
            pen.setColor(QColor(255, 255, 255))
        elif self.hover:
            pen.setColor(QColor(255, 30, 30))
        painter.setPen(pen)
        painter.drawPath(self.path())


    def shape(self):
        '''
        Description:
            This is super important for creating a more accurate path used for 
            collision detection by cursor.
        '''
        qp = QPainterPathStroker()
        qp.setWidth(15)
        qp.setCapStyle(Qt.SquareCap)
        return qp.createStroke(self.path())


    def hoverEnterEvent(self, event):
        self.hover = True
        self.update()
        super(self.__class__, self).hoverEnterEvent(event)


    def hoverLeaveEvent(self, event):
        self.hover = False
        self.update()
        super(self.__class__, self).hoverEnterEvent(event)


    def addKnot(self, pos=QPointF(0,0)):
        '''
        Description:
            Add not based on current location of cursor or inbetween points on path.
        '''
        knotItem = KnotItem(parent=self)
        knotItem.setPos(pos)
        self.knots.append(knotItem)
        self.update()


    def addKnotByPercent(self, percentage=0.0):
        '''
        Description:
            The percentage value should be between 0.0 and 100.0. This value
            determines the location of the point and it's index in the knots list.
        '''
        if percentage < 0.0 or percentage > 100.0:
            return

        # add item
        pos = self.shape().pointAtPercent(percentage*.01)
        knotItem = KnotItem(parent=self)
        knotItem.setPos(pos)

        index = int(len(self.knots) * (percentage*.01))
        print len(self.knots), (percentage), index
        self.knots.insert(index, knotItem)
        self.update()


    # properties
    @property
    def hover(self):
        return self._hover

    @hover.setter
    def hover(self, value=False):
        self._hover = value
        self.update()


class MyMainWindow(QMainWindow):

    def __init__(self):
        super(MyMainWindow, self).__init__()
        self.setWindowTitle("Test")
        self.resize(800,600)

        self.gv = MyGraphicsView()
        self.gv.setScene(MyGraphicsScene(self))
        self.btnReset = QPushButton('Reset')

        lay_main = QVBoxLayout()
        lay_main.addWidget(self.btnReset)
        lay_main.addWidget(self.gv)
        widget_main = QWidget()
        widget_main.setLayout(lay_main)
        self.setCentralWidget(widget_main)

        self.populate()

        # connect
        self.btnReset.clicked.connect(self.populate)


    def populate(self):
        scene = self.gv.scene()
        for x in scene.items():
            scene.removeItem(x)
            del x

        con = ConnectionItem(QPointF(-150,150), QPointF(250,-150))
        scene.addItem(con)


def main():
    app = QApplication(sys.argv)
    ex = MyMainWindow()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

1 个答案:

答案 0 :(得分:2)

一个可能的解决方案是使用pointAtPercent(),该函数返回给定点的百分比,并计算到该点的距离,找到最小索引,然后将其乘以步长。但是为此必须进行搜索优化,因为先前的算法即使在路径之外也可以在任何点使用。这种情况下的想法是,使用QPainterPathStroker在某个区域使用QPainterPath,并验证该点是否属于该点,以及是否该点不在QPainterPath之外。

C ++

#include <QtGui>

static qreal percentageByPoint(const QPainterPath & path, const QPointF & p, qreal precision=0.5, qreal width=3.0){
    qreal percentage = -1;
    QPainterPathStroker stroker;
    stroker.setWidth(width);
    QPainterPath strokepath = stroker.createStroke(path);
    if(strokepath.contains(p)){
        std::vector<qreal> d;
        qreal t=0.0;
        while(t<=100.0){
            d.push_back(QVector2D(p - path.pointAtPercent(t/100)).length());
            t+= precision;
        }
        std::vector<qreal>::iterator result = std::min_element(d.begin(), d.end());
        int j= std::distance(d.begin(), result);
        percentage = j*precision;
    }
    return percentage;
}

int main(int argc, char *argv[])
{
    Q_UNUSED(argc)
    Q_UNUSED(argv)

    QPainterPath path;
    path.moveTo( QPointF(10.00, -10.00) );
    path.cubicTo(
                QPointF(114.19, -10.00),
                QPointF(145.80, -150.00),
                QPointF(250.00, -150.00)
                );

    // User Clicked Point
    QPointF p(187.00, -130.00);
    qreal percentage = percentageByPoint(path, p);
    qDebug() << percentage;

    return 0;
}

python:

def percentageByPoint(path, point, precision=0.5, width=3.0):
    percentage = -1.0
    stroker = QtGui.QPainterPathStroker()
    stroker.setWidth(width)
    strokepath = stroker.createStroke(path) 
    if strokepath.contains(point):
        t = 0.0
        d = []
        while t <=100.0: 
            d.append(QtGui.QVector2D(point - path.pointAtPercent(t/100)).length())
            t += precision
        percentage = d.index(min(d))*precision
    return percentage

if __name__ == '__main__':
    path = QtGui.QPainterPath()
    path.moveTo(QtCore.QPointF(10.00, -10.00) )
    path.cubicTo(
        QtCore.QPointF(114.19, -10.00),
        QtCore.QPointF(145.80, -150.00),
        QtCore.QPointF(250.00, -150.00)
        )

    point = QtCore.QPointF(187.00, -130.00)
    percentage = percentageByPoint(path, point)
    print(percentage)

输出:

76.5

您必须在项目中执行此操作,而不是在QGraphicsView中实现逻辑,然后在更新路径时,必须相对于百分比对点进行排序。

import math
from PySide import QtCore, QtGui
from functools import partial

class MyGraphicsView(QtGui.QGraphicsView):
    def __init__(self):
        super(MyGraphicsView, self).__init__()
        self.setDragMode(QtGui.QGraphicsView.RubberBandDrag)
        self.setCacheMode(QtGui.QGraphicsView.CacheBackground)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        scene = QtGui.QGraphicsScene(self)
        scene.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(50,50,50)))
        self.setScene(scene)

class KnotItem(QtGui.QGraphicsEllipseItem):
    def __init__(self, parent=None,):
        super(self.__class__, self).__init__(parent)
        self.setAcceptHoverEvents(True)
        self.setFlag(self.ItemSendsScenePositionChanges, True)
        self.setFlag(self.ItemIsSelectable, True)
        self.setFlag(self.ItemIsMovable, True) 
        self.setRect(-6, -6, 12, 12)
        self.setPen(QtGui.QPen(QtGui.QColor(30,30,30), 2, QtCore.Qt.SolidLine))
        self.setBrush(QtGui.QBrush(QtGui.QColor(255,30,30)))

    def itemChange(self, change, value):
        if change == self.ItemScenePositionHasChanged:
            if isinstance(self.parentItem(), ConnectionItem):
                self.parentItem().updatePath()
                # QtCore.QTimer.singleShot(60, partial(self.parentItem().setSelected,False))
        return super(self.__class__, self).itemChange(change, value)


class ConnectionItem(QtGui.QGraphicsPathItem):
    def __init__(self, startPoint, endPoint, parent=None):
        super(ConnectionItem, self).__init__(parent)
        self._start_point = startPoint
        self._end_point = endPoint

        self._hover = False
        self.setAcceptHoverEvents(True)
        self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable )
        self.setFlag(QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
        self.setZValue(-100)
        self.updatePath()

    def updatePath(self):
        p = [self._start_point]
        for children in self.childItems():
            if isinstance(children, KnotItem):
                p.append(children.pos())
        p.append(self._end_point)
        v = sorted(p, key=partial(ConnectionItem.percentageByPoint, self.path()))
        self.setPath(ConnectionItem.getBezierPath(v))

    def paint(self, painter, option, widget):
        painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform | QtGui.QPainter.HighQualityAntialiasing, True )
        pen = QtGui.QPen(QtGui.QColor(170,170,170), 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
        if self.isSelected():
            pen.setColor(QtGui.QColor(255, 255, 255))
        elif self._hover:
            pen.setColor(QtGui.QColor(255, 30, 30))
        painter.setPen(pen)
        painter.drawPath(self.path())

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            item = KnotItem(parent=self)
            item.setPos(event.pos())

    def hoverEnterEvent(self, event):
        self._hover = True
        self.update()
        super(self.__class__, self).hoverEnterEvent(event)

    def hoverLeaveEvent(self, event):
        self._hover = False
        self.update()
        super(self.__class__, self).hoverEnterEvent(event)

    def shape(self):
        qp = QtGui.QPainterPathStroker()
        qp.setWidth(15)
        qp.setCapStyle(QtCore.Qt.SquareCap)
        return qp.createStroke(self.path())

    @staticmethod
    def getBezierPath(points=[], curving=1.0):
        # Calculate Bezier Line
        path = QtGui.QPainterPath()
        curving = 1.0 # range 0-1
        if len(points) < 2:
            return path
        path.moveTo(points[0])
        for i in range(len(points)-1):
            startPoint = points[i]
            endPoint = points[i+1]
            # use distance as mult, closer the nodes less the bezier
            dist = math.hypot(endPoint.x() - startPoint.x(), endPoint.y() - startPoint.y())
            # multiply distance by 0.375 
            offset = dist * 0.375 * curving
            ctrlPt1 = startPoint + QtCore.QPointF(offset,0);
            ctrlPt2 = endPoint + QtCore.QPointF(-offset,0);
            # print startPoint, ctrlPt1, ctrlPt2, endPoint
            path.cubicTo(ctrlPt1, ctrlPt2, endPoint)
        return path

    @staticmethod
    def percentageByPoint(path, point, precision=0.5):
        t = 0.0
        d = []
        while t <=100.0: 
            d.append(QtGui.QVector2D(point - path.pointAtPercent(t/100.0)).length())
            t += precision
            percentage = d.index(min(d))*precision
        return percentage


class MyMainWindow(QtGui.QMainWindow):
    def __init__(self):
        super(MyMainWindow, self).__init__()
        central_widget = QtGui.QWidget()
        self.setCentralWidget(central_widget)
        button = QtGui.QPushButton("Reset")
        self._view = MyGraphicsView()
        button.clicked.connect(self.reset)

        lay = QtGui.QVBoxLayout(central_widget)
        lay.addWidget(button)
        lay.addWidget(self._view)
        self.resize(640, 480)
        self.reset()

    @QtCore.Slot()
    def reset(self):
        self._view.scene().clear()
        it = ConnectionItem(QtCore.QPointF(-150,150), QtCore.QPointF(250,-150))
        self._view.scene().addItem(it)

def main():
    import sys
    app =QtGui.QApplication(sys.argv)
    ex = MyMainWindow()
    ex.show()
    sys.exit(app.exec_())

main()