Qt callout example with more than one y axis - python

I have a QChart from the Callout example of PySide6. Now I have rewritten some of the code for my project, but when I hover over a `QLineSeries' the callout appears higher or lower than where I am actually pointing.
Here is some code:
The Callout class (Almost the same as in the example)
class Callout(QGraphicsItem):
def __init__(self, chart):
QGraphicsItem.__init__(self, chart)
self.chart = chart
self._text = ""
self._textRect = QRectF()
self._anchor = QPointF()
self._font = QFont()
self._rect = QRectF()
def boundingRect(self):
anchor = self.mapFromParent(self.chart.mapToPosition(self._anchor))
rect = QRectF()
rect.setLeft(min(self._rect.left(), anchor.x()))
rect.setRight(max(self._rect.right(), anchor.x()))
rect.setTop(min(self._rect.top(), anchor.y()))
rect.setBottom(max(self._rect.bottom(), anchor.y()))
return rect
def paint(self, painter, option, widget):
path = QPainterPath()
path.addRoundedRect(self._rect, 5, 5)
anchor = self.mapFromParent(self.chart.mapToPosition(self._anchor))
if not self._rect.contains(anchor) and not self._anchor.isNull():
point1 = QPointF()
point2 = QPointF()
# establish the position of the anchor point in relation to _rect
above = anchor.y() <= self._rect.top()
above_center = (anchor.y() > self._rect.top() and
anchor.y() <= self._rect.center().y())
below_center = (anchor.y() > self._rect.center().y() and
anchor.y() <= self._rect.bottom())
below = anchor.y() > self._rect.bottom()
on_left = anchor.x() <= self._rect.left()
left_of_center = (anchor.x() > self._rect.left() and
anchor.x() <= self._rect.center().x())
right_of_center = (anchor.x() > self._rect.center().x() and
anchor.x() <= self._rect.right())
on_right = anchor.x() > self._rect.right()
# get the nearest _rect corner.
x = (on_right + right_of_center) * self._rect.width()
y = (below + below_center) * self._rect.height()
corner_case = ((above and on_left) or (above and on_right) or
(below and on_left) or (below and on_right))
vertical = abs(anchor.x() - x) > abs(anchor.y() - y)
x1 = (x + left_of_center * 10 - right_of_center * 20 + corner_case *
int(not vertical) * (on_left * 10 - on_right * 20))
y1 = (y + above_center * 10 - below_center * 20 + corner_case *
vertical * (above * 10 - below * 20))
point1.setX(x1)
point1.setY(y1)
x2 = (x + left_of_center * 20 - right_of_center * 10 + corner_case *
int(not vertical) * (on_left * 20 - on_right * 10))
y2 = (y + above_center * 20 - below_center * 10 + corner_case *
vertical * (above * 20 - below * 10))
point2.setX(x2)
point2.setY(y2)
path.moveTo(point1)
path.lineTo(anchor)
path.lineTo(point2)
path = path.simplified()
painter.setBrush(QColor(255, 255, 255))
painter.drawPath(path)
painter.drawText(self._textRect, self._text)
def mousePressEvent(self, event):
if event.button() == Qt.RightButton:
self.removecallout()
event.setAccepted(True)
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
self.setPos(self.mapToParent(
event.pos() - event.buttonDownPos(Qt.LeftButton)))
event.setAccepted(True)
else:
event.setAccepted(False)
def set_text(self, text):
self._text = text
metrics = QFontMetrics(self._font)
self._textRect = QRectF(metrics.boundingRect(
QRect(0.0, 0.0, 150.0, 150.0), Qt.AlignLeft, self._text))
self._textRect.translate(5, 5)
self.prepareGeometryChange()
self._rect = self._textRect.adjusted(-5, -5, 5, 5)
def set_anchor(self, point):
self._anchor = QPointF(point)
def update_geometry(self):
self.prepareGeometryChange()
self.setPos(self.chart.mapToPosition(
self._anchor) + QPointF(10, -50))
def removecallout(self):
self.hide()
The Class which produces the chart:
class CreateChart(QChartView):
def __init__(self, data):
super().__init__()
self.serieses = self.getserieses(data)
i = 0
self.chart = QChart()
self.buddy = None
self.chart.legend().setVisible(False)
xaxis = QValueAxis()
xaxis.setTitleText("Time")
self.chart.addAxis(xaxis, Qt.AlignBottom)
for key in self.serieses.keys():
if i >= 100:
break
else:
self.serieses[key].setName(key)
self.chart.addSeries(self.serieses[key])
self.serieses[key].hovered.connect(self.tooltip) #The connections for the temporary callout of the coordinates
self.serieses[key].clicked.connect(self.keep_callout) #The connection for the permanent callout of the coordinates
axis = QValueAxis()
axis.setTitleText(key)
axis.setTitleBrush(self.serieses[key].color())
self.chart.addAxis(axis, Qt.AlignLeft if ((i % 2) == 0) else Qt.AlignRight)
self.serieses[key].attachAxis(axis)
self.serieses[key].attachAxis(xaxis)
i += 1
print("Finished Loading")
self.chart.legend().setMarkerShape(QLegend.MarkerShapeFromSeries)
self.chart_view = super()
self.chart_view.setRenderHint(QPainter.Antialiasing)
self.chart_view.setChart(self.chart)
#self.chart_view.setMaximumWidth(300)
#QGraphicsView.RubberbandDrag = Selecting an area which can be retrieved by **Your QChartView**.rubberBandRect()
self.chart_view.setDragMode(QGraphicsView.RubberBandDrag)
self.chart_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) #Don't need these, as they don't move the Graph. but the whole window
self.chart_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # ^
self.chart.setFocusPolicy(Qt.NoFocus)
self._tooltip = Callout(self.chart)
self._callouts = []
self.setMouseTracking(True) #Must be on
def tooltip(self, point, state):
#point = self.Mouse
if self._tooltip == 0:
self._tooltip = Callout(self._chart)
if state:
x = point.x()
y = point.y()
self._tooltip.set_text(f"X: {x:.1f} \nY: {y:.1f} ")
self._tooltip.set_anchor(point)
self._tooltip.setZValue(11)
self._tooltip.update_geometry()
self._tooltip.show()
else:
self._tooltip.hide()
def keep_callout(self):
self._callouts.append(self._tooltip)
self._tooltip = Callout(self.chart)
Now when I execute this the callouts appear perfect on one series, but on all the others the callouts appear above or below where my mouse is actually located, as the callout gets drawn on a different axis than the series is

Showing tooltip in a Qt chart with multiple y axes
Answer to this question could be found here in C++, answered by eyllanesc.
Here is a PySide6 version of the answer.
"""PySide6 port of the Callout example from Qt v5.x"""
import sys
from PySide6.QtWidgets import (QApplication, QGraphicsScene,
QGraphicsView, QGraphicsSimpleTextItem, QGraphicsItem)
from PySide6.QtCore import Qt, QPointF, QRectF, QRect
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QSplineSeries, QValueAxis
from PySide6.QtGui import QPainter, QFont, QFontMetrics, QPainterPath, QColor
class Callout(QGraphicsItem):
def __init__(self, chart, series):
QGraphicsItem.__init__(self, chart)
self._chart = chart
self._series = series
self._text = ""
self._textRect = QRectF()
self._anchor = QPointF()
self._font = QFont()
self._rect = QRectF()
def boundingRect(self):
anchor = self.mapFromParent(
self._chart.mapToPosition(self._anchor, self._series))
rect = QRectF()
rect.setLeft(min(self._rect.left(), anchor.x()))
rect.setRight(max(self._rect.right(), anchor.x()))
rect.setTop(min(self._rect.top(), anchor.y()))
rect.setBottom(max(self._rect.bottom(), anchor.y()))
return rect
def paint(self, painter, option, widget):
path = QPainterPath()
path.addRoundedRect(self._rect, 5, 5)
anchor = self.mapFromParent(
self._chart.mapToPosition(self._anchor, self._series))
if not self._rect.contains(anchor) and not self._anchor.isNull():
point1 = QPointF()
point2 = QPointF()
# establish the position of the anchor point in relation to _rect
above = anchor.y() <= self._rect.top()
above_center = (anchor.y() > self._rect.top() and
anchor.y() <= self._rect.center().y())
below_center = (anchor.y() > self._rect.center().y() and
anchor.y() <= self._rect.bottom())
below = anchor.y() > self._rect.bottom()
on_left = anchor.x() <= self._rect.left()
left_of_center = (anchor.x() > self._rect.left() and
anchor.x() <= self._rect.center().x())
right_of_center = (anchor.x() > self._rect.center().x() and
anchor.x() <= self._rect.right())
on_right = anchor.x() > self._rect.right()
# get the nearest _rect corner.
x = (on_right + right_of_center) * self._rect.width()
y = (below + below_center) * self._rect.height()
corner_case = ((above and on_left) or (above and on_right) or
(below and on_left) or (below and on_right))
vertical = abs(anchor.x() - x) > abs(anchor.y() - y)
x1 = (x + left_of_center * 10 - right_of_center * 20 + corner_case *
int(not vertical) * (on_left * 10 - on_right * 20))
y1 = (y + above_center * 10 - below_center * 20 + corner_case *
vertical * (above * 10 - below * 20))
point1.setX(x1)
point1.setY(y1)
x2 = (x + left_of_center * 20 - right_of_center * 10 + corner_case *
int(not vertical) * (on_left * 20 - on_right * 10))
y2 = (y + above_center * 20 - below_center * 10 + corner_case *
vertical * (above * 20 - below * 10))
point2.setX(x2)
point2.setY(y2)
path.moveTo(point1)
path.lineTo(anchor)
path.lineTo(point2)
path = path.simplified()
painter.setBrush(QColor(255, 255, 255))
painter.drawPath(path)
painter.drawText(self._textRect, self._text)
def mousePressEvent(self, event):
event.setAccepted(True)
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
self.setPos(self.mapToParent(
event.pos() - event.buttonDownPos(Qt.LeftButton)))
event.setAccepted(True)
else:
event.setAccepted(False)
def set_text(self, text):
self._text = text
metrics = QFontMetrics(self._font)
self._textRect = QRectF(metrics.boundingRect(
QRect(0.0, 0.0, 150.0, 150.0), Qt.AlignLeft, self._text))
self._textRect.translate(5, 5)
self.prepareGeometryChange()
self._rect = self._textRect.adjusted(-5, -5, 5, 5)
def set_anchor(self, point):
self._anchor = QPointF(point)
def update_geometry(self):
self.prepareGeometryChange()
self.setPos(self._chart.mapToPosition(
self._anchor, self._series) + QPointF(10, -50))
def setSeries(self, series):
self._series = series
class View(QChartView):
def __init__(self, parent=None):
super().__init__(parent)
self.setScene(QGraphicsScene(self))
self.setDragMode(QGraphicsView.RubberBandDrag)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# Chart
self._chart = QChart()
self._chart.setMinimumSize(640, 480)
self._chart.setTitle("Hover the line to show callout. Click the line "
"to make it stay")
self._chart.legend().hide()
self.series = QLineSeries()
self.series.append(1, 3)
self.series.append(4, 5)
self.series.append(5, 4.5)
self.series.append(7, 1)
self.series.append(11, 2)
self._chart.addSeries(self.series)
self.series2 = QSplineSeries()
self.series2.append(1.6, 1.4)
self.series2.append(2.4, 3.5)
self.series2.append(3.7, 2.5)
self.series2.append(7, 4)
self.series2.append(10, 2)
self._chart.addSeries(self.series2)
xaxis = QValueAxis()
xaxis.setTitleText("X")
self._chart.addAxis(xaxis, Qt.AlignBottom)
yaxis = QValueAxis()
yaxis.setTitleText("YR")
self._chart.addAxis(yaxis, Qt.AlignRight)
y2axis = QValueAxis()
y2axis.setTitleText("YL")
self._chart.addAxis(y2axis, Qt.AlignLeft)
self.series.attachAxis(xaxis)
self.series.attachAxis(y2axis)
self.series2.attachAxis(xaxis)
self.series2.attachAxis(yaxis)
self._chart.setAcceptHoverEvents(True)
self.setRenderHint(QPainter.Antialiasing)
self.scene().addItem(self._chart)
self._coordX = QGraphicsSimpleTextItem(self._chart)
self._coordX.setPos(
self._chart.size().width() / 2 - 50, self._chart.size().height())
self._coordX.setText("X: ")
self._coordY = QGraphicsSimpleTextItem(self._chart)
self._coordY.setPos(
self._chart.size().width() / 2 + 50, self._chart.size().height())
self._coordY.setText("Y: ")
self._callouts = []
self._tooltip = Callout(self._chart, self.series)
self.series.clicked.connect(self.keep_callout)
self.series.hovered.connect(self.tooltip)
self.series2.clicked.connect(self.keep_callout)
self.series2.hovered.connect(self.tooltip)
self.setMouseTracking(True)
def resizeEvent(self, event):
if self.scene():
self.scene().setSceneRect(QRectF(QPointF(0, 0), event.size()))
self._chart.resize(event.size())
self._coordX.setPos(
self._chart.size().width() / 2 - 50,
self._chart.size().height() - 20)
self._coordY.setPos(
self._chart.size().width() / 2 + 50,
self._chart.size().height() - 20)
for callout in self._callouts:
callout.update_geometry()
QGraphicsView.resizeEvent(self, event)
def mouseMoveEvent(self, event):
pos = self._chart.mapToValue(event.pos())
x = pos.x()
y = pos.y()
self._coordX.setText(f"X: {x:.2f}")
self._coordY.setText(f"Y: {y:.2f}")
QGraphicsView.mouseMoveEvent(self, event)
def keep_callout(self):
series = self.sender()
self._callouts.append(self._tooltip)
self._tooltip = Callout(self._chart, series)
def tooltip(self, point, state):
series = self.sender()
if self._tooltip == 0:
self._tooltip = Callout(self._chart, series)
if state:
x = point.x()
y = point.y()
self._tooltip.setSeries(series)
self._tooltip.set_text(f"X: {x:.2f} \nY: {y:.2f} ")
self._tooltip.set_anchor(point)
self._tooltip.setZValue(11)
self._tooltip.update_geometry()
self._tooltip.show()
else:
self._tooltip.hide()
if __name__ == "__main__":
app = QApplication(sys.argv)
v = View()
v.show()
sys.exit(app.exec())

Related

Trajectory plalnification

I have a program that generates circles and lines, where the circles can not collide with each other and the lines can not collide with the circles, the problem is that it only draws a line but not the others, it does not mark any error and as much as I think the reason I do not understand why, (I'm new to python, so excuse me if maybe the error is obvious)
I tried to remove the for from my CreaLin class and it does generate the lines but they all collide with the circles, I also thought that the collisionL function could be the problem since the method does not belong as such to the line class, but I need values from the circle class, so I don't know what would be another way to do it, I would like to know a method.
my code:
class CreaCir:
def __init__(self, figs):
self.figs = figs
def update(self):
if len(self.figs) <70:
choca = False
r = randint(5, 104)
x = randint(0, 600 + r)
y = randint(0, 400 + r)
creOne = Circulo(x, y, r)
for fig in (self.figs):
choca = creOne.colisionC(fig)
if choca == True:
break
if choca == False:
self.figs.append(creOne)
def dibujar(self, ventana):
pass
class CreaLin:
def __init__(self, lins):
self.lins = lins
def update(self):
if len(self.lins) <70:
choca = False
x = randint(0, 700)
y = randint(0, 500)
a = randint(0, 700)
b = randint(0, 500)
linOne = Linea(x, y, a, b)
for lin in (self.lins):
choca = linOne.colisionL(lin)
if choca == True:
break
if choca == False:
self.lins.append(linOne)
def dibujar(self, ventana):
pass
class Ventana:
def __init__(self, Ven_Tam= (700, 500)):
pg.init()
self.ven_tam = Ven_Tam
self.ven = pg.display.set_caption("Linea")
self.ven = pg.display.set_mode(self.ven_tam)
self.ven.fill(pg.Color('#404040'))
self.figs = []
self.lins = []
self.reloj = pg.time.Clock()
def check_events(self):
for event in pg.event.get():
if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE):
quit()
pg.display.flip()
def run(self):
cirCreater = CreaCir(self.figs)
linCreater = CreaLin(self.lins)
while True:
self.check_events()
cirCreater.update()
linCreater.update()
for fig in self.figs:
fig.dibujar(self.ven)
for lin in self.lins:
lin.dibujar(self.ven)
self.reloj.tick(60)
if __name__ == '__main__':
ven = Ventana()
ven.run()
class Circulo:
class Circulo(PosGeo):
def __init__(self, x, y, r):
self.x = x
self.y = y
self.radio = r
self.cx = x+r
self.cy = y+r
def __str__(self):
return f"Circulo, (X: {self.x}, Y: {self.y}), radio: {self.radio}"
def dibujar(self, ventana):
pg.draw.circle(ventana, "white", (self.cx, self.cy), self.radio, 1)
pg.draw.line(ventana, "white", (self.cx+2, self.cy+2),(self.cx-2, self.cy-2))
pg.draw.line(ventana, "white", (self.cx-2, self.cy+2),(self.cx+2, self.cy-2))
def update(self):
pass
def colisionC(self, c2):
return self.radio + c2.radio > sqrt(pow(self.cx - c2.cx, 2) + pow(self.cy - c2.cy, 2))
def colisionL(self, L2):
l0 = [L2.x, L2.y]
l1 = [L2.a, L2.b]
cp = [self.cx, self.cy]
x1 = l0[0] - cp[0]
y1 = l0[1] - cp[1]
x2 = l1[0] - cp[0]
y2 = l1[1] - cp[1]
dx = x2 - x1
dy = y2 - y1
dr = sqrt(dx*dx + dy*dy)
D = x1 * y2 - x2 * y1
discriminant = self.radio*self.radio*dr*dr - D*D
if discriminant < 0:
return False
else:
return True`
and finally my class line:
class Linea(PosGeo):
def __init__(self, x, y, a, b):
super().__init__(x, y)
self.x = x
self.y = y
self.a = a
self.b = b
def dibujar(self, ventana):
pg.draw.line(ventana, "#7B0000", (self.x, self.y), (self.a, self.b))
def update(self):
pass
def colisionL(self, l1):
pass
Result:
You need to implement the colisionC and colisionL methods in Linea and Circulo. See Problem with calculating line intersections for the line-line intersection algorithm. When checking for collisions between lines and circles, in addition to checking for collisions between circles and endless lines, you must also consider the beginning and end of the line segment:
class Linea:
# [...]
def colisionC(self, c2):
return c2.colisionL(self)
def colisionL(self, l1):
return Linea.intersect_line_line((self.x, self.y), (self.a, self.b), (l1.x, l1.y), (l1.a, l1.b))
def intersect_line_line(P0, P1, Q0, Q1):
d = (P1[0]-P0[0]) * (Q1[1]-Q0[1]) + (P1[1]-P0[1]) * (Q0[0]-Q1[0])
if d == 0:
return None
t = ((Q0[0]-P0[0]) * (Q1[1]-Q0[1]) + (Q0[1]-P0[1]) * (Q0[0]-Q1[0])) / d
u = ((Q0[0]-P0[0]) * (P1[1]-P0[1]) + (Q0[1]-P0[1]) * (P0[0]-P1[0])) / d
if 0 <= t <= 1 and 0 <= u <= 1:
return P1[0] * t + P0[0] * (1-t), P1[1] * t + P0[1] * (1-t)
return None
class Circulo
# [...]
def colisionC(self, c2):
return self.radio + c2.radio > sqrt(pow(self.cx - c2.cx, 2) + pow(self.cy - c2.cy, 2))
def colisionL(self, L2):
l0 = [L2.x, L2.y]
l1 = [L2.a, L2.b]
cp = [self.cx, self.cy]
x1 = l0[0] - cp[0]
y1 = l0[1] - cp[1]
x2 = l1[0] - cp[0]
y2 = l1[1] - cp[1]
if sqrt(x1*x1+y1*y1) < self.radio or sqrt(x2*x2+y2*y2) < self.radio:
return True
dx = x2 - x1
dy = y2 - y1
dr = sqrt(dx*dx + dy*dy)
D = x1 * y2 - x2 * y1
discriminant = self.radio*self.radio*dr*dr - D*D
if discriminant < 0:
return False
sign = lambda x: -1 if x < 0 else 1
xa = (D * dy + sign(dy) * dx * sqrt(discriminant)) / (dr * dr)
xb = (D * dy - sign(dy) * dx * sqrt(discriminant)) / (dr * dr)
ya = (-D * dx + abs(dy) * sqrt(discriminant)) / (dr * dr)
yb = (-D * dx - abs(dy) * sqrt(discriminant)) / (dr * dr)
ta = (xa-x1)*dx/dr + (ya-y1)*dy/dr
tb = (xb-x1)*dx/dr + (yb-y1)*dy/dr
return 0 < ta < dr or 0 < tb < dr
Put all objects into one container. Before you add a new Linea you have to check if the new "Line" intersects another object with "ColisionL". Before you add a new Circulo, you must check if the new "Line" intersects another object with CollisionC:
class CreaObjects:
def __init__(self, figs):
self.figs = figs
self.no_lines = 0
self.no_circles = 0
def update(self):
if self.no_circles <70:
r = randint(5, 104)
creOne = Circulo(randint(0, 600 - 2*r), randint(0, 400 - 2*r), r)
if not any(fig.colisionC(creOne) for fig in self.figs):
self.figs.append(creOne)
self.no_circles += 1
if self.no_lines <70:
linOne = Linea(randint(0, 700), randint(0, 500), randint(0, 700), randint(0, 500))
if not any(fig.colisionL(linOne) for fig in self.figs):
self.figs.append(linOne)
self.no_lines += 1
Complete example:
import pygame as pg
from random import randint
from math import sqrt, hypot
class Linea:
def __init__(self, x, y, a, b):
self.x = x
self.y = y
self.a = a
self.b = b
def dibujar(self, ventana):
pg.draw.line(ventana, "#7B0000", (self.x, self.y), (self.a, self.b))
def update(self):
pass
def colisionC(self, c2):
return c2.colisionL(self)
def colisionL(self, l1):
return Linea.intersect_line_line((self.x, self.y), (self.a, self.b), (l1.x, l1.y), (l1.a, l1.b))
def intersect_line_line(P0, P1, Q0, Q1):
d = (P1[0]-P0[0]) * (Q1[1]-Q0[1]) + (P1[1]-P0[1]) * (Q0[0]-Q1[0])
if d == 0:
return None
t = ((Q0[0]-P0[0]) * (Q1[1]-Q0[1]) + (Q0[1]-P0[1]) * (Q0[0]-Q1[0])) / d
u = ((Q0[0]-P0[0]) * (P1[1]-P0[1]) + (Q0[1]-P0[1]) * (P0[0]-P1[0])) / d
if 0 <= t <= 1 and 0 <= u <= 1:
return P1[0] * t + P0[0] * (1-t), P1[1] * t + P0[1] * (1-t)
return None
class Circulo:
def __init__(self, x, y, r):
self.x = x
self.y = y
self.radio = r
self.cx = x+r
self.cy = y+r
def __str__(self):
return f"Circulo, (X: {self.x}, Y: {self.y}), radio: {self.radio}"
def dibujar(self, ventana):
pg.draw.circle(ventana, "white", (self.cx, self.cy), self.radio, 1)
pg.draw.line(ventana, "white", (self.cx+2, self.cy+2),(self.cx-2, self.cy-2))
pg.draw.line(ventana, "white", (self.cx-2, self.cy+2),(self.cx+2, self.cy-2))
def update(self):
pass
def colisionC(self, c2):
return self.radio + c2.radio > sqrt(pow(self.cx - c2.cx, 2) + pow(self.cy - c2.cy, 2))
def colisionL(self, L2):
l0 = [L2.x, L2.y]
l1 = [L2.a, L2.b]
cp = [self.cx, self.cy]
x1 = l0[0] - cp[0]
y1 = l0[1] - cp[1]
x2 = l1[0] - cp[0]
y2 = l1[1] - cp[1]
if sqrt(x1*x1+y1*y1) < self.radio or sqrt(x2*x2+y2*y2) < self.radio:
return True
dx = x2 - x1
dy = y2 - y1
dr = sqrt(dx*dx + dy*dy)
D = x1 * y2 - x2 * y1
discriminant = self.radio*self.radio*dr*dr - D*D
if discriminant < 0:
return False
sign = lambda x: -1 if x < 0 else 1
xa = (D * dy + sign(dy) * dx * sqrt(discriminant)) / (dr * dr)
xb = (D * dy - sign(dy) * dx * sqrt(discriminant)) / (dr * dr)
ya = (-D * dx + abs(dy) * sqrt(discriminant)) / (dr * dr)
yb = (-D * dx - abs(dy) * sqrt(discriminant)) / (dr * dr)
ta = (xa-x1)*dx/dr + (ya-y1)*dy/dr
tb = (xb-x1)*dx/dr + (yb-y1)*dy/dr
return 0 < ta < dr or 0 < tb < dr
class CreaObjects:
def __init__(self, figs):
self.figs = figs
self.no_lines = 0
self.no_circles = 0
def update(self):
if self.no_circles <70:
r = randint(5, 104)
creOne = Circulo(randint(0, 600 - 2*r), randint(0, 400 - 2*r), r)
if not any(fig.colisionC(creOne) for fig in self.figs):
self.figs.append(creOne)
self.no_circles += 1
if self.no_lines <70:
linOne = Linea(randint(0, 700), randint(0, 500), randint(0, 700), randint(0, 500))
if not any(fig.colisionL(linOne) for fig in self.figs):
self.figs.append(linOne)
self.no_lines += 1
class Ventana:
def __init__(self, Ven_Tam= (700, 500)):
pg.init()
self.ven_tam = Ven_Tam
self.ven = pg.display.set_caption("Linea")
self.ven = pg.display.set_mode(self.ven_tam)
self.figs = []
self.reloj = pg.time.Clock()
def check_events(self):
for event in pg.event.get():
if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE):
quit()
def run(self):
cirCreater = CreaObjects(self.figs)
while True:
self.check_events()
cirCreater.update()
self.ven.fill(pg.Color('#404040'))
for fig in self.figs:
fig.dibujar(self.ven)
pg.display.flip()
self.reloj.tick(60)
if __name__ == '__main__':
ven = Ventana()
ven.run()

QGraphicsItem Rotation handle

I'm new to PyQt5. I'm trying to implement item control like this
there you can rotate item by dragging the rotation handle. What a have for now:
import math
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from math import sqrt, acos
class QDMRotationHandle(QGraphicsPixmapItem):
def __init__(self, item):
super().__init__(item)
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable)
self.setPixmap(QPixmap('rotation_handle.png').scaledToWidth(20))
self.setTransformOriginPoint(10, 10)
self.item = item
self.item.boundingRect().moveCenter(QPointF(50, 50))
self.hypot = self.parentItem().boundingRect().height()
self.setPos(self.item.transformOriginPoint().x()-10, self.item.transformOriginPoint().y() - self.hypot)
def getSecVector(self, sx, sy, ex, ey):
return {"x": ex - sx, "y": 0}
def getVector(self, sx, sy, ex, ey):
if ex == sx or ey == sy:
return 0
return {"x": ex - sx, "y": ey - sy}
def getVectorAngleCos(self, ax, ay, bx, by):
ma = sqrt(ax * ax + ay * ay)
mb = sqrt(bx * bx + by * by)
sc = ax * bx + ay * by
res = sc / ma / mb
return res
def mouseMoveEvent(self, event):
pos = self.mapToParent(event.pos())
# parent_pos = self.mapToScene(self.parent.transformOriginPoint())
parent_pos = self.parentItem().pos()
parent_center_pos = self.item.boundingRect().center()
parent_pos_x = parent_pos.x() + self.item.transformOriginPoint().x()
parent_pos_y = parent_pos.y() + self.item.transformOriginPoint().y()
print("mouse: ", pos.x(), pos.y())
print("handle: ", self.pos().x(), self.pos().y())
print("item: ", parent_pos.x(), parent_pos.y())
#print(parent_center_pos.x(), parent_center_pos.y())
vecA = self.getVector(parent_pos_x, parent_pos_y, pos.x(), pos.y())
vecB = self.getSecVector(parent_pos_x, parent_pos_y, pos.x(), parent_pos_y)
#
vect.setLine(parent_pos_x, parent_pos_y, pos.x(), pos.y())
if pos.x() > parent_pos_x:
#
secVect.setLine(parent_pos_x, parent_pos_y, pos.x(), parent_pos_y)
vecB = self.getSecVector(parent_pos_x, parent_pos_y, pos.x(), parent_pos_y)
elif pos.x() < parent_pos_x:
#
secVect.setLine(parent_pos_x, parent_pos_y, pos.x(), parent_pos_y)
vecB = self.getSecVector(parent_pos_x, parent_pos_y, pos.x(), -parent_pos_y)
if vecA != 0:
cos = self.getVectorAngleCos(vecA["x"], vecA["y"], vecB["x"], vecB["y"])
cos = abs(cos)
if cos > 1:
cos = 1
sin = abs(sqrt(1 - cos ** 2))
lc = self.hypot * cos
ld = self.hypot * sin
#self.ell = scene.addRect(parent_pos_x, parent_pos_y, 5, 5)
if pos.x() < parent_pos_x and pos.y() < parent_pos_y:
print(parent_pos_x, parent_pos_y )
#self.ell.setPos(parent_pos.x(), parent_pos.y())
self.setPos(parent_pos_x - lc, parent_pos_y - ld)
elif pos.x() > parent_pos_x and pos.y() < parent_pos_y:
#self.ell.setPos(parent_pos_x, parent_pos_y)
self.setPos(parent_pos_x + lc, parent_pos_y - ld)
elif pos.x() > parent_pos_x and pos.y() > parent_pos_y:
#self.ell.setPos(parent_pos_x, parent_pos_y)
self.setPos(parent_pos_x + lc, parent_pos_y + ld)
elif pos.x() < parent_pos_x and pos.y() > parent_pos_y:
#self.ell.setPos(parent_pos_x, parent_pos_y)
self.setPos(parent_pos_x - lc, parent_pos_y + ld)
else:
if pos.x() == parent_pos_x and pos.y() < parent_pos_y:
self.setPos(parent_pos_x, parent_pos_y - self.hypot)
elif pos.x() == parent_pos_x and pos.y() > parent_pos_y:
self.setPos(parent_pos_x, parent_pos_y + self.hypot)
elif pos.y() == parent_pos_x and pos.x() > parent_pos_y:
self.setPos(parent_pos_x + self.hypot, parent_pos_y)
elif pos.y() == parent_pos_x and pos.x() < parent_pos_y:
self.setPos(parent_pos_x - self.hypot, parent_pos_y)
item_position = self.item.transformOriginPoint()
handle_pos = self.pos()
#print(item_position.y())
angle = math.atan2(item_position.y() - handle_pos.y(),
item_position.x() - handle_pos.x()) / math.pi * 180 - 90
self.item.setRotation(angle)
self.setRotation(angle)
class QDMBoundingRect(QGraphicsRectItem):
def __init__(self, item, handle):
super().__init__()
self.item = item
self.handle = handle
item.setParentItem(self)
handle.setParentItem(self)
self.setRect(0, 0, item.boundingRect().height(), item.boundingRect().width())
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable)
app = QtWidgets.QApplication([])
scene = QtWidgets.QGraphicsScene()
item = scene.addRect(0, 0, 100, 100)
item.setTransformOriginPoint(50, 50)
handle_item = QDMRotationHandle(item)
#handle_item.setParentItem(item)
# handle_item.setOffset(10, 10)
#handle_item.setPos(40, -40)
scene.addItem(handle_item)
vect = scene.addLine(0, 0, 100, 100)
secVect = scene.addLine(50, 50, 100, 100)
secVect.setPen(QPen(Qt.green))
boundingRect = QDMBoundingRect(item, handle_item)
scene.addItem(boundingRect)
view = QtWidgets.QGraphicsView(scene)
view.setFixedSize(500, 500)
view.show()
app.exec_()
It works fine when item is in the initial position, but if you move it, the rotation stops working as it should. It seems like i do something wrong with coordinates, but i cant understand what. Please help...
Your object structure is a bit disorganized and unnecessarily convoluted.
For instance:
you're adding handle_item to the scene, but since you've made it a child of item, you shall not try to add it to the scene again;
the ItemIsMovable is useless if you don't call the default implementation in mouseMoveEvent, but for your purpose you actually don't need to make it movable at all;
the whole computation is unnecessarily complex (and prone to bugs, since you're both setting the object position and rotation);
using a pixmap for the handle seems quite unnecessary, use Qt primitives if you can, to avoid graphical artifacts and unnecessary transform computations;
What you could do, instead, is to use a single "bounding rect item", and add controls as its children. Then, by filtering mouse events of those children, you can then alter the rotation based on the scene position of those events.
In the following example, I took care of the above, considering:
control/handle items should always be positioned at 0, 0, so that the computation required for getting/setting their position is much easier;
since items (including vector items) are actually children of the QDMBoundingRect, you don't need to compute coordinates for each point of the vectors: the rotation of the parent will be applied to them automatically;
the secVect (still a child item) is simplified by just setting its x offset using the mapped scene positions of center and handle;
Also, by calling setFiltersChildEvents(True) the sceneEventFilter() receives any scene event from its children, allowing us to track mouse events; we return True for all the events that we handle so that they are not propagated to the parent, and also because move events can only received if mouse press events are accepted (the default implementation ignores them, unless the item is movable).
class QDMBoundingRect(QGraphicsRectItem):
_startPos = QPointF()
def __init__(self, item):
super().__init__()
self.setRect(item.boundingRect())
self.setFlags(self.ItemIsMovable)
self.setFiltersChildEvents(True)
self.item = item
self.center = QGraphicsEllipseItem(-5, -5, 10, 10, self)
self.handle = QGraphicsRectItem(-10, -10, 20, 20, self)
self.vect = QGraphicsLineItem(self)
self.secVect = QGraphicsLineItem(self)
self.secVect.setPen(Qt.green)
self.secVect.setFlags(self.ItemIgnoresTransformations)
self.setCenter(item.transformOriginPoint())
def setCenter(self, center):
self.center.setPos(center)
self.handle.setPos(center.x(), -40)
self.vect.setLine(QLineF(center, self.handle.pos()))
self.secVect.setPos(center)
self.setTransformOriginPoint(center)
def sceneEventFilter(self, item, event):
if item == self.handle:
if (event.type() == event.GraphicsSceneMousePress
and event.button() == Qt.LeftButton):
self._startPos = event.pos()
return True
elif (event.type() == event.GraphicsSceneMouseMove
and self._startPos is not None):
centerPos = self.center.scenePos()
line = QLineF(centerPos, event.scenePos())
self.setRotation(90 - line.angle())
diff = self.handle.scenePos() - centerPos
self.secVect.setLine(0, 0, diff.x(), 0)
return True
if (event.type() == event.GraphicsSceneMouseRelease
and self._startPos is not None):
self._startPos = None
return True
return super().sceneEventFilter(item, event)
app = QApplication([])
scene = QGraphicsScene()
item = scene.addRect(0, 0, 100, 100)
item.setPen(Qt.red)
item.setTransformOriginPoint(50, 50)
boundingRect = QDMBoundingRect(item)
scene.addItem(boundingRect)
view = QGraphicsView(scene)
view.setFixedSize(500, 500)
view.show()
app.exec_()
With the above code you can also implement the movement of the "center" handle, allowing to rotate around a different position:
def sceneEventFilter(self, item, event):
if item == self.handle:
# ... as above
elif item == self.center:
if (event.type() == event.GraphicsSceneMousePress
and event.button() == Qt.LeftButton):
self._startPos = event.pos()
return True
elif (event.type() == event.GraphicsSceneMouseMove
and self._startPos is not None):
newPos = self.mapFromScene(
event.scenePos() - self._startPos)
self.setCenter(newPos)
return True
if (event.type() == event.GraphicsSceneMouseRelease
and self._startPos is not None):
self._startPos = None
return True
return super().sceneEventFilter(item, event)

Is there a good way to speed up raytracing in Python?

I've been following the Raytracing in one weekend tutorial series (which is coded in C++), but I've run into a problem. I know Python is not really the language for raytracing, and it shows. Rendering 4 spheres with 100 samples per pixel at 600x300px with multithreading took a little over an quarter of an hour.
I figured that multithreading wasn't really doing anything. I needed multiprocessing. I tried to implement it, but it gave me the error that pygame.Surface wasn't serializable. Is there a solution for this, or maybe even a better way of implementing this? Or is the best way forward to recreate this in C#/C++?
The render:
Multithreaded code:
vec3.py
import math
import random
class vec3:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __add__(self, other):
return vec3(
self.x + other.x,
self.y + other.y,
self.z + other.z
)
def __sub__(self, other):
return vec3(
self.x - other.x,
self.y - other.y,
self.z - other.z
)
def __mul__(self, val):
if type(val) == vec3:
return vec3(
self.x * val.x,
self.y * val.y,
self.z * val.z
)
else:
return vec3(
self.x * val,
self.y * val,
self.z * val
)
def __truediv__(self, val):
return vec3(
self.x / val,
self.y / val,
self.z / val
)
def __pow__(self, pow):
return vec3(
self.x ** pow,
self.y ** pow,
self.z ** pow
)
def __rshift__(self, val):
return vec3(
self.x >> val,
self.y >> val,
self.z >> val
)
def __repr__(self):
return "vec3({}, {}, {})".format(self.x, self.y, self.z)
def __str__(self):
return "({}, {}, {})".format(self.x, self.y, self.z)
def dot(self, other):
return (
self.x * other.x +
self.y * other.y +
self.z * other.z
)
def cross(self, other):
return vec3(
(self.y * other.z) - (self.z * other.y),
-(self.x * other.z) - (self.z * other.x),
(self.x * other.y) - (self.y * other.x)
)
def length(self):
return math.sqrt(
(self.x ** 2) +
(self.y ** 2) +
(self.z ** 2)
)
def squared_length(self):
return (
(self.x ** 2) +
(self.y ** 2) +
(self.z ** 2))
def make_unit_vector(self):
return self / (self.length() + 0.0001)
def reflect(self, normal):
return self - normal * (self.dot(normal) * 2)
Raytracing.py
import uuid
import math
import pygame
from random import random
from vec3 import vec3
from threading import Thread
#import time
imprecision = 0.00000001
mindist = 0.001
maxdist = 999999999
class Material:
def random_in_unit_sphere():
p = vec3(random(), random(), random())*2 - vec3(1, 1, 1)
while p.squared_length() >= 1:
p = vec3(random(), random(), random())*2 - vec3(1, 1, 1)
return p
class Matte:
def __init__(self, albedo):
self.albedo = albedo
def scatter(self, ray, rec, scattered):
target = rec.p + rec.normal + Material.random_in_unit_sphere()
scattered.origin = rec.p
scattered.direction = target - rec.p
return True, self.albedo
class Metal:
def __init__(self, albedo, fuzz = 0):
self.albedo = albedo
self.fuzz = fuzz
def scatter(self, ray, rec, scattered):
reflected = ray.direction.make_unit_vector().reflect(rec.normal)
scattered.origin = rec.p
scattered.direction = reflected + (Material.random_in_unit_sphere() * self.fuzz)
return (scattered.direction.dot(rec.normal) > 0), self.albedo
class Transparent:
def __init__(self, refractive_index):
self.ri = refractive_index
def refract(self, v, normal, ni_over_nt):
uv = v.make_unit_vector()
dt = uv.dot(normal)
discriminant = 1 - (ni_over_nt * ni_over_nt * (1-dt*dt))
if discriminant > 0:
refracted = (uv - normal*dt) * ni_over_nt - (normal * math.sqrt(discriminant))
return True, refracted
else:
return False, None
def scatter(self, ray, rec, scattered):
reflected = ray.direction.reflect(rec.normal)
if ray.direction.dot(rec.normal) > 0:
outward_normal = 0 - rec.normal
ni_over_nt = self.ri
else:
outward_normal = rec.normal
ni_over_nt = 1 / self.ri
refracted = self.refract(ray.direction, outward_normal, ni_over_nt)
return bool(refracted[0]), vec3(1, 1, 1)
class Object:
class Sphere:
def __init__(self, pos, r, material=Material.Matte(0.5)):
self.center = pos
self.r = r
self.material = material
def hit(self, ray, t_min, t_max, rec):
oc = ray.origin - self.center
a = ray.direction.dot(ray.direction)
b = oc.dot(ray.direction)
c = oc.dot(oc) - (self.r * self.r)
discriminant = (b*b) - (a*c)
if discriminant < 0:
return False
else:
t = (-b - math.sqrt(discriminant)) / (a + imprecision)
if (t_min < t < t_max):
rec.t = t
rec.p = ray.point_at_parameter(t)
rec.normal = (rec.p - self.center) / self.r
rec.material = self.material
return True
t = (-b + math.sqrt(discriminant)) / (a + imprecision)
if (t_min < t < t_max):
rec.t = t
rec.p = ray.point_at_parameter(t)
rec.normal = (rec.p - self.center) / self.r
rec.material = self.material
return True
class Scene:
def genUUID(self):
id = uuid.uuid4()
while id in self.objects.keys():
id = uuid.uuid4()
return id
def __init__(self):
self.objects = {}
def add(self, object):
id = self.genUUID()
self.objects[id] = object
return id
def hit(self, ray, t_min, t_max, hit_record):
tmp_hit_record = Camera.HitRec()
hit_any = False
closest = maxdist
for object in self.objects.keys():
if self.objects[object].hit(ray, t_min, closest, tmp_hit_record):
hit_any = True
closest = tmp_hit_record.t
hit_record.t = tmp_hit_record.t
hit_record.p = tmp_hit_record.p
hit_record.normal = tmp_hit_record.normal
hit_record.material = tmp_hit_record.material
return hit_any
class Camera:
class Ray:
def __init__(self, A, B):
self.origin = A
self.direction = B
def point_at_parameter(self, t):
return self.origin + (self.direction * t)
class HitRec:
def __init__(self):
self.t = 0
self.p = vec3(0, 0, 0)
self.normal = vec3(0, 0, 0)
def __init__(self, w, screen, scene = Scene(), screen_ratio = 16/9, focal_length = 1):
self.w = w
self.h = int(w / screen_ratio)
self.screen = screen
self.scene = scene
self.screen_ratio = screen_ratio
self.screen_unit_width = 4
self.focal_length = focal_length
self.sampels_per_pixel = 1
self.max_hit_depth = 5
self.max_block_size = 100
# self.depthT = pygame.Surface((w, self.h))
self.origin = vec3(0, 0, 0)
self.horizontal = vec3(self.screen_unit_width, 0, 0)
self.vertical = vec3(0, (-self.screen_unit_width / self.screen_ratio), 0)
self.top_left_corner = \
self.origin - (self.horizontal/2) - (self.vertical/2) - vec3(0, 0, self.focal_length)
def color(self, ray, depth=0):
# Check for sphere hit
rec = self.HitRec()
if self.scene.hit(ray, mindist, maxdist, rec):
scattered = self.Ray(vec3(0, 0, 0), vec3(0, 0, 0))
hit = rec.material.scatter(ray, rec, scattered) # Scattered is the output ray
if depth < self.max_hit_depth and hit[0]:
return self.color(scattered, depth + 1) * hit[1]
else:
return hit[1]
# target = rec.p + rec.normal + self.random_in_unit_sphere()
# return self.color(self.Ray(rec.p, target - rec.p)) * 0.5
## return vec3(rec.normal.x + 1, rec.normal.y + 1, rec.normal.z + 1) * 0.5, rec.t
# Background if nothing's hit
unit_dir = ray.direction.make_unit_vector()
t = 0.5 * (unit_dir.y + 1)
return (vec3(1, 1, 1) * (1 - t)) + (vec3(0.5, 0.7, 1) * t)#, maxdist
def getRay(self, u, v):
return self.Ray(
self.origin,
self.top_left_corner + (self.horizontal * (u + imprecision)) + (self.vertical * (v + imprecision))
)
def renderSquare(self, xoff, yoff, w, h):
pygame.draw.rect(self.screen, (255, 255, 255), (xoff, yoff, w, h))
pygame.display.update()
for y in range(h):
for x in range(w):
col = vec3(0, 0, 0)
for n in range(self.sampels_per_pixel):
u = (x + xoff + random()-0.5) / self.w
v = (y + yoff + random()-0.5) / self.h
r = self.getRay(u, v)
col += (self.color(r) / self.sampels_per_pixel)
col = vec3(math.sqrt(col.x), math.sqrt(col.y), math.sqrt(col.z))
self.screen.set_at((x + xoff, y + yoff), (col.x * 255, col.y * 255, col.z * 255))
pygame.display.update()
def render(self):
blocks = []
for i in range(math.ceil(self.w / self.max_block_size)):
for j in range(math.ceil(self.h / self.max_block_size)):
blocks.append(Thread( target = self.renderSquare, args=(
i * self.max_block_size,
j * self.max_block_size,
self.max_block_size,
self.max_block_size
)))
for job in blocks:
job.start()
for job in blocks:
job.join()
if __name__ == '__main__':
from Raytracing import Camera, Scene, Object
import pygame
pygame.init()
WIDTH, HEIGHT = 600, 300
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Multithreaded raytracer')
scene = Scene()
scene.add(Object.Sphere(vec3( 0, 0, -1), 0.5, Material.Matte(vec3(0, 0.3, 0.8))))
scene.add(Object.Sphere(vec3(-1, 0, -1), 0.5, Material.Metal(vec3(0.8, 0.8, 0.8), 0.1)))
scene.add(Object.Sphere(vec3( 1, 0, -1), 0.5, Material.Metal(vec3(0.8, 0.6, 0.2), 1)))
scene.add(Object.Sphere(vec3(0, -100.5, -1), 100, Material.Matte(vec3(0.8, 0.8, 0))))
tracer = Camera(WIDTH, screen, scene, screen_ratio=2 / 1)
# tracer.render()
tracer.sampels_per_pixel = 100
tracer.render()
loop = True
while loop:
for event in pygame.event.get():
if event.type==pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
loop = False
if event.type==pygame.QUIT:
loop = False
pygame.display.update()
pygame.quit()

qtpropertyanimation only change the scale

I used animator class to generate ellipseobject and also add animation to them, the goal is only change the scale (from double size to normal size) of the dot, but not do the translate. Right now what I am facing is the dot does shrink, but it moves from the location where (x y value are double with its original x y value) to its original x y location.
Here is the animator class
class Animator:
def __init__(self, animation_config, num_rows, num_cols, background_colour, parent, user_idx = 0, resolution_width = 1920,
resolution_height = 1200, log_file_path = "coordinates.txt"):
with open(log_file_path, "w") as log_file:
log_file.write("")
circle = animation_config["circle"]
self.__cur_circle = None
self.__cur_colour = None
self.__animated_colour = QColor(*circle["colour"])
self.__log_file_path = log_file_path
# Initialize the circle table
diamond_square = DiamondSquare()
self.__user_idx = user_idx % 64
shift_xn = diamond_square.dx[user_idx]
shift_yn = diamond_square.dy[user_idx]
intv_x = (resolution_width - 20) / 10
intv_y = (resolution_height - 20) / 10
print(shift_xn,shift_yn)
self.__circle_table = []
y = 0
for i in range (11):
circles = []
x = 0
for j in range (11):
cir_x = (j * intv_x) + shift_xn
cir_y = (i * intv_y) + shift_yn
print(cir_x,cir_y)
if cir_x <= resolution_width and cir_y <= resolution_height:
circles.append(EllipseObject(parent,cir_x,cir_y, x, y, intv_x, intv_y,
hidden_colour = background_colour,
display_colour=self.__animated_colour))
x += 1
y += 1
self.__circle_table.append(circles)
# Initalize the first animation
self.__first = QPropertyAnimation()
self.__first.setPropertyName(b"scale")
self.__first.setDuration(animation_config["animation_duration"])
self.__first.setStartValue(2)
self.__first.setEndValue(1)
and here is my EllipseObject class
class EllipseObject(QGraphicsWidget):
def __init__(
self,
parent,
x = 0,
y = 0,
ind_x = 0,
ind_y = 0,
intv_x = 0,
intv_y = 0,
width = 20,
height = 20,
hidden = True,
hidden_colour = QColor(Qt.white),
display_colour = QColor(Qt.black)
):
super().__init__(parent)
self.__x = x
self.__y = y
self.__ind_x = ind_x
self.__ind_y = ind_y
self.__intv_x = intv_x
self.__intv_y = intv_y
self.__height = height
self.__width = width
self.hidden = hidden
self.hidden_colour = hidden_colour
self.display_colour = display_colour
def paint(self, painter, option, widget = None):
colour = QColor(Qt.white) if self.hidden else self.display_colour
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(colour)
painter.setBrush(QBrush(colour))
# painter.drawEllipse(self.__x, self.__y, self.__width, self.__height)
painter.drawEllipse(self.boundingRect())
# point = self.mapToScene(self.boundingRect().center())
# print("Draw this ",point.x(), point.y())
def get_coordinates(self):
point = self.mapToScene(self.boundingRect().center())
return (point.x(), point.y())
def boundingRect(self):
return QRectF(self.__x,
self.__y,
self.__height, self.__width)
What you are probably observing is that it is rising with respect to the upper left corner and will give you the appearance that it is moving. The QGraphicsItem has a property called transformOriginPoint with respect to which transformations, such as rotation and scaling, are performed and which is found at point (0, 0). In your case your code has several errors, you must set the position with setPos() and not through boundingRect(), so that the scaling is with respect to the center of the item establishes that the boundingRect() is symmetric.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class EllipseObject(QGraphicsWidget):
def __init__(
self,
parent=None,
x = 0,
y = 0,
ind_x = 0,
ind_y = 0,
intv_x = 0,
intv_y = 0,
width = 20,
height = 20,
hidden = True,
hidden_colour = QColor(Qt.white),
display_colour = QColor(Qt.black)
):
super().__init__(parent)
self.__x = x
self.__y = y
self.__ind_x = ind_x
self.__ind_y = ind_y
self.__intv_x = intv_x
self.__intv_y = intv_y
self.__height = height
self.__width = width
self.hidden = hidden
self.hidden_colour = hidden_colour
self.display_colour = display_colour
self.setPos(self.__x, self.__y)
def paint(self, painter, option, widget = None):
colour = QColor(Qt.white) if self.hidden else self.display_colour
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(colour)
painter.setBrush(QBrush(colour))
# painter.drawEllipse(self.__x, self.__y, self.__width, self.__height)
painter.drawEllipse(self.boundingRect())
def get_coordinates(self):
return (self.pos().x(), self.pos().y())
def boundingRect(self):
return QRectF(-0.5*self.__width,
-0.5*self.__height,
self.__height, self.__width)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = QGraphicsView()
scene = QGraphicsScene(w)
w.setScene(scene)
it = EllipseObject(x=100, y=100, hidden=False)
scene.addItem(it)
animation = QPropertyAnimation(it, b"scale")
animation.setDuration(2000)
animation.setStartValue(2)
animation.setEndValue(1)
animation.start()
w.show()
sys.exit(app.exec_())
Note:
Instead of using flag called hidden, use setVisible(boolean), show() or hide() directly. Qt already has many methods implemented, the wheel does not reinvent.

paintevent in continuous loop?

I have the following code which draws ellipse on the circumference of a circle and then draws chords between ellipses. The chords are drawn fine but the event bombs on me and goes into a infinite loop.
def binomial(i, n):
return math.factorial(n) / float(
math.factorial(i) * math.factorial(n - i))
def bernstein(t, i, n):
return binomial(i, n) * (t ** i) * ((1 - t) ** (n - i))
def bezier(t, points):
n = len(points) - 1
x = y = 0
for i, pos in enumerate(points):
bern = bernstein(t, i, n)
x += pos[0] * bern
y += pos[1] * bern
return x, y
def bezier_curve_range(n, points):
for i in xrange(n):
t = i / float(n - 1)
yield bezier(t, points)
def getControlPt(x1, y1, x2, y2, t):
new_px = (1 - t) * x1 + t * x2
new_py = (1 - t) * y1 + t * y2
return new_px, new_py
class Temp1(QWidget):
def __init__(self, c, parent=None):
super(Temp1, self).__init__(parent)
self.text = "Hi"
self.cords = c
def paintEvent(self, e):
qp = QPainter(self)
qp.setPen(Qt.red)
bluePen = QPen(Qt.blue, 1, Qt.DashLine)
steps = 10000
for i in range(15):
for j in range(15):
if i == j:
continue
px, py = getControlPt(self.cords[i][0], self.cords[i][1], self.cords[j][0], self.cords[j][1], 0.55)
px1, py1 = getControlPt(px, py, self.cords[j][0], self.cords[j][1], 0.25)
px2, py2 = getControlPt(px1, py1, self.cords[j][0], self.cords[j][1], 0.75)
cp = (self.cords[i], (px, py) , (px1, py1), (px2, py2), self.cords[j])
oldPoint = cp[0]
qp.setPen(bluePen)
for point in bezier_curve_range(steps, cp):
qp.drawLine(oldPoint[0], oldPoint[1], point[0], point[1])
oldPoint = point
class Temp(QPushButton):
def __init__(self, x, y, w, h, theta, cords, parent=None):
super(Temp, self).__init__(parent)
self.w = w
self.h = h
self.x = x
self.y = y
self.text = "test"
self.angle = theta
self.cords = cords
def paintEvent(self, e):
qp = QPainter(self)
qp.setPen(Qt.red)
qp.drawEllipse(0, 0, self.w - 2, self.h -2)
class Example(QWidget):
def __init__(self):
super(Example, self).__init__()
self.cords = []
self.initUI()
def initUI(self):
self.setWindowTitle('Points')
self.drawP()
def drawP(self):
theta = 2 * np.pi / 15
for i in range(15):
angle = theta * (i + 1)
dx = int(round(400 + 300 * np.cos(angle)))
dy = int(round(400 + 300 * np.sin(angle)))
self.cords.append((dx, dy))
for i in range(15):
t = Temp(self.cords[i][0], self.cords[i][1], 45, 20, np.degrees(angle), self.cords, parent=self)
t.setGeometry(self.cords[i][0], self.cords[i][1] , 45, 20)
t1 = Temp1(self.cords, parent = self)
t1.setGeometry(0, 0, 800, 600)
app = QApplication(sys.argv)
ex = Example()
ex.setGeometry(0, 0, 800, 600)
ex.show()
sys.exit(app.exec_())
The problem is with the paintevent function of class Temp1.
If you throw a print 'Temp1.paintEvent', e, i, j inside the nested for loops inside of Temp1.paintEvent, you'll see that it's not hanging, it's just slow, and every time you hide the window, then bring it back up, it has to go through the whole paintEvent again before actually showing it. If you're patient the window will eventually pop up.

Categories