qtqt5

How to draw a semi-transparent overlay with dynamic "holes"/negative space with QPainter?


I have an image like this:

[![enter image description here](https://i.sstatic.net/st2qY.png)](https://i.sstatic.net/st2qY.png)

I have a custom widget that reimplements paintEvent().

On top of the image I'd like to draw an overlay with one or more rectangles spared out. With rounded corners and proper anti-aliasing.

enter image description here

I'm guessing this might be possible with QPainter::CompositionMode. But how? Maybe draw the overlay into a QImage? Invert it?

In paint.net I drew a rectangle into a second layer, then selected the inside with the magic wand, and deleted it. I don't know how to do that with QPainter.


Solution

  • Working with composition modes is possible, but it requires complete understanding of their behavior (frankly, I was never able to wrap my head around them).

    Also, when working with pixmaps, it's often necessary a further step, which is to draw one of the layers on a temporary "buffer" (a further QPixmap or QImage).

    As long as the "masking" is simple, it doesn't involve alpha blending of multiple images, and the image is opaque (no transparency at all), we could just follow a simpler path, which is painting over and eventually "clear" the required areas by clipping the contents.

    The procedure is actually easy:

    1. draw the pixmap;
    2. fill the whole area with the "shaded" color;
    3. create a QPainterPath() and add all areas we need to be "clear";
    4. use setClipPath() on the QPainter (with the Antialiasing render hint set);
    5. draw again the pixmap;

    I cannot write in C++, but the following simple example in Python should be clear enough:

    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    
    class ClipTest(QWidget):
        def __init__(self):
            super().__init__()
            # This is the original image used in the question
            self.pixmap = QPixmap('baseimage.png')
            self.setFixedSize(self.pixmap.size())
    
        def paintEvent(self, event):
            qp = QPainter(self)
    
            # draw the source image
            qp.drawPixmap(0, 0, self.pixmap)
    
            # draw the darker "shade" on the whole widget
            qp.fillRect(self.rect(), QColor(32, 32, 32, 127))
    
            path = QPainterPath()
            # add a couple of rounded rects, which are also colliding
            path.addRoundedRect(250, 110, 140, 140, 20, 20)
            path.addRoundedRect(380, 110, 140, 140, 20, 20)
    
            # to fill all enclosed regions, including those of colliding
            # sub paths that may be otherwise be unpainted
            path.setFillRule(Qt.WindingFill)
    
            # clip the painting on the path above
            qp.setClipPath(path)
    
            # for proper antialiased clipping
            qp.setRenderHint(qp.Antialiasing)
    
            # redraw the pixmap again
            qp.drawPixmap(0, 0, self.pixmap)
    
    
    app = QApplication([])
    win = Test()
    win.show()
    app.exec()
    

    Which gives the following result:

    Screenshot of the running code