PyQt5中如何区分QListWidget与内部QWidget的右键点击事件
问题:PyQt5右键点击列表项时同时弹出两项菜单的解决方法
我使用Python+PyQt5开发界面程序,在QListWidget中动态插入多个QWidget作为列表项,通过子类化QObject实现eventFilter区分左右点击,交由控制器类处理逻辑。目前右键点击列表项时,列表项和QListWidget的右键菜单会同时弹出,需要实现:点击列表项仅显示其专属菜单,点击QListWidget空白区域才显示窗口菜单。尝试过判断事件source,但未找到有效解决办法。
可复现最小示例代码
custom_classes.py
from PyQt5.QtCore import Qt, QEvent, QObject class MyFilter(QObject): def __init__(self, parent, ctrl): super().__init__(parent) self._parent = parent self.ctrl = ctrl self.parent.installEventFilter(self) @property def parent(self): return self._parent def eventFilter(self, source, event): if event.type() == QEvent.MouseButtonPress: if event.button() == Qt.LeftButton: self.ctrl.handle_left_click() elif event.button() == Qt.RightButton: self.ctrl.handle_right_click(event) return super().eventFilter(source, event)
gui.py
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QLabel from PyQt5.QtWidgets import QHBoxLayout from PyQt5.QtWidgets import QVBoxLayout from PyQt5.QtWidgets import QListWidget from PyQt5.QtWidgets import QScrollArea from PyQt5.QtCore import Qt class MainFrame(QWidget): def __init__(self): super().__init__() self.main_layout = QHBoxLayout() self.setLayout(self.main_layout) class ListItemFrame(QWidget): def __init__(self): super().__init__() self.main = QHBoxLayout() self.main.setContentsMargins(0,0,0,0) self.name_layout = QHBoxLayout() self.name_layout.setContentsMargins(0,0,0,0) self.main.addLayout(self.name_layout) self.name = QLabel("") self.name.setMaximumHeight(20) self.name_layout.addWidget(self.name) self.setLayout(self.main) class ListFrame(QListWidget): def __init__(self): super().__init__() self.main = QVBoxLayout() self.scroll_widget = QScrollArea() self.scroll_widget.setWidgetResizable(True) self.scroll_layout = QVBoxLayout() self.scroll_layout.setAlignment(Qt.AlignTop) self.scroll_layout_widget = QWidget() self.scroll_layout_widget.setLayout(self.scroll_layout) self.scroll_widget.setWidget(self.scroll_layout_widget) self.main.addWidget(self.scroll_widget) self.setLayout(self.main)
ctrl.py
from PyQt5.QtWidgets import QMenu from gui import ListFrame, ListItemFrame from custom_classes import MyFilter class Controller: def __init__(self, ui, app): self.ui = ui self.app = app self.list_ = ListControl(self) class ListControl: def __init__(self, ctrl): self.ctrl = ctrl self.ui = ListFrame() self.the_list = self.get_list() #list of stuff self.item_list = [] #list of list items self.ctrl.ui.main_page.main_layout.addWidget(self.ui) self.index = self.ctrl.ui.main_page.main_layout.count() - 1 self.filter = MyFilter(self.ui, self) self.show_list() def handle_left_click(self): pass #other irrelevant function def handle_right_click(self, event): self.show_options(event) def show_options(self, event): menu = QMenu() one_action = menu.addAction("Something!") quit_action = menu.addAction("Quit") action = menu.exec_(self.ui.mapToGlobal(event.pos())) if action == quit_action: self.ctrl.ui.close() elif action == one_action: self.something() def something(self): print("Something!") def show_list(self): for info in self.the_list: item = ListItem(self, info) self.item_list.append(item) def get_list(self): return [x for x in "qwertzuiopasdfghjklyxcvbnm"] class ListItem: def __init__(self, main, info): self.main = main self.info = info*10 self.ui = ListItemFrame() self.filter = MyFilter(self.ui, self) self.set_ui() self.add_to_ui() self.main.ui.scroll_layout.addWidget(self.ui) def handle_left_click(self): pass #other irrelevant function def handle_right_click(self, event): self.show_options(event) def show_options(self, event): menu = QMenu() item_action = menu.addAction("Hello!") quit_action = menu.addAction("Quit") action = menu.exec_(self.ui.mapToGlobal(event.pos())) if action == quit_action: self.main.ctrl.ui.close() elif action == item_action: self.hello() def hello(self): print(f"Hello! I am {self.info}") def set_ui(self): self.ui.name.setText(self.info) def add_to_ui(self): self.main.ui.scroll_layout.insertWidget( self.main.ui.scroll_layout.count() - 1, self.ui )
main.py
import sys from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QStackedLayout from PyQt5.QtWidgets import QWidget from gui import MainFrame from ctrl import Controller class Window(QWidget): def __init__(self): super().__init__() self.setWindowTitle("minimal example") self.stacked = QStackedLayout() self.main_page = MainFrame() self.stacked.addWidget(self.main_page) self.setLayout(self.stacked) if __name__ == "__main__": app = QApplication(sys.argv) app.setStyle("Fusion") window = Window() window.show() c = Controller(window, app) sys.exit(app.exec())
解决方案
问题根源是事件冒泡:右键点击列表项时,事件先被列表项的eventFilter处理,随后继续向上传递到QListWidget的eventFilter,导致两个菜单同时弹出。解决核心是在列表项处理右键事件后,阻止事件继续传播。
具体修改步骤
- 修改
custom_classes.py中的eventFilter方法,在列表项处理右键点击后返回True,终止事件传递:
def eventFilter(self, source, event): if event.type() == QEvent.MouseButtonPress: if event.button() == Qt.LeftButton: self.ctrl.handle_left_click() elif event.button() == Qt.RightButton: self.ctrl.handle_right_click(event) # 判断当前过滤器绑定的是列表项,阻止事件向上传播 from gui import ListItemFrame if isinstance(self.parent, ListItemFrame): return True return super().eventFilter(source, event)
或者采用更简洁的标记方式(避免导入):
- 在
ListItem类的__init__中给过滤器添加标记:
self.filter = MyFilter(self.ui, self) self.filter.is_item_filter = True # 添加列表项过滤器标记
- 然后在
MyFilter的eventFilter中判断:
elif event.button() == Qt.RightButton: self.ctrl.handle_right_click(event) # 检查是否为列表项过滤器,是则阻止事件传播 if hasattr(self, 'is_item_filter') and self.is_item_filter: return True
- 额外建议:你的
ListFrame继承了QListWidget但内部使用自定义QScrollArea和布局,违背了QListWidget的设计逻辑。后续如果出现其他异常,建议改用QListWidget标准用法:创建QListWidgetItem,通过setItemWidget将ListItemFrame绑定到项上,这样事件处理会更符合Qt原生机制。
内容的提问来源于stack exchange,提问作者Stefan Šućur
相关产品推荐
相关产品推荐

