I need to be able to know what item I've clicked in a dynamically generated menu system. I only want to know what I've clicked on, even if it's simply a string representation.
def populateShotInfoMenus(self):
self.menuFilms = QMenu()
films = self.getList()
for film in films:
menuItem_Film = self.menuFilms.addAction(film)
self.connect(menuItem_Film, SIGNAL('triggered()'), self.onFilmSet)
self.menuFilms.addAction(menuItem_Film)
def onFilmRightClick(self, value):
self.menuFilms.exec_(self.group1_inputFilm.mapToGlobal(value))
def onFilmSet(self, value):
print 'Menu Clicked ', value
Instead of using onFilmSet directly as the receiver of your connection, use a lambda function so you can pass additional parameters:
receiver = lambda film=film: self.onFilmSet(self, film)
self.connect(menuItem_Film, SIGNAL('triggered()'), receiver)
I found this answer here for dealing with this issue in PyQt5, python3. I don't like it, the bVal variable to be precise, as I don't fully understand it but it took a long time to find so I thought I'd share it here. The bVal picks up the boolean value from triggered and allows the taskType to be passed.
self.taskMenu = QtGui.QMenu("Task")
self.tasks = self.getTasks() #FETCHES A LIST OF LIST
self.menuTasks = QtGui.QMenu()
for item in self.tasks:
menuItem_Task = self.taskMenu.addAction(item[1])
receiver = lambda: bVal, taskType=item: self.setTask(bVal, taskType)
menuItem_Task.triggered.connect(receiver)
self.taskMenu.addAction(menuItem_Task)
def setTask(self, ignore_bVal, taskType):
print taskType
Take a look at the Qt's property system. You can dynamically add a property containing a string or anything you desire, which defines the action. Then you can use sender() method in the slot to obtain the QObject calling the slot. Then, query the property you set and do whatever you want accordingly.
But, this is not the best method to do this. Using sender() is not advised because it violates the object oriented principle of modularity.
The best method would be using QSignalMapper class. This class maps signals from different objects to the same slot with different arguments.
I haven't used PyQt therefore i cannot give you exact syntax or an example, but it shouldn't be hard to find with a little research.
I was trying to figure out a similar issue and after looking at the code above this is what worked for me. Thought it would be good to show it all together. =)
self.taskMenu = QtGui.QMenu("Task")
self.tasks = self.getTasks() #FETCHES A LIST OF LIST
self.menuTasks = QtGui.QMenu()
for item in self.tasks:
menuItem_Task = self.taskMenu.addAction(item[1])
receiver = lambda taskType=item[0]: self.setTask(taskType)
self.connect(menuItem_Task, QtCore.SIGNAL('triggered()'), receiver)
self.taskMenu.addAction(menuItem_Task)
def setTask(self,taskType):
print taskType
Just some additional information,
I don't know why, but lambda function doesn't work with new pyqt syntax for connections :
Example of code not working :
self.contextTreeMenuAssignTo = QtGui.QMenu(self)
actionAssign = contextMenu.addMenu( self.contextTreeMenuAssignTo )
actionAssign.setText("Assign to : ")
for user in self.whoCanBeAssignated() :
actionAssignTo = QtGui.QAction( user[0] ,self)
self.contextTreeMenuAssignTo.addAction( actionAssignTo )
actionAssignTo.triggered.connect( lambda userID = user[1] : self.assignAllTo( userID ) )
But if you subsitute the last line with the old style connection syntax :
self.connect(actionAssignTo, QtCore.SIGNAL('triggered()'), lambda userID = user[1] : self.assignAllTo( userID ) )
Everything is fine.
With the new connection syntax, you only get the last element of the loop :(
Related
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?
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).
Codenerix
Has anyone knows how to use correctly ng-readonly in a GenModelForm when coming from a sublist tab (GenList) who calls a GenCreateModal windows?
Structure is a master-detail, sublist tab has pk of master table and calls GenCreateModal with this pk argument of master table.
GenCreateModal receives pk argument in his asociated form (the mentioned GenModelForm) and can use it. The goal is to disable field with ng-disabled if pk argument of master table is filled. This way when create from another list of detail table without arguments, field can be filled with a value selecting it with the dropdown, and when coming from master table it cannot be modified and it will be assigned to master pk value.
I tried to do it that way:
First assign 'client' on GenCreateModal with:
def get_initial(self):
client = self.kwargs.get('pk', None)
if client:
self.kwargs['client'] = client
return self.kwargs
Then read it on the GenModelform with:
def __init__(self, *args, **kwargs):
super(DetailForm, self).__init__(*args, **kwargs)
if kwargs.get('initial', None) and kwargs['initial'].get('client', None):
self.fields['client'].widget.attrs[u'ng-readonly'] = 'true'
But it do not work with dropdown fields. Field can be modified.
Cause is that in templatetags_list.py of codenerix we have:
def inireadonly(attrs, i):
field = ngmodel(i)
return addattr(attrs, 'ng-readonly=readonly_{0}'.format(field))
This code set ng-readonly to "true readonly_client" instead of "true" when it comes with value "true" from GenModelForm, values are concatenated.
I found a workaround with:
self.fields['client'].widget.attrs[u'ng-readonly'] = 'true || '
this way the end value will be "true || readonly_client" that result on "true" as desired when evaluated, but I think it is not the proper way.
On my personal fork of django-codenerix I have changed the function to (functions is on two py files, should change both):
def inireadonly(attrs, i):
field = ngmodel(i)
if attrs.get('ng-readonly', None) is None:
attrs = addattr(attrs, 'ng-readonly=readonly_{0}'.format(field))
return attrs
This way it respects the value when it comes filled form GenModelForm, but I'm not sure about inconveniences and collateral effects. For example when want to concatenate conditions, with that change should read old value, concatenate manually and set new value. I think it should be a better way to do it and 'ng-readonly=readonly_{0}'.format(field) should have a functionality that I haven't discovered yet. Don't want to lose it when I discover it. So I revert the change and look for another solution.
Currently I'm using
self.fields['client'].widget.attrs[u'ng-disabled'] = 'true'
and it goes OK, I'm using this way and I have no problem now, but I'm curious about the way to use ng-readonly if I need it on the future. That's because with ng-readonly we can select text on fields with the mouse for example and can not select it with ng-disabled. In some cases it could be of interest.
Has anyone knows how to use ng-readonly in a correct way?
Has anyone knows the functionality of 'ng-readonly=readonly_{0}'.format(field)?
You can define an extra attribute to your fields in your forms. Add {'extra': ['ng-disabled=true']} in the field of your GenModelForm, inside your __groups__ method. Example:
def __groups__(self):
g = [
(_('Info'), 12,
['client', 6, {'extra': ['ng-disabled=true']}],
)
]
return g
You should use ng-disabled as you are doing. This is the way we do it in Django Codenerix Example # Gibhub (lines 41 and 42) and this is how it has been developed for StaticSelects (line 228) as well.
I have this: (there are class methods, which inherience from QWizard)
def getForms(self):
return [
(
QtWidgets.QLabel("Name"),
QtWidgets.QLineEdit()
),
(
QtWidgets.QLabel("Roll"),
QtWidgets.QDoubleSpinBox()
)
]
def registerFields(self, page, forms):
page.registerField("name*", forms[0][1])
page.registerField("roll", forms[1][1])
And in other place in code
id = self.currentId()
if id == 1:
print self.field("name") # this rightly give me a name from LineEdit
print self.field("roll") # but this give me just None, why?
When I changed
QtWidgets.QDoubleSpinBox()
to
QtWidgets.QSpinBox()
Line:
print self.field("roll")
works fine.
Why do I get None instead double value?
EDIT
I've just noticed that when I'm trying make 'roll' field as a mandatory.
page.registerField("roll*", forms[1][1])
And I fill this 'spinbox' in program, I can not click 'next' (next is disabled). I have spinbox in my form in program. I can set the value there. But this looks like this field is not connected with QWizard(?)?
The QWizardPage class only has internal knowledge of a few widget types. When registering a widget it does not know about, you need to specify the property for reading the value, along with the signal that is emitted when a value is changed, as a string.
For QDoubleSpinBox this would be:
page.registerField("roll", forms[1][1], "value", "valueChanged")
The list of widget types QWizardPage knows about is listed in the c++ documentation here.
You can also register this information globally using a method of QWizard, so that you don't have to specify it each time you call registerField(). To do this, call:
my_wizard.setDefaultProperty("QDoubleSpinBox", "value", "valueChanged")
Note: This is a method of the wizard, not the page.
I am building a program that will store some complex objects, and I am using wxPython for the UI. The objects hierarchy will have a tree representation (TreeCtrl). I am using a dictionary to map objects from the UI to the database, using the TreeItemIds returned by AppendItem as keys and the objects themselves as values (actually I am not using the objects as values, but it simplifies the problem). The following snippet exemplifies what I am trying to do:
import wx
class ComplexObject(object):
def __init__(self, name, otherdata):
self.name = name
self.otherdata = otherdata
class TestFrame(wx.Frame):
def __init__(self, *args, **kwargs):
super(TestFrame, self).__init__(*args, **kwargs)
self.tree = wx.TreeCtrl(self)
self.rootid = self.tree.AddRoot("Root")
self.tree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.onrightclick)
self.objectmap = {}
def addobject(self, obj):
itemid = self.tree.AppendItem(self.rootid, obj.name)
self.objectmap[itemid] = obj
def onrightclick(self, event):
itemid = event.GetItem()
if itemid == self.rootid:
return
obj = self.objectmap[itemid]
print "Do something with ComplexObject {}".format(obj.name)
if __name__ == '__main__':
app = wx.App(False)
testframe = TestFrame(None)
for i in range(3):
obj = ComplexObject('obj{}'.format(i), i)
testframe.addobject(obj)
testframe.Show()
app.MainLoop()
When I right-click an entry in the tree I get a KeyError, because the object I get from the event (itemid = event.GetItem()) is not the same I get when I add an item (itemid = self.tree.AppendItem(self.rootid, obj.name)). Is this the expected behavior? How should I proceed to achieve what I am trying to do? I am starting to experiment with SetPyData and GetPyData, but I hope there is a better way to do that.
Thank you very much.
Platform Information: MS Windows 7, Python 2.7.9, wxPython 2.8.12.1
Yes, it is expected.
You can think of the TreeItemId as a closed box with an implementation dependent handle inside, and that the treectrl is the only one that can open the box to get the handle out. You may see different boxes at different times for the same tree item, but they will all have the same handle inside. But since the handle itself is an implementation detail there is no program access to it.
Using SetPyData to associate data to tree items is the proper way to do things like this. If you want to keep your associated data in a separate dictionary then you could generate unique dictionary keys when adding the items, and then pass they key to SetPyData, and use GetPyData to fetch the key later when you need to fetch the value object from the dictionary.
My question is if we can assign/bind some value to a certain item and hide that value(or if we can do the same thing in another way).
Example: Lets say the columns on ListCtrl are "Name" and "Description":
self.lc = wx.ListCtrl(self, -1, style=wx.LC_REPORT)
self.lc.InsertColumn(0, 'Name')
self.lc.InsertColumn(1, 'Description')
And when I add a item I want them to show the Name parameter and the description:
num_items = self.lc.GetItemCount()
self.lc.InsertStringItem(num_items, "Randomname")
self.lc.SetStringItem(num_items, 1, "Some description here")
Now what I want to do is basically assign something to that item that is not shown so I can access later on the app.
So I would like to add something that is not shown on the app but is on the item value like:
hiddendescription = "Somerandomthing"
Still didn't undestand? Well lets say I add a button to add a item with some other TextCtrls to set the parameters and the TextCtrls parameters are:
"Name"
"Description"
"Hiddendescription"
So then the user fills this textctrls out and clicks the button to create the item, and I basically want only to show the Name and Description and hide the "HiddenDescription" but to do it so I can use it later.
Sorry for explaining more than 1 time on this post but I want to make sure you understand what I pretend to do.
Instead of using the ListCtrl as your data structure, you could keep a separate list/dict of objects that contain all the information you want and refresh the ListCtrl from your other data structure.
For example:
class MyObject(object):
def __init__(self, name, description, hidden_description):
self.name = name
self.description = description
self.hidden_description = hidden_description
Then in your application:
def __init__(self):
self.my_items = {}
self.lc = wx.ListCtrl(self, -1, style=wx.LC_REPORT)
self.lc.InsertColumn(0, 'Name')
self.lc.InsertColumn(1, 'Description')
def addItemToMyListCtrl(self, name, description, hidden):
new_item = MyObject(name, description, hidden)
self.my_items[name] = new_item
self.lc.Append((new_item.name, new_item.description))
Then when you want to use your additional data you can just look up the correct item in the dictionary and your data will be there.
the wxListCtrl lets you associate arbitrary data with an item, that will not be displayed - read the docs for the following methods:
SetItemData
GetItemData
FindItemData
The wxListItem class also has GetData and SetData methods.
You could always set the width of the hidden column to zero, that might accomplish what you want. I just tried it in a C++ (non-wx) program and it worked fine.
wx.ListCtrl doesn't let you associate a python object with an item like wx.TreeCtrl does with its extremely useful SetPyData() method (and corresponding GetPyData()).
I haven't tried it myself, but there is code here that shows how to create a class to mix in python data with a list. Although I'll admit, it's not clear to me how you're meant to use it.
It also might be possible to directly inherit from wx.ListCtrl, and add the appropriate methods, but I haven't seen any attempts at that anywhere, so it may be harder than I'm thinking.
Alternately you can just use SetItemData() to store an int in each item, and use that int to index a dict (or list, if the items are ordered reliably and consistently) that contains the associated objects. tgray has already shown how to do this, and it's listed at the page I link above as well, so I won't go over it again.