pythonqtuser-interfacepyqtpyqt5

How to set a QWidget hidden when mouse hovering and reappear when mouse leaving?


I am trying to create a small widget to display information. This widget is intended to be always on top, and set hidden when the mouse hovers over it so you can click or see whatever is underneath it without disruption, and then reappear once your mouse leaves this widget. The problem I am currently facing is that once the widget is hidden, there is no pixel drawed thus no mouse actitvity is tracked anymore, which immediately triggers the leaveEvent, thus the widget keeps blinking. Here is an example:

import sys
from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
from PyQt5.QtCore import Qt

class TransparentWindow(QWidget):
    def __init__(self):
        super().__init__()

        # Set window attributes
        self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) # | Qt.WindowTransparentForInput)
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setMouseTracking(True)
        
        # Set example text
        self.layout = QVBoxLayout()
        self.label = QLabel(self)
        self.label.setText("Hello, World!")
        self.label.setStyleSheet("background-color: rgb(255, 255, 255); font-size: 50px;")
        self.label.setAlignment(Qt.AlignCenter)
        self.layout.addWidget(self.label)
        self.setLayout(self.layout)
        
    def enterEvent(self, event):
        print("Mouse entered the window")
        self.label.setHidden(True)
        
    def leaveEvent(self, event):
        print("Mouse left the window")
        self.label.setHidden(False)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = TransparentWindow()
    window.show()
    sys.exit(app.exec_())

Now I have tried to add an almost transparent Qwidget item under it so I can pick up mouse events with these nearly transparent pixels:

    def __init__(self):
        super().__init__()

        # Set window attributes
        self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setMouseTracking(True)
        
        # Set example text
        self.layout = QVBoxLayout()
        self.label = QLabel(self)
        self.label.setText("Hello, World!")
        self.label.setStyleSheet("background-color: rgb(255, 255, 255); font-size: 50px;")
        self.label.setAlignment(Qt.AlignCenter)
        self.layout.addWidget(self.label)
        self.setLayout(self.layout)

        # Set an almost transparent widget
        self.box = QWidget(self)
        self.box.setStyleSheet("background-color: rgba(255, 255, 255, 0.01)")
        self.layout.addWidget(self.box)

which makes the disappear-then-reappear part work. But I can no longer click whatever is underneath it. I have tried to add Qt.WindowTransparentForInput, but it made the window transparent to enter/leave event as well. Is there any solution to make this window only transparent to click event but not enter/leave event? Or do I have to use other global mouse tracking libraries to make this work?


Platform: Windows 11 23H2


Thanks for all your help! This is how I've decided to implement it for the moment:

import sys
from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
from PyQt5.QtGui import QCursor
from PyQt5.QtCore import Qt, QTimer

class TransparentWindow(QWidget):
    def __init__(self):
        super().__init__()

        # Set window attributes
        self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool) # | Qt.WindowTransparentForInput)
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.setMouseTracking(True)

        # Set example text
        self.layout = QVBoxLayout()
        self.label = QLabel(self)
        self.label.setText("Hello, World!")
        self.label.setStyleSheet("background-color: rgb(255, 255, 255); font-size: 50px;")
        self.label.setAlignment(Qt.AlignCenter)
        self.layout.addWidget(self.label)
        self.setLayout(self.layout)

        self.hidetimer = QTimer(self)
        self.hidetimer.setSingleShot(True)
        self.hidetimer.timeout.connect(self.hidecheck)
        self.hidecheckperiod = 300

    def hidecheck(self):
        if self.geometry().contains(QCursor.pos()):
            self.hidetimer.start(self.hidecheckperiod)
            return
        print("Showing.....")
        self.setHidden(False)
            
    def enterEvent(self, event):
        self.setHidden(True)
        self.hidetimer.start(self.hidecheckperiod)
        print("Hiding.....")

if __name__ == "__main__":

    app = QApplication(sys.argv)
    window = TransparentWindow()
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":

    app = QApplication(sys.argv)
    window = TransparentWindow()
    window.show()
    sys.exit(app.exec_())

Solution

  • You could poll the cursor position to see if it's contained by the current window geometry. This has the side benefit of allowing for a short delay, so that the window isn't continually shown/hidden when the cursor moves quickly over it. The delay could be configurable by the user. I think this should work on all platforms, but I've only tested it on Linux.

    Here's a basic demo:

    import sys
    from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
    from PyQt5.QtGui import QCursor
    from PyQt5.QtCore import Qt, QTimer
    
    class TransparentWindow(QWidget):
        def __init__(self):
            super().__init__()
    
            # Set window attributes
            self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) # | Qt.WindowTransparentForInput)
            self.setAttribute(Qt.WA_TranslucentBackground)
    
            # Set example text
            self.layout = QVBoxLayout()
            self.label = QLabel(self)
            self.label.setText("Hello, World!")
            self.label.setStyleSheet("background-color: rgb(255, 255, 255); font-size: 50px;")
            self.label.setAlignment(Qt.AlignCenter)
            self.layout.addWidget(self.label)
            self.setLayout(self.layout)
    
            self._timer = QTimer(self)
            self._timer.setInterval(500)
            self._timer.timeout.connect(self.pollCursor)
            self._timer.start()
    
        def pollCursor(self):
            over = self.geometry().contains(QCursor.pos())
            if over != self.isHidden():
                self.setHidden(over)
                self._timer.start()
                print("Mouse is over the window:", over)
    
    if __name__ == "__main__":
    
        app = QApplication(sys.argv)
        window = TransparentWindow()
        window.show()
        sys.exit(app.exec_())