滚动通过封装 QScrollArea() 时,小部件的 leaveEvent() 被错过

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

我在 QScrollArea() 中有一个 QWidget() 的网格。我出于特定原因覆盖了他们的 leaveEvent()(稍后会详细介绍)。当用鼠标指针四处移动时, leaveEvent() 被触发就好了。但是当我保持鼠标指针静止并使用滚轮滚动 QScrollArea() 时,鼠标指针有效地离开了小部件而不会触发 leaveEvent().

1。一些上下文

我正在开发一个浏览 Arduino 库的应用程序:

您在屏幕截图中看到的是中间有一个 QScrollArea(),右侧有一个滚动条,底部有一个滚动条。此 QScrollArea() 包含一个大型 QFrame(),其中包含其 QGridLayout() 中的所有小部件。每个小部件 - 通常是 QLabel()QPushButton() - 都有浅灰色边框。这样,我就可以看到他们所在的网格。

2。点亮一整排

单击小部件时,我希望整行都亮起。为此,我为每个人覆盖了 mousePressEvent()
注意:实际上,我只是让每个小部件成为自定义 class
的子classes CellWidget(),这样我只需要重写一次这个方法。

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

变量self.__neighbors是每个小部件对其左右两侧所有相邻小部件的引用。这样,每个小部件都可以访问其自己行中的所有小部件。

如您所见,我从每个小部件设置了 属性 "blue"。这对样式表有影响。例如,这是我给 QLabel() 小部件的样式表:

# 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;
    }
""")

unpolish()polish() 方法确保重新加载样式表(我认为?)并且 update() 方法调用重绘。该程序效果很好。我单击任何给定的小部件,整行都亮起蓝色!

3。关闭(蓝色)灯

要关闭它,我覆盖了 leaveEvent() 方法:

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

所以整行保持点亮,直到您用鼠标指针离开小部件。 leaveEvent() 清除所有邻居的 'blue' 属性 并强制重新绘制它们。

4。问题

但是有一个弱点。当我将鼠标保持在同一位置并向下滚动鼠标滚轮时,鼠标指针有效地离开了小部件而不会触发 leaveEvent()。缺少这样的 leaveEvent() 会破坏我的解决方案。在保持鼠标不动并向下滚动的同时,您可以单击几行,它们都保持蓝色。

5。系统信息

我正在为 Windows 和 Linux 开发一个应用程序。该应用程序使用 Python 3.8 编写并使用 PyQt5.

enterEventleaveEvent 显然都依赖于鼠标的移动,并且由于滚动不涉及任何移动,您的方法只有在用户稍微移动鼠标后才会起作用。

我可以考虑两种可能的解决方法,但它们都有点 hacky 而且不是很优雅。在这两种情况下,我都将滚动区域子类化并覆盖 viewportEvent

轻轻移动鼠标

由于这些事件需要鼠标移动,让我们移动鼠标(然后将其移回):

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)

模拟进入和离开事件

这使用 widgetAt() 并创建发送到相关小部件的假进入和离开事件,这对性能来说不是很好:widgetAt 可能很慢,重新抛光小部件也需要一些时间.

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)