Overlapping semitransparent QRectangles in QPainter - python

When two rectangles overlap, I noticed that the intersection between the two become more opaque, which, as I understand, is due to the addition of the QBrush alphas.
Visual example of the overlapping intersection becoming more opaque:
I would like to prevent the overlapping intersection from becoming more opaque, the area should have the same color as the rest of the rectangles. (Which is what happens when using fully opaque QColors, but I want to use semitransparent ones).
I have read several topics on the subject (including this very similar question: Qt: Overlapping semitransparent QgraphicsItem), every answer is stating that the problem can be solved by changing the Composition Mode of the painter, however I just can't find a composition mode that provide the expected result, I have tried all of the 34 Composition Modes listed in the Qpainter documentation (https://doc.qt.io/qt-5/qpainter.html#setCompositionMode). Some of the modes just turn the QColor fully opaque, and most are irrelevant to this problem, none of them allow me to have the intersection of the two rectangles keep the same semitransparent color as the rest.
I would be thankful if anyone has an idea.
You can find below a short code reproducing the problem:
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QColor
from PyQt5.QtCore import QRect
import sys
class Drawing(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(300, 300, 350, 300)
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
painter.setPen(QColor(128, 128, 255, 128))
painter.setBrush(QColor(128, 128, 255, 128))
rect1 = QRect(10,10,100,100)
rect2 = QRect(50,50,100,100)
painter.drawRect(rect1)
painter.drawRect(rect2)
def main():
app = QApplication(sys.argv)
ex = Drawing()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

The simple solution is to use a QPainterPath, ensuring that it both uses setFillRule(Qt.WindingFill) and use the simplified() version of the path, that merges all subpaths:
path = QPainterPath()
path.setFillRule(Qt.WindingFill)
path.addRect(10, 10, 100, 100)
path.addRect(50, 50, 100, 100)
painter.drawPath(path.simplified())

Related

How to limit the area in QGraphicsScene where a custom QGraphicsItem can be moved?

I have a QGraphicsScene where I have QGraphicsItems. These items are movable and I can move them all over the QGraphicsScene but I would like to limit the area where these items can be moved. The sizes of the QGraphicsScene don't have to change. I would really appreciate if someone gave me an example of how to do it in python.
Here's what I have now
from PySide2.QtCore import QPointF
from PySide2.QtWidgets import QWidget, QVBoxLayout, QGraphicsView, \
QGraphicsScene, QGraphicsPolygonItem, QApplication
from PySide2.QtGui import QPen, QColor, QBrush, QPolygonF
class Test(QWidget):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
self.resize(1000, 800)
self.layout_ = QVBoxLayout()
self.view_ = GraphicsView()
self.layout_.addWidget(self.view_)
self.setLayout(self.layout_)
class GraphicsView(QGraphicsView):
def __init__(self):
super(GraphicsView, self).__init__()
self.scene_ = QGraphicsScene()
self.polygon_creation = self.PolyCreation()
self.scene_.setSceneRect(0, 0, 400, 400)
self.setScene(self.scene_)
self.polyCreator()
def polyCreator(self):
self.polygon_creation.poly()
polygon = self.polygon_creation.polygon()
new_poly = self.scene().addPolygon(polygon)
new_poly.setBrush(QBrush(QColor("gray")))
new_poly.setPen(QPen(QColor("gray")))
new_poly.setFlag(QGraphicsPolygonItem.ItemIsSelectable)
new_poly.setFlag(QGraphicsPolygonItem.ItemIsMovable)
new_poly.setFlag(QGraphicsPolygonItem.ItemIsFocusable)
new_poly.setPos(0, 0)
class PolyCreation(QGraphicsPolygonItem):
def __init__(self):
super().__init__()
self.setAcceptHoverEvents(True)
def poly(self):
self.poly_points = (QPointF(0, 0),
QPointF(0, 50),
QPointF(50, 50),
QPointF(50, 0))
self.shape = QPolygonF(self.poly_points)
self.setPolygon(self.shape)
if __name__ == '__main__':
app = QApplication([])
win = Test()
win.show()
app.exec_()
I've also found an answer in cpp, but I can't understand it very well, so if someone could "translate" it in python that'd be great too.
Here's the link restrict movable area of qgraphicsitem (Please check #Robert's answer)
The concept is to restrict the new position before it's finally applied.
To achieve so, you need to also set the ItemSendsGeometryChanges flag and check for ItemPositionChange changes, then compare the item bounding rect with that of the scene, and eventually return a different position after correcting it.
class PolyCreation(QGraphicsPolygonItem):
def __init__(self):
super().__init__(QPolygonF([
QPointF(0, 0),
QPointF(0, 50),
QPointF(50, 50),
QPointF(50, 0)
]))
self.setBrush(QBrush(QColor("gray")))
self.setPen(QPen(QColor("blue")))
self.setFlags(
self.ItemIsSelectable
| self.ItemIsMovable
| self.ItemIsFocusable
| self.ItemSendsGeometryChanges
)
self.setAcceptHoverEvents(True)
def itemChange(self, change, value):
if change == self.ItemPositionChange and self.scene():
br = self.polygon().boundingRect().translated(value)
sceneRect = self.scene().sceneRect()
if not sceneRect.contains(br):
if br.right() > sceneRect.right():
br.moveRight(sceneRect.right())
if br.x() < sceneRect.x():
br.moveLeft(sceneRect.x())
if br.bottom() > sceneRect.bottom():
br.moveBottom(sceneRect.bottom())
if br.y() < sceneRect.y():
br.moveTop(sceneRect.top())
return br.topLeft()
return super().itemChange(change, value)
class GraphicsView(QGraphicsView):
def __init__(self):
super(GraphicsView, self).__init__()
self.scene_ = QGraphicsScene()
self.scene_.setSceneRect(0, 0, 400, 400)
self.setScene(self.scene_)
self.scene_.addItem(PolyCreation())
Notes:
the above code will obviously only work properly for top level items (not children of other items);
it will work as long as the item doesn't have any transformation applied (rotation, scale, etc.); if you want to support that, you have to consider the sceneTransform() to get the actual bounding rect of the polygon;
it doesn't consider the pen width, so if the item has a thick pen, the resulting polygon may go beyond the scene boundaries; to avoid that, use the actual boundingRect() of the item and adjust it by using half the pen width;
avoid nested classes, they are rarely required and they only tend to make code unnecessarily convoluted;
you were not actually using that subclass, since you're in fact adding another polygon item based on the polygon of that instance;
all items are always positioned at (0, 0) by default, specifying it again is pointless;
shape() is an existing (and quite important) function of all items, you shall not overwrite it;

Zoom QMainwindow

Short context:
I have a QMainwindow with an mpv player inside. I play videos with mpv, create overlay images with PIL and run it all in the pyqt window. The overlay image is updated more or less every frame of the video.
Here is my issue:
If the mpv picture is large, then updating the overlay images is far too slow (I have optimized a lot to improve performance, using separate processes and threads, only using one overlay etc.). If the picture is small, however, it all works flawlessly (thus indicating that it is not far from satisfactory in performance).
I wouldn't mind losing resolution to gain performance, so I want to have a large window with lower resolution content. Is this possible?
The bottleneck here is mpv's overlay.update function
My main idea is to zoom the QMainwindow, but I cannot seem to find a way to do this. Any other solution is of course sufficient.
Example code (note that test.mp4 is the hardcoded video, provide anything you have)
#!/usr/bin/env python3
import mpv
import sys
from PIL import Image, ImageDraw
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Test(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.container = QWidget(self)
self.setCentralWidget(self.container)
self.container.setAttribute(Qt.WA_DontCreateNativeAncestors)
self.container.setAttribute(Qt.WA_NativeWindow)
self.w = 1800
self.h = int(self.w / 16 * 9)
self.setFixedSize(self.w, self.h)
self.player = mpv.MPV(wid=str(int(self.container.winId())),
log_handler=print)
self.player.play('test.mp4')
self.overlay = self.player.create_image_overlay()
self.coords = [20, 20, 50, 50]
def play(self):
#self.player.property_observer("time-pos")
def _time_observer(_name: str, value: float) -> None:
for i in range(len(self.coords)):
self.coords[i] = self.coords[i]*2 % self.h
img = Image.new("RGBA", (self.w, self.h), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.rectangle(self.coords, outline=(255,255,255,255), width=4)
self.overlay.update(img)
app = QApplication(sys.argv)
# This is necessary since PyQT stomps over the locale settings needed by libmpv.
# This needs to happen after importing PyQT before creating the first mpv.MPV instance.
import locale
locale.setlocale(locale.LC_NUMERIC, 'C')
win = Test()
win.show()
win.play()
sys.exit(app.exec_())
Short Summary
Having a large window causes mpv's overlay.update method to consume too much time/computation. It is acceptable to decrease the dpi (resolution) of the overlay pictures, and even the video, to make it run faster.

expand the size of QPaintDevice after it is initialzed

A little Context
I'm trying to make a QSplashScreen with a custom animation. I've tried many different approaches, with each failures. Mostly, my main technique was to create a new Class which inherits from QSplashScreen and then plan with the paintEvent(). It worked... ish. The animation is not the problem, It is really the QPaintDevice which seems corrupted.
Because I was calling the super(classname, self).__init__(args) only way after in my init and passing it arguments that I modified in the init, I always had corrupted pixels; The image was in weird tones and had lines of colorful pixels in the background. Sometimes it is patterns sometimes it is completely random.
I have tried changing every line of code and the only thing that removed those lines was calling the super() at the beginning of the __init__. Unfortunately, I was making a frame that I was passing to the init. Now that this isn't possible, I would like to modify the size of the QPaintDevice on which my QSplashScreen initializes, because my animation is displayed beyond that frame. I won't post all the code since the custom animation is quite heavy.
Minimal Working Exemple
from PyQt5.QtWidgets import QApplication, QSplashScreen, QMainWindow
from PyQt5.QtCore import Qt, QSize, pyqtSignal, QPoint
from PyQt5.QtGui import QPixmap, QPainter, QIcon, QBrush
import time, sys
class FakeAnimatedSplash(QSplashScreen):
def __init__(self, image):
self.image = image
self.newFrame = QPixmap(self.image.size()+QSize(0, 20))
super(FakeAnimatedSplash, self).__init__(self.newFrame, Qt.WindowStaysOnTopHint)
def showEvent(self, event):
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setPen(Qt.NoPen)
painter.drawPixmap(self.image.rect(), self.image)
painter.drawEllipse(QPoint(0, 110), 8, 8)
class App(QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.main = QMainWindow()
self.setAttribute(Qt.AA_EnableHighDpiScaling)
self.newSplash()
self.main.show()
def newSplash(self):
pixmap = QPixmap("yourImage.png")
smallerPixmap = pixmap.scaled(100, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation)
splash = FakeAnimatedSplash(smallerPixmap)
splash.setEnabled(False)
splash.show()
start = time.time()
while time.time() < start + 10:
self.processEvents()
def main():
app = App(sys.argv)
app.setWindowIcon(QIcon("ABB_icon.png"))
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Potential Solution
Changing the super() to the beginning makes it work but reduces the QPaintDevice window which hides my animation. I would like to expand it, but there are no methods which can do it after the initialization.
def __init__(self, image):
super(LoadingDotsSplash, self).__init__(image, QtCore.Qt.WindowStaysOnTopHint)
self.image = image
# here a function or method that changes the size of the QPaintDevice
The problem is that newFrame is an uninitialized QPixmap and for efficiency reasons the pixels are not modified so they have random values, and that is because the size of the FakeAnimatedSplash is larger than the QPixmap that is painted. The solution is to set the value of the newFrame pixels to transparent:
class FakeAnimatedSplash(QSplashScreen):
def __init__(self, image):
self.image = image
pix = QPixmap(self.image.size() + QSize(0, 20))
pix.fill(Qt.transparent)
super(FakeAnimatedSplash, self).__init__(pix, Qt.WindowStaysOnTopHint)
def paintEvent(self, event):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setPen(Qt.NoPen)
painter.drawPixmap(self.image.rect(), self.image)
painter.drawEllipse(QPoint(0, 110), 8, 8)

How to reduce the size of a QComboBox with PyQt?

I created a small program with PyQt in which I put a QComboBox, but this one contains only a list of 2 letters. The program window being small, to save space, I would like to reduce the width of the QComboBox.
This is what it looks like now. The width is too large.
I searched the internet, but after a lot of searching time, I still haven't found anything. Thank you in advance if you have an idea.
There are several methods to resize a widget's size. Lets say the QComboBox is defined as this:
combo = QComboBox(self)
One way is to use QWidget.resize(width, height)
combo.resize(200,100)
To obtain a proper size automatically, you can use QWidget.sizeHint() or sizePolicy()
combo.resize(combo.sizeHint())
If you want to set fixed size, you can use setFixedSize(width, height), setFixedWidth(width), or setFixedHeight(height)
combo.setFixedSize(400,100)
combo.setFixedWidth(400)
combo.setFixedHeight(100)
Here's an example:
from PyQt5.QtWidgets import (QWidget, QLabel, QComboBox, QApplication)
import sys
class ComboboxExample(QWidget):
def __init__(self):
super().__init__()
self.label = QLabel("Ubuntu", self)
self.combo = QComboBox(self)
self.combo.resize(200,25)
# self.combo.resize(self.combo.sizeHint())
# self.combo.setFixedWidth(400)
# self.combo.setFixedHeight(100)
# self.combo.setFixedSize(400,100)
self.combo.addItem("Ubuntu")
self.combo.addItem("Mandriva")
self.combo.addItem("Fedora")
self.combo.addItem("Arch")
self.combo.addItem("Gentoo")
self.combo.move(25, 25)
self.label.move(25, 75)
self.combo.activated[str].connect(self.onActivated)
# self.setGeometry(0, 0, 500, 125)
self.setWindowTitle('QComboBox Example')
self.show()
def onActivated(self, text):
self.label.setText(text)
self.label.adjustSize()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = ComboboxExample()
sys.exit(app.exec_())

How to paint in colors QtGui.QPainterPath.addRect()?

I am trying to paint rect in PyQt5. But something always is not working with me. I was referring to "QPainterPath documentation' and there was this example :
path = QPainterPath()
path.addRect(20, 20, 60, 60)
path.moveTo(0, 0)
path.cubicTo(99, 0, 50, 50, 99, 99)
path.cubicTo(0, 99, 50, 50, 0, 0)
QPainter painter(self)
painter.fillRect(0, 0, 100, 100, Qt.white)
painter.setPen(QPen(QColor(79, 106, 25), 1, Qt.SolidLine,
Qt.FlatCap, Qt.MiterJoin))
painter.setBrush(QColor(122, 163, 39))
painter.drawPath(path)
I tried myself , but I can't under stand what is "QPainter painter(self)" and how it works there, I couldn't find QPainter command.
Here is my example of code :
from PyQt5 import QtGui, QtCore, QtWidgets
import sys
class testUi(QtWidgets.QDialog):
def __init__(self, parent=None):
super(testUi, self).__init__(parent)
self.window = 'vl_test'
self.title = 'Test Remastered'
self.size = (1000, 650)
self.create( )
def create(self):
self.setWindowTitle(self.title)
self.resize(QtCore.QSize(*self.size))
self.testik = test(self)
self.mainLayout = QtWidgets.QVBoxLayout( )
self.mainLayout.addWidget(self.testik)
self.setLayout(self.mainLayout)
class test(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(test, self).__init__(parent)
self._scene = QtWidgets.QGraphicsScene( )
self.setScene(self._scene)
self.drawSomething( )
def drawSomething(self):
self.path = QtGui.QPainterPath( )
self.path.moveTo(0, 0)
self.path.addRect(20, 20, 60, 60)
self._scene.addPath(self.path)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
v = testUi()
v.show()
sys.exit(app.exec_())
I didn't write anything after "addRect" in my code. So can anybody please clear how to use that "QPainter". Thank you for your time.
P.S
Bonus question : What is fastest way to draw and interact with primitives in QGraphicScene? Because I can see there a lot of ways to draw curves/polygons and manipulate them, but what is most efficient for workflow ? Sorry for my english
The addPath() method returns a QGraphicsPathItem that inherits from QAbstractGraphicsShapeItem that allows you to set the fill and border color with setBrush() and setPen(), respectively
def drawSomething(self):
self.path = QtGui.QPainterPath()
self.path.moveTo(0, 0)
self.path.addRect(20, 20, 60, 60)
item = self._scene.addPath(self.path)
# or
# item = QtWidgets.QGraphicsPathItem(path)
# self._scene.addItem(item)
item.setPen(
QtGui.QPen(
QtGui.QColor(79, 106, 25),
1,
QtCore.Qt.SolidLine,
QtCore.Qt.FlatCap,
QtCore.Qt.MiterJoin,
)
)
item.setBrush(QtGui.QColor(122, 163, 39))
What is fastest way to draw and interact with primitives in QGraphicScene? Because I can there is a lot of ways to draw curves/polygons and manipulate them, but what is going to be most efficient for workflow?
The entire painting system in Qt ultimately uses QPainter, although the code I showed earlier does not see the explicit use but it is being used since QGraphicsItem has a paint that uses the QPainter, the brush and the pen.
Also QPainter is optimized so do not worry about the optimization from the Qt side.
QGraphicsView and QGraphicsScene is a high level framework that is based on the QGraphicsItems so in general you should be able to build anything with the predefined items, but if you can not then create your own QGraphicsItem (implementing the method paint() and boundingRect())
If you want to paint something permanent with which you do not want to interact (move it, click it, etc) then you can use the drawBackground or drawForeGround method of QGraphicsScene as you want to paint before or after the items are painted, respectively
For more information read: Graphics View Framework

Categories