There are two main problems with your code:
- variables inside lambdas are evaluated at execution, so
e
always corresponds to the last reference assigned in the loop;
- 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).