Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
238 views
in Technique[技术] by (71.8m points)

python - widget's leaveEvent() is missed when scrolling through the encapsulating QScrollArea()

I have a grid of QWidget()s in a QScrollArea(). I override their leaveEvent() for a particular reason (more on that later). When moving around with the mouse pointer, the leaveEvent() is triggered just fine. But when I keep the mouse pointer still and scroll through the QScrollArea() with my scrollwheel, the mouse pointer effectively leaves the widget without triggering a leaveEvent().

1. Some context

I'm working on an application to browse through Arduino libraries:

enter image description here

What you see in the screenshot is a QScrollArea() in the middle with a scrollbar on the right and one at the bottom. This QScrollArea() contains a large QFrame() that holds all the widgets in its QGridLayout(). Each widget - usually a QLabel() or QPushButton() - has a lightgrey border. This way, I visualize the grid they're in.

2. Lighting up a whole row

When clicking on a widget, I want the whole row to light up. To achieve this, I override the mousePressEvent() for each of them:
Note: In practice, I just make each widget a subclasses from a custom class
CellWidget(), such that I need this method-override only once.

def mousePressEvent(self, e:QMouseEvent) -> None:
    '''
    Set 'blue' property True for all neighbors.
    '''
    for w in self.__neighbors:
        w.setProperty("blue", True)
        w.style().unpolish(w)
        w.style().polish(w)
        w.update()
    super().mousePressEvent(e)
    return

The variable self.__neighbors is a reference each widget keeps to all its neighboring widgets at the left and right side. This way, each widget has access to all widgets from its own row.

As you can see, I set the property "blue" from each widget. This has an effect in the StyleSheet. For example, this is the stylesheet I give to the QLabel() widgets:

# Note: 'self' is the QLabel() widget.
self.setStyleSheet(f"""
    QLabel {
        background-color: #ffffff;
        color: #2e3436;
        border-left:   1px solid #eeeeec;
        border-top:    1px solid #eeeeec;
        border-right:  1px solid #d3d7cf;
        border-bottom: 1px solid #d3d7cf;
    }
    QLabel[blue = true] {
        background-color: #d5e1f0;
    }
""")

The unpolish() and polish() methods ensure that the stylesheet gets reloaded (I think?) and the update() method invokes a repaint. The procedure works nice. I click on any given widget, and the whole row lights up blue!

3. Turn off the (blue) light

To turn it off, I override the leaveEvent() method:

def leaveEvent(self, e:QEvent) -> None:
    '''
    Clear 'blue' property for all neighbors.
    '''
    for w in self.__neighbors:
        w.setProperty("blue", False)
        w.style().unpolish(w)
        w.style().polish(w)
        w.update()
    super().leaveEvent(e)
    return

So the whole row remains lighted up until you leave the widget with your mouse pointer. The leaveEvent() clears the 'blue' property for all neighbors and forces a repaint on them.

4. The problem

But there is a weakness. When I keep my mouse at the same position and I scroll the mousewheel down, the mouse pointer effectively leaves the widget without triggering a leaveEvent(). The absence of such leaveEvent() breaks my solution. While keeping the mouse still and scrolling down, you can click several rows and they all remain blue.

5. System info

I'm working on an application for both Windows and Linux. The application is written in Python 3.8 and uses PyQt5.

question from:https://stackoverflow.com/questions/65922274/widgets-leaveevent-is-missed-when-scrolling-through-the-encapsulating-qscroll

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Both enterEvent and leaveEvent obviously depend on mouse movements, and since the scrolling does not involve any movement your approach won't work until the user slightly moves the mouse.

I can think about two possible workarounds, but both of them are a bit hacky and not very elegant. In both cases, I'm subclassing the scroll area and override the viewportEvent

Slightly move the mouse

Since those events require a mouse movement, let's move the mouse (and then move it back):

class ScrollArea(QtWidgets.QScrollArea):
    def fakeMouseMove(self):
        pos = QtGui.QCursor.pos()
        # slightly move the mouse
        QtGui.QCursor.setPos(pos + QtCore.QPoint(1, 1))
        # ensure that the application correctly processes the mouse movement
        QtWidgets.QApplication.processEvents()
        # restore the previous position
        QtGui.QCursor.setPos(pos)

    def viewportEvent(self, event):
        if event.type() == event.Wheel:
            # let the viewport handle the event correctly, *then* move the mouse
            QtCore.QTimer.singleShot(0, self.fakeMouseMove)
        return super().viewportEvent(event)

Emulate the enter and leave events

This uses widgetAt() and creates fake enter and leave events sent to the relative widgets, which is not very good for performance: widgetAt might be slow, and repolishing the widgets also takes some time.

class ScrollArea(QtWidgets.QScrollArea):
    def viewportEvent(self, event):
        if event.type() == event.Wheel:
            old = QtWidgets.QApplication.widgetAt(event.globalPos())
            res = super().viewportEvent(event)
            new = QtWidgets.QApplication.widgetAt(event.globalPos())
            if new != old:
                QtWidgets.QApplication.postEvent(old, 
                    QtCore.QEvent(QtCore.QEvent.Leave))
                QtWidgets.QApplication.postEvent(new, 
                    QtCore.QEvent(QtCore.QEvent.Enter))
            return res
        return super().viewportEvent(event)

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...