python pyqt6 contextmenuevent奇怪的行为我不理解

发布于 2025-02-12 05:32:17 字数 1914 浏览 2 评论 0 原文

我有以下代码给我麻烦:

class TableView(qt.QTableView):
    def __init__(self, param):
        super().__init__()
        self.model = param.model
        self.view = self
        self.mains = [QAction('Remove row'), QAction('Split expense')]
        self.types = [QAction('Bills'), QAction('Vapors')]

    def contextMenuEvent(self, event):
        row = self.view.rowAt(event.y())
        col = self.view.columnAt(event.x())
        main_menu = qt.QMenu()
        type_menu = qt.QMenu('Update')
        main_menu.addActions(self.mains)
        type_menu.addActions(self.types)
        if col == 1:
            main_menu.addMenu(type_menu)
        #mains[0].triggered.connect(lambda: self.remove_row(row))
        for e in self.types:
            print(e)
            e.triggered.connect(lambda: self.update_type(row, e))
        main_menu.exec(QCursor.pos())

    def remove_row(self, row):
        self.model.removeRow(row)

    def update_type(self, row, action):
        print(action)

它应该根据所选上下文菜单更新正确的 qaction 。循环返回...

<PyQt6.QtGui.QAction object at 0x7f77fd619480>
<PyQt6.QtGui.QAction object at 0x7f77fd619510>

每次。 &lt; pyqt6.qtgui.qaction对象在0x7f77fd619480&gt; 应与“账单”和&lt; pyqt6.qtgui.qations息息”。当我运行它时,无论我选择哪种菜单选项,它都会返回&lt; pyqt6.qtgui.qaction对象,at 0x7f77f77fd619510&gt; 。更糟糕的是,右键单击应打印一次循环,然后进行菜单选择(始终是&lt; pyqt6.qtgui.qation.qaction.qations对象,位于0x7f77f77fd619510&gt; ),但是之后会发生什么。 表中的第一行被右键单击,是&lt; pyqt6.qtgui.qaction对象,at 0x777f77fd619510&gt; 被打印了两次。什么给?

编辑

好吧,我在其他帖子的帮助下设法解决了部分问题。

for e in self.types:
            e.triggered.connect(lambda d, e=e: self.update_type(row, e))

但是我仍然有问题。该信号每次打开GUI时,我按下上下文菜单项的每次都会发射次数。因此,我启动GUI,右键单击并选择一些东西,然后发射一次。然后,我再次右键单击,它可以预见两次,然后三次,依此类推,依此类推,我右键单击。 为什么?

I have the following code giving me trouble:

class TableView(qt.QTableView):
    def __init__(self, param):
        super().__init__()
        self.model = param.model
        self.view = self
        self.mains = [QAction('Remove row'), QAction('Split expense')]
        self.types = [QAction('Bills'), QAction('Vapors')]

    def contextMenuEvent(self, event):
        row = self.view.rowAt(event.y())
        col = self.view.columnAt(event.x())
        main_menu = qt.QMenu()
        type_menu = qt.QMenu('Update')
        main_menu.addActions(self.mains)
        type_menu.addActions(self.types)
        if col == 1:
            main_menu.addMenu(type_menu)
        #mains[0].triggered.connect(lambda: self.remove_row(row))
        for e in self.types:
            print(e)
            e.triggered.connect(lambda: self.update_type(row, e))
        main_menu.exec(QCursor.pos())

    def remove_row(self, row):
        self.model.removeRow(row)

    def update_type(self, row, action):
        print(action)

It should update print the correct QAction based on the chosen context menu. The loop returns...

<PyQt6.QtGui.QAction object at 0x7f77fd619480>
<PyQt6.QtGui.QAction object at 0x7f77fd619510>

...every time. <PyQt6.QtGui.QAction object at 0x7f77fd619480> should be tied to "Bills" and <PyQt6.QtGui.QAction object at 0x7f77fd619510> should be tied to "Vapors". When I run it, no matter what menu option I choose, it returns <PyQt6.QtGui.QAction object at 0x7f77fd619510>. To make matters worse, right-clicking should print the loop once, followed by the menu selection (which is always <PyQt6.QtGui.QAction object at 0x7f77fd619510>), but what happens after the first row in the table gets right-clicked, is <PyQt6.QtGui.QAction object at 0x7f77fd619510> is printed twice. What gives?

EDIT

Okay, I managed to fix part of the problem with the help of other posts.

for e in self.types:
            e.triggered.connect(lambda d, e=e: self.update_type(row, e))

But I still have a problem. The signal fires each the number of times I press a context menu item per time the GUI is open. So, I launch the GUI, right-click and select some thing and it fires once. Then I right-click again and it fores twice, then three times and so on for the number of times I right-clicked.
Why?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

懵少女 2025-02-19 05:32:18

您的代码有两个主要问题:

  1. 在执行时评估了lambdas内部的变量,因此 e 始终对应于循环中分配的最后一个 last 参考;
  2. 发出信号时,将其连接多次调用功能:每次创建菜单时,您都会再次连接信号;

根据情况,有很多方法可以实现所需的目标。以下是一些可能的选项:

比较触发 exec()返回的操作

qmenu.exec()始终返回已触发的操作,知道您可以将其进行比较并最终决定要做什么:

class TableView(QTableView):
    def __init__(self, param):
        super().__init__()
        self.setModel(param.model)
        self.mains = [QAction('Remove row'), QAction('Split expense')]
        self.types = [QAction('Bills'), QAction('Vapors')]

    def contextMenuEvent(self, event):
        index = self.indexAt(event.pos())
        main_menu = QMenu()
        for action in self.mains:
            main_menu.addAction(action)
            action.setEnabled(index.isValid())

        if index.column() == 1:
            type_menu = main_menu.addMenu('Update')
            type_menu.addActions(self.types)

        action = main_menu.exec(event.globalPos())
        if action in self.mains:
            if action == self.mains[0]:
                self.remove_row(index.row())
        elif action in self.types:
            self.update_type(index.row(), action)

    def remove_row(self, row):
        self.model().removeRow(row)

    def update_type(self, row, action):
        print(action)

使用 action.data()作为参数

qactions支持设置任意数据,因此我们可以将该数据设置为行。如果我们使用 action.tigred 信号,我们可以通过 self.sender() (返回发出信号的对象)。否则,我们可以使用 菜单。 /a>以将其触发为参数的动作调用目标函数。

class TableView(QTableView):
    def __init__(self, param):
        super().__init__()
        self.setModel(param.model)
        self.mains = [QAction('Remove row'), QAction('Split expense')]
        self.mains[0].triggered.connect(self.remove_row)
        self.types = [QAction('Bills'), QAction('Vapors')]

    def contextMenuEvent(self, event):
        index = self.indexAt(event.pos())
        main_menu = QMenu()
        for action in self.mains:
            main_menu.addAction(action)
            action.setEnabled(index.isValid())
            action.setData(index.row())
        if index.column() == 1:
            type_menu = main_menu.addMenu('Update')
            type_menu.triggered.connect(self.update_type)
            for action in self.types:
                type_menu.addAction(action)
                action.setData(index.row())

        main_menu.exec(event.globalPos())

    def remove_row(self):
        sender = self.sender()
        if isinstance(sender, QAction):
            row = sender.data()
            if row is not None:
                self.model().removeRow(row)

    def update_type(self, action):
        print(action, action.data())

所以,没有兰伯达?

lambdas肯定可以使用,但是考虑到上述内容,并且您的要求是使用 dynamic 参数,这可能很棘手。

您可以将其用于A 完全动态菜单(包括创建动作),否则您需要始终尝试 disconnect()信号,这可能很棘手:

  • 使用lambda作为目标插槽意味着您没有任何必须断开连接功能的引用;
  • 如果信号先前已连接到其他函数,则完全断开信号(使用通用 signal.disconnect())的连接可能不是一个好选择;

完全动态的菜单

上述解决方案是基于以下事实:在上下文菜单事件时已经存在操作。

这通常不是要求。实际上,QT 始终中的许多小部件及其动作创建一个全新的菜单。都是基于文本的小部件(qlineedit,qtextedit,甚至具有适当的文本交互标志的Qlabels)的情况:菜单是始终临时。

考虑到这一点,我们可以根据上述解释采取一种方法,但无需考虑“更改”或“恢复”以前的状态,数据或连接:菜单将在关闭后立即被破坏,以及其任何一项动作(由于它们是作为菜单的孩子创建的),因此Python和QT将负责释放不再需要的资源。

虽然这种连续的创建/破坏物体似乎似乎不是最佳的,记忆/性能明智,但实际上在概念上更好,相当有效:菜单通常不需要极端性能,而创建/破坏它们实际上比管理持续的行为更简单一组菜单/操作取决于上下文。

class TableView(QTableView):
    def __init__(self, param):
        super().__init__()
        self.setModel(param.model)

    def contextMenuEvent(self, event):
        index = self.indexAt(event.pos())
        isValid = index.isValid()
        main_menu = QMenu()

        removeAction = main_menu.addAction('Remove row')
        if isValid:
            removeAction.triggered.connect(lambda:
                self.remove_row(index.row()))
        else:
            removeAction.setEnabled(False)

        splitAction = main_menu.addAction('Split expanse')
        if isValid:
            splitAction.triggered.connect(lambda:
                self.split_expanse(index.row()))
        else:
            splitAction.setEnabled(False)

        type_menu = main_menu.addMenu('Update')
        if index.column() != 1:
            type_menu.setEnabled(False)
        else:
            billsAction = type_menu.addAction('Bills')
            billsAction.triggered.connect(lambda:
                self.bills(index.row()))
            vaporsAction = type_menu.addAction('Vapors')
            vaporsAction.triggered.connect(lambda:
                self.doVapors(index.row()))

        main_menu.exec(event.globalPos())

进一步的选项

在某些情况下需要保留持久的动作或菜单,例如,一个菜单上有很多需要创建时间的项目。

如已经解释的那样,信号可以同时连接到多个功能(甚至相同的功能不止一次)。

Lambdas的问题是我们通常“排队”使用它们。这样做,我们始终失去对它们连接的引用:

    self.someAction.triggered.connect(lambda: self.whatever(xyz))

虽然我们只能使用通用 signal.disconnect(),它可以断开信号与任何函数或连接到的插槽的连接它可能不是一个可行的选择:也许信号也连接到了始终的其他函数,无论上下文不管动作)。这意味着我们不能明确地与上述使用的lambda断开连接。

幸运的是,众所周知,在python中,包括lambdas在内:

    doWhatever = lambda: self.whatever(xyz)
    self.someAction.triggered.connect(doWhatever)
    # ...
    menu.exec(pos)
    self.someAction.triggered.disconnect(doWhatever)

这样,我们确保我们仅在菜单事件的上下文中连接到动作,然后将其断开连接,无论已触发的实际操作。

请注意,上面的实际上与使用本地函数相同(从概念上讲,这是Lambdas的内容):

    def doWhatever():
        self.whatever(xyz)

    self.someAction.triggered.connect(doWhatever)
    # ...
    menu.exec(pos)
    self.someAction.triggered.disconnect(doWhatever)

上述方法的好处是,与简单的lambda相比,本地函数可以更容易地扩展。

结论

QACTION是一个奇怪的课程。它不是小部件,但可以用于此目的,它不需要父母,并且可以在许多对象(菜单,工具栏等)之间共享。与小部件相反,即使在同一UI中,也可以在许多地方出现一个动作:工具栏,梅纳巴尔,上下文菜单,qtoolbutton。

尽管如此,设置新操作的父母不会自动将操作添加到该操作的父列表中,因此 someobject.actions()不列出该操作,除非 addaction()< /代码>已被明确调用。

QT6从Qtwidgets到QTGUI的“迁移”使这些方面变得更加清晰,但仍然可以造成混乱。

由于其“抽象”性质(并考虑到上述方面),您可以以多种方式触发动作,如果开发人员清楚整个QACTION概念,则触发的动作可以以意外的方式调用连接的插槽。

了解所有这些非常重要,因为其“触发”的实施可能会发生巨大变化,并且对这些方面的认识是必须正确地实施其用法的意识。

例如,使用一个列表,该列表可能不是适当的选择,您可以考虑 QActionGroup 而不是(无论这些操作是否可检查或组是独家)。

There are two main problems with your code:

  1. variables inside lambdas are evaluated at execution, so e always corresponds to the last reference assigned in the loop;
  2. when a signal is emitted, functions are called as many times they have been connected: each time you create the menu, you're connecting the signal once again;

Depending on the situations, there are many ways to achieve what you need. Here are some possible options:

Compare the triggered action returned by exec()

QMenu.exec() always returns the action that has been triggered, knowing that you can just compare it and eventually decide what to do:

class TableView(QTableView):
    def __init__(self, param):
        super().__init__()
        self.setModel(param.model)
        self.mains = [QAction('Remove row'), QAction('Split expense')]
        self.types = [QAction('Bills'), QAction('Vapors')]

    def contextMenuEvent(self, event):
        index = self.indexAt(event.pos())
        main_menu = QMenu()
        for action in self.mains:
            main_menu.addAction(action)
            action.setEnabled(index.isValid())

        if index.column() == 1:
            type_menu = main_menu.addMenu('Update')
            type_menu.addActions(self.types)

        action = main_menu.exec(event.globalPos())
        if action in self.mains:
            if action == self.mains[0]:
                self.remove_row(index.row())
        elif action in self.types:
            self.update_type(index.row(), action)

    def remove_row(self, row):
        self.model().removeRow(row)

    def update_type(self, row, action):
        print(action)

Use the action.data() as argument

QActions supports setting arbitrary data, so we can set that data to the row. If we are using the action.triggered signal, we can retrieve the action through self.sender() (which returns the object that emitted the signal). Otherwise, we can use menu.triggered() to call the target function with the action that has triggered it as argument.

class TableView(QTableView):
    def __init__(self, param):
        super().__init__()
        self.setModel(param.model)
        self.mains = [QAction('Remove row'), QAction('Split expense')]
        self.mains[0].triggered.connect(self.remove_row)
        self.types = [QAction('Bills'), QAction('Vapors')]

    def contextMenuEvent(self, event):
        index = self.indexAt(event.pos())
        main_menu = QMenu()
        for action in self.mains:
            main_menu.addAction(action)
            action.setEnabled(index.isValid())
            action.setData(index.row())
        if index.column() == 1:
            type_menu = main_menu.addMenu('Update')
            type_menu.triggered.connect(self.update_type)
            for action in self.types:
                type_menu.addAction(action)
                action.setData(index.row())

        main_menu.exec(event.globalPos())

    def remove_row(self):
        sender = self.sender()
        if isinstance(sender, QAction):
            row = sender.data()
            if row is not None:
                self.model().removeRow(row)

    def update_type(self, action):
        print(action, action.data())

So, no lambda?

Lambdas can certainly be used, but considering what explained above, and that your requirement is to use dynamic arguments, that can be tricky.

You can use it for a fully dynamical menu (including creation of actions), otherwise you'd need to always try to disconnect() the signal, and that might be tricky:

  • using a lambda as target slot means that you don't have any previous reference to the function that has to be disconnected;
  • completely disconnecting the signal (using the generic signal.disconnect()) might not be a good choice, if the signal was previously connected to other functions;

A fully dynamical menu

The above solutions are based on the fact that the actions already existed at the time of the context menu event.

This is usually not a requirement. In fact, many widgets in Qt always create a brand new menu along with its actions. This is the case of all text-based widgets (QLineEdit, QTextEdit and even QLabels with the proper text interaction flags): the menu is always temporary.

With this in mind, we can take an approach based on what explained above, but without thinking about "changing" or "restoring" previous states, data or connections: the menu will be destroyed as soon as it's closed, along with any of its actions (since they've been created as children of the menu), so Python and Qt will take care of releasing resources that are not needed anymore.

While this continuous creation/destroy of objects might not seem optimal, memory/performance wise, it's actually conceptually better and quite effective: menus don't usually need extreme performance, and creating/destroying them is actually simpler than managing the behavior of a persistent set of menu/actions depending on the context.

class TableView(QTableView):
    def __init__(self, param):
        super().__init__()
        self.setModel(param.model)

    def contextMenuEvent(self, event):
        index = self.indexAt(event.pos())
        isValid = index.isValid()
        main_menu = QMenu()

        removeAction = main_menu.addAction('Remove row')
        if isValid:
            removeAction.triggered.connect(lambda:
                self.remove_row(index.row()))
        else:
            removeAction.setEnabled(False)

        splitAction = main_menu.addAction('Split expanse')
        if isValid:
            splitAction.triggered.connect(lambda:
                self.split_expanse(index.row()))
        else:
            splitAction.setEnabled(False)

        type_menu = main_menu.addMenu('Update')
        if index.column() != 1:
            type_menu.setEnabled(False)
        else:
            billsAction = type_menu.addAction('Bills')
            billsAction.triggered.connect(lambda:
                self.bills(index.row()))
            vaporsAction = type_menu.addAction('Vapors')
            vaporsAction.triggered.connect(lambda:
                self.doVapors(index.row()))

        main_menu.exec(event.globalPos())

Further options

There are occasions for which keeping persistent actions or menus is required, for instance a menu that has lots of items that require some amount of time to be created.

As already explained, signals can be connected to multiple functions at the same time (and even the same function more than once).

The issue with lambdas is that we usually use them "in line". Doing this, we always lose the reference to their connection:

    self.someAction.triggered.connect(lambda: self.whatever(xyz))

While we could just use the generic signal.disconnect(), which disconnects the signal from any function or slot connected to it, that might not be a viable option: maybe the signal is also connected to some other function that is always required to be triggered, no matter of the context (such as a visual hint about the activation of actions). This means that we cannot specifically disconnect from a lambda used as above.

Luckily, as we know, in Python "everything is an object", including lambdas:

    doWhatever = lambda: self.whatever(xyz)
    self.someAction.triggered.connect(doWhatever)
    # ...
    menu.exec(pos)
    self.someAction.triggered.disconnect(doWhatever)

In this way, we ensure that we only connect to the action in the context of the menu event, and disconnect it afterwards, no matter of the actual action that has been triggered.

Note that the above is actually the same as using a local function (which is what lambdas are, conceptually speaking):

    def doWhatever():
        self.whatever(xyz)

    self.someAction.triggered.connect(doWhatever)
    # ...
    menu.exec(pos)
    self.someAction.triggered.disconnect(doWhatever)

The benefit of the above approach is that a local function can be extended more easily than a simple lambda.

Conclusions

QAction is quite a strange class. It's not a widget, but it can be used for that purpose, it doesn't need a parent, and can be shared between many objects (menus, toolbars, etc.). As opposite to widgets, an action can appear in many places at the same time even in the same UI: a tool bar, a menubar, context menu, a QToolButton.

Nonetheless, setting the parent of a new action doesn't automatically add the action to that parent list of actions, so someObject.actions() won't list that action unless addAction() has been explicitly called.

The "migration" of Qt6 from QtWidgets to QtGui made these aspect partially more clear, but it can still create confusion.

Due to their "abstract" nature (and considering the above aspects), you can trigger an action in many ways, and a triggered action can call connected slots in unexpected ways if the whole QAction concept is clear to the developer.

It's extremely important to understand all that, as the implementation of their "triggering" might change dramatically, and awareness of those aspects is mandatory to properly implement their usage.

For instance, using a list that groups actions might not be the proper choice, and you may consider QActionGroup instead (no matter if the actions are checkable or the group is exclusive).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文