I am using PyQT for one of the first times, and I'm having trouble figuring out where a mouse click is in 3D space. Obviously it is not a perfect 1-to-1 mapping, but let's say that I click on a location (x, y, 0) on my QtGui. How can I transform that mouse click to its 3D location using the camera?
Context: I am trying to have users draw splines in 3-D, and in order to do this, I need to know where the user is clicking for when I render the spline. I'm building an application using PythonOCC. I've attached my code below.
import random
import sys
import IPython
from OCC.Display.qtDisplay import qtViewer3d, get_qt_modules
from OCC.gp import gp_Pnt2d, gp_Pnt
from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeVertex,
BRepBuilderAPI_MakeWire)
from OCC.BRepFill import BRepFill_Filling
from OCC.GeomAbs import GeomAbs_C0
from OCC.GeomAPI import GeomAPI_PointsToBSpline
from OCC.TColgp import TColgp_Array1OfPnt
QtCore, QtGui, QtOpenGL = get_qt_modules()
try:
from OpenGL.GL import (glViewport, glMatrixMode, glOrtho, glLoadIdentity,
GL_PROJECTION, GL_MODELVIEW)
except ImportError:
msg = "for this example, the OpenGL module is required" \
"why not run \"pip install PyOpenGL\"\?"
sys.exit(status=1)
class GLWidget(qtViewer3d):
def __init__(self, parent=None):
super(GLWidget, self).__init__(parent)
self._initialized = False
midnight = QtCore.QTime(0, 0, 0)
random.seed(midnight.secsTo(QtCore.QTime.currentTime()))
self.object = 0
self.xRot = 0
self.yRot = 0
self.zRot = 0
self.image = QtGui.QImage()
self.bubbles = []
self.lastPos = QtCore.QPoint()
self.lines = []
self.current_point = None
self.pts = []
self.shiftHeld = True
self.trolltechGreen = QtGui.QColor.fromCmykF(0.40, 0.0, 1.0, 0.0)
self.trolltechPurple = QtGui.QColor.fromCmykF(0.39, 0.39, 0.0, 0.0)
self.animationTimer = QtCore.QTimer()
self.animationTimer.setSingleShot(False)
self.animationTimer.timeout.connect(self.animate)
self.animationTimer.start(25)
self.setAutoFillBackground(False)
self.setMinimumSize(200, 200)
self.setWindowTitle("Overpainting a Scene")
# parameters for overpainting
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, 0)
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
def setXRotation(self, angle):
if angle != self.xRot:
self.xRot = angle
def setYRotation(self, angle):
if angle != self.yRot:
self.yRot = angle
def setZRotation(self, angle):
if angle != self.zRot:
self.zRot = angle
def mousePressEvent(self, event):
self.lastPos = event.pos()
super(GLWidget, self).mousePressEvent(event)
worldCoords = super(GLWidget, self).mapToGlobal( self.lastPos )
print self.lastPos
if event.buttons() & QtCore.Qt.RightButton and not (event.modifiers() & QtCore.Qt.ShiftModifier):
print 'first'
self.pts.append(gp_Pnt(self.lastPos.x(), self.lastPos.y(), 0.0))
elif event.buttons() & QtCore.Qt.RightButton and (event.modifiers() & QtCore.Qt.ShiftModifier):
print 'second'
curve = self.points_to_bspline(self.pts)
self._display.DisplayShape(curve, update=True)
self.pts = [] #clear it
def mouseMoveEvent(self, event):
dx = event.x() - self.lastPos.x()
dy = event.y() - self.lastPos.y()
"""
if (event.buttons() & QtCore.Qt.LeftButton):
self.setXRotation(self.xRot + 8 * dy)
self.setYRotation(self.yRot + 8 * dx)
elif (event.buttons() & QtCore.Qt.RightButton):
self.setXRotation(self.xRot + 8 * dy)
self.setZRotation(self.zRot + 8 * dx)
"""
self.lastPos = event.pos()
super(GLWidget, self).mouseMoveEvent(event)
def paintGL(self):
if self._inited:
self._display.Context.UpdateCurrentViewer()
def paintEvent(self, event):
if self._inited:
self._display.Context.UpdateCurrentViewer()
self.makeCurrent()
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
if self.context().isValid():
self.swapBuffers()
if self._drawbox:
painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0), 1))
rect = QtCore.QRect(*self._drawbox)
painter.drawRect(rect)
"""
for bubble in self.bubbles:
if bubble.rect().intersects(QtCore.QRectF(event.rect())):
bubble.drawBubble(painter)
"""
painter.end()
self.doneCurrent()
else:
print('invalid OpenGL context: Qt cannot overpaint viewer')
def showEvent(self, event):
pass
#self.createBubbles(20 - len(self.bubbles))
def sizeHint(self):
return QtCore.QSize(400, 400)
def animate(self):
pass
"""
for bubble in self.bubbles:
bubble.move(self.rect())
self.update()
"""
def setupViewport(self, width, height):
side = min(width, height)
glViewport((width - side) // 2, (height - side) // 2, side, side)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-0.5, +0.5, +0.5, -0.5, 4.0, 15.0)
glMatrixMode(GL_MODELVIEW)
def points_to_bspline(self, pnts):
pts = TColgp_Array1OfPnt(0, len(pnts)-1)
for n, i in enumerate(pnts):
pts.SetValue(n, i)
crv = GeomAPI_PointsToBSpline(pts)
return crv.Curve()
if __name__ == '__main__':
def TestOverPainting():
class AppFrame(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setWindowTitle(self.tr("qtDisplay3d overpainting example"))
self.resize(640, 480)
self.canva = GLWidget(self)
mainLayout = QtGui.QHBoxLayout()
mainLayout.addWidget(self.canva)
mainLayout.setContentsMargins(0, 0, 0, 0)
self.setLayout(mainLayout)
def runTests(self):
self.canva._display.Test()
app = QtGui.QApplication(sys.argv)
frame = AppFrame()
frame.show()
frame.canva.InitDriver()
frame.runTests()
app.exec_()
TestOverPainting()
To answer my own question:
(x, y, z, vx, vy, vz) = self._display.View.ConvertWithProj(self.lastPos.x(), self.lastPos.y())
Gives the entire line of points that x and y can map to, with x, y, and z being one point, and vx, vy, and vz giving the parameterization of the line.
Related
I have two QRects lined side by side:
def paintEvent(self, event):
painter = QPainter(self)
self.rect1 = QRect(0, 0, 500, 40)
painter.fillRect(self.rect1, Qt.GlobalColor.black)
self.rect2 = QRect(500, 0, 500, 40)
painter.fillRect(self.rect2, Qt.GlobalColor.white)
If user clicks somewhere in second QRect, I want to know the location of the click relative to the (0, 0) of the QRect:
def mousePressEvent(self, event):
pos = event.pos()
if self.rect2.contains(pos):
relative_pos = QPoint(pos.x() - self.rect2.x(), pos.y() - self.rect2.y())
Is there a built-in way to do this?
I need to draw a cursor on the QChartView object. something like this:
Cursor on the Chart
Whenever a user clicks on the chart the cursor should be moved there.
I have no idea how it is possible. As I searched It seems that this is not a built-in feature of QChartView. So How can I do it?
BTW, I'm newbie to the QT.
I am facing the same problem. Drawing a cursor at the mouse position seems to be trivial by referring to this anwser. Note that I modify the codes from this anwser in x.setter from self.update() to self.scene().update(). This is important for updating the cursor. To be honest, I don't know why. You can leave a comment if you know the difference.
# refer to https://stackoverflow.com/a/67596291/9758790
import sys
import time
from PyQt5.QtCore import Qt, QPointF
from PyQt5.QtGui import QColor, QPainter, QPen
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtChart import (
QChart,
QChartView,
QLineSeries,
)
from PyQt5.Qt import *
class ChartView(QChartView):
_x = None
#property
def x(self):
return self._x
#x.setter
def x(self, x):
self._x = x
# self.update()
self.scene().update()
def drawForeground(self, painter, rect):
if self.x is None:
return
painter.save()
pen = QPen(QColor("indigo"))
pen.setWidth(3)
painter.setPen(pen)
# p = self.chart().mapToPosition(QPointF(self.x, 0))
p = QPointF(self.x, 0)
r = self.chart().plotArea()
p1 = QPointF(p.x(), r.top())
p2 = QPointF(p.x(), r.bottom())
painter.drawLine(p1, p2)
painter.restore()
def mousePressEvent(self, env):
# refer to https://stackoverflow.com/a/44078533/9758790
scene_position = self.mapToScene(env.pos())
chart_position = self.chart().mapFromScene(scene_position)
value_at_position = self.chart().mapToValue(chart_position)
if self.chart().axisX().min() < value_at_position.x() < self.chart().axisX().max():
self.x = scene_position.x()
def main():
app = QApplication(sys.argv)
series1 = QLineSeries() << QPointF(0, 3) << QPointF(1, 1) << QPointF(3, 9) << QPointF(4, 1)
series2 = QLineSeries() << QPointF(0, 9) << QPointF(1, 8) << QPointF(3, 6) << QPointF(4, 6)
series3 = QLineSeries() << QPointF(0, 4) << QPointF(1, 2) << QPointF(3, 2) << QPointF(4, 3)
chart = QChart()
chart.addSeries(series1)
chart.addSeries(series2)
chart.addSeries(series3)
chart.createDefaultAxes()
chart.legend().setVisible(True)
chart.legend().setAlignment(Qt.AlignBottom)
chartView = ChartView(chart)
chartView.setRenderHint(QPainter.Antialiasing)
chartView.x = None
window = QMainWindow()
window.setCentralWidget(chartView)
window.resize(420, 300)
window.show()
app.exec()
if __name__ == "__main__":
main()
Which gives:
However, I find it difficult to draw the intersection point of the cursor and the line chart. As mentioned in this forum and this forum, if the cursor points at the interpolated part between two data point, it is difficult to decide the y coordinates of the intersection points, because Qt doesn't provide a build-in function to get the interpolated function value at value_at_position.x(). This question is also related to find the interpolated value. So I tried to do interpolation by my self.
The following version of codes will also draw the intersection point.
# refer to https://stackoverflow.com/a/67596291/9758790
import sys
import time
from PyQt5.QtCore import Qt, QPointF
from PyQt5.QtGui import QColor, QPainter, QPen
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtChart import (
QChart,
QChartView,
QLineSeries,
)
from PyQt5.Qt import *
import math
class ChartView(QChartView):
_cursor = None
_y = []
#property
def cursor(self):
return self._cursor
#cursor.setter
def cursor(self, point):
# refer to https://stackoverflow.com/a/44078533/9758790
scene_position = self.mapToScene(point)
chart_position = self.chart().mapFromScene(scene_position)
value_at_position = self.chart().mapToValue(chart_position)
if self.chart().axisX().min() < value_at_position.x() < self.chart().axisX().max():
self._cursor = scene_position
# self.update()
self.scene().update()
def drawForeground(self, painter, rect):
if self.cursor is None:
return
painter.save()
pen = QPen(QColor("indigo"))
pen.setWidth(1)
painter.setPen(pen)
p = self.cursor
r = self.chart().plotArea()
p1 = QPointF(p.x(), r.top())
p2 = QPointF(p.x(), r.bottom())
painter.drawLine(p1, p2)
chart_position = self.chart().mapFromScene(self.cursor)
value_at_position = self.chart().mapToValue(chart_position)
for series_i in self.chart().series():
pen2 = QPen(series_i.color())
pen2.setWidth(10)
painter.setPen(pen2)
# find the nearest points
min_distance_left = math.inf
min_distance_right = math.inf
nearest_point_left = None
nearest_point_right = None
exact_point = None
for p_i in series_i.pointsVector():
if p_i.x() > value_at_position.x():
if p_i.x() - value_at_position.x() < min_distance_right:
min_distance_right = p_i.x() - value_at_position.x()
nearest_point_right = p_i
elif p_i.x() < value_at_position.x():
if value_at_position.x() - p_i.x() < min_distance_right:
min_distance_left = value_at_position.x() - p_i.x()
nearest_point_left = p_i
else:
exact_point = p_i
nearest_point_left = None
nearest_point_right = None
break
if nearest_point_right is not None and nearest_point_left is not None:
# do linear interpolated by my self
k = ((nearest_point_right.y() - nearest_point_left.y()) / (nearest_point_right.x() - nearest_point_left.x()))
point_interpolated_y = nearest_point_left.y() + k * (value_at_position.x() - nearest_point_left.x())
point_interpolated_x = value_at_position.x()
point_interpolated = QPointF(point_interpolated_x, point_interpolated_y)
painter.drawPoint(self.chart().mapToScene(self.chart().mapToPosition(point_interpolated)))
if exact_point is not None:
painter.drawPoint(self.chart().mapToScene(self.chart().mapToPosition(exact_point)))
painter.restore()
def mousePressEvent(self, env):
self.cursor = env.pos()
def main():
app = QApplication(sys.argv)
series1 = QLineSeries() << QPointF(0, 3) << QPointF(1, 1) << QPointF(3, 9) << QPointF(4, 1)
series2 = QLineSeries() << QPointF(0, 9) << QPointF(1, 8) << QPointF(3, 6) << QPointF(4, 6)
series3 = QLineSeries() << QPointF(0, 4) << QPointF(1, 2) << QPointF(3, 2) << QPointF(4, 3)
chart = QChart()
chart.addSeries(series1)
chart.addSeries(series2)
chart.addSeries(series3)
chart.createDefaultAxes()
chart.legend().setVisible(True)
chart.legend().setAlignment(Qt.AlignBottom)
chartView = ChartView(chart)
chartView.setRenderHint(QPainter.Antialiasing)
chartView.x = None
window = QMainWindow()
window.setCentralWidget(chartView)
window.resize(420, 300)
window.show()
app.exec()
if __name__ == "__main__":
main()
Which gives:
This link Track Line with Data Labels provide a solution to find a nearest data point to the mouse position and draw a line there, which avoids dealing with interpolation. My codes can also be modified to draw in this way.
Here is the code. It runs. To exhibit this bug. Right-click the ellipse, scale it by click-dragging on the ellipse. Right-click it > "Done editing". Then do the same thing with "Rotate."
I've tried over 10 different permutations of using self.setRotation, self.setTransform, painter.rotate, and so on... The only time it did rotate was when I did self.setTransform(self.transform().rotate(r)) but the result was wrong: scale & rotate in the wrong order or something.
from PyQt5.QtWidgets import QGraphicsItem, QMenu
from PyQt5.QtGui import QTransform, QPen, QPainter, QColor, QBrush
from PyQt5.QtCore import Qt, QPointF, QRectF, QEvent
from math import sqrt
def scaleRect(rect, scale_x, scale_y):
T = QTransform.fromScale(scale_x, scale_y)
return T.mapRect(rect)
def debugPrintTransformMatrix(T):
print(str(T.m11()) + ' ' + str(T.m12()) + ' ' + str(T.m13()))
print(str(T.m21()) + ' ' + str(T.m22()) + ' ' + str(T.m23()))
print(str(T.m31()) + ' ' + str(T.m32()) + ' ' + str(T.m33()))
# Assumes no shearing or stretching.
# Only Rotation, Translation, and Scaling.
def extractTransformScale(T):
# This is math matrix notation transposed (debug print self.sceneTransform() to see)
Sx = sqrt(T.m11()**2 + T.m12()**2)
Sy = sqrt(T.m21()**2 + T.m22()**2)
return Sx, Sy
def extractTransformTranslate(T):
return T.m31(), T.m32()
class Object(QGraphicsItem):
def sceneEvent(self, event):
if event.type() == QEvent.GraphicsSceneMouseMove:
# move, scale, or rotate
if self._mode in ['scale', 'rotate']:
mouse_pos = event.scenePos()
last_pos = event.lastScenePos()
if self._mode == 'scale':
s = self.mouseScalingFactors(mouse_pos, last_pos)
self.setTransform(self.transform().scale(*s))
if self._mode == 'rotate':
r = self.mouseRotationAngle(mouse_pos, last_pos)
self.setRotation(self.rotation() + r)
return True
return super().sceneEvent(event)
def __init__(self):
super().__init__()
self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsSelectable)
self._selectionPen = QPen(Qt.black, 1.0, style=Qt.DotLine, cap=Qt.FlatCap)
self._lastPos = QPointF(0, 0)
self.setPos(self._lastPos)
self._mode = 'neutral'
def setRenderHints(self, painter):
painter.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing)
def boundingRectExtraScale(self):
return (1.2 , 1.2)
def mouseScalingFactors(self, pos, last_pos):
delta = pos - last_pos
return (1.01 ** delta.x(), 1.01 ** delta.y())
def mouseRotationAngle(self, pos, last_pos):
return 1 #TODO
def createDefaultContextMenu(self):
menu = QMenu()
if self._mode == 'neutral':
menu.addAction('Scale').triggered.connect(lambda: self.setMode('scale'))
menu.addAction('Rotate').triggered.connect(lambda: self.setMode('rotate'))
else:
menu.addAction('Done Editing').triggered.connect(lambda: self.setMode('neutral'))
return menu
def contextMenuEvent(self, event):
menu = self.createDefaultContextMenu()
menu.exec(event.screenPos())
def setMode(self, mode):
self._mode = mode
def setSelected(self, selected):
super().setSelected(selected)
self.update()
class ShapedObject(Object):
def __init__(self):
super().__init__()
self._shape = {
'name' : 'ellipse',
'radius': 35
}
self._brush = QBrush(Qt.darkGreen)
self._pen = QPen(Qt.yellow, 3)
def shapeDef(self):
return self._shape
def boundingRect(self):
rect = self.shapeRect()
s = self.boundingRectExtraScale()
return scaleRect(rect, *s)
def shape(self): #TODO QPainterPath shape for collision detection
# Should call self.boundingRectExtraScale()
return super().shape()
def paint(self, painter, option, widget):
self.setRenderHints(painter)
#super().paint(painter, option, widget)
shape = self.shapeDef()
name = shape['name']
painter.setBrush(self._brush)
painter.setPen(self._pen)
painter.save()
# ********** HERE IS THE PROBLEM *************
debugPrintTransformMatrix(painter.transform())
painter.rotate(5)
debugPrintTransformMatrix(painter.transform())
rect = self.shapeRect()
if name == 'ellipse':
painter.drawEllipse(rect)
painter.restore()
def shapeRect(self):
shape = self.shapeDef()
name = shape['name']
if name == 'ellipse':
r = shape['radius']
rect = QRectF(-r, -r, 2*r, 2*r)
return rect
####
import sys
from PyQt5.QtWidgets import QMainWindow, QGraphicsScene, QGraphicsView, QApplication
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QMainWindow()
view = QGraphicsView()
scene = QGraphicsScene()
view.setScene(scene)
window.setCentralWidget(view)
ellipse = ShapedObject()
scene.addItem(ellipse)
window.show()
sys.exit(app.exec_())
Got it to work. It was the order of the operations rotate / scale. Never use an circle to test your rotation code, lol!
from PyQt5.QtWidgets import QGraphicsItem, QMenu, QGraphicsRotation, QGraphicsScale
from PyQt5.QtGui import QTransform, QPen, QPainter, QColor, QBrush, QPainterPath
from PyQt5.QtCore import Qt, QPointF, QRectF, QEvent
from math import sqrt
def scaleRect(rect, scale_x, scale_y):
T = QTransform.fromScale(scale_x, scale_y)
return T.mapRect(rect)
def debugPrintTransformMatrix(T):
print(str(T.m11()) + ' ' + str(T.m12()) + ' ' + str(T.m13()))
print(str(T.m21()) + ' ' + str(T.m22()) + ' ' + str(T.m23()))
print(str(T.m31()) + ' ' + str(T.m32()) + ' ' + str(T.m33()))
# Assumes no shearing or stretching.
# Only Rotation, Translation, and Scaling.
def extractTransformScale(T):
# This is math matrix notation transposed (debug print self.sceneTransform() to see)
Sx = sqrt(T.m11()**2 + T.m12()**2)
Sy = sqrt(T.m21()**2 + T.m22()**2)
return Sx, Sy
def extractTransformTranslate(T):
return T.m31(), T.m32()
class Object(QGraphicsItem):
def sceneEvent(self, event):
if event.type() == QEvent.GraphicsSceneMouseMove:
# move, scale, or rotate
if self._mode in ['scale', 'rotate']:
mouse_pos = event.scenePos()
last_pos = event.lastScenePos()
if self._mode == 'scale':
s = self.mouseScalingFactors(mouse_pos, last_pos)
self.applyScaleTransform(*s)
if self._mode == 'rotate':
r = self.mouseRotationAngle(mouse_pos, last_pos)
self.applyRotateTransform(r)
return True
return super().sceneEvent(event)
def __init__(self):
super().__init__()
self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsSelectable)
self._selectionPen = QPen(Qt.black, 1.0, style=Qt.DotLine, cap=Qt.FlatCap)
self._lastPos = QPointF(0, 0)
self.setPos(self._lastPos)
self._mode = 'neutral'
self._scale = QGraphicsScale()
self._rotate = QGraphicsRotation()
self.setTransformations([self._rotate, self._scale])
def applyRotateTransform(self, angle):
self._rotate.setAngle(self._rotate.angle() + 15)
def applyScaleTransform(self, sx, sy):
self._scale.setXScale(sx * self._scale.xScale())
self._scale.setYScale(sy * self._scale.yScale())
def setRenderHints(self, painter):
painter.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing)
def boundingRectExtraScale(self):
return (1.2 , 1.2)
def mouseScalingFactors(self, pos, last_pos):
delta = pos - last_pos
return (1.01 ** delta.x(), 1.01 ** delta.y())
def mouseRotationAngle(self, pos, last_pos):
return 1 #TODO
def createDefaultContextMenu(self):
menu = QMenu()
if self._mode == 'neutral':
menu.addAction('Scale').triggered.connect(lambda: self.setMode('scale'))
menu.addAction('Rotate').triggered.connect(lambda: self.setMode('rotate'))
else:
menu.addAction('Done Editing').triggered.connect(lambda: self.setMode('neutral'))
return menu
def contextMenuEvent(self, event):
menu = self.createDefaultContextMenu()
menu.exec(event.screenPos())
def setMode(self, mode):
self._mode = mode
def setSelected(self, selected):
super().setSelected(selected)
self.update()
class ShapedObject(Object):
def __init__(self):
super().__init__()
self._shape = {
'name' : 'ellipse',
'radius': 35
}
self._brush = QBrush(Qt.darkGreen)
self._pen = QPen(Qt.yellow, 3)
def shapeDef(self):
return self._shape
def boundingRect(self):
rect = self.shapeRect()
s = self.boundingRectExtraScale()
return scaleRect(rect, *s)
def shape(self): #TODO QPainterPath shape for collision detection
# Should call self.boundingRectExtraScale()
return super().shape()
def paint(self, painter, option, widget):
self.setRenderHints(painter)
#super().paint(painter, option, widget)
shape = self.shapeDef()
name = shape['name']
painter.setBrush(self._brush)
painter.setPen(self._pen)
painter.save()
path = QPainterPath()
if name == 'ellipse':
r = shape['radius']
path.addEllipse(QPointF(0, 0), r, r)
painter.drawPath(path)
painter.restore()
def shapeRect(self):
shape = self.shapeDef()
name = shape['name']
if name == 'ellipse':
r = shape['radius']
rect = QRectF(-r, -r, 2*r, 2*r)
return rect
####
import sys
from PyQt5.QtWidgets import QMainWindow, QGraphicsScene, QGraphicsView, QApplication
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QMainWindow()
view = QGraphicsView()
scene = QGraphicsScene()
view.setScene(scene)
window.setCentralWidget(view)
ellipse = ShapedObject()
scene.addItem(ellipse)
window.show()
sys.exit(app.exec_())
I am working on a simple Paint application using pyqt5. My goal is:
1. Draw freely according to mouse events. It's done!
2. Erase freely according to mouse events. It isn't work as expected!
I have a QGraphicsView, QGraphicsScene where I'm adding QGraphicsItems, specifically QGraphicsPathItem. My eraser has a rect shape, and what I want is to erase part of my drawing while moving my eraser on it.
On MouseMoveEvent, I check if my eraser shape intersects my drawing path, if so, I just subtract it from my drawing path. Then the awkward behavior occurs, it closes my drawing path.
Figure_1, shows my drawing (QGraphicsPathItem).
Figure_2, shows what happens when my eraser(QGraphicsRectItem) intersects my drawing.
Drawing freely lines working fine. Figure_1
Erasing drawing. Figure_2
As we can see, it draws a line linking my first and end points, closing my path. I dont want it. I just want to erase my drawing path.
from PyQt5.QtCore import QRectF, Qt
from PyQt5.QtGui import QPainterPath, QPen
from PyQt5.QtWidgets import QApplication, QGraphicsScene, \
QGraphicsView, QPushButton, QWidget, \
QVBoxLayout, QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem
class Window(QWidget):
scene = None
def __init__(self):
QWidget.__init__(self)
self.view = View(self)
self.button = QPushButton('Clear View', self)
self.button.clicked.connect(self.handleClearView)
layout = QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.button)
def handleClearView(self):
self.view.scene.clear()
class View(QGraphicsView):
def __init__(self, parent):
self.scribing = False
self.erasing = False
QGraphicsView.__init__(self, parent)
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.free_draw_item = None
self.eraser_item = None
def resizeEvent(self, QResizeEvent):
self.setSceneRect(QRectF(self.viewport().rect()))
def mousePressEvent(self, event):
if event.buttons() == Qt.LeftButton:
self.scribing = True
pp = QPainterPath(event.pos())
self.free_draw_item = QGraphicsPathItem(pp)
self.free_draw_item.setPen(QPen(Qt.green))
self.free_draw_item.setFlags(QGraphicsItem.ItemIsSelectable)
self.free_draw_item.setPath(pp)
self.scene.addItem(self.free_draw_item)
if event.buttons() == Qt.RightButton:
self.erasing = True
self.eraser_item = QGraphicsRectItem(event.pos().x() - 5,
event.pos().y() - 5, 10, 10)
self.eraser_item.setPen(QPen(Qt.red))
self.eraser_item.setBrush(Qt.transparent)
self.scene.addItem(self.eraser_item)
def mouseMoveEvent(self, event):
if (event.buttons() & Qt.LeftButton) and self.scribing:
if self.free_draw_item:
path = self.free_draw_item.path()
path.lineTo(event.pos())
self.free_draw_item.setPath(path)
if event.buttons() & Qt.RightButton and self.erasing:
self.eraser_item.setRect(event.pos().x() - 5, event.pos().y() - 5,
10, 10)
for item in self.scene.collidingItems(self.eraser_item):
if isinstance(item, QGraphicsPathItem):
if item.path().intersected(self.eraser_item.shape()):
new = item.path().subtracted(self.eraser_item.shape())
item.setPath(new)
#item.setBrush(Qt.red)
def mouseReleaseEvent(self, event):
self.scribing = False
self.erasing = False
if self.eraser_item != None:
self.scene.removeItem(self.eraser_item)
# if self.free_draw_item != None:
# self.free_draw_item.setSelected(True)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Window()
window.resize(640, 480)
window.show()
sys.exit(app.exec_())
This is my code.
Left mouse button draws green path on scene.
Right mouse button erase my drawings.
I've had same problem several and several times.
After some headache and a thousand unsuccessful researches about this specific problem I've come to on answer.
Forget having only one path
You cant subtract without closing the path turning it into a closed polygon
Think as having many paths and working around
Now you have all sub items and can "delete" them when colliding with your eraser
Use the subtract on your favor
Have many paths with 3 not align points each path(least points on a plan to have a polygon)
Group it as QGraphicsItemGroup() if you want to manipulate the whole path made of several (3 points path) as a single item
See my example
-
from PyQt5.QtCore import QPoint
from PyQt5.QtCore import QRectF, Qt
from PyQt5.QtGui import QBrush
from PyQt5.QtGui import QPainter
from PyQt5.QtGui import QPainterPath, QPen
from PyQt5.QtWidgets import QApplication, QGraphicsScene, \
QGraphicsView, QPushButton, QWidget, \
QVBoxLayout, QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem
from PyQt5.QtWidgets import QGraphicsItemGroup
from core.IQGraphicsPathItem import InteractQGraphicsPathItem
class Window(QWidget):
scene = None
def __init__(self):
QWidget.__init__(self)
self.view = View(self)
self.button = QPushButton('Clear View', self)
self.button.clicked.connect(self.handleClearView)
layout = QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.button)
def handleClearView(self):
self.view.scene.clear()
class View(QGraphicsView):
dic = {}
num_item = 0
_from_x_ = None
_from_y_ = None
pp = None
pen = None
group = None
def __init__(self, parent):
self.scribing = False
self.erasing = False
QGraphicsView.__init__(self, parent)
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.setRenderHint(QPainter.Antialiasing)
self.free_draw_item = QGraphicsPathItem()
# self.free_draw_item.setPen(QPen(Qt.green))
# self.free_draw_item.setFlags(QGraphicsItem.ItemIsSelectable)
self.eraser_item = None
self.pen = QPen(Qt.green,8)
self.pen.setBrush(QBrush(Qt.green))
self.pen.setWidth(1)
self.group = QGraphicsItemGroup()
def resizeEvent(self, QResizeEvent):
self.setSceneRect(QRectF(self.viewport().rect()))
def mousePressEvent(self, event):
if event.buttons() == Qt.LeftButton:
self._from_x_ = event.pos().x()
self._from_y_ = event.pos().y()
self.scribing = True
self.group = QGraphicsItemGroup()
self.group.setFlags(QGraphicsItem.ItemIsSelectable)
self.scene.addItem(self.group)
self.pp = QPainterPath(event.pos())
self.free_draw_item = QGraphicsPathItem(self.pp)
self.free_draw_item.setPen(self.pen)
# self.free_draw_item.setFlags(QGraphicsItem.ItemIsSelectable)
self.free_draw_item.setPath(self.pp)
self.scene.addItem(self.free_draw_item)
self.group.addToGroup(self.free_draw_item)
if event.buttons() == Qt.RightButton:
self.erasing = True
self.eraser_item = QGraphicsRectItem(event.pos().x() - 5,
event.pos().y() - 5, 10, 10)
self.eraser_item.setPen(QPen(Qt.red))
self.eraser_item.setBrush(Qt.white)
self.scene.addItem(self.eraser_item)
def mouseMoveEvent(self, event):
x_adjust = 1
y_adjust = 1
# if event.pos().x() >= self._from_x_:
# x_adjust = 1
# else:
# x_adjust = -1
# if event.pos().y() >= self._from_y_:
# y_adjust = 1
# else:
# y_adjust = -1
if (event.buttons() & Qt.LeftButton) and self.scribing:
if self.free_draw_item:
# path = self.free_draw_item.path()
# path.lineTo(event.pos())
# self.free_draw_item.setPath(path)
self.pp2 = QPainterPath(QPoint(self._from_x_, self._from_y_))
self.pp2.lineTo(event.pos().x(), event.pos().y())
self.pp2.lineTo(event.pos().x()+x_adjust, event.pos().y()+y_adjust)
self.pp2.lineTo(self._from_x_ + x_adjust, self._from_y_ + y_adjust)
self.free_draw_item.setPen(QPen(Qt.green, 8))
self.free_draw_item.path = QGraphicsPathItem()
self.free_draw_item.path.setPath(self.pp2)
self.scene.addItem(self.free_draw_item.path)
self.group.addToGroup(self.free_draw_item)
self._from_x_ = event.pos().x()
self._from_y_ = event.pos().y()
if event.buttons() & Qt.RightButton and self.erasing:
self.eraser_item.setRect(event.pos().x() - 5, event.pos().y() - 5,
10, 10)
for item in self.scene.collidingItems(self.eraser_item):
new = item.path() - self.eraser_item.shape()
item.setPath(new)
print('collided')
def mouseReleaseEvent(self, event):
self.scribing = False
self.erasing = False
self.group.setSelected(True)
print(self.scene.items())
if self.eraser_item != None:
self.scene.removeItem(self.eraser_item)
# if self.free_draw_item != None:
# self.free_draw_item.setSelected(True)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Window()
window.resize(640, 480)
window.show()
sys.exit(app.exec_())
If you want to personalize your own Pen I suggest you to overwrite your own QGraphicsPathItem().
I think it'll work pretty great.
Ok so all night me and 5 other guys have been working on a project for our pygame module.Our lecturer hasn't given us any programs to reference from he has just throw a bunch of code at us (without commenting) and expecting us to understand it.We only started python since the start of september.We really need some help.
So we have one human player which is controlled by W A S & D and 15 random dots moving around the screen.We need to use vectors(which we have never used) to shoot at the random dots.
Please someone help.
edit code added~
import pygame
class Terminator(pygame.sprite.Sprite):
def __init__(self, screen):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((20, 20))
self.image.fill((0, 0, 0))
pygame.draw.circle(self.image, (255, 0, 0), (10, 10), 10, 0)
self.rect = self.image.get_rect()
self.dx = screen.get_width()/2
self.dy = screen.get_height()/2
self.rect.center = (self.dx, self.dy)
self.speed = 5
self.screen = screen
def update(self):
self.rect.center = (self.dx, self.dy)
def MoveLeft(self):
if self.rect.left < 0:
self.dx += 0
else:
self.dx -= self.speed
def MoveRight(self):
if self.rect.right > self.screen.get_width():
self.dx += 0
else:
self.dx += self.speed
def MoveUp(self):
if self.rect.top <0:
self.dy += 0
else:
self.dy -= self.speed
def MoveDown(self):
if self.rect.bottom > self.screen.get_height():
self.dy += 0
else:
self.dy += self.speed
<code>
humansprite.py
<pre>
import pygame
import random
import math
class Humans(pygame.sprite.Sprite):
def __init__(self, screen):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((16, 16))
self.image.fill((0, 0, 0))
pygame.draw.circle(self.image, (0, 0, 255), (8, 8), 8, 0)
self.rect = self.image.get_rect()
self.dx = random.randrange(0, screen.get_height())
self.dy = random.randrange(0, screen.get_width())
self.screen = screen
self.speed = 1
self.alive = True
def update(self):
self.rect.centerx -= self.dx
self.rect.centery -= self.dy
if self.rect.right < 0:
self.reset()
def reset(self):
self.rect.left = self.screen.get_width()
self.rect.centery = random.randrange(0, self.screen.get_height())
self.dy = random.randrange(-2, 2)
self.dx = random.randrange(1, 4)
<code>
seekandDestory.py
<pre>
import pygame
from TerminatorSprite import Terminator
from humansSprite import Humans
pygame.init()
def checkKeys(myData):
(event, ship) = myData
if event.key == pygame.K_LEFT:
print 'LEFT'
ship.MoveLeft()
if event.key == pygame.K_RIGHT:
print 'RIGHT'
ship.MoveRight()
if event.key == pygame.K_UP:
print 'UP'
ship.MoveUp()
if event.key == pygame.K_DOWN:
print 'DOWN'
ship.MoveDown()
def main():
screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption("Seek and Destroy")
background = pygame.Surface(screen.get_size())
clock = pygame.time.Clock()
screen.blit(background, (0, 0))
terminator = Terminator(screen)
humans = []
for people in range(15):
people = Humans(screen)
humans.append(people)
terminatorGroup = pygame.sprite.Group(terminator)
humanGroup = pygame.sprite.Group(humans)
clock = pygame.time.Clock()
keepGoing = True
pygame.key.set_repeat(10, 10)
while keepGoing:
clock.tick(60)
pygame.mouse.set_visible(False)
for event in pygame.event.get():
if event.type == pygame.QUIT:
keepGoing = False
if event.type == pygame.KEYDOWN:
myData = (event, terminator)
checkKeys(myData)
#if pygame.sprite.spritecollide(terminator, humanGroup, False):
terminatorGroup.clear(screen, background)
terminatorGroup.update()
terminatorGroup.draw(screen)
humanGroup.clear(screen, background)
humanGroup.update()
humanGroup.draw(screen)
pygame.display.flip()
pygame.mouse.set_visible(True)
if __name__ == "__main__":
main()
<code>
I just finished up this set of examples last night! Check out the Lunar Lander example, especially the way I'm handling vectors in lunarlander.py - it provides a class, V, for vector management.
What you'll probably need is a sprite for your guy with a vector representing his speed (moment-to-moment, probably 0) and angle, then a sprite for each random dot with a similar vector and angle. When you shoot, create a new sprite with a vector whose angle matches the current angle your guy is facing and magnitude matches the speed of the bullet. Then, in the update() method, move each sprite (bullet, random-dot). Very similar to what I'm doing in lunarlander.py.
Good luck! If you've just started Python, that's a tough assignment. Happy coding!