I'm learning PySide6, and my goal is to use Qt3DCore to draw some 3D wireframe entities (points, lines, bezier curves ..uso..), in a 3D viewer. I like that high level interface.
I used the nice basic torus sphere example (https://doc.qt.io/qtforpython/examples/example_3d__simple3d.html) of the documentation, and added some QLineF lines, but I don't see the lines in the viewer scene.
I think it's because I don't have the correct graphics setting for such wireframe entities... But I don't find any other example or documentation on the subject :-(.
As requested by musicamante, to be as simple as possible, I add the QT example with only 2 modifications : QLine import, and QLine statement in line 121 :
"""PySide6 port of the qt3d/simple-cpp example from Qt v5.x"""
import sys
from PySide6.QtCore import (Property, QObject, QPropertyAnimation, Signal,QPoint,QLine,QLineF)
from PySide6.QtGui import (QGuiApplication, QMatrix4x4, QQuaternion, QVector3D)
from PySide6.Qt3DCore import (Qt3DCore)
from PySide6.Qt3DExtras import (Qt3DExtras)
from PySide6.Qt3DRender import (Qt3DRender)
class OrbitTransformController(QObject):
def __init__(self, parent):
super().__init__(parent)
self._target = None
self._matrix = QMatrix4x4()
self._radius = 1
self._angle = 0
def setTarget(self, t):
self._target = t
def getTarget(self):
return self._target
def setRadius(self, radius):
if self._radius != radius:
self._radius = radius
self.updateMatrix()
self.radiusChanged.emit()
def getRadius(self):
return self._radius
def setAngle(self, angle):
if self._angle != angle:
self._angle = angle
self.updateMatrix()
self.angleChanged.emit()
def getAngle(self):
return self._angle
def updateMatrix(self):
self._matrix.setToIdentity()
self._matrix.rotate(self._angle, QVector3D(0, 1, 0))
self._matrix.translate(self._radius, 0, 0)
if self._target is not None:
self._target.setMatrix(self._matrix)
angleChanged = Signal()
radiusChanged = Signal()
angle = Property(float, getAngle, setAngle, notify=angleChanged)
radius = Property(float, getRadius, setRadius, notify=radiusChanged)
class Window(Qt3DExtras.Qt3DWindow):
def __init__(self):
super().__init__()
# Camera
self.camera().lens().setPerspectiveProjection(45, 16 / 9, 0.1, 1000)
self.camera().setPosition(QVector3D(0, 0, 40))
self.camera().setViewCenter(QVector3D(0, 0, 0))
# For camera controls
self.createScene()
self.camController = Qt3DExtras.QOrbitCameraController(self.rootEntity)
self.camController.setLinearSpeed(50)
self.camController.setLookSpeed(180)
self.camController.setCamera(self.camera())
self.setRootEntity(self.rootEntity)
def createScene(self):
# Root entity
self.rootEntity = Qt3DCore.QEntity()
# Material
self.material = Qt3DExtras.QPhongMaterial(self.rootEntity)
# Torus
self.torusEntity = Qt3DCore.QEntity(self.rootEntity)
self.torusMesh = Qt3DExtras.QTorusMesh()
self.torusMesh.setRadius(5)
self.torusMesh.setMinorRadius(1)
self.torusMesh.setRings(100)
self.torusMesh.setSlices(20)
self.torusTransform = Qt3DCore.QTransform()
self.torusTransform.setScale3D(QVector3D(1.5, 1, 0.5))
self.torusTransform.setRotation(QQuaternion.fromAxisAndAngle(QVector3D(1, 0, 0), 45))
self.torusEntity.addComponent(self.torusMesh)
self.torusEntity.addComponent(self.torusTransform)
self.torusEntity.addComponent(self.material)
# Sphere
self.sphereEntity = Qt3DCore.QEntity(self.rootEntity)
self.sphereMesh = Qt3DExtras.QSphereMesh()
self.sphereMesh.setRadius(3)
self.sphereTransform = Qt3DCore.QTransform()
self.controller = OrbitTransformController(self.sphereTransform)
self.controller.setTarget(self.sphereTransform)
self.controller.setRadius(20)
self.sphereRotateTransformAnimation = QPropertyAnimation(self.sphereTransform)
self.sphereRotateTransformAnimation.setTargetObject(self.controller)
self.sphereRotateTransformAnimation.setPropertyName(b"angle")
self.sphereRotateTransformAnimation.setStartValue(0)
self.sphereRotateTransformAnimation.setEndValue(360)
self.sphereRotateTransformAnimation.setDuration(10000)
self.sphereRotateTransformAnimation.setLoopCount(-1)
self.sphereRotateTransformAnimation.start()
self.sphereEntity.addComponent(self.sphereMesh)
self.sphereEntity.addComponent(self.sphereTransform)
self.sphereEntity.addComponent(self.material)
self.lineEntity = QLineF(-2000.0, -2000.0, 2000.0, 2000.0)
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
view = Window()
view.show()
sys.exit(app.exec
())
Related
I am trying to draw colliders of Box2D. Now I have only physics in this example without graphics for simplicity. The DrawSegment() method must be called to print hello. I inherited the DebugDrawer class from the b2Draw class:
debug_drawer.py
from Box2D import b2Draw
class DebugDrawer(b2Draw):
def DrawSegment(self, p1, p2, color):
print("hello")
def DrawSolidPolygon(self, vertices, color):
pass
def DrawPoint(self, p, size, color):
pass
def DrawPolygon(self, vertices, color):
pass
def DrawCircle(self, center, radius, color, drawwidth=1):
pass
def DrawSolidCircle(self, center, radius, axis, color):
pass
def DrawTransform(self, xf):
pass
I created one object with the box shape. I have the animationLoop() method that I call with timer. Inside of the animationLoop() method I the self.world.Step() method and I call the paintGL() method by calling the self.update() method. Inside of the paintGL() method I call the self.world.DrawDebugData() method. I expect that the DrawSegment() will be called but it does not happen.
widget.py
from Box2D import (b2_staticBody, b2Body, b2BodyDef, b2FixtureDef,
b2PolygonShape, b2Vec2, b2World)
from OpenGL import GL as gl
from PyQt6.QtCore import QElapsedTimer, QSize, QTimer
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
from debug_drawer import DebugDrawer
class Widget(QOpenGLWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Box2D, OpenGL3, PyQt6")
self.setFixedSize(QSize(500, 500))
self.deltaTime = 0
self.WORLD_SCALE = 30.0
self.world = b2World(gravity=b2Vec2(0.0, 9.8))
def initializeGL(self):
gl.glClearColor(0.2, 0.2, 0.2, 1.0)
gl.glEnable(gl.GL_DEPTH_TEST)
self.debugDrawer = DebugDrawer()
self.world.renderer = self.debugDrawer
self.debugDrawer.flags = { 'drawShapes': True,
'drawJoints': True, 'drawAABBs': True, 'drawPairs': True }
# print(self.debugDrawer.flags)
shape = b2PolygonShape()
shape.SetAsBox(50.0 / self.WORLD_SCALE, 50.0 / self.WORLD_SCALE)
bodyDef = b2BodyDef()
bodyDef.type = b2_staticBody
self.body: b2Body = self.world.CreateBody(bodyDef)
fixtureDef = b2FixtureDef()
fixtureDef.shape = shape
fixtureDef.density = 2
self.body.CreateFixture(fixtureDef)
self.timer = QTimer()
self.timer.timeout.connect(self.animationLoop)
self.elapsedTimer = QElapsedTimer()
self.elapsedTimer.start()
self.timer.start(1000//60)
def paintGL(self):
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
self.world.DrawDebugData()
def resizeGL(self, w: int, h: int):
gl.glViewport(0, 0, w, h)
def animationLoop(self):
self.deltaTime = self.elapsedTimer.elapsed() / 1000.0
self.elapsedTimer.restart()
self.world.Step(self.deltaTime, 8, 3)
self.update()
main.py
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QSurfaceFormat
from PyQt6.QtWidgets import QApplication
from widget import Widget
def main():
QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseDesktopOpenGL)
app = QApplication(sys.argv)
format = QSurfaceFormat()
format.setSamples(8)
w = Widget()
w.setFormat(format)
w.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
I should use DrawPolygon to draw segments of colliders when I use boxes to draw borders around game objects. DrawSegment() will be called when an instance of b2EdgeShape is created:
edgeShape = b2EdgeShape()
edgeShape.vertices = [(0.0, 0.0), (1.0, 0.0)]
self.edgeBody: b2Body = self.world.CreateBody(bodyDef)
edgeFixtureDef = b2FixtureDef()
edgeFixtureDef.shape = edgeShape
edgeFixtureDef.density = 2
self.edgeBody.CreateFixture(edgeFixtureDef)
I'm trying to make fading looped rectangle area. I used base code from here
Just decided expand it.
Its just blinking rectangle, but I need smooth fade-in and fade-out effects. So I decided to make method which will calulate new opacity percent and set it to painter. But it doesnt work in cycle.
This is my class now
class HighlightRect(QFrame):
board_width = 400 # width of frame
board_height = 400 #height of frame
def __init__(self, parent, x, y, width=50, height=50, blink_speed=1000):
super().__init__(parent)
self.blink_speed = blink_speed
self.opacity_timer = self.blink_speed
self.board_height = self.parent().height()
self.board_width = self.parent().width()
self.square_height = height
self.square_width = width
self.highlight_x = x
self.highlight_y = y
#self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.timer_draw = QtCore.QTimer(self)
self.timer_draw.timeout.connect(self.draw)
self.timer_draw.start(self.blink_speed)
self.color = QtCore.Qt.red
self.is_draw = False
self.x_apple = 0
self.y_apple = 0
self.draw()
def blink(self, painter):
self.color = QtCore.Qt.red
while self.opacity_timer >= 0:
self.opacity_timer -= 1 / 10 # просто подбор
percents = round(int(self.opacity_timer / self.blink_speed * 100)/100, 1)
print(percents)
painter.setOpacity(percents)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
print ("Paint Event?")
if self.is_draw == True:
print ("Draw")
#self.color = QtCore.Qt.red
self.blink_thread = threading.Thread(name='background', target=lambda: self.blink(painter))
self.blink_thread.start()
else:
self.opacity_timer = self.blink_speed
print ("Do not draw")
self.color = QtCore.Qt.transparent
threading.SystemExit = SystemExit
painter.setPen(self.color)
painter.drawRect(self.rect)
def draw(self):
self.is_draw = not self.is_draw
self.rect = QRect(self.highlight_x, self.highlight_y, self.square_width, self.square_height)
self.update()
Changind of opacity inside blink function but outside while loop works as well, but its static. No changes. Changing opacity in loop isn't work.
Whats wrong?
Maybe somewhere here is another more correct way to get what I want?
One possible solution is to create a QProperty that handles opacity and then use QPropertyAnimation to make the change smooth.
import random
import sys
from PySide6.QtCore import Property, Signal, QPropertyAnimation, QRect, Qt
from PySide6.QtGui import QPainter
from PySide6.QtWidgets import QFrame, QApplication
class Board(QFrame):
rect_opacity_changed = Signal(name="rectOpacityChanged")
def __init__(self, parent=None):
super(Board, self).__init__(parent)
self._rect_opacity = 1.0
self._rect = QRect(0, 0, 50, 50)
self._opacity_animation = QPropertyAnimation(
targetObject=self, propertyName=b"rect_opacity", duration=3000
)
for p, v in ((0.0, 0.0), (0.3, 1.0), (0.7, 1.0), (1.0, 0.0)):
self._opacity_animation.setKeyValueAt(p, v)
self._opacity_animation.finished.connect(self.change)
self.change()
#Property(float, notify=rect_opacity_changed)
def rect_opacity(self):
return self._rect_opacity
#rect_opacity.setter
def rect_opacity(self, opacity):
self._rect_opacity = opacity
self.rect_opacity_changed.emit()
self.update()
def change(self):
x = random.randint(0, self.width() - self._rect.width())
y = random.randint(0, self.height() - self._rect.height())
self._rect.moveTo(x, y)
self._opacity_animation.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setOpacity(self.rect_opacity)
painter.setPen(Qt.red)
painter.drawRect(self._rect)
def main():
app = QApplication([])
board = Board()
board.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
I'd like to plot the horizontal distance between 2 points on an image with PyQtGraph, but I can't draw it.
I think it a way of doing this would be to use 3 instances of LineSegmentROI and make them look connected as one right arc, because they already have many features that would be great for this idea.
Like being draggable, which could be very useful to measure a different distance by simply dragging a side.
The problem are the handles, that can't be removed, or even hidden.
Has anyone done something like this?
# import the necessary packages
from pyqtgraph.graphicsItems.ImageItem import ImageItem
from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem
import requests
import numpy as np
import cv2
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
image = cv2.imread('example.png') # Change if you save the image with a different name
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
app = QtGui.QApplication([])
## Create window with GraphicsView widget
w = pg.GraphicsView()
w.show()
w.resize(image.shape[0], image.shape[1]) # Depending on the picture you may need to resize
w.setWindowTitle('Test')
view = pg.ViewBox()
view.setLimits(xMin=-image.shape[0]*0.05, xMax=image.shape[0]*1.05,
minXRange=100, maxXRange=2000,
yMin=-image.shape[1]*0.05, yMax=image.shape[1]*1.05,
minYRange=100, maxYRange=2000)
w.setCentralItem(view)
## lock the aspect ratio
view.setAspectLocked(True)
## Add image item
item = ImageItem(image)
view.addItem(item)
# Add Line
line = pg.QtGui.QGraphicsLineItem(200, -100, 400, -100, view)
line.setPen(pg.mkPen(color=(255, 0, 0), width=10))
view.addItem(line)
def mouseClicked(evt):
pos = evt[0]
print(pos)
proxyClicked = pg.SignalProxy(w.scene().sigMouseClicked, rateLimit=60, slot=mouseClicked)
## Start Qt event loop unless running in interactive mode.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
I ended up borrowing RectItem from drawing a rectangle in pyqtgraph and using its code for LineItem.
With three LineItems I draw the distance meter between the 2 points.
I still have to add some signals and slots to handle resizing, but I'm working on it.
However the core of the solution is here and I'll follow up with my improvements
# import the necessary packages
from PySide2.QtCore import QLineF, Qt, Signal, Slot, QObject, QPointF, QRectF, QSizeF
from PySide2.QtGui import QRegion
from PySide2.QtWidgets import QGraphicsItem, QLabel, QWidget
from pyqtgraph.graphicsItems.ImageItem import ImageItem
from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem
import numpy as np
import cv2
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from pyqtgraph.graphicsItems.ViewBox.ViewBox import ViewBox
image = cv2.imread('image.jpg')
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
app = QtGui.QApplication([])
## Create window with GraphicsView widget
w = pg.GraphicsView()
w.show()
w.setWindowTitle('Test')
view = pg.ViewBox()
view.setLimits(xMin=0, xMax=image.shape[0],
minXRange=100, maxXRange=2000,
yMin=0, yMax=image.shape[1],
minYRange=100, maxYRange=2000)
w.setCentralItem(view)
## lock the aspect ratio
view.setAspectLocked(True)
## Add image item
item = ImageItem(image)
view.addItem(item)
class LineItem(pg.UIGraphicsItem):
moved = Signal(QPointF)
def __init__(self, line, extend=0, horizontal=False, parent=None):
super().__init__(parent)
self.initialPos = QLineF(line)
self._line = line
self.extend = extend
self.horizontal = horizontal
self._extendLine()
self.picture = QtGui.QPicture()
self._generate_picture()
self.setFlag(QGraphicsItem.ItemIsSelectable)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
#property
def line(self):
return self._line
def _extendLine(self):
if (self.extend != 0 and not self.horizontal):
self._line.setP1( QPointF(self._line.x1(), self._line.y1() - abs(self.extend)) )
# if (self.horizontal):
# self.extend = 0
# self._line.setP1( QPointF(self._line.x1(), self._line.y1() - abs(self.extend)) )
def _generate_picture(self):
painter = QtGui.QPainter(self.picture)
painter.setPen(pg.mkPen(color="y", width=2))
painter.drawLine(self.line)
painter.end()
def paint(self, painter, option, widget=None):
painter.drawPicture(0, 0, self.picture)
def boundingRect(self):
lineShape = self.picture.boundingRect()
lineShape.adjust(-10, -10, 10, 10)
return QtCore.QRectF(lineShape)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange:
# value is the new position.
if self.horizontal:
if value.x() != 0:
value = QPointF(0, value.y())
else:
if value.y() != 0:
value = QPointF(value.x(), 0)
self.moved.emit(value)
return pg.UIGraphicsItem.itemChange(self, change, value)
class Distance(QObject):
def __init__(self, A: QPointF, B: QPointF, view: ViewBox, parent: QWidget=None):
super().__init__(parent)
self.A = A
self.B = B
if A.x() > B.x():
self.A, self.B = B, A
self.distance = abs(B.x() - A.x())
print(self.A)
print(self.B)
extend = 50
top = max(self.A.y(), self.B.y()) + 200
self.left = LineItem(QtCore.QLineF(self.A.x(), self.A.y(), self.A.x(), top), extend)
self.right = LineItem(QtCore.QLineF(self.B.x(), self.B.y(), self.B.x(), top), extend)
self.top = LineItem(QtCore.QLineF(self.A.x(), top, self.B.x(), top), horizontal=True)
self.top.setPos(0, 0)
self.left.moved.connect(self.onLeftSegmentMoved)
self.right.moved.connect(self.onRightSegmentMoved)
self.top.moved.connect(self.onTopSegmentMoved)
self.label = pg.TextItem(str(round(self.distance, 2)), color=(0xFF, 0xFF, 0x00), anchor=(1, 1))
# self.label.setParentItem(self.top)
self.label.setPos(self.A.x()+self.distance/2, top + 5)
view.addItem(self.label)
view.addItem(self.left)
view.addItem(self.top)
view.addItem(self.right)
#Slot(QPointF)
def onLeftSegmentMoved(self, delta: QPointF):
topLeft = self.top.initialPos.p1()
newX = topLeft.x() + delta.x()
newTopLeft = QPointF(newX, topLeft.y())
self.top.line.setP1(newTopLeft)
self.top._generate_picture()
pos = self.label.pos()
self.distance = abs(self.top.line.x2() - self.top.line.x1())
self.label.setPos(newX + (self.top.line.x2() - self.top.line.x1())/2, pos.y())
self.label.setText(str(round(self.distance, 2)))
#Slot(QPointF)
def onTopSegmentMoved(self, delta: QPointF):
leftTop = self.top.initialPos.p1()
newY = leftTop.y() + delta.y()
newLeftTop = QPointF(leftTop.x(), newY)
self.left.line.setP2(newLeftTop)
self.left._generate_picture()
rightTop = self.top.initialPos.p2()
newY = rightTop.y() + delta.y()
newRightTop = QPointF(rightTop.x(), newY)
self.right.line.setP2(newRightTop)
self.right._generate_picture()
pos = self.label.pos()
self.label.setPos(pos.x(), newY)
#Slot(QPointF)
def onRightSegmentMoved(self, delta: QPointF):
topRight = self.top.initialPos.p2()
newX = topRight.x() + delta.x()
newTopRight = QPointF(newX, topRight.y())
self.top.line.setP2(newTopRight)
self.top._generate_picture()
pos = self.label.pos()
self.distance = abs(self.top.line.x2() - self.top.line.x1())
self.label.setPos(newX - (self.top.line.x2() - self.top.line.x1())/2, pos.y())
self.label.setText(str(round(self.distance, 2)))
distance = Distance(QPointF(925, 425), QPointF(138, 500), view)
How can I get the percentage representing the point clicked along a QPainterPath. For example say I have a line, like the image below, and a user clicks on the QPainterPath, represented by the red dot. I would like to log what percentage the point falls along the path. In this case it would print 0.75 since the point is located around 75%.
These are the known variables:
# 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)
Updated!
My goal is to give the user the ability to click on the path and insert a point. Below is the code i have so far. You'll see in the video that it appears to fail when adding points between points. simply click on the path to insert a point.
Video Link to Watch bug:
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()
A possible solution is to use pointAtPercent () that returns a given point a percentage and calculate the distance to the point and find the minimum index and multiply it by the step. But for this the search must be refined because the previous algorithm works for any point even if it is outside the path. The idea in this case is to use a QPainterPath with a certain area using QPainterPathStroker and verify if the point belongs, and if not the value is outside the 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)
Output:
76.5
Instead of implementing the logic in QGraphicsView, you must do it in the item, and then when you update the path, the points must be ordered with respect to the percentage.
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()
I'm trying to guess how to update the position of the edge when the nodes are moved or why it's not been automatically updated. I have found that the position of the nodes is not updated if I remove the self.update() from the mouseReleaseEvent but I don't know which is the mecanism to get the same on the edge class. Can anybody help me with this?
EDIT: With "position" I mean the value got by getPos() or getScenePos() for the QEdgeGraphicItem. It's printed on the output for the nodes and the Edge.
#!/usr/bin/env python
import math
from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import QGraphicsView, QGraphicsTextItem
class QEdgeGraphicItem(QtGui.QGraphicsItem):
Type = QtGui.QGraphicsItem.UserType + 2
def __init__(self, sourceNode, destNode, label=None):
super(QEdgeGraphicItem, self).__init__()
self.sourcePoint = QtCore.QPointF()
self.destPoint = QtCore.QPointF()
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable)
self.setFlag(QtGui.QGraphicsItem.ItemSendsGeometryChanges)
# self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
self.source = sourceNode
self.dest = destNode
self.label = QGraphicsTextItem("WHY IN THE HELL IS IT IN 0,0", self)
self.label.setParentItem(self)
self.label.setDefaultTextColor(QtCore.Qt.black)
self.source.addEdge(self)
self.dest.addEdge(self)
self.adjust()
def adjust(self):
if not self.source or not self.dest:
return
line = QtCore.QLineF(self.mapFromItem(self.source, 0, 0),
self.mapFromItem(self.dest, 0, 0))
self.prepareGeometryChange()
self.sourcePoint = line.p1()
self.destPoint = line.p2()
def boundingRect(self):
if not self.source or not self.dest:
return QtCore.QRectF()
extra = 2
return QtCore.QRectF(self.sourcePoint,
QtCore.QSizeF(self.destPoint.x() - self.sourcePoint.x(),
self.destPoint.y() - self.sourcePoint.y())).normalized().adjusted(-extra,
-extra,
extra,
extra)
def paint(self, painter, option, widget):
if not self.source or not self.dest:
return
# Draw the line itself.
line = QtCore.QLineF(self.sourcePoint, self.destPoint)
if line.length() == 0.0:
return
painter.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.SolidLine,
QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
painter.drawLine(line)
painter.setBrush(QtCore.Qt.NoBrush)
painter.setPen(QtCore.Qt.red)
painter.drawRect(self.boundingRect())
print "Edge:"+str(self.scenePos().x()) + " " + str(self.scenePos().y())
class QNodeGraphicItem(QtGui.QGraphicsItem):
Type = QtGui.QGraphicsItem.UserType + 1
def __init__(self, label):
super(QNodeGraphicItem, self).__init__()
# self.graph = graphWidget
self.edgeList = []
self.newPos = QtCore.QPointF()
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable)
self.setFlag(QtGui.QGraphicsItem.ItemSendsGeometryChanges)
# self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
# self.setZValue(1)
self.size = 40
self.border_width = 4
def type(self):
return QNodeGraphicItem.Type
def addEdge(self, edge):
self.edgeList.append(edge)
edge.adjust()
def boundingRect(self):
x_coord = y_coord = (-1*(self.size/2)) - self.border_width
width = height = self.size+23+self.border_width
return QtCore.QRectF(x_coord, y_coord , width,
height)
def paint(self, painter, option, widget):
x_coord = y_coord = -(self.size / 2)
width = height = self.size
painter.save()
painter.setBrush(QtGui.QBrush(QtGui.QColor(100, 0, 200, 127)))
painter.setPen(QtCore.Qt.black)
painter.drawEllipse(x_coord, y_coord, width, height)
painter.restore()
print "Node: " + str(self.scenePos().x()) + " " + str(self.scenePos().y())
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionHasChanged:
for edge in self.edgeList:
edge.adjust()
return super(QNodeGraphicItem, self).itemChange(change, value)
def mousePressEvent(self, event):
self.update()
super(QNodeGraphicItem, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
self.update()
super(QNodeGraphicItem, self).mouseReleaseEvent(event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
QtCore.qsrand(QtCore.QTime(0, 0, 0).secsTo(QtCore.QTime.currentTime()))
node1 = QNodeGraphicItem("Node1")
node2 = QNodeGraphicItem("Node2")
edge = QEdgeGraphicItem(node1,node2)
view = QGraphicsView()
view.setCacheMode(QtGui.QGraphicsView.CacheBackground)
view.setViewportUpdateMode(QtGui.QGraphicsView.BoundingRectViewportUpdate)
view.setRenderHint(QtGui.QPainter.Antialiasing)
view.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
view.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)
view.scale(0.8, 0.8)
view.setMinimumSize(400, 400)
view.setWindowTitle("Example")
scene = QtGui.QGraphicsScene(view)
scene.setItemIndexMethod(QtGui.QGraphicsScene.NoIndex)
scene.setSceneRect(-400, -400, 800, 800)
view.setScene(scene)
scene.addItem(node1)
scene.addItem(node2)
scene.addItem(edge)
view.show()
sys.exit(app.exec_())
Thank you.