Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

How to make QPainter elements clickable-through using PyQt



  • I am trying to create a desktop application for Windows 10 using PyQt.

    The application will be an overlay, so it has to be transparent, clickable-through and always on top. An object detector will detect some regions of interest and a rectangle will be painted in that region.

    The overlay is almost fine. Right now it's able to draw random rectangles and while the background is transparent you can click through the background. The problem is that I can't click through the rectangles.

    I've tried the WA_TransparentForMouseEvents attribute, but it didn't work correctly. When I set this attribute the buttons stop working (I would expect this behaviour, and I don't care since the buttons are just for debuging), but I still can't click through the buttons or the rectangles.

    This is my code sample:

    import sys
    from PyQt6.QtCore import Qt
    from PyQt6.QtGui import QBrush, QColor, QPainter, QPen
    from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QStackedWidget
    import numpy as np
    
    
    class OverlayScreen(QMainWindow):
        def __init__(self):
            super().__init__()
            self.setFixedHeight(1080)
            self.setFixedWidth(1920)
            self.setObjectName("overlay")
            self.setStyleSheet("QMainWindow#overlay {background-color: rgba(0, 0, 0, 125)}")
            self.bg_transparent = False
    
            self.toggle_btn = QPushButton("Toggle", self)
            self.toggle_btn.setGeometry(200, 150, 100, 30)
            self.toggle_btn.clicked.connect(self.toggle_background)
            self.draw_btn = QPushButton("Draw rectangle", self)
            self.draw_btn.setGeometry(320, 150, 100, 30)
            self.draw_btn.clicked.connect(self.draw_rectangle)
            self.rectangles = []
    
            self.close_btn = QPushButton("Close", self)
            self.close_btn.setGeometry(200, 200, 100, 30)
            self.close_btn.clicked.connect(sys.exit)
    
        def toggle_background(self):
            self.bg_transparent = not self.bg_transparent
            if self.bg_transparent:
                self.setStyleSheet("QMainWindow#overlay {background-color: transparent}")
            else:
                self.setStyleSheet("QMainWindow#overlay {background-color: rgba(0, 0, 0, 125)}")
    
        def draw_rectangle(self):
            # self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
            rect = [np.random.randint(1920 - 150),
                    np.random.randint(1080 - 100),
                    np.random.randint(150),
                    np.random.randint(100)]
            self.rectangles.append(rect)
            self.update()
    
        def paintEvent(self, event):
            painter = QPainter(self)
            painter.setPen(QPen(Qt.GlobalColor.green, 1))
            painter.setBrush(QBrush(QColor(0, 255, 0, 80), Qt.BrushStyle.SolidPattern))
            for rect in self.rectangles:
                painter.drawRect(*rect)
            painter.end()
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        main = QStackedWidget()
        main.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        main.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
        main.move(0, 0)
        overlay = OverlayScreen()
        main.addWidget(overlay)
        main.show()
    
        try:
            sys.exit(app.exec())
        except Exception:
            print("Error")
    

  • Lifetime Qt Champion

    Hi and welcome to devnet,

    Which version of PyQt are you using ?
    How did you install it ?



  • @igonro said in How to make QPainter elements clickable-through using PyQt:

    I am trying to create a desktop application for Windows 10 using PyQt.

    The application will be an overlay, so it has to be transparent, clickable-through and always on top. An object detector will detect some regions of interest and a rectangle will be painted in that region.

    The overlay is almost fine. Right now it's able to draw random rectangles and while the background is transparent you can click through the background. The problem is that I can't click through the rectangles.

    Running the example on macOS, clicks also don't pass through the transparent region. I'm surprised they do on Windows.

    I've tried the WA_TransparentForMouseEvents attribute, but it didn't work correctly. When I set this attribute the buttons stop working (I would expect this behaviour, and I don't care since the buttons are just for debuging), but I still can't click through the buttons or the rectangles.

    This is my code sample:

    if __name__ == "__main__":
        app = QApplication(sys.argv)
        main = QStackedWidget()
        main.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        main.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
        main.move(0, 0)
        overlay = OverlayScreen()
        main.addWidget(overlay)
        main.show()
    

    Is there a reason to add the QMainWindow-based overlay to a QStackedWidget? I would guess that's the problem. Even though the overlay doesn't accept mouse input, the widget it is embedded in can, and the QWindow looks like a normal input accepting window to the OS.

    This works for me with macOS, as far as I understand the problem. The buttons and extraneous code have been removed for simplicity.

    import sys
    import random
    
    from PyQt5.QtCore import Qt
    from PyQt5.QtGui import QBrush, QColor, QPainter, QPen, QGuiApplication
    from PyQt5.QtWidgets import QApplication, QWidget
    
    
    class OverlayScreen(QWidget):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.screen = QGuiApplication.screens()[0]
            self.setFixedHeight(self.screen.size().width())
            self.setFixedWidth(self.screen.size().height())
            self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
    
            self.rectangles = []
            self.draw_rectangle()
            self.draw_rectangle()
            self.draw_rectangle()
    
        def draw_rectangle(self):
            rect = [random.randint(151, self.screen.size().height() - 1),
                    random.randint(101, self.screen.size().width() - 1),
                    random.randint(1, 150),
                    random.randint(1, 100)]
            self.rectangles.append(rect)
            self.update()
    
        def paintEvent(self, event):
            super().paintEvent(event)
            painter = QPainter(self)
            painter.setPen(QPen(Qt.GlobalColor.green, 1))
            painter.setBrush(QBrush(QColor(0, 255, 0, 80), Qt.BrushStyle.SolidPattern))
            for rect in self.rectangles:
                painter.drawRect(*rect)
            painter.end()
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        overlay = OverlayScreen()
        overlay.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        overlay.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint )
        # WindowTransparentForInput can be used instead of WA_TransparentForMouseEvents
        #overlay.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowTransparentForInput)
        overlay.show()
    
        try:
            sys.exit(app.exec())
        except Exception:
            print("Error")
    


  • @SGaist I'm using PyQt6, I just installed with pip install pyqt6.



  • @jeremy_k Yes. Someone told me that clicks don't pass through in Ubuntu too, but in Windows it really works (but only where the app it's 100% transparent).

    I add the QMainWindow to a QStackedWidget because is the method I use to navigate through different views. In the full code I also have a welcome screen, login screen... Is there a better way for navigating through different views? I saw this method in a Youtube tutorial.

    Your code is working fine for me. I will try to adapt the code without using the QStackedWidget if possible. Thank you so much :)



  • @igonro said in How to make QPainter elements clickable-through using PyQt:

    @jeremy_k Yes. Someone told me that clicks don't pass through in Ubuntu too, but in Windows it really works (but only where the app it's 100% transparent).

    I add the QMainWindow to a QStackedWidget because is the method I use to navigate through different views. In the full code I also have a welcome screen, login screen... Is there a better way for navigating through different views? I saw this method in a Youtube tutorial.

    That sounds like a reasonable use, but the QStackedWidget should also be set transparent to input events while the overlay is displayed.

    Your code is working fine for me. I will try to adapt the code without using the QStackedWidget if possible. Thank you so much :)

    Happy to hear it.



  • @jeremy_k Ohh, you're right. I've tried setting the QStackedWidget to transparent and it works. The problem I'm facing now is that in some windows (welcome and login) I need it to be clickable and in the overlay window I need to make it clickable through. But it seems that changing the Qt.WidgetAttribute.WA_TransparentForMouseEvents attribute during the execution doesn't work.

    Do you have any idea of why is this happening?



  • I've updated the code sample in order to make it work with shortcuts (I think it's easier this way).

    When I add the main.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) line, it is 100% clickable through, and it works as intended; but I can't make it toggleable. The Ctrl+C shortcut is not working. :(

    Is there any problem with the setAttribute() function? Maybe I have to call another function (instead of repaint()) to take effect?

    import sys
    from PyQt6.QtCore import Qt
    from PyQt6.QtGui import QBrush, QColor, QKeySequence, QPainter, QPen, QShortcut
    from PyQt6.QtWidgets import QApplication, QMainWindow, QStackedWidget
    import numpy as np
    
    
    class OverlayScreen(QMainWindow):
        def __init__(self):
            super().__init__()
            self.setFixedHeight(1080)
            self.setFixedWidth(1920)
            self.setObjectName("overlay")
            self.setStyleSheet("QMainWindow#overlay {background-color: rgba(0, 0, 0, 125)}")
            self.bg_transparent = False
            self.clickable_thr = False
    
            self.toggle_short = QShortcut(QKeySequence("Ctrl+T"), self)
            self.toggle_short.activated.connect(self.toggle_background)
            self.draw_short = QShortcut(QKeySequence("Ctrl+D"), self)
            self.draw_short.activated.connect(self.draw_rectangle)
            self.rectangles = []
            self.clickable_short = QShortcut(QKeySequence("Ctrl+C"), self)
            self.clickable_short.activated.connect(self.clickable_through)
            self.close_short = QShortcut(QKeySequence("Ctrl+X"), self)
            self.close_short.activated.connect(sys.exit)
    
        def toggle_background(self):
            self.bg_transparent = not self.bg_transparent
            if self.bg_transparent:
                print("Transparency ON")
                self.setStyleSheet("QMainWindow#overlay {background-color: transparent}")
            else:
                print("Transparency OFF")
                self.setStyleSheet("QMainWindow#overlay {background-color: rgba(0, 0, 0, 125)}")
    
        def clickable_through(self):
            self.clickable_thr = not self.clickable_thr
            if self.clickable_thr:
                print("Clickable through ON")
                main.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
                self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
            else:
                print("Clickable through OFF")
                main.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
                self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
            self.repaint()
    
        def draw_rectangle(self):
            print("Draw rectangle")
            rect = [np.random.randint(1920 - 150),
                    np.random.randint(1080 - 100),
                    np.random.randint(150),
                    np.random.randint(100)]
            self.rectangles.append(rect)
            self.update()
    
        def paintEvent(self, event):
            painter = QPainter(self)
            painter.setPen(QPen(Qt.GlobalColor.green, 1))
            painter.setBrush(QBrush(QColor(0, 255, 0, 80), Qt.BrushStyle.SolidPattern))
            for rect in self.rectangles:
                painter.drawRect(*rect)
            painter.end()
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        main = QStackedWidget()
        # main.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
        main.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        main.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
        main.move(0, 0)
        overlay = OverlayScreen()
        main.addWidget(overlay)
        main.show()
    
        try:
            sys.exit(app.exec())
        except Exception:
            print("Error")
    


  • https://code.woboq.org/qt5/qtbase/src/widgets/kernel/qwidget.cpp.html#1120

    When WA_TransparentForMouseEvents is set on a top level widget, it sets the WindowTransparentForInput window flag. This explains why the keyboard shortcut doesn't work. The window won't receive keyboard input.

    https://code.woboq.org/qt5/qtbase/src/widgets/kernel/qwidget.cpp.html#11444

    Changing WA_TransparentForMouseEvents is a no-op for Qt5 and 6.

    Directly applying or removing the Qt::WindowTransparentForInput window flag appears to work. I found it necessary to show() the top level widget after toggling.



  • @jeremy_k I think I didn't explain it very well. When I add the main.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) line, it is 100% clickable through, but I can't make it toggleable, the Ctrl+C shortcut is working (this is, "Clickable through ON/OFF" is being printed), but it doesn't toggle the click-through behaviour: it always remains as it is.

    But yes, your solution is perfect! I've changed the setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) for the setWindowFlag(Qt.WindowType.WindowTransparentForInput) and after applying the .show() it works as intended!

    Thank you very much!


Log in to reply