Returning handles of elements using QPainter - python

I am developing a GUI based on PyQt5, and I am wondering if I can get the handles of individual elements that have been created using the QPainter class.
I mean, supose that we have three rectangles that have been painted using that class with the following code:
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QColor, QBrush
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 350, 100)
self.setWindowTitle('Colours')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawRectangles(qp)
qp.end()
def drawRectangles(self, qp):
col = QColor(0, 0, 0)
col.setNamedColor('#d4d4d4')
qp.setPen(col)
qp.setBrush(QColor(200, 0, 0))
qp.drawRect(10, 15, 90, 60)
qp.setBrush(QColor(255, 80, 0, 160))
qp.drawRect(130, 15, 90, 60)
qp.setBrush(QColor(25, 0, 90, 200))
qp.drawRect(250, 15, 90, 60)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
I want to retrieve a kind of handle of each rectangle, for instance, handle_1, handle_2 and handle_3, in order to modify the properties of one of them in another thread without plotting the rest of them.
handle_3.setColor(...)
If this is not possible, I was wondering if I could create a kind of transparent containter with Qt (which effectively has a handle to modify the stylesheet) and put a QLabel inside it. If so, what container would be the best choice?

As #ekumoro says, the paint-event does not create new elements, so updating one of the rectangles without re-painting the rest of them is impossible.
The only way to achieve this is using containers. Attending to the second question, I found that the best one is the QFrame.

Related

Draw line and shapes with more sharpness

Am using pyqt5 and Python in my program which is drawing shapes.
My problem is after am drawing my shapes when I zoom in they look in a really bad way like the image below.
I want to get sharpness like in the SVG image.
So how to get these results in pyqt5?
This what I get:
And this what I want:
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtCore import Qt
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 900, 900)
self.setWindowTitle('Pen styles')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawLines(qp)
qp.end()
def drawLines(self, qp):
pen = QPen(Qt.black, 2, Qt.SolidLine)
qp.setPen(pen)
qp.drawLine(20, 40, 800, 800)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
By default QPainter only uses the TextAntialiasing render hint, so shapes are always aliased.
In order to draw "smooth" lines, you need to activate the Antialiasing hint.
Note that you don't need to call begin() if you provide the paint device in the QPainter constructor, and since it's a local variable in the scope of paintEvent() you don't need to call end() either, as it will be called anyway when the painter is destroyed by the garbage collector when the function returns.
def paintEvent(self, e):
qp = QPainter(self)
qp.setRenderHints(qp.Antialiasing)
self.drawLines(qp)

PyQt5 : Drawing surface in which user can draw [duplicate]

This question already has answers here:
How to draw a line from mouse to a point in PyQt5?
(3 answers)
PyQt5 triggering a paintEvent() with keyPressEvent()
(1 answer)
PyQt5: painting using events
(2 answers)
Closed 2 years ago.
I'm learning PyQt5, and I'd like my GUI to have kind of a drawing surface, on which the user can draw anything, so the app can then get this drawing as an image (the goal is to perform classification on that drawing).
How can I do that ? All I found is Qpainter, which allow me to draw when coding the app, but it won't let the user dynamically draw when using the app.
QPainter is a surface for drawing. Here is an example of drawing a rectangular flag:
import sys
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QColor
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 200, 200)
self.setWindowTitle('Drawing')
self.show()
def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
self.drawFlag(qp)
qp.end()
def drawFlag(self,qp):
qp.setBrush(QColor(255, 0, 0))
qp.drawRect(30, 30, 120, 30)
qp.setBrush(QColor(0, 255, 0))
qp.drawRect(30, 60, 120, 30)
qp.setBrush(QColor(0, 0, 255))
qp.drawRect(30, 90, 120, 30)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
If you want to draw a line use:
qp.drawLine(x1, y1, x2, y2)
Mouse position:
def mouseMoveEvent(self, event):
print(event.x, event.y)

Mechanics behind the synchronisation of QPainters

Context
I have a custom Widget which is supposed to make an animation of dots moving in order to make a kind of loading widget. To acheive that goal, I started using QPainter and QVariantAnimation objects, which seemed like a decent tools to do the job. The problem is that I think that the QPainters I initialise when drawing come in conflict with each other.
Technique
To acheive that, I initialize multiple QVariantAnimation, which signal .valueChanged() I connect to a function update(), which is supposed to launch the painEvent(), such as written in the docs
A paint event is a request to repaint all or part of a widget. It can happen for one of the following reasons:repaint() or update() was invoked,
the widget was obscured and has now been uncovered, or
many other reasons.
Since I start different animation at different times, I suppose that the update() is called many times, thus interfering with another QPainter already working. But, as I read in the docs,
When update() is called several times or the window system sends several paint events, Qt merges these events into one event with a larger region.
But it specifies nothing id the QPainter has the same region, which is why I suposse it crashes. It logs messages such as:
QBackingStore::endPaint() called with active painter on backingstore paint device
Minimal Working Example
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QDialog, QPushButton
from PyQt5.QtCore import Qt, pyqtSlot, QVariantAnimation, QVariant, QTimer
from PyQt5.QtGui import QColor, QPainter, QBrush
import time
class Dialog(QDialog):
def __init__(self, *args, **kwargs):
QDialog.__init__(self, *args, **kwargs)
self.resize(500, 500)
self.setLayout(QVBoxLayout())
self.button = QPushButton()
self.layout().addWidget(self.button)
self.paintWidget = PaintWidget()
self.layout().addWidget(self.paintWidget)
self.button.clicked.connect(self.paintWidget.startPainting)
self.button.clicked.connect(self.reverse)
def reverse(self):
if self.paintWidget.isMoving:
self.paintWidget.stopPainting()
class PaintWidget(QWidget):
def __init__(self):
super(PaintWidget, self).__init__()
self.dotRadius = 10
self.dotColor = QColor(255, 100, 100)
self.numberOfDots = 3
self.isMoving = False
self.animation = []
self.createAnimation()
self.dotPosition = [[0, 0], [0, 0], [0, 0]]
def startPainting(self):
for i in range(self.numberOfDots):
self.animation[i].start()
time.sleep(200)
self.isActive = True
def createAnimation(self):
for i in range(self.numberOfDots):
self.animation.append(QVariantAnimation(self, startValue=0, endValue=500, duration=3000))
self.animation[i].valueChanged.connect(self.updatePosition)
#pyqtSlot(QVariant)
def updatePosition(self, position):
self.dotPosition = [position, 0]
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setPen(Qt.NoPen)
for i in range(self.numberOfDots):
painter.save()
painter.translate(0, 0)
position = (self.dotPosition[i][0], self.dotPosition[i][1])
color = self.dotColor
painter.setBrush(QBrush(color, Qt.SolidPattern))
painter.drawEllipse(position[0], position[1], self.dotRadius, self.dotRadius)
painter.restore()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
dial = Dialog()
dial.show()
sys.exit(app.exec_())
Results
I know that for now this code wouldn't work because I can't retreive which dot's animation got updated, but I believe the major problem here is the interference between the painters. Thus, could anyone tell me why this is hapenning and point me in a potential solution? Also, For knowing the dot that got updated and chose the good position, I'm really unsure of how to do this as well.
Your code has the following errors:
Never use time.sleep() in the main GUI thread since it blocks the event loop generating the freezing of the application.
the variable dotPosition that must store all the positions you are replacing it with only one position in the updatePosition method.
You should use QPoint if you are going to store a position instead of a list, use a list is not bad but using QPoint makes your code more readable.
Do not use painter.save() and painter.restore() unnecessarily, neither painter.translate().
Considering the above, the solution is as follows:
from functools import partial
from PyQt5 import QtCore, QtGui, QtWidgets
class Dialog(QtWidgets.QDialog):
def __init__(self, *args, **kwargs):
super(Dialog, self).__init__(*args, **kwargs)
self.resize(500, 500)
self.button = QtWidgets.QPushButton()
self.paintWidget = PaintWidget()
self.button.clicked.connect(self.paintWidget.startPainting)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.button)
lay.addWidget(self.paintWidget)
class PaintWidget(QtWidgets.QWidget):
def __init__(self):
super(PaintWidget, self).__init__()
self.dotRadius = 10
self.dotColor = QtGui.QColor(255, 100, 100)
self.animations = []
self.dotPosition = [
QtCore.QPoint(0, 0),
QtCore.QPoint(0, 0),
QtCore.QPoint(0, 0),
]
self.createAnimation()
def startPainting(self):
for i, animation in enumerate(self.animations):
QtCore.QTimer.singleShot(i * 200, animation.start)
def createAnimation(self):
for i, _ in enumerate(self.dotPosition):
wrapper = partial(self.updatePosition, i)
animation = QtCore.QVariantAnimation(
self,
startValue=0,
endValue=500,
duration=3000,
valueChanged=wrapper,
)
self.animations.append(animation)
#QtCore.pyqtSlot(int, QtCore.QVariant)
def updatePosition(self, i, position):
self.dotPosition[i] = QtCore.QPoint(position, 0)
self.update()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), QtCore.Qt.transparent)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(
QtGui.QBrush(self.dotColor, QtCore.Qt.SolidPattern)
)
for position in self.dotPosition:
painter.drawEllipse(position, self.dotRadius, self.dotRadius)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
dial = Dialog()
dial.show()
sys.exit(app.exec_())

Possible rendering issue with QScrollArea and QPainter

I'm trying to create a character map visualization tool with PyQt5. With fontTools library, I'm extracting the UNICODE code points supported in a given ttf file. Then using QPainter.drawText I'm drawing the glyphs on labels. The labels are stored in a QGridLayout and the layout is in a QScrollArea
Everything works fine, except when I try to scroll. The drawn images are overlapped whenever I try to scroll too fast. It looks like this.
The labels are redrawn properly the moment the window loses focus.
Here's an MWE of what I've so far.
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFontDatabase, QFont, QColor, QPainter
from fontTools.ttLib import TTFont
class App(QWidget):
def __init__(self):
super().__init__()
self.fileName = "mukti.ttf" #the ttf file is located in the same path as the script
self.initUI()
def initUI(self):
self.setWindowTitle("Glyph Viewer")
self.setFixedSize(640, 480)
self.move(100, 100)
vBox = QtWidgets.QVBoxLayout()
self.glyphView = GlyphView()
vBox.addWidget(self.glyphView)
self.setLayout(vBox)
self.showGlyphs()
self.show()
def showGlyphs(self):
#Using the fontTools libray, the unicode blocks are obtained from the ttf file
font = TTFont(self.fileName)
charMaps = list()
for cmap in font['cmap'].tables:
charMaps.append(cmap.cmap)
charMap = charMaps[0]
fontDB = QFontDatabase()
fontID = fontDB.addApplicationFont("mukti.ttf")
fonts = fontDB.applicationFontFamilies(fontID)
qFont = QFont(fonts[0])
qFont.setPointSize(28)
self.glyphView.populateGrid(charMap, qFont)
class GlyphView(QtWidgets.QScrollArea):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setWidgetResizable(True)
def populateGrid(self, charMap, qFont):
glyphArea = QtWidgets.QWidget(self)
gridLayout = QtWidgets.QGridLayout()
glyphArea.setLayout(gridLayout)
row, col = 1, 1
for char in charMap:
uni = charMap[char]
gridLayout.addWidget(Glyph(qFont, chr(char)), row, col)
if not col % 4:
col = 1
row += 1
else:
col += 1
self.setWidget(glyphArea)
class Glyph(QtWidgets.QLabel):
def __init__(self, font, char):
super().__init__()
self.font = font
self.char = char
self.initUI()
def initUI(self):
self.setFixedSize(48, 48)
self.setToolTip(self.char)
def paintEvent(self, event):
qp = QPainter(self)
qp.setBrush(QColor(0,0,0))
qp.drawRect(0, 0, 48, 48)
qp.setFont(self.font)
qp.setPen(QColor(255, 255, 255))
qp.drawText(event.rect(), Qt.AlignCenter, self.char)
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
I'm not sure what is causing this. Any help is appreciated!
The paintEvent() method gives us an object that belongs to QPaintEvent, that object provides a QRect through the rect() method, that QRect is the area that is currently visible, and that information could be used to optimize the painting, for example let's say we have a widget that shows texts in several lines, if the text is large few lines will look so painting all is a waste of resources, so with a proper calculation using the aforementioned QRect we could get those lines and paint that part spending few resources. In this answer I show an example of the use of event.rect().
In your case there is no need to use event.rect() since you would be painting the text in a part of the widget widget. what you should use is self.rect():
def paintEvent(self, event):
qp = QPainter(self)
qp.setBrush(QColor(0,0,0))
qp.drawRect(0, 0, 48, 48)
qp.setFont(self.font())
qp.setPen(QColor(255, 255, 255))
# change event.rect() to self.rect()
qp.drawText(self.rect(), Qt.AlignCenter, self.text())
I also see unnecessary to overwrite paintEvent() method since you can point directly to the QLabel the font, the text and the alignment:
class Glyph(QtWidgets.QLabel):
def __init__(self, font, char):
super().__init__(font=font, text=char, alignment=Qt.AlignCenter, toolTip=char)

How to get movable graphical items inside a QGraphicsItemGroup?

My goal is to write a software that displays two movable disks that live inside the same QGraphicsItemGroup. I'd like to use QGraphicsItemGroup because in this way each disk can access the position of the other. To make the objects movable, I use the flag ItemIsMovable which unfortunately doesn't seem to work inside a QGraphicsItemGroup The following program exemplifies my issue:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsItem,\
QApplication, QGraphicsItemGroup, QGraphicsEllipseItem
class MyDisk(QGraphicsEllipseItem):
def __init__(self, top_left_x, top_left_y, radius, color):
super().__init__(top_left_x, top_left_y, radius, radius)
self.setBrush(color)
self.setFlag(QGraphicsItem.ItemIsMovable)
class MyGroup(QGraphicsItemGroup):
def __init__(self):
super().__init__()
self.disk1 = MyDisk(50, 50, 20, Qt.red)
self.disk2 = MyDisk(150, 150, 20, Qt.red)
self.addToGroup(self.disk1)
self.addToGroup(self.disk2)
# self.setFlag(QGraphicsItemGroup.ItemIsMovable)
class MyView(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.setWindowTitle('Red disks are not movable')
self.setSceneRect(0, 0, 250, 250)
self.group = MyGroup()
self.scene.addItem(self.group)
self.scene.addItem(MyDisk(150, 50, 20, Qt.green))
if __name__ == '__main__':
app = QApplication([])
f = MyView()
f.show()
sys.exit(app.exec_())
My problem is that the green disk (not in the group) is movable but the two red disks (in the group) are not. How can I make the two red disks movable? Note that setting the flag ItemIsMovable inside MyGroup doesn't solve the problem because the red disks would then move together (you can try this by uncomment the comment in the code).

Categories

Resources