带有上下文菜单的 QTreeWidget,无法获取正确的项目

Posted

技术标签:

【中文标题】带有上下文菜单的 QTreeWidget,无法获取正确的项目【英文标题】:QTreeWidget with contextmenu, can't get correct item 【发布时间】:2021-09-30 02:01:19 【问题描述】:

我有以下代码来创建一个QTreeWidget 和一个包含 2 个操作的上下文菜单。

import sys
from PyQt5 import QtCore, QtWidgets

class Dialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super(Dialog, self).__init__()

        self.tw = QtWidgets.QTreeWidget()
        self.tw.setHeaderLabels(['Name', 'Cost ($)'])
        cg = QtWidgets.QTreeWidgetItem(['carrots', '0.99'])
        c1 = QtWidgets.QTreeWidgetItem(['carrot', '0.33'])
        self.tw.addTopLevelItem(cg)
        self.tw.addTopLevelItem(c1)
        self.tw.installEventFilter(self)
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.tw)

    def eventFilter(self, source: QtWidgets.QTreeWidget, event):
        if (event.type() == QtCore.QEvent.ContextMenu and
            source is self.tw):
            menu = QtWidgets.QMenu()
            AAction = QtWidgets.QAction("AAAAA")
            AAction.triggered.connect(lambda :self.a(source.itemAt(event.pos())))
            BAction = QtWidgets.QAction("BBBBBB")
            BAction.triggered.connect(lambda :self.b(source, event))
            menu.addAction(AAction)
            menu.addAction(BAction)
            menu.exec_(event.globalPos())
            return True
        return super(Dialog, self).eventFilter(source, event)

    def a(self, item):
        if item is None:
            return
        print("A: ".format([item.text(i) for i in range(self.tw.columnCount())]))
    def b(self, source, event):
        item = source.itemAt(event.pos())
        if item is None:
            return
        print("B: ".format([item.text(i) for i in range(source.columnCount())]))

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = Dialog()
    window.setGeometry(600, 100, 300, 200)
    window.show()
    sys.exit(app.exec_())

当打开标题中的上下文菜单并单击其中一个操作时,它会打印胡萝卜或胡萝卜,具体取决于我在上下文菜单中单击的位置。但是我把右键事件的位置给了函数。

那么为什么会发生这种情况,我该怎么做才能阻止它?

【问题讨论】:

【参考方案1】:

您的代码有 2 个错误:

主要错误是 itemAt() 方法使用相对于 viewport() 的坐标,而不是相对于视图(QTreeWidget),所以你会得到不正确的项目(标题占据了一个空间,使位置与相对于 QTreeWidget 和 viewport() 有一个偏移量)。

你不应该阻塞事件循环,例如阻塞 eventFilter 你可能会阻塞其他会导致难以调试的错误的事件,在这种情况下最好使用信号。

class Dialog(QtWidgets.QDialog):
    rightClicked = QtCore.pyqtSignal(QtCore.QPoint)

    def __init__(self, parent=None):
        super(Dialog, self).__init__()

        self.tw = QtWidgets.QTreeWidget()
        self.tw.setHeaderLabels(["Name", "Cost ($)"])
        cg = QtWidgets.QTreeWidgetItem(["carrots", "0.99"])
        c1 = QtWidgets.QTreeWidgetItem(["carrot", "0.33"])
        self.tw.addTopLevelItem(cg)
        self.tw.addTopLevelItem(c1)

        self.tw.viewport().installEventFilter(self)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.tw)

        self.rightClicked.connect(self.handle_rightClicked)

    def eventFilter(self, source: QtWidgets.QTreeWidget, event):
        if event.type() == QtCore.QEvent.ContextMenu and source is self.tw.viewport():
            self.rightClicked.emit(event.pos())
            return True

        return super(Dialog, self).eventFilter(source, event)

    def handle_rightClicked(self, pos):
        item = self.tw.itemAt(pos)
        if item is None:
            return
        menu = QtWidgets.QMenu()
        print_action = QtWidgets.QAction("Print")
        print_action.triggered.connect(lambda checked, item=item: self.print_item(item))
        menu.addAction(print_action)
        menu.exec_(self.tw.viewport().mapToGlobal(pos))

    def print_item(self, item):
        if item is None:
            return
        texts = []
        for i in range(item.columnCount()):
            text = item.text(i)
            texts.append(text)

        print("B: ".format(",".join(texts)))

虽然没有必要使用 eventFilter 来处理 contextmenu,因为更简单的解决方案是将 QTreeWidget 的 contextMenuPolicy 设置为 Qt::CustomContextMenu 并使用 customContextMenuRequested 信号:

class Dialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super(Dialog, self).__init__()

        self.tw = QtWidgets.QTreeWidget()
        self.tw.setHeaderLabels(["Name", "Cost ($)"])
        cg = QtWidgets.QTreeWidgetItem(["carrots", "0.99"])
        c1 = QtWidgets.QTreeWidgetItem(["carrot", "0.33"])
        self.tw.addTopLevelItem(cg)
        self.tw.addTopLevelItem(c1)

        self.tw.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.tw.customContextMenuRequested.connect(self.handle_rightClicked)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.tw)

    def handle_rightClicked(self, pos):
        item = self.tw.itemAt(pos)
        if item is None:
            return
        menu = QtWidgets.QMenu()
        print_action = QtWidgets.QAction("Print")
        print_action.triggered.connect(lambda checked, item=item: self.print_item(item))
        menu.addAction(print_action)
        menu.exec_(self.tw.viewport().mapToGlobal(pos))

    def print_item(self, item):
        if item is None:
            return
        texts = []
        for i in range(item.columnCount()):
            text = item.text(i)
            texts.append(text)

        print("B: ".format(",".join(texts)))

【讨论】:

嗯。据我所知,事件过滤器中的 QMenu.exec() 应该不是问题,即使它是阻塞的,因为它运行自己的事件循环并且它只会阻塞 ContextMenu 事件的过滤(这几乎是发生在contextMenuEvent())。事实上,即使在使用信号时,您的第一个示例仍然会阻止过滤后的事件,直到控制从信号发射和插槽执行返回。

以上是关于带有上下文菜单的 QTreeWidget,无法获取正确的项目的主要内容,如果未能解决你的问题,请参考以下文章

qtreewidget设置正则表达式

带有 QGraphicsWidget 的上下文菜单事件

带有多个 qlineedit 小部件的 pyqt 上下文菜单

带有列表子项可聚焦android的列表视图上下文菜单

获取解析行错误的 ObjectId

获取特定任务栏按钮的上下文菜单文本