Getting a QTreeView on a QWizardPage to update properly - python

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)

Related

Python PyQt6 contextMenuEvent strange behavior I don't understand

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).

how to tell filterAcceptsRow to not to filter parent if expanded

I have a searchable tree view that works very well for all root nodes. However, I have no idea how to stop it from removing the parent to search in the children. It only works if the parent and the child match the search criteria. For intance if I search for "fresh" in the example from the image it will not display the third line as the parent will be hidden.
in the filterAcceptsRow I only have access to the proxy model and I cannot check if something is expanded or not. At least I have no idea how to do it to simply ignore all expanded items from the filter to allow searching in their children.
the newer versions of QT have this functionality built in setRecursiveFilteringEnabled but unfortunately I'm stuck with an old one for a while.
def filterAcceptsRow(self, source_row, source_parent_index):
model = self.sourceModel()
source_index = model.index(source_row, 0, source_parent_index)
# my naive attempt that only works for views that dynamically populate the children
# that totally fails on statically popluated ones as it thinks that everythign is expanded
# if model.hasChildren(source_index) and not model.canFetchMore(source_index):
# return True
d = model.searchableData(source_index) #this simply returns a string that I can regex in the filter
return self.isVisible(d) #some custom regex magic not important here
ideally, I would love to keep the parent if the filter matches anything in the children (or in the parent itself)
You need to call the filter function recursively and return True if any of the children also returns True:
def filterAcceptsRow(self, source_row, source_parent_index):
model = self.sourceModel()
source_index = model.index(source_row, 0, source_parent_index)
for child_row in range(model.rowCount(source_index)):
if self.filterAcceptsRow(child_row, source_index):
return True
d = model.searchableData(source_index)
return self.isVisible(d)
Obviously, if the model is very extended, you should consider some caching, but you probably would need to use QPersistentModelIndex as keys, and override whatever functions you use to update the filter (assuming they're being called programmatically, since they are not virtual and overriding them for an internal Qt implementation, such as a QCompleter, will not work).

PyQt5 double spin box returning none value

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.

Refreshing panel contents in wxPython

What is the approach to update widgets in a wxPanel based on events from other controls on same panel?
Scenario 1 is updating the list of a comboBox based on what has been selected from another comboBox , where both are in same panel.
Scenario 2 is showing a new control/widget in a panel based on an event.
Basically creating new controls is easy but I dont know how to refresh/update my panel so immedialtly shows them.
Scenario 1
To change the choices of a combobox self.cbx you can use any of the following methods:
self.cbx.SetItems(choices) where choices is the full list of choices.
self.cbx.SetString(n, string) that sets the string at position n.
InsertItems(items, pos) Inserts the list of strings in the items argument into the list box before the position in the pos argument.
Note that the method Set(choices) of listboxes does not exist for the list in comboboxes. You must use SetItems(choices) instead (this is not clearly indicated in some textbooks).
If you want these changes to occur as a result of a selection in another combobox self.cbx_1 , just get the event (self.Bind(wx.EVT_COMBOBOX, on_combo_1, self.cbx_1)) of the first combobox, process your data as you like in the corresponding self.on_combo method and use one of the above methods to modify the second combobox.
For example:
def on_combo_1(self, evt):
"append cbx_1 selection to cbx if not already in cbx"
selection = self.cbx_1.GetStringSelection()
cbx_choices = self.cbx.GetItems()
if selection not in cbx_choices:
cbx_choices.append(selection)
self.cbx.SetItems(cbx_choices)
The fact the comboboxes are in the same or different panel is irrelevant for that.
Scenario 2
Normally you put your widgets inside sizers. To hide or made visible elements on the sizer you call the methods Show, Hide or Layout:
Show(self, item, show=True, recursive=false)
Shows or hides an item managed by the sizer. To make a sizer item disappear or reappear, use Show followed by Layout. The item parameter can be either a window, a sizer, or the zero-based index of the item. Use the recursive parameter to show or hide an item in a subsizer. Returns True if the item was found.
Hide(self, item, recursive)
A convenience method for Show (item, False, recursive).
Layout(self)
This method will force the recalculation and layout of the items controlled by the sizer using the current space allocated to the sizer. Normally this is called automatically from the owning window's EVT_SIZE handler, but it is also useful to call it from user code when one of the items in a sizer change size, or items are added or removed.
References: wxPython in Action, Noel Rappin and Robin Dunn
For scenario one, you'd do something like the following (assuming the first combobox is bound to its EVT_COMBOBOX:
value = self.cboOne.GetValue()
if value == "something":
self.cboTwo.SetItems(someList)
For showing a new widget, you could create it and then use Show()/Hide() as necessary. If the widget is in a sizer, then use the Sizer's Append or Insert methods. It also has a Detach method that can be used to hide widgets or you just call Hide itself. See the documentation for more information: http://www.wxpython.org/docs/api/wx.Sizer-class.html

Access gtk.TreeIter inside render() Function of gtk.CellRenderer -- pygtk

I am trying to code the following: Two Columns. One contains a itemId, the other one contains a typeId. I want to render the itemId only when the typeId equals a specific value.
class IDRenderer(gtk.CellRendererText):
def __init__(self):
gtk.CellRendererText.__init__(self)
def do_render(self,window, widget, background_area, cell_area, expose_area, flags):
if ----} Condition to ask for value of the typeId - Cell {-----:
gtk.CellRendererText.do_render(self, window, widget, background_area, cell_area,
expose_area, flags)
gobject.type_register(IDRenderer)
I don't know how to get the iter of the currently rendered row which i need to determine the value of the typeId. Is this even possible?
I now found out, thanks to a nice guy on #pygtk on gimpIRC:
You can do that, with binding so called cell data functions to the corresponding gtk.TreeViewColumn as done here in this example
def renderId(celllayout, cell, model, iter):
if model.get_value(iter,1) == 3:
cell.set_property('visible',True)
else:
cell.set_property('visible',False)
treeviewcolumn = gtk.TreeViewColumn()
renderer = gtk.CellRendererText()
treeviewcolumn.add_attribute(renderer,'text',0)
treeviewcolumn.set_cell_data_func(renderer,renderId)
I ommited some code relevant to render a complete treeview, but i think it shows what i wanted to do and how to do it.
The column renderes the value in the first column (0) of the model only if the value in the second modelcolumn (1) equals 3
I hope this could help someone some time.
It's not possible as far as I know. You need to use properties of the custom renderer which will be set automatically by the code calling the rendering function. (Like the text property of CellRendererText -- the rendering code doesn't get the text from the tree model, but the tree model sets the text property of the renderer before calling the rendering code.)

Categories