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).
Related
I have a QTreeView widget on a QWizardPage (say, qwpage2) where the flags for one of the columns of information depends on the state of a QRadioButton on another QWizardPage (say, qwpage1).
The original properties of the treeview column are set in the initializePage method of qwpage2. If I Next > through the pages, everything works fine. However, if I < Back from qwpage2, change the radiobutton on qwpage1, and Next > to qwpage2, the flags are not updated.
I have something like the following (in PyQt5) on qwpage1:
rbtn1 = QRadioButton()
rbtn2 = QRadioButton()
self.registerField ("remove_checkbox", rbtn1)
self.registerField ("add_checkbox", rbtn2)
self.checked_choice.addButton(rbtn1)
self.checked_choice.addButton(rbtn2)
and on qwpage2
add_checkbox = self.field("add_checkbox")
p = QStandardItem()
if add_checkbox:
p.setFlags(p.flags() | Qt.ItemIsUserCheckable)
else:
p.setFlags(Qt.NoItemFlags)
How can I change the flag state (essentially the presence of checkboxes) associated with my treeview column if the page has been initialized previously?
Thank you for your help
The problem is not about whenever you go back to the page after going backward from the next one, as initializePage() is always called, no matter if the "previous" page was the previous or the next in the page index (which makes sense, as page order doesn't always have to follow the "plain index order", since it's returned from QWizard.nextId(), which by default calls QWizardPage.nextId() of the current page and could even return an index that's less than the current).
That said, there's one case for which initializePage() is only called once, and that happens if you set the QWizard.IndependentPages option. If you really need to use that, I think that the only alternative is to set values by overriding the page's showEvent(event), and only if not event.spontaneous() (otherwise it will always process everything even when the page is shown again after being minimized).
What really matters here is that the checkbox of an item delegate is usually shown if a Qt.CheckStateRole is set for its index, because setting Qt.ItemIsUserCheckable only means that the user can set the item state, not that the checkbox is visible.
In fact, unless some specific OS/QtStyle comes in action, setting that flag won't have any effect at all, and even if that happens, disabling it once the check state is set to any of the three states (Unchecked, PartiallyChecked or Checked) won't make any difference: it will be shown anyway.
While this might seem a bit counterintuitive, its behavior is clear from the source code, where the StyleOptionViewItem feature HasCheckIndicator is set to True when and only the data(role=CheckStateRole) is not "Null", as in None for Python.
Slighlty unrelated note: be aware that if you're using more advanced models (such as QSql ones), an "unset" value (as in a "Null" QVariant, eg. the field has no data set) is not always Python's None, but a "QPyNullVariant".
Considering the aforementioned notions, you should set the model and its items in the __init__ of the QWizardPage, then use initializePage only to set its flags.
class Page2(QtWidgets.QWizardPage):
def __init__(self, parent=None):
QtWidgets.QWizardPage.__init__(self, parent)
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.tree = QtWidgets.QTreeView()
layout.addWidget(self.tree)
self.model = QtGui.QStandardItemModel()
self.tree.setModel(self.model)
self.model.dataChanged.connect(self.setCurrentState)
self.addCheckItem = QtGui.QStandardItem('item')
self.model.appendRow(self.addCheckItem)
# remember the default flags
self.defaultFlags = self.addCheckItem.flags()
# set the current "add_checkbox" value to None, which means that it
# has *no* state set at all, not even an Unchecked one
self.currentState = None
def setCurrentState(self, topLeft, bottomRight):
# remember the new check state
self.currentState = self.addCheckItem.checkState()
def initializePage(self):
if self.field('add_checkbox'):
# apply the new flags to allow the user to set the check state
self.addCheckItem.setFlags(
self.defaultFlags | QtCore.Qt.ItemIsUserCheckable)
# set the state if it has been previously set
if self.currentState is None:
self.addCheckItem.setCheckState(QtCore.Qt.Unchecked)
else:
self.addCheckItem.setCheckState(self.currentState)
else:
# prevent notifying setCurrentState() slot that we're changing the
# value, while still remembering the check state;
# note that blogking model signals is not a good practice, as it
# prevents the view to receive model changes, which usually results
# in painting, size, scrolling and mouse interaction issues, but we
# can ignore that in this case, since those changes are only taken
# into account once the view is shown, assuming that the view will
# update once it will be shown, and that will only happen *after*
# initializePage returns
self.model.blockSignals(True)
self.addCheckItem.setData(None, QtCore.Qt.CheckStateRole)
self.model.blockSignals(False)
I am trying to make a listview with checkboxes that checks the selected boxes when the enter/return key is pressed. I do this with an override of the eventfilter for my MainWindow (yes I ought to subclass it, but I couldn't get that working)
In the eventfilter i get a None value returned from the itemFromIndex method even though I just passed the index through a .isValid() without problems. Obviously i am missing something, but i can't figure it out - is it looking at completely different indices? is the model not updated?
Any advice on alternate approaches are welcome
This is the method I use to fill the model (QStandardItemModel) with items, it's only called when i load a file.
def update_siNLV(self,names,model):
model.clear()
for name in names:
item = Qg.QStandardItem(name)
item.setCheckState(Qc.Qt.Unchecked)
item.setCheckable(True)
model.appendRow(item)
This is from the init method where I create a variable for the selectionmodel and install the eventfilter on my QListView
self.sigInSelection = self.siNLV.selectionModel()
self.siNLV.installEventFilter(self)
The eventFilter method looks like this and the filtering part of the method works (I've made it print the selected indices with press on the enter key)
def eventFilter(self,receiver,event):
if event.type() == QtCore.QEvent.KeyPress:
if event.key() == QtCore.Qt.Key_Return or event.key() == Qc.Qt.Key_Enter:
indexes = self.sigInSelection.selectedIndexes()
for index in indexes:
if index.isValid():
print(str(index.row())+" "+str(index.column()))
item = self.sigInModel.itemFromIndex(index)
item.setCheckState(qtCore.Qt.Checked)
return True
return super(form,self).eventFilter(receiver,event)
As discussed in the comments:
The indices returned by QItemSelectionModel.selectedIndexes() come from the view and relate to the connection between the view and its immediate model. The identity of that model can be found by calling QModelIndex.model() and in this case it is not the model that you want: it is instead a proxy model that is in-between your desired QStandardItemModel and the view.
To get to the model you want you need to use QAbstractProxyModel.mapToSource(). So you might use code something like this:
source_index = self.proxy.mapToSource(index)
item = self.sigInModel.itemFromIndex(source_index)
More generally you could traverse an arbitrary proxy structure and avoid this hard-coded usage of a single known proxy by code something like:
proxy_model = index.model()
while proxy_model != self.sigInModel:
index = proxy_model.mapToSource(index)
proxy_model = index.model()
item = self.sigInModel.itemFromIndex(index)
But this is probably overkill in this case where you know there is a simple single proxy.
I'm writing an exporter for a game my friend and I are making and it involves setting custom properties and tags to objects which are then recognized in the game and dealt with accordingly. Our engine, which is written in C/C++ has been successfully tested with my current version of the export script, and I''m currently working on tidying it up.
The script uses Blender's feature of custom properties to write custom tags to output file. The model typically consists of multiple 'parts' (Blender mesh objects that are parented to form a tree, with one 'parent' and multiple 'child' objects) and some of those parts are simple Blender Empty objects (for only it's X, Y and Z coordinates are needed) with custom properties that mark where things like ship's propulsion (it's a 3D shooter) are placed, or where the flames/explosions appear when ship's been shot. Those empty parts are also parented to either 'root' object or any of it's children. So far it's been working good, I have written a generic Operator class and some extended classes that reside in a panel which set part's properties (pretty handy since you don't have to add those custom properties by hand).
Now I want to speed thing up even more, that is to be able to simply click on an operator of desired type, and it should automatically add it to the scene and parent it to the active/selected object. I know how to do that, but I can't get those operators to change their labels. Basically, what I want is to operator to say 'Bullet point' when an existing empty is selected (I've got that part done), and when there's a mesh object selected to say 'Add bullet point'. So I just need a way to dynamically change operators' labels depending on the context (as the title of the question states clearly :))
This is what I got so far:
class OBJECT_OT_tg_generic (bpy.types.Operator):
bl_label = "Sets Generic Part Type"
bl_idname = "rbm.set_generic_part_type"
OB_TYPE = None
#classmethod
def poll (cls, context):
act = context.active_object
if 'Type' in act.keys ():
if act['Type'] == cls.OB_TYPE:
cls.bl_label = 'foo'
print (cls.bl_label)
# this prints foo but doesn't change the label
return False
return True
def execute (self, context):
# TODO: add code to automatically place empties and parent them to active object
bpy.context.active_object['Type'] = self.OB_TYPE
return{"FINISHED"}
And an example of a subclass:
class OBJECT_OT_tg_bullet_point (OBJECT_OT_tg_generic):
bl_label = "Bullet Point"
bl_idname = "rbm.set_bullet_point"
OB_TYPE = OT_BULLET_POINT
Here's how it looks in Blender:
http://i.imgur.com/46RAS.png
Guess I solved it. When you're adding an operator to a panel, you can do something like this:
def draw (self, context):
layout = self.layout
row = layout.row()
row.operator("foo.bar", text="Whatever you want")
and the "Whatever you want" is going to be your button's label. But what I did was something else. I didn't change the operators' labels, but instead gave them a different icons depending on whether it's a mesh or an empty currently selected/active:
def draw (self, context):
# (...) we're skipping some code here, obviously
act = context.active_object
if act.type == 'MESH':
op_icon = 'ZOOMIN'
else:
op_icon = 'EMPTY_DATA'
row = layout.column(align=True)
row.operator('rbm.set_bullet_point', icon=op_icon)
row.operator('rbm.set_rocket_point', icon=op_icon)
# (...) rest of the code
I have a number of items in a QTreeView. Each item is generated using this class:
class Branch(QStandardItem):
def __init__(self, label, uri = None):
QStandardItem.__init__(self, label)
self.uri = uri
This is my actual tree:
class FileTree(QTreeView):
def __init__(self):
QTreeView.__init__(self)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Space or event.key() == Qt.Key_Return:
crawler = self.selectedIndexes()[0].model().item(self.selectedIndexes()[0].row())
print(crawler.uri)
QTreeView.keyPressEvent(self, event)
As you can see, I'm a little unsure as to how to get the uri variable from the selected item. I found that selectedIndexes() returns a model and not the item itself. I'm not sure how to get from one to the other. Trying to get the item number using self.selectedIndexes()[0].row() was a bit of a shot in the dark, but it seems to ignore the various branches in the tree (for instance, it will give me a 0 for the first row in a branch, but won't tell me anything about what branch it's in).
What's the proper way to get the selected item from the QTreeView? Or is there a better way of detecting the spacebar or return keys being hit that would make this easier? There's a severe lack of Python documentation for Qt, so it's really hard to know if I'm ever doing things in a sensical manner.
You are calling the right function, it actually returns a QModelIndexList which is just a typedef for QList<QModelIndex> with the QModelIndex being the data structure that can point to any part of the tree. QModelIndex is not a Model in the sense of Model View Controller (MVC) but an adress of an object in a QAbstractItemModel which is the datastructure under all of Qt's ItemView objects, including your tree. You are actually pretty close, QAbstractModelIndex consists of a row, a column and a parent, which lets it adress any position in a hierarchical data structure. If you use the line
index = self.selectedIndexes()[0]
crawler = index.model().itemFromIndex(index)
you should get to the object that you are looking for.
As for documentation, even though there is no python specific documentation it helps to read through the official Qt documentation, the class hierarchy and functionality is still the same. There is very little C++ specific information in the docs.
Harald's answer didn't work for me, because I'm using a QSqlQueryModel as the model (I got the error {AttributeError}'QSqlQueryModel' object has no attribute 'itemFromIndex').
The below did the trick for me though, to get the 0th column data of the selected row:
dbQueryModel.itemData(treeView.selectedIndexes()[0])
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 :(