在 Qt6 中,许多小部件(QComboBox、QSpinBox)将窃取应由其父小部件(如 QScrollArea)处理的鼠标滚轮事件,即使它们没有焦点,甚至当
focusPolicy
已设置为StrongFocus
。
我可以对这些小部件类进行子类化,并重新实现
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)
但我的问题是,这个事件过滤器似乎没有按照我的预期过滤事件。我希望它仅过滤掉 watched
对象的事件,同时还允许将事件传播到父窗口小部件来处理,但这并没有发生。 是否可以使用
wheelEvent
实现与上面定义的
eventFilter
处理程序相同的效果?
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()
在 Qt6 中,似乎有必要在事件过滤器中返回 true 之前显式忽略滚轮事件,而在 Qt5 中则不需要。但请注意,我仅在 arch-linux 上使用 Qt-5.15.11 和 Qt-6.6.0 / Qt-6.2.2 进行了测试,因此我不能保证这适用于所有可能的 Qt 版本和/或平台。 [这里还值得指出的是,QComboBox(和其他类似小部件)当前的焦点策略行为有些问题,因为即使将焦点策略设置为
NoFocus
,小部件still 也接受wheel-滚动事件 - 请参阅QTBUG-19730。实际上,重新实现轮子事件处理是一种有点粗糙的解决方法,没有必要]。 下面是基于问题中的代码的简化工作演示。我假设所需的行为是在
unfocused 组合框上滚轮滚动仍应滚动父滚动区域,但 focused 组合框应正常滚动其项目:
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_()