Error in super methods - python

I try using PyQt5 to connect two ellipses with a line. To do this, slightly changed the class taken with github. Instead of PySide, i use PyQt5.
The code is taken from here: https://github.com/PySide/Examples/blob/master/examples/graphicsview/diagramscene/diagramscene.py
class Arrow(QGraphicsLineItem):
def __init__(self, start_item, end_item, parent=None, scene=None):
super(Arrow, self).__init__(parent, scene)
self.arrowHead = QPolygonF()
self.my_start_item = start_item
self.my_end_item = end_item
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.my_color = QtCore.Qt.black
self.setPen(QPen(self.my_color, 2, QtCore.Qt.SolidLine,
QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
def set_color(self, color):
self.my_color = color
def start_item(self):
return self.my_start_item
def end_item(self):
return self.my_end_item
def boundingRect(self):
extra = (self.pen().width() + 20) / 2.0
p1 = self.line().p1()
p2 = self.line().p2()
return QtCore.QRectF(p1, QtCore.QSizeF(p2.x() - p1.x(), p2.y() - p1.y())).normalized().adjusted(-extra, -extra, extra, extra)
def shape(self):
path = super(Arrow, self).shape()
path.addPolygon(self.arrowHead)
return path
def update_position(self):
line = QtCore.QLineF(self.mapFromItem(self.my_start_item, 0, 0), self.mapFromItem(self.my_end_item, 0, 0))
self.setLine(line)
def paint(self, painter, option, widget=None):
if self.my_start_item.collidesWithItem(self.my_end_item):
return
my_start_item = self.my_start_item
my_end_item = self.my_end_item
my_color = self.my_color
my_pen = self.pen()
my_pen.setColor(self.my_color)
arrow_size = 20.0
painter.setPen(my_pen)
painter.setBrush(self.my_color)
center_line = QtCore.QLineF(my_start_item.pos(), my_end_item.pos())
end_polygon = my_end_item.polygon()
p1 = end_polygon.at(0) + my_end_item.pos()
intersect_point = QtCore.QPointF()
for i in end_polygon:
p2 = i + my_end_item.pos()
poly_line = QtCore.QLineF(p1, p2)
intersect_type, intersect_point = poly_line.intersect(center_line)
if intersect_type == QtCore.QLineF.BoundedIntersection:
break
p1 = p2
self.setLine(QtCore.QLineF(intersect_point, my_start_item.pos()))
line = self.line()
angle = math.acos(line.dx() / line.length())
if line.dy() >= 0:
angle = (math.pi * 2.0) - angle
arrow_p1 = line.p1() + QtCore.QPointF(math.sin(angle + math.pi / 3.0) * arrow_size,
math.cos(angle + math.pi / 3) * arrow_size)
arrow_p2 = line.p1() + QtCore.QPointF(math.sin(angle + math.pi - math.pi / 3.0) * arrow_size,
math.cos(angle + math.pi - math.pi / 3.0) * arrow_size)
self.arrowHead.clear()
for point in [line.p1(), arrow_p1, arrow_p2]:
self.arrowHead.append(point)
painter.drawLine(line)
painter.drawPolygon(self.arrowHead)
if self.isSelected():
painter.setPen(QPen(my_color, 1, QtCore.Qt.DashLine))
my_line = QtCore.QLineF(line)
my_line.translate(0, 4.0)
painter.drawLine(my_line)
my_line.translate(0, -8.0)
painter.drawLine(my_line)
Creating a arrow
arrow = Arrow(start, end, scene=scene)
scene.addItem(arrow)
arrow.update_position()
Error that occurs when the code is run

PyQt5 is a wrapper of Qt5 created by the company riverbankcomputing, and PySide is a wrapper of Qt4 created by the same creators of Qt at the time. Besides Qt4 and Qt5 have many differences, so these libraries are not compatible at all, for example in your case PySide items you can pass the scene as a parameter, but in the case of PyQt4 or PyQt5 that parameter is unnecessary:
PySide:
class PySide.QtGui.QGraphicsLineItem([parent=None[, scene=None]])
class PySide.QtGui.QGraphicsLineItem(line[, parent=None[, scene=None]])
class PySide.QtGui.QGraphicsLineItem(x1, y1, x2, y2[,parent=None[, scene=None]])
PyQt5:
QGraphicsLineItem(parent: QGraphicsItem = None)
QGraphicsLineItem(QLineF, parent: QGraphicsItem = None)
QGraphicsLineItem(float, float, float, float, parent: QGraphicsItem = None)
Besides that difference are the classes Signal() and pyqtSignal, and obviously the step from Qt4 to Qt5 supposed the separation of some classes that belonged to QtGui to QtWidgets.
The solution in your case is to eliminate the scene parameter, for it changes:
class Arrow(QGraphicsLineItem):
def __init__(self, start_item, end_item, parent=None, scene=None):
super(Arrow, self).__init__(parent, scene)
to:
class Arrow(QGraphicsLineItem):
def __init__(self, start_item, end_item, parent=None):
super(Arrow, self).__init__(parent)
I have translated that project to PyQt5, you can check it in the following link.

Related

Qt3DCore simple wireframe viewer

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
())

Add Mouse Motion functionality to PyQt based Bezier Drawer

I am trying to modify my code below to take-in mouse events (click, drag, release) such that the control points can be selected and moved, resulting in change in the curve. I am not sure where to begin, any suggestions? The control points are marked as red dots, the curve is in blue.
This would basically let me modify the curve within the gui. Any reference would be appreciated as well.
import sys
import random
import functools
from PyQt5 import QtWidgets, QtGui, QtCore
#functools.lru_cache(maxsize=100)
def factorial(n):
prod = 1
for i in range(1,n+1):
prod *= i
return prod
def randPt(minv, maxv):
return (random.randint(minv, maxv), random.randint(minv, maxv))
def B(i,n,u):
val = factorial(n)/(factorial(i)*factorial(n-i))
return val * (u**i) * ((1-u)**(n-i))
def C(u, pts):
x = 0
y = 0
n = len(pts)-1
for i in range(n+1):
binu = B(i,n,u)
x += binu * pts[i][0]
y += binu * pts[i][1]
return (x, y)
class BezierDrawer(QtWidgets.QWidget):
def __init__(self):
super(BezierDrawer, self).__init__()
self.setGeometry(300, 300, 1500,1000)
self.setWindowTitle('Bezier Curves')
def paintEvent(self, e):
qp = QtGui.QPainter()
qp.begin(self)
qp.setRenderHints(QtGui.QPainter.Antialiasing, True)
self.doDrawing(qp)
qp.end()
def doDrawing(self, qp):
blackPen = QtGui.QPen(QtCore.Qt.black, 3, QtCore.Qt.DashLine)
redPen = QtGui.QPen(QtCore.Qt.red, 30, QtCore.Qt.DashLine)
bluePen = QtGui.QPen(QtCore.Qt.blue, 3, QtCore.Qt.DashLine)
greenPen = QtGui.QPen(QtCore.Qt.green, 3, QtCore.Qt.DashLine)
redBrush = QtGui.QBrush(QtCore.Qt.red)
steps = 400
min_t = 0.0
max_t = 1.0
dt = (max_t - min_t)/steps
# controlPts = [randPt(0,1000) for i in range(6)]
# controlPts.append(controlPts[1])
# controlPts.append(controlPts[0])
controlPts = [(500,500), (600,700), (600,550), (700,500),(700,500), (800,400), (1000,200), (1000,500)]
oldPt = controlPts[0]
pn = 1
qp.setPen(redPen)
qp.setBrush(redBrush)
qp.drawEllipse(oldPt[0]-3, oldPt[1]-3, 6,6)
#qp.drawText(oldPt[0]+5, oldPt[1]-3, '{}'.format(pn))
for pt in controlPts[1:]:
pn+=1
qp.setPen(blackPen)
qp.drawLine(oldPt[0],oldPt[1],pt[0],pt[1])
qp.setPen(redPen)
qp.drawEllipse(pt[0]-3, pt[1]-3, 6,6)
#xv=qp.drawText(pt[0]+5, pt[1]-3, '{}'.format(pn))
#xv.setTextWidth(3)
oldPt = pt
qp.setPen(bluePen)
oldPt = controlPts[0]
for i in range(steps+1):
t = dt*i
pt = C(t, controlPts)
qp.drawLine(oldPt[0],oldPt[1], pt[0],pt[1])
oldPt = pt
def main(args):
app = QtWidgets.QApplication(sys.argv)
ex = BezierDrawer()
ex.show()
app.exec_()
if __name__=='__main__':
main(sys.argv[1:])
Premise note: while I normally don't answer such questions with a complete new implementation, the subject of higher-order Bézier curves is quite interesting and not directly available in Qt (and in any general toolkit), so I'm breaking my general rule to provide an expanded answer, as I believe it could be useful to others.
While the accepted answer properly addresses the issue, as stated, using the Graphics View Framework is almost always a better choice, as it provides more modularity, advanced interaction, optimization and features that would be difficult to implement in a standard widget (notably, transformations like scaling and rotation).
Before explaining the implementation, some important notes about the original code:
the factorial() function is slow, and its caching is almost useless, since it will probably never be able to use any cached data due to the amount and diversity of results (there are 400 hardcoded steps, but the cache limit is set to 100); the math module already provides factorial(), which is much faster since it's purely implemented on the C side;
the B() function only executes a computation, so it is almost useless; the same goes for C(): while creating functions may be important for readability and code separation, in this case their usability and usage makes them pointless;
if you want a continuous dashed line for the curve, you cannot use drawLine(), as it will draw distinct segments: since those segments will almost always be very short, no dashed line will be ever shown; use QPainterPath instead;
such extensive and repetitive computation should never be executed in the paintEvent() (which can be called very frequently) and some level of caching should be preferably used; for instance, you can create an empty instance attribute (eg. self.cachePath = None), check if the attribute is empty in the paintEvent() and eventually call a function that creates the path in that case; also consider QPicture;
(unrelated, but still important) calling main with sys.argv[1:] is a bit pointless if you still create the QApplication with the full sys.argv;
In the following implementation I created a main BezierItem as subclass of QGraphicsPathItem which will contain the complete curve and embeds a child QGraphicsPathItem for the line segments and a variable number of ControlPoint objects, which are QGraphicsObject subclasses. The QObject inheritance is to add signal support, which is required to notify about item position changes (it could be technically avoided by using a basic QGraphicsItem and calling the function of the parent, but that wouldn't be very elegant).
Note that, since the curve can potentially call the point computation thousands of times, optimization is of utmost importance: consider that for a "simple" 8th-order curve with a mere 20-step-per-point precision, the result is that the inner computation will be executed about 1200 times. With 20 points and a 40-step precision, the result is more than 16000 cycles.
The BezierItem also contains a list of control points as QPointFs for optimization reasons, and it uses two important functions: updatePath(), which updates the segments that connect each control point, and then calls _rebuildPath() which actually creates the final curve as a QPainterPath. The distinction is important as you may need to rebuild the path when the resolution changes, but the points are still the same.
Note that you used a peculiar way to show control points (a small ellipse with a very thick pen width, which results in a fancy rounded octagon due to the way Qt draw shapes). This can cause some issues in mouse detection, as shaped QGraphicsItems use their internal path and pen to build the shape for collision detection (including mouse events). To avoid these issues, I created a simplified() path based on that shape that will improve performance and ensure proper behavior.
import random
from math import factorial
from PyQt5 import QtWidgets, QtGui, QtCore
class ControlPoint(QtWidgets.QGraphicsObject):
moved = QtCore.pyqtSignal(int, QtCore.QPointF)
removeRequest = QtCore.pyqtSignal(object)
brush = QtGui.QBrush(QtCore.Qt.red)
# create a basic, simplified shape for the class
_base = QtGui.QPainterPath()
_base.addEllipse(-3, -3, 6, 6)
_stroker = QtGui.QPainterPathStroker()
_stroker.setWidth(30)
_stroker.setDashPattern(QtCore.Qt.DashLine)
_shape = _stroker.createStroke(_base).simplified()
# "cache" the boundingRect for optimization
_boundingRect = _shape.boundingRect()
def __init__(self, index, pos, parent):
super().__init__(parent)
self.index = index
self.setPos(pos)
self.setFlags(
self.ItemIsSelectable
| self.ItemIsMovable
| self.ItemSendsGeometryChanges
| self.ItemStacksBehindParent
)
self.setZValue(-1)
self.setToolTip(str(index + 1))
self.font = QtGui.QFont()
self.font.setBold(True)
def setIndex(self, index):
self.index = index
self.setToolTip(str(index + 1))
self.update()
def shape(self):
return self._shape
def boundingRect(self):
return self._boundingRect
def itemChange(self, change, value):
if change == self.ItemPositionHasChanged:
self.moved.emit(self.index, value)
elif change == self.ItemSelectedHasChanged and value:
# stack this item above other siblings when selected
for other in self.parentItem().childItems():
if isinstance(other, self.__class__):
other.stackBefore(self)
return super().itemChange(change, value)
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
removeAction = menu.addAction(QtGui.QIcon.fromTheme('edit-delete'), 'Delete point')
if menu.exec(QtGui.QCursor.pos()) == removeAction:
self.removeRequest.emit(self)
def paint(self, qp, option, widget=None):
qp.setBrush(self.brush)
if not self.isSelected():
qp.setPen(QtCore.Qt.NoPen)
qp.drawPath(self._shape)
qp.setPen(QtCore.Qt.white)
qp.setFont(self.font)
r = QtCore.QRectF(self.boundingRect())
r.setSize(r.size() * 2 / 3)
qp.drawText(r, QtCore.Qt.AlignCenter, str(self.index + 1))
class BezierItem(QtWidgets.QGraphicsPathItem):
_precision = .05
_delayUpdatePath = False
_ctrlPrototype = ControlPoint
def __init__(self, points=None):
super().__init__()
self.setPen(QtGui.QPen(QtCore.Qt.blue, 3, QtCore.Qt.DashLine))
self.outlineItem = QtWidgets.QGraphicsPathItem(self)
self.outlineItem.setFlag(self.ItemStacksBehindParent)
self.outlineItem.setPen(QtGui.QPen(QtCore.Qt.black, 3, QtCore.Qt.DashLine))
self.controlItems = []
self._points = []
if points is not None:
self.setPoints(points)
def setPoints(self, pointList):
points = []
for p in pointList:
if isinstance(p, (QtCore.QPointF, QtCore.QPoint)):
# always create a copy of each point!
points.append(QtCore.QPointF(p))
else:
points.append(QtCore.QPointF(*p))
if points == self._points:
return
self._points = []
self.prepareGeometryChange()
while self.controlItems:
item = self.controlItems.pop()
item.setParentItem(None)
if self.scene():
self.scene().removeItem(item)
del item
self._delayUpdatePath = True
for i, p in enumerate(points):
self.insertControlPoint(i, p)
self._delayUpdatePath = False
self.updatePath()
def _createControlPoint(self, index, pos):
ctrlItem = self._ctrlPrototype(index, pos, self)
self.controlItems.insert(index, ctrlItem)
ctrlItem.moved.connect(self._controlPointMoved)
ctrlItem.removeRequest.connect(self.removeControlPoint)
def addControlPoint(self, pos):
self.insertControlPoint(-1, pos)
def insertControlPoint(self, index, pos):
if index < 0:
index = len(self._points)
for other in self.controlItems[index:]:
other.index += 1
other.update()
self._points.insert(index, pos)
self._createControlPoint(index, pos)
if not self._delayUpdatePath:
self.updatePath()
def removeControlPoint(self, cp):
if isinstance(cp, int):
index = cp
else:
index = self.controlItems.index(cp)
item = self.controlItems.pop(index)
self.scene().removeItem(item)
item.setParentItem(None)
for other in self.controlItems[index:]:
other.index -= 1
other.update()
del item, self._points[index]
self.updatePath()
def precision(self):
return self._precision
def setPrecision(self, precision):
precision = max(.001, min(.5, precision))
if self._precision != precision:
self._precision = precision
self._rebuildPath()
def stepRatio(self):
return int(1 / self._precision)
def setStepRatio(self, ratio):
'''
Set the *approximate* number of steps per control point. Note that
the step count is adjusted to an integer ratio based on the number
of control points.
'''
self.setPrecision(1 / ratio)
self.update()
def updatePath(self):
outlinePath = QtGui.QPainterPath()
if self.controlItems:
outlinePath.moveTo(self._points[0])
for point in self._points[1:]:
outlinePath.lineTo(point)
self.outlineItem.setPath(outlinePath)
self._rebuildPath()
def _controlPointMoved(self, index, pos):
self._points[index] = pos
self.updatePath()
def _rebuildPath(self):
'''
Actually rebuild the path based on the control points and the selected
curve precision. The default (0.05, ~20 steps per control point) is
usually enough, lower values result in higher resolution but slower
performance, and viceversa.
'''
self.curvePath = QtGui.QPainterPath()
if self._points:
self.curvePath.moveTo(self._points[0])
count = len(self._points)
steps = round(count / self._precision)
precision = 1 / steps
n = count - 1
# we're going to iterate through points *a lot* of times; with the
# small cost of a tuple, we can cache the inner iterator to speed
# things up a bit, instead of creating it in each for loop cycle
pointIterator = tuple(enumerate(self._points))
for s in range(steps + 1):
u = precision * s
x = y = 0
for i, point in pointIterator:
binu = (factorial(n) / (factorial(i) * factorial(n - i))
* (u ** i) * ((1 - u) ** (n - i)))
x += binu * point.x()
y += binu * point.y()
self.curvePath.lineTo(x, y)
self.setPath(self.curvePath)
class BezierExample(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.bezierScene = QtWidgets.QGraphicsScene()
self.bezierView = QtWidgets.QGraphicsView(self.bezierScene)
self.bezierView.setRenderHints(QtGui.QPainter.Antialiasing)
self.bezierItem = BezierItem([
(500, 500), (600, 700), (600, 550), (700, 500),
(700, 500), (800, 400), (1000, 200), (1000, 500)
])
self.bezierScene.addItem(self.bezierItem)
mainLayout = QtWidgets.QVBoxLayout(self)
topLayout = QtWidgets.QHBoxLayout()
mainLayout.addLayout(topLayout)
topLayout.addWidget(QtWidgets.QLabel('Resolution:'))
resSpin = QtWidgets.QSpinBox(minimum=1, maximum=100)
resSpin.setValue(self.bezierItem.stepRatio())
topLayout.addWidget(resSpin)
topLayout.addStretch()
addButton = QtWidgets.QPushButton('Add point')
topLayout.addWidget(addButton)
mainLayout.addWidget(self.bezierView)
self.bezierView.installEventFilter(self)
resSpin.valueChanged.connect(self.bezierItem.setStepRatio)
addButton.clicked.connect(self.addPoint)
def addPoint(self, point=None):
if not isinstance(point, (QtCore.QPoint, QtCore.QPointF)):
point = QtCore.QPointF(
random.randrange(int(self.bezierScene.sceneRect().width())),
random.randrange(int(self.bezierScene.sceneRect().height())))
self.bezierItem.addControlPoint(point)
def eventFilter(self, obj, event):
if event.type() == event.MouseButtonDblClick:
pos = self.bezierView.mapToScene(event.pos())
self.addPoint(pos)
return True
return super().eventFilter(obj, event)
def sizeHint(self):
return QtWidgets.QApplication.primaryScreen().size() * 2 / 3
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
ex = BezierExample()
ex.show()
app.exec_()
I will probably change the above code in the future, mostly to provide a valid backend for Nth-order curves without control point interaction and eventually a further subclass to add that support.
But, right now, the requested support is complete, and I strongly urge you to carefully take your time in studying every part of that code. The Graphics View framework is as powerful as it's complex to understand, and it may take weeks (at least!) to really get it.
As always, the basic rule remains: study the documentation. And, possibly, the source code.
As it was suggested, for more "serious" application, You should go for GraphicsView.
Your app was however almost done with simple drawing. It's not a big deal to to modify it to work as You want.
I made few changes to Your code. You have to make list of control points as a attribute of Your BezierDrawer. Then You can use events: mousePressEvent, mouseMoveEvent and mouseReleaseEvent to interact with control points.
First we need to find out, if any point was clicked in mousePressEvent and then just update it's position while dragging. Every change of point position must end with update method, to repaint widget.
Here is modified code:
import functools
import random
import sys
from PyQt5 import QtWidgets, QtGui, QtCore
#functools.lru_cache(maxsize=100)
def factorial(n):
prod = 1
for i in range(1, n + 1):
prod *= i
return prod
def randPt(minv, maxv):
return (random.randint(minv, maxv), random.randint(minv, maxv))
def B(i, n, u):
val = factorial(n) / (factorial(i) * factorial(n - i))
return val * (u ** i) * ((1 - u) ** (n - i))
def C(u, pts):
x = 0
y = 0
n = len(pts) - 1
for i in range(n + 1):
binu = B(i, n, u)
x += binu * pts[i][0]
y += binu * pts[i][1]
return (x, y)
class BezierDrawer(QtWidgets.QWidget):
def __init__(self):
super(BezierDrawer, self).__init__()
# Dragged point index
self.dragged_point = None
# List of control points
self.controlPts = [(500, 500), (600, 700), (600, 550), (700, 500), (800, 400), (1000, 200),
(1000, 500)]
self.setGeometry(300, 300, 1500, 1000)
self.setWindowTitle('Bezier Curves')
def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None:
"""Get through all control points and find out if mouse clicked on it"""
for control_point, (x, y) in enumerate(self.controlPts):
if a0.x() - 15 <= x <= a0.x() + 15 and a0.y() - 15 <= y <= a0.y() + 15:
self.dragged_point = control_point
return
def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None:
"""If any point is dragged, change its position and repaint scene"""
if self.dragged_point is not None:
self.controlPts[self.dragged_point] = (a0.x(), a0.y())
self.update()
def mouseReleaseEvent(self, a0: QtGui.QMouseEvent) -> None:
"""Release dragging point and repaint scene again"""
self.dragged_point = None
self.update()
def paintEvent(self, e):
qp = QtGui.QPainter()
qp.begin(self)
qp.setRenderHints(QtGui.QPainter.Antialiasing, True)
self.doDrawing(qp)
qp.end()
def doDrawing(self, qp):
blackPen = QtGui.QPen(QtCore.Qt.black, 3, QtCore.Qt.DashLine)
redPen = QtGui.QPen(QtCore.Qt.red, 30, QtCore.Qt.DashLine)
bluePen = QtGui.QPen(QtCore.Qt.blue, 3, QtCore.Qt.DashLine)
greenPen = QtGui.QPen(QtCore.Qt.green, 3, QtCore.Qt.DashLine)
redBrush = QtGui.QBrush(QtCore.Qt.red)
steps = 400
min_t = 0.0
max_t = 1.0
dt = (max_t - min_t) / steps
oldPt = self.controlPts[0]
pn = 1
qp.setPen(redPen)
qp.setBrush(redBrush)
qp.drawEllipse(oldPt[0] - 3, oldPt[1] - 3, 6, 6)
for pt in self.controlPts[1:]:
pn += 1
qp.setPen(blackPen)
qp.drawLine(oldPt[0], oldPt[1], pt[0], pt[1])
qp.setPen(redPen)
qp.drawEllipse(pt[0] - 3, pt[1] - 3, 6, 6)
oldPt = pt
qp.setPen(bluePen)
oldPt = self.controlPts[0]
for i in range(steps + 1):
t = dt * i
pt = C(t, self.controlPts)
qp.drawLine(oldPt[0], oldPt[1], pt[0], pt[1])
oldPt = pt
def main(args):
app = QtWidgets.QApplication(sys.argv)
ex = BezierDrawer()
ex.show()
app.exec_()
if __name__ == '__main__':
main(sys.argv[1:])

Drawing line and points in PySide2 with coordination

I want to draw some lines and points by PySide2 and I followed the documentations and provide code below, but it is not showing any thing after I call the function.
class Window2(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Deformed Structure")
self.DrawWindows = QtGui.QWindow()
self.DrawButton23 = QPushButton('Draw', self)
self.DrawButton23.setStyleSheet("Background-color: orange")
self.DrawButton23.move(100, 200)
self.DrawButton23.show()
self.DrawButton23.clicked.connect(self.PaintEvent)
def PaintEvent(self, painter):
painter = QtGui.QPainter()
painter.begin(self)
pen = QPen(Qt.green)
painter.setPen(pen)
for i in range(0, 10):
x0 = i * 30
y0 = i * 30
x1 = 100 + i * 50
y1 = 100 + i * 50
point1 = QPointF(x0, y0)
point2 = QPointF(x1, y1)
line1 = QLineF(point1, point2)
painter.drawPoint(point1)
painter.drawLine(line1)
print("OK123") #Just to check the loop, it prints 10 time
painter.end()
You must understand that:
Python and C++ are case sensitive so paintEvent is different from PaintEvent.
You should not invoke paintEvent directly but using the update() or repaint() method.
From what I understand is that you want the painting to be executed when you press the button but you cannot control the painting directly, the logic is to activate a certain part of the painting using some flag.
Considering the above, the solution is:
from PySide2 import QtCore, QtGui, QtWidgets
class Window2(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Deformed Structure")
self.flag = False
self.draw_button = QtWidgets.QPushButton("Draw", self)
self.draw_button.setStyleSheet("Background-color: orange")
self.draw_button.move(100, 200)
self.draw_button.clicked.connect(self.on_clicked)
def on_clicked(self):
self.flag = True
self.update()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
if self.flag:
pen = QtGui.QPen(QtCore.Qt.green)
painter.setPen(pen)
for i in range(0, 10):
x0 = i * 30
y0 = i * 30
x1 = 100 + i * 50
y1 = 100 + i * 50
point1 = QtCore.QPointF(x0, y0)
point2 = QtCore.QPointF(x1, y1)
line1 = QtCore.QLineF(point1, point2)
painter.drawPoint(point1)
painter.drawLine(line1)
if __name__ == "__main__":
app = QtWidgets.QApplication()
w = Window2()
w.show()
app.exec_()

Implementing one dimensional drag operation in Qt

I want to allow a QGraphicsItem to be dragged only in certain directions, such as +/-45 degrees, horizontally or vertically, and to be able to "jump" to a new direction once the cursor is dragged far enough away from the current closest direction. This would replicate behaviour in e.g. Inkscape when drawing a straight line and holding Ctrl (see e.g. this video), but I am unsure how to implement it.
I've implemented a drag handler that grabs the new position of the item as it is moved:
class Circle(QGraphicsEllipseItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Flags to allow dragging and tracking of dragging.
self.setFlag(self.ItemSendsGeometryChanges)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
def itemChange(self, change, value):
if change == self.ItemPositionChange and self.isSelected():
# do something...
# Return the new position to parent to have this item move there.
return super().itemChange(change, value)
Since the position returned to the parent by this method is used to update the position of the item in the scene, I expect that I can modify this QPointF to limit it to one axis, but I am unsure how to do so in a way that lets the line "jump" to another direction once the cursor is dragged far enough. Are there any "standard algorithms" for this sort of behaviour? Or perhaps some built-in Qt code that can do this for me?
The problem is reduced to calculate the projection of the point (position of the item) on the line. Doing a little math as explained in this post.
Let p1 and p2 be two different points on the line and p the point then the algorithm is:
e1 = p2 - p1
e2 = p - p1
dp = e1 • e2 # dot product
l = e1 • e1 # dot product
pp = p1 + dp * e1 / l
Implementing the above the solution is:
import math
import random
from PyQt5 import QtCore, QtGui, QtWidgets
class Circle(QtWidgets.QGraphicsEllipseItem):
def __init__(self, *args, **kwargs):
self._line = QtCore.QLineF()
super().__init__(*args, **kwargs)
# Flags to allow dragging and tracking of dragging.
self.setFlags(
self.flags()
| QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
| QtWidgets.QGraphicsItem.ItemIsMovable
| QtWidgets.QGraphicsItem.ItemIsSelectable
)
#property
def line(self):
return self._line
#line.setter
def line(self, line):
self._line = line
def itemChange(self, change, value):
if (
change == QtWidgets.QGraphicsItem.ItemPositionChange
and self.isSelected()
and not self.line.isNull()
):
# http://www.sunshine2k.de/coding/java/PointOnLine/PointOnLine.html
p1 = self.line.p1()
p2 = self.line.p2()
e1 = p2 - p1
e2 = value - p1
dp = QtCore.QPointF.dotProduct(e1, e2)
l = QtCore.QPointF.dotProduct(e1, e1)
p = p1 + dp * e1 / l
return p
return super().itemChange(change, value)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QtWidgets.QGraphicsScene(QtCore.QRectF(-200, -200, 400, 400))
view = QtWidgets.QGraphicsView(scene)
points = (
QtCore.QPointF(*random.sample(range(-150, 150), 2)) for _ in range(4)
)
angles = (math.pi / 4, math.pi / 3, math.pi / 5, math.pi / 2)
for point, angle in zip(points, angles):
item = Circle(QtCore.QRectF(-10, -10, 20, 20))
item.setBrush(QtGui.QColor("salmon"))
scene.addItem(item)
item.setPos(point)
end = 100 * QtCore.QPointF(math.cos(angle), math.sin(angle))
line = QtCore.QLineF(QtCore.QPointF(), end)
item.line = line.translated(item.pos())
line_item = scene.addLine(item.line)
line_item.setPen(QtGui.QPen(QtGui.QColor("green"), 4))
view.resize(640, 480)
view.show()
sys.exit(app.exec_())

PyQt5 connection doesn't work: item cannot be converted to PyQt5.QtCore.QObject in this context

I am trying to connect a signal from a created object and am getting an error. Here is a simplified version of my code:
class OverviewWindow(QMainWindow):
def __init__(self, projectClusters, users, contributorDict, userLastRevPerProj):
QMainWindow.__init__(self)
# Code....
def createUserNodes(self):
userNodes = {}
nodeSpread = 50
yPos = -400
nodeSpan = nodeSpread + 100
width = (len(self.usersFilt) - 1) * nodeSpan
xPos = 0 - (width / 2)
for user in self.usersFilt:
newItem = NodeItem(xPos, yPos, self.nodeDiameter, user, True)
newItem.nodeDoubleClicked.connect(self.dc)
userNodes[user] = newItem
self.graphicsScene.addItem(newItem)
xPos += nodeSpan
return userNodes
#pyqtSlot(str)
def dc(self, text):
print(text)
class NodeItem(QGraphicsItem):
nodeDoubleClicked = pyqtSignal(str)
def __init__(self, xPos, yPos, diameter, text, isUserNode):
super(NodeItem, self).__init__()
# Code...
def mouseDoubleClickEvent(self, event):
self.nodeDoubleClicked.emit(self.texts)
When trying to run it it give me this error:
line 84, in createUserNodes
newItem.nodeDoubleClicked[str].connect(self.dc)
TypeError: NodeItem cannot be converted to PyQt5.QtCore.QObject in this context
I have no idea what this means or how to fix it.
QGraphicsItem does not inherit from QObject, therefore it is not possible to emit a signal from an instance of QGraphicsItem. You can solve this by subclassing QGraphicsObject instead of QGraphicsItem: http://doc.qt.io/qt-5/qgraphicsobject.html.

Categories