I am new to PyQt5 and I am programming a small Logo-Editor for generating simple Logos. I use PyQt5 Version: 5.15.7 in Python 3.10 all together in PyCharm PyCharm 2022.1.3 (Community Edition) on Windows 11.
I am using the QGraphicsScene to draw all my lines and then I can customize length, color, zValue, etc of the created logo. I use MouseEvents to click on one QGraphicsItem and so I am able to change the color and zValue. That's just for the introduction.
The Problem I have is on creating a diagonal line. Then the resulting default boundingRect() of the QGraphicsItem is much too big, and this makes problems when I have several lines on my Scene, which I would like to select with the mouse. Then the clicking on one Item results in the selection of a nearby diagonal line item.
Here is a screenshot of what I mean: Diagonal line with selection box (black), the red line shows the boundingRect or shape I would like to use)
I made a small QtApp to demonstrate my problem:
from PyQt5.QtWidgets import (
QApplication,
QWidget,
QGraphicsView,
QGraphicsScene,
QGraphicsSceneMouseEvent,
QGraphicsItem,
QTextEdit,
)
from PyQt5.QtGui import QPolygonF, QPen, QTransform
from PyQt5.QtCore import Qt, QPointF, QLineF
import sys
# Own QGraphicsScene Subclass with some drawings and points as example
class MyScene( QGraphicsScene ):
def __init__( self ):
super().__init__( -300, -300, 600, 600 )
# Set QPen for drawings
self.my_pen = QPen( Qt.darkBlue )
self.my_pen.setWidthF( 15 )
self.my_pen.setCapStyle( Qt.RoundCap )
# Set Start- & End-Points for my line
self.start = QPointF( 0, 0 )
self.end = QPointF( 200, 200 )
# Draw a line (boundingRect is created automatically)
self.lin = self.addLine( QLineF( self.start, self.end ), self.my_pen )
self.lin.setFlags( QGraphicsItem.ItemIsSelectable )
# Change Pen-Setttings for new Item
self.my_pen.setWidthF( 2 )
self.my_pen.setColor( Qt.darkRed )
self.my_pen.setStyle( Qt.DotLine )
# Draw polygon, which I would like to apply on my line as a bounding rect
self.poly = self.addPolygon(
QPolygonF(
[
QPointF( 20, -30 ),
QPointF( -30, 20 ),
QPointF( 180, 230 ),
QPointF( 230, 180 ),
] ),
self.my_pen
)
# Reimplementing the mousePressEvent for catching some information
def mousePressEvent( self, sceneEvent: QGraphicsSceneMouseEvent ):
# Get position and item at the position of the event
#### EDIT after Comments from musicamente
#### FIRST pass the event to the original implementation of the mousePressEvent
super().mousePressEvent( sceneEvent )
#### and THEN get the position of the item at the event-scenePosition
pp = sceneEvent.scenePos()
current_item = self.itemAt( pp, QTransform() )
# So if there is an item at the clicked position, then write some information
if current_item is not None:
text = f"scenePos() = {pp} \n"\
f"screenPos() = {sceneEvent.screenPos()}\n"\
f"current_item.boundingRect() = "\
f"{current_item.boundingRect()}\n"\
f"current_item.shape() = {current_item.shape()}\n"\
f"current_item.shape().boundingRect() = "\
f"{current_item.shape().boundingRect()}\n"\
f"current_item.shape().controlPointRect() = "\
f"{current_item.shape().controlPointRect()}\n"\
f""
my_gui.my_textedit.setText( text )
current_item.mousePressEvent( sceneEvent )
# The Class/Widget for placing the view and a QTextEdit
class MyGui( QWidget ):
def __init__( self ):
super().__init__()
self.setGeometry( 50, 50, 800, 800 )
self.my_scene = MyScene()
self.my_view = QGraphicsView( self )
self.my_view.setScene( self.my_scene )
# QTextEdit for displaying some stuff for debuggin
self.my_textedit = QTextEdit( self )
self.my_textedit.setGeometry( 0, 610, 600, 150 )
# Starting the App
my_app = QApplication( sys.argv )
my_gui = MyGui()
my_gui.show()
sys.exit( my_app.exec() )
In the textEdit I see roughly, what I have to do:
current_item.shape() = <PyQt5.QtGui.QPainterPath object at 0x00000213C8DD9A10>
current_item.shape().boundingRect() = PyQt5.QtCore.QRectF(-30.707106781186553, -30.999999999983665, 261.70710678117024, 261.70710678117024)
current_item.shape().controlPointRect() = PyQt5.QtCore.QRectF(-30.707106781186553, -31.09763107291604, 261.8047378541026, 261.8047378541026)
I found some questions here addressing the same problem, but they are in C++
I understand that I have to reimplement the shape() of my line... BUT I don't have a clue how to do this in my example...
Can somebody give me a hint, how to solve this?
I haven't found some information on the internet regarding problems like this. If you know any Websites with tutorials on these topics, I would be very pleased to know them. Or a book dealing with Graphics in PyQt5 would be also great.
Thanks in advance :-)
I found a solution for my problem...Or better I tried out something and I realized that my original problem is not a problem, but a feature lol
My original problem was that I cannot select the GraphicsItem I would like to select, because there are more than one Item at the scenePosition, or they are intersecting, which is the same when you look on a certain scenePostion.
It's like a stack of paper...if you want the 3rd piece of paper from the top, then you have to chose the 3rd paper of the top...using itemAt(scenePosition, ...)
is like saying "Give me a paper from the paper stack on the table!"...so you get the topmost piece of paper.
Therefore I decided to take items(scenePos)
of the QGraphicsScene and to loop through the list of the items at the scenePos and to pick the ones I need, and/or let the user decide.
But I added some Code to my original script. Now I can see the actual shape of a GraphicsItem. At least for the "standard"-ones, like a line, circle, polygon. And what I saw is that the shapes are VERY precise: The black outline shows the shape of the drawn polygon
And I also tried to implement an own method for the shape of a GraphicsItem.
But I am pretty sure that it's not that easy...
I use my_GraphicsItem.shape = my_shape()
Here's the Code:
# coding=utf-8
# File 4 stackoverflow question about the usage of PyQt5.QtWidgets.QGraphicsItem.shape()
from PyQt5.QtWidgets import (
QApplication,
QWidget,
QGraphicsView,
QGraphicsScene,
QGraphicsSceneMouseEvent,
QGraphicsItem,
QTextEdit,
QGraphicsLineItem,
QStyleOptionGraphicsItem, QGraphicsEllipseItem, QGraphicsTextItem, QGraphicsRectItem
)
from PyQt5.QtGui import (
QPolygonF, QPen, QTransform, QPainter,
QPainterPath, QPixmap, QBrush, QColor, QFont, QIcon)
from PyQt5.QtCore import Qt, QPointF, QLineF, QRectF
import sys
ppath = None
# Own QGraphicsScene Subclass with some drawings and points as example
class MyScene(QGraphicsScene):
def __init__(self):
super().__init__(-300, -300, 600, 600)
self.ppath = None
# Set QPen for drawings
self.my_pen = QPen(Qt.darkRed)
self.my_pen.setWidthF(15)
self.my_pen.setCapStyle( Qt.RoundCap )
# Set QBrush for drawings
self.my_brush = QBrush( QColor( Qt.darkRed ),
Qt.BrushStyle( Qt.SolidPattern ) )
# Draw a circle ( shape is created by my method.
# The created shape is senseless for a circle,
# and I guess, that this is NOT the right way to do it...)
self.circle = self.addEllipse(
QRectF( QPointF( -200, 100 ), QPointF( -100, 50 )),
self.my_pen,
self.my_brush
)
self.circle.setData(0, "Circle")
self.circle.setFlags(QGraphicsItem.ItemIsMovable)
self.circle.shape = self.my_shape() # This ca
# Set Start- & End-Points for my line
self.start = QPointF(-250, -250)
self.end = QPointF(-100, -100)
# Draw a line (shape is created automatically)
self.my_pen.setColor(Qt.green)
self.my_pen.setWidth(25)
self.lin = QGraphicsLineItem(QLineF(self.start, self.end))
self.lin.setPen(self.my_pen)
self.lin.setFlags(QGraphicsItem.ItemIsMovable)
self.lin.setData( 0, "Line" )
self.addItem(self.lin)
# Change Pen-Settings
self.my_pen.setWidthF(5)
self.my_pen.setColor(Qt.black)
self.my_pen.setStyle(Qt.SolidLine)
# Draw polygon
self.my_brush.setColor(Qt.darkBlue)
self.poly = self.addPolygon(
QPolygonF(
[ QPointF( 10, -30 ),
QPointF( -30, 20 ),
QPointF( 180, 230 ),
QPointF( 230, 180 ),
QPointF( 270, 270 ),
QPointF( 240, 290 )
] ),
self.my_pen,
self.my_brush
)
self.poly.setFlags( QGraphicsItem.ItemIsMovable)
self.poly.setData( 0, "Polygon" )
# Reimplementing the mousePressEvent for catching some information
def mousePressEvent(self, sceneEvent: 'QGraphicsSceneMouseEvent'):
if sceneEvent.button() == Qt.LeftButton:
text: str = ""
# Get position and item at the position of the event
pp = sceneEvent.scenePos()
current_item = self.itemAt( pp, QTransform() )
# So if there is an item at the clicked position, then write some information
if current_item is not None:
if current_item.data(0) != "Circle":
text = f"ItemData = {current_item.data( 0 )}\n" \
f"scenePos() = {pp} \n" \
f"screenPos() = {sceneEvent.screenPos()}\n" \
f"current_item.boundingRect() = " \
f"{current_item.boundingRect()}\n" \
f"current_item.shape() = {current_item.shape()}\n" \
f"current_item.shape().toFillPolygon() = " \
f"{current_item.shape().toFillPolygon()}\n"
self.my_pen.setColor(Qt.black)
self.my_pen.setWidth(2)
if self.ppath is not None:
self.removeItem(self.ppath)
self.ppath = self.addPolygon(current_item.shape().toFillPolygon(), self.my_pen)
# took this from here:
# https://doc.qt.io/qtforpython-5/PySide2/QtGui/QPainterPath.html#PySide2.QtGui.PySide2.QtGui.QPainterPath.toFillPolygon
else:
text = f"My Circle shape is not callable....however...\n" \
f"ItemData = {current_item.data( 0 )}\n" \
f"scenePos() = {pp} \n" \
f"screenPos() = {sceneEvent.screenPos()}\n" \
f"current_item.boundingRect() = " \
f"{current_item.boundingRect()}\n"
else:
text = f"scenePos = {pp} But here is nothing"
my_gui.my_textedit.setText( text )
super().mousePressEvent( sceneEvent )
# @ musicamente: As in this example I have only 2 Items which could be detected by
# sceneEvent.scenePos() I call the super().mousePressEvent( sceneEvent ) at the end, so that I can move around
# my items after all the stuff done above in the code.
# This is surely NOT the right way...however
def my_shape( self):
my_painter_path = QPainterPath()
my_painter_path.addPolygon(QPolygonF( [
QPointF( 20, -30 ),
QPointF( -30, 20 ),
QPointF( 180, 230 ),
QPointF( 230, 180 ),
] ))
return my_painter_path
#
# The Class/Widget for placing the view for placing
class MyGui(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(50, 50, 800, 800)
self.my_scene = MyScene()
self.my_view = QGraphicsView(self)
self.my_view.setScene(self.my_scene)
# QTextEdit for displaying some stuff for debugging
self.my_textedit = QTextEdit(self)
self.my_textedit.setGeometry(0, 610, 600, 150)
# Starting the App
my_app = QApplication(sys.argv)
my_gui = MyGui()
my_gui.show()
sys.exit(my_app.exec())
I hope this might be helpful to somebody, except of me ;-)