如何使用事件过滤器处理 QComboBox wheel 事件?

How to handle QComboBox wheel-events with an event-filter?

提问人:Brendan Abel 提问时间:10/24/2023 最后编辑:ekhumoroBrendan Abel 更新时间:10/24/2023 访问量:64

问:

在 Qt6 中,许多小部件(QComboBox、QSpinBox)会窃取应该由其父小部件(如 QScrollArea)处理的鼠标滚轮事件,即使它们没有焦点,即使 已设置为 .focusPolicyStrongFocus

我可以对这些小部件类进行子类化,并重新实现处理程序以忽略这些事件以完成我想要的功能,但这样做感觉不优雅,因为我需要对表现出这种行为的每个小部件类进行子类化。wheelEvent

class MyComboBox(QtWidgets.QComboBox):

    def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
        if not self.hasFocus():
            event.ignore()
        else:
            super().wheelEvent(event)

Qt提供了另一种忽略事件的方法,这感觉更具可扩展性和优雅性,因为我可以创建一个事件过滤器并将其应用于任意数量的不同小部件。installEventFilter

class WheelEventFilter(QtCore.QObject):
    """Ignores wheel events when a widget does not already have focus."""

    def eventFilter(self, watched: QtCore.QObject, event: QtCore.QEvent) -> bool:
        if (
            isinstance(watched, QtWidgets.QWidget)
            and not watched.hasFocus()
            and event.type() == QtCore.QEvent.Type.Wheel
        ):
            # This filters the event, but it also stops the event 
            # from propagating up to parent widget.
            return True
            # This doesn't actually ignore the event for the given widget.
            event.ignore()
            return False

        else:
            return super().eventFilter(watched, event)

但是,我的问题是,此事件过滤器似乎没有像我预期的那样过滤事件。我希望它只过滤掉被监视对象的事件,同时还允许将事件传播到父小部件进行处理,但这并没有发生。

是否可以使用上面定义的处理程序实现与上述处理程序相同的效果?wheelEventeventFilter


下面是一个显示此行为的独立可重现示例。如果您尝试将鼠标滚动到其中一个组合框上滚动滚动区域,则该组合框将窃取焦点和滚轮事件。

import sys
from PySide6 import QtWidgets, QtCore


class MyWidget(QtWidgets.QWidget):

    def __init__(self) -> None:
        super().__init__()

        # # layout
        self._layout = QtWidgets.QVBoxLayout()
        self.setLayout(self._layout)

        # layout for widget
        self._mainwidget = QtWidgets.QWidget()
        self._mainlayout = QtWidgets.QVBoxLayout()
        self._mainwidget.setLayout(self._mainlayout)

        # widgets for widget
        self._widgets = {}
        num_widgets = 20
        for i in range(num_widgets):
            combo = QtWidgets.QComboBox()
            combo.addItems([str(x) for x in range(1, 11)])
            combo.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
            self._mainlayout.addWidget(combo)
            self._widgets[i] = combo

        # scroll area
        self._scrollarea = QtWidgets.QScrollArea(self)
        self._scrollarea.setWidgetResizable(True)
        self._scrollarea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
        self._scrollarea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
        self._layout.addWidget(self._scrollarea)

        # widget for scroll area
        self._scrollarea.setWidget(self._mainwidget)


def main() -> None:
    app = QtWidgets.QApplication(sys.argv)
    widget = MyWidget()
    widget.show()
    app.exec()


if __name__ == "__main__":
    main()
python 事件 mousewheel pyside6 qcombobox

评论

1赞 ekhumoro 10/24/2023
将焦点策略设置为低于 WheelFocus 的值应该有效,但如果没有看到适当的最小可重现示例,就很难确定。
0赞 Brendan Abel 10/24/2023
@ekhumoro我已将其设置为强焦点,这样可以防止小部件从滚轮事件中获得焦点,但它们仍然会吃掉滚轮事件,因此,如果您在 QScrollArea 中滚动并且鼠标悬停在这些小部件之一上,滚动区域将停止滚动。
0赞 ekhumoro 10/24/2023
您问是否可以使用事件过滤器实现相同的效果,并且设置适当的焦点策略应该允许这样做。
0赞 musicamante 10/24/2023
如果返回 ,事件仍将发送到“已监视”小部件。如果返回不会导致区域滚动,则说明您做错了其他事情,因此请按照上述要求提供完整的 MRE。FalseTrue
0赞 Brendan Abel 10/24/2023
@ekhumoro 请参阅发布的示例。

答:

1赞 ekhumoro 10/24/2023 #1

我现在找到了一个适用于 Qt5 和 Qt6 的解决方案。

似乎在 Qt6 中,有必要在事件过滤器中返回 true 之前显式忽略 wheel-event,而在 Qt5 中则不是。但请注意,我只在 arch-linux 上使用 Qt-5.15.11 和 Qt-6.6.0 / Qt-6.2.2 对此进行了测试,因此我不能保证这适用于所有可能的 Qt 版本和/或平台。[这里还值得指出的是,QComboBox(和其他类似的小部件)当前的焦点策略行为有些值得怀疑,因为即使焦点策略设置为 ,小部件仍然接受滚轮滚动事件 - 参见 QTBUG-19730。实际上,重新实现轮子事件处理是一种有点粗略的解决方法,不应该是必要的]。NoFocus

下面是一个基于问题中代码的简化工作演示。我假设期望的行为是,在聚焦的组合框上滚动的滚轮仍应滚动父滚动区域,但聚焦的组合框应正常滚动浏览其项目:

import sys
from PySide6 import QtWidgets, QtCore
# from PySide2 import QtWidgets, QtCore
# from PyQt6 import QtWidgets, QtCore
# from PyQt5 import QtWidgets, QtCore

class MyWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self._layout = QtWidgets.QVBoxLayout()
        self.setLayout(self._layout)
        self._mainwidget = QtWidgets.QWidget()
        self._mainlayout = QtWidgets.QVBoxLayout()
        self._mainwidget.setLayout(self._mainlayout)

        for i in range(20):
            combo = QtWidgets.QComboBox()
            combo.addItems(list('ABCDEF'))
            combo.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
            combo.installEventFilter(self)
            self._mainlayout.addWidget(combo)

        self._scrollarea = QtWidgets.QScrollArea(self)
        self._scrollarea.setWidgetResizable(True)
        self._layout.addWidget(self._scrollarea)
        self._scrollarea.setWidget(self._mainwidget)

    def eventFilter(self, watched, event):
        if (event.type() == QtCore.QEvent.Type.Wheel and
            not watched.hasFocus()):
            event.ignore()
            return True
        else:
            return super().eventFilter(watched, event)

if __name__ == "__main__":

    app = QtWidgets.QApplication(sys.argv)
    widget = MyWidget()
    widget.show()

    print(f'{QtCore.__package__} (Qt-{QtCore.qVersion()})')

    if hasattr(app, 'exec'):
        app.exec()
    else:
        app.exec_()