QTreeWidget clear empty space at the bottom of widget - python

Is this example i have a QTreeWidget with 4 columns. The last column is filled by QFrames.
File ui.py
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
app.setStyle("Windows")
treeWidget = QtWidgets.QTreeWidget()
treeWidget.headerItem().setText(0, "Α/Α")
treeWidget.headerItem().setText(1,"Τύπος")
treeWidget.headerItem().setText(2,"Τίτλος")
treeWidget.headerItem().setText(3,"Προεπισκόπιση")
treeWidget.setStyleSheet("QTreeWidget::item{height:60px;}")
l = []
for i in range(0,30):
l.append(QtWidgets.QTreeWidgetItem(["1","1","1","1"]))
treeWidget.addTopLevelItems(l) # add everything to the tree
treeWidget.show()
right_height = treeWidget.header().height()
for el in l:
right_height += treeWidget.visualItemRect(el).height()
print(right_height)
sys.exit(app.exec_())
Output (after scrolling to the bottom of QTreeWidget):
The desired total height of ScrollArea (inside QTreeWidget) is 1823 and it's calculated as the sum of header height and height of each line.
As you can see there is empty space after last row in QTreeWidget. This problem doesn't appear after resizing QDialog manually.
Edit: This may be usefull.

After checking the code for QTreeWidget and inherited/related classes (QTreeView, QAbstractItemView, QAbstractScrollArea and QWidget, but also QAbstractSlider, used for the scroll bars), it seems clear that QTreeView does not respect the behavior shown in QTableView, which automatically scrolls the view to the bottom (without any further margin) whenever the scroll bar reaches the maximum.[1]
Note that this only happens when the (default) verticalScrollMode property is set to ScrollPerItem. For obvious reasons, whenever it is set to ScrollPerPixel, the scroll bar/area will only extend to the visible area of the viewport.
Unfortunately, the laying out of items (and related function results) of QTreeView is based on this aspect, meaning that we cannot try to just paint the tree (by overriding drawTree() and translating the painter), because in that case painting would be only partially consistent, but the behavior will not. For instance, when hovering or using drag&drop.
The above is most probably caused by optimization reasons: there is no way of knowing the whole extent of a tree, and, unless the uniformRowHeights property is True and all items actually have the same heights (which is clearly not your case), the view should always compute again the geometries of each items; while that could be feasible for a table (2d) model, that becomes quite unreasonable for an undefinite tree (3d) model, as it could theoretically block the view updates. At least, based on the default implementation of QTreeView.
There is a possibility, though: completely override the behavior of the scroll bar, and as long as you know that your model has a known and relatively limited extent.
By default, when ScrollPerItem is active, the scroll bar will always have a range that is equal to total_item_count - visible_item_count: if the viewport has x items and it can currently show y items (with y > x) in its viewport, the scroll bar maximum will be y - x (eg: with 10 visible items, if the viewport can only fully show 9, the maximum will be 1).
When the ScrollPerPixel mode is set instead, the extent will always be the maximum pixel height minus the viewport pixel size. Which means that we can know if the top left item is fully shown or not.
Now, the following requires a bit of trickery and ingenuity.
We need to consider the following aspects:
QScrollBar (based on QAbstractSlider) provides an actionTriggered signal that tells us whenever the user tries to manually change the value using the arrow buttons or by clicking on the "sub/add" page areas (the space within the "groove" that is not covered by the slider handle);
QAbstractItemView internally installs an event filter on the scroll bars, and connects to its valueChanged signals;
bonus: any well designed QObject will update its property (and emit its related changed signal) only when the new value is different from the current one, so we can normally be sure that trying to set the scroll bar value to the same one won't trigger anything;
Considering the above, we could implement a few functions in a subclass and connect them (directly or not) to user generated signals and events. The only catch is that we must use the ScrollPerPixel scroll mode for the vertical scroll bar, which will result in a slightly inconsistent display of the scroll bar handle size.
Well, we can live with that.
Here is a possible implementation that considers the above aspects:
class TreeScrollFix(QTreeWidget):
_ignoreScrollBarChange = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.verticalScrollBar().actionTriggered.connect(self.vbarTriggered)
self.verticalScrollBar().valueChanged.connect(self.fixVBarValue)
self.setVerticalScrollMode(self.ScrollPerPixel)
def vbarTriggered(self, action):
if action in (
QAbstractSlider.SliderNoAction,
QAbstractSlider.SliderToMinimum,
QAbstractSlider.SliderToMaximum,
QAbstractSlider.SliderMove,
):
# we can safely ignore the above, eventually relying on the
# fixVBarValue function
return
if action in (
QAbstractSlider.SliderSingleStepAdd,
QAbstractSlider.SliderSingleStepSub
):
delta = 1
else:
delta = QApplication.wheelScrollLines()
if not delta:
# this should not happen...
return
if action in (
QAbstractSlider.SliderSingleStepAdd,
QAbstractSlider.SliderPageStepAdd
):
func = self.indexBelow
else:
func = self.indexAbove
if self.verticalScrollBar().value() == self.verticalScrollBar().maximum():
delta -= 1
index = self.indexAt(QPoint(0, 1)) # note the extra pixel
while delta:
newIndex = func(index)
if not newIndex.isValid():
break
index = newIndex
delta -= 1
self.scrollTo(index, self.PositionAtTop)
def fixVBarValue(self, value):
vbar = self.verticalScrollBar()
if not value or vbar.maximum() == value:
return
topLeftIndex = self.indexAt(QPoint(0, 0))
topLeftRect = self.visualRect(topLeftIndex)
# adjust the theoretical value to the actual y of the item (which is
# a negative one)
value += topLeftRect.y()
showTop = topLeftRect.center().y() > 0
if not showTop:
# the item currently shown on the top left is not fully shown, and
# the visible height is less than half of its height;
# let's show the next one instead by adding that item's height
value += topLeftRect.height()
if value != vbar.value():
vbar.setValue(value)
def eventFilter(self, obj, event):
if event.type() == event.Wheel and obj == self.verticalScrollBar():
delta = event.angleDelta().y()
if delta: # delta != 0 -> no vertical scrolling
# "synthesize" the event by explicitly calling the custom
# vbarTriggered function just as it would be normally called;
# note that this is a real workaround that will never work with
# normal implicit or explicit event handling, which means that
# QApplication.postEvent and QApplication.sendEvent might be
# potentially ignored by this if another event filter exists.
self.vbarTriggered(
QAbstractSlider.SliderPageStepSub if delta > 1
else QAbstractSlider.SliderPageStepAdd
)
# the event has been handled, do not let the scroll bar handle it.
return True
return super().eventFilter(obj, event)
def scrollTo(self, index, hint=QAbstractItemView.EnsureVisible):
if hint in (self.PositionAtTop, self.PositionAtTop):
if hint == self.PositionAtBottom:
self._ignoreScrollBarChange = True
super().scrollTo(index, hint)
self._ignoreScrollBarChange = False
return
itemRect = self.visualRect(index)
viewRect = self.viewport().rect()
if hint == self.EnsureVisible and itemRect.y() < viewRect.y():
super().scrollTo(index, self.PositionAtTop)
return
vbar = self.verticalScrollBar()
if not self.indexBelow(index).isValid():
# last item
vbar.setValue(vbar.maximum())
return
self._ignoreScrollBarChange = True
if hint == self.PositionAtCenter:
super().scrollTo(index, self.PositionAtCenter)
elif itemRect.bottom() > viewRect.bottom():
super().scrollTo(index, self.PositionAtBottom)
topLeftIndex = self.indexAt(QPoint(0, 0))
topLeftRect = self.visualRect(topLeftIndex)
if topLeftRect.y() < 0:
delta = topLeftRect.height() + topLeftRect.y()
vbar.setValue(vbar.value() + delta)
self._ignoreScrollBarChange = False
And an example code to test it:
from random import randrange
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class TreeScrollFix(QTreeWidget):
# as above...
app = QApplication([])
treeWidget = TreeScrollFix()
treeWidget.setColumnCount(2)
for i in range(1, 31):
topLevel = QTreeWidgetItem(treeWidget, ["top item {}".format(i)])
for j in range(randrange(5)):
child = QTreeWidgetItem(topLevel,
['', topLevel.text(0)])
# a random vertical size hint
hint = QSize(100, randrange(30, 80))
child.setSizeHint(1, hint)
child.setText(0, 'height: {}'.format(hint.height()))
treeWidget.header().setSectionResizeMode(QHeaderView.ResizeToContents)
# expand top level indexes randomly
for i in range(randrange(5, treeWidget.topLevelItemCount())):
topIndex = randrange(treeWidget.topLevelItemCount())
treeWidget.setExpanded(treeWidget.model().index(topIndex, 0), True)
treeWidget.setStyleSheet('''
QTreeView::item {
border: 1px solid palette(highlight);
}
QTreeView::item:selected {
border-color: red;
background: palette(highlight);
color: palette(highlighted-text);
}
''')
treeWidget.resize(app.primaryScreen().size() * 2 / 3)
treeWidget.show()
app.exec_()
Note that I added an override for scrollTo(), which is always called when using keyboard navigation. Normally, the item view takes care of the top alignment when ScrollPerItem is active, but in our case the pixel scrolling could create some issues for items that do not have uniform row heights, and when scrolling to the bottom. The override takes care of that depending on the hint argument of that function, so that whenever scrolling won't show the top item in full, it automatically scrolls down to show the next item on top, otherwise it will just scroll to the bottom for the last available, not expaned item. To avoid unnecessary calls, I also used a _ignoreScrollBarChange flag that will make ignore any further and unnecessary computing in fixVBarValue(). This will also work for the internally delayed call to scrollTo() that happens when selecting any item.
Be aware that I've done some testing and it should work as expected. Unfortunately, QAbstractItemView and QTreeView use delayed item layout management, and I cannot completely be sure about these aspects. At least in one case in dozens, I got a UI freeze, but I was not able to reproduce the issue (which might have been caused by external causes). I strongly advice you to take your time to check the code above, the documentation and the Qt sources, and consider using some carefully thought test suite.
Also, for obvious reasons, if you want to use a custom QScrollBar, you'd need to properly disconnect the previous functions and connect them again to the new one.
[1] I am not sure, but it is probably related to a comment in the QTreeView code (near line 3500), which says: optimize (maybe do like QHeaderView by letting items have startposition); see the official sources or the KDAB code browser

Related

How to limit the rightest Gtk.TreeViewColumn's width inside a Gtk.ScrolledWindow?

I cannot prevent the rightest column of a Gtk.TreeView to expand.
As the real Gtk.TreeView may display a greater number of rows, making it usually somewhat greater than the screen's height, it is embedded in a Gtk.ScrolledWindow. This is required. Without it, attaching an empty grid at the right of the treeview, expanding itself horizontally, would fix the problem. Based on this idea, I've tried a workaround that introduces another difficulty (see below).
I have built a minimal working example from the example from https://python-gtk-3-tutorial.readthedocs.io/en/latest/treeview.html#filtering, without filtering nor buttons; and the columns are 80 px wide at least (this works) and their content is horizontally centered. This last detail makes the horizontal expansion of the rightest column visible. In the original example, it does expand too, but as everything is left aligned, this is not really visible. I'd liked to keep the columns' content centered, without seeing the rightest expanded.
This example is minimal, but contains some helping features: you'll find clickable column titles, that will display some information about the clicked column in the console; a remove button (works fine, remove the selected rows) and a paste button that allows to paste new rows from a selection (e.g. from selected lines from a spreadsheet, but there's nothing to check the data are correct, if you paste something that does not convert to int, it will simply crash).
Workaround
A workaround I've tried consist of gathering both the treeview and a horizontally expanding empty right grid at its right inside a grid that would be put inside the Gtk.ScrolledWindow. It works, but causes other subtle problems: in some situations, the treeview does not get refreshed (it happens after a while), yet nothing prevents the main loop to refresh the view (there's no other processing in the background, for instance). To experiment this workaround: comment and uncomment the lines as described in the code below; run the program via python script.py (if you need to install pygobject in a venv, see here), notice the rightest column does not expand to the right any longer, select the 3 first rows and press "remove", then from a spread sheet, select 3 lines of dummy integers as shown below and then press "paste". Scroll down to the last rows: you'll see most of the time that the 3 pasted lines do not show up, even if it is possible to scroll over the last row. Maybe one of them will show up after some time, then another... (or simply select a row, and they'll show up). Strangely, it happens if one has just removed as many lines as one wants to paste after the removal (3 removed, 3 pasted; or 4 removed, 4 pasted etc.).
Example spreadsheet selection:
Question
So, I'd prefer to avoid the workaround (I'm afraid I may find other situations triggering a bad refreshing of the treeview), that I could not fix itself (for instance, setting self.scrollable_treelist.set_propagate_natural_height(True) proved useless, maybe I'm not using it correctly though?) and only attach the treeview itself directly in the Gtk.ScrolledWindow. How to prevent the rightest column to expand, then?
(I've tried to use a fair amount of setters and properties of the cell renderers, the treeview, the treeview columns, the scrolled window, to no avail. Some of them are still in the code below.)
Any solution using and fixing the workaround above would be accepted though.
In any case, the treeview may be scrolled, and lines may be added and removed from it without any refreshing problem.
Source Code
import gi
try:
gi.require_version('Gtk', '3.0')
except ValueError:
raise
else:
from gi.repository import Gtk, Gdk
# ints to feed the store
data_list = [(i, 2 * i, 3 * i, 4 * i, 5 * i) for i in range(40)]
class AppWindow(Gtk.Window):
def __init__(self):
super().__init__(title="Treeview Columns Size Demo")
self.set_border_width(10)
# Setting up the self.grid in which the elements are to be positioned
self.grid = Gtk.Grid()
self.grid.set_column_homogeneous(True)
self.grid.set_row_homogeneous(True)
self.add(self.grid)
# Creating the ListStore model
self.store = Gtk.ListStore(int, int, int, int, int)
for data_ref in data_list:
self.store.append(list(data_ref))
# creating the treeview and adding the columns
self.treeview = Gtk.TreeView(model=self.store)
rend = Gtk.CellRendererText()
rend.set_alignment(0.5, 0.5)
for i, column_title in enumerate([f'n×{p}' for p in [1, 2, 3, 4, 5]]):
column = Gtk.TreeViewColumn(column_title, rend, text=i)
column.set_min_width(80)
# column.set_max_width(80)
# column.set_fixed_width(80)
# column.set_sizing(Gtk.TreeViewColumnSizing(1))
column.set_alignment(0.5)
column.set_clickable(True)
column.connect('clicked', self.on_column_clicked)
self.treeview.append_column(column)
self.treeview.set_hexpand(False)
self.treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
# Put the treeview in a scrolled window
self.scrollable_treelist = Gtk.ScrolledWindow()
self.scrollable_treelist.set_vexpand(True)
self.grid.attach(self.scrollable_treelist, 0, 0, 8, 10)
self.scrollable_treelist.add(self.treeview)
# WORKAROUND
# Alternatively, embed the treeview inside a grid containing an
# empty grid to the right of the treeview
# To try it: comment out the previous line; uncomment next lines
# scrolled_grid = Gtk.Grid()
# empty_grid = Gtk.Grid()
# empty_grid.set_hexpand(True)
# scrolled_grid.attach(self.treeview, 0, 0, 8, 10)
# scrolled_grid.attach_next_to(empty_grid, self.treeview,
# Gtk.PositionType.RIGHT, 1, 1)
# self.scrollable_treelist.add(scrolled_grid)
# self.scrollable_treelist.set_propagate_natural_height(True)
# Buttons
self.remove_button = Gtk.Button(label='Remove')
self.remove_button.connect('clicked', self.on_remove_clicked)
self.paste_button = Gtk.Button(label='Paste')
self.paste_button.connect('clicked', self.on_paste_clicked)
self.grid.attach_next_to(self.remove_button, self.scrollable_treelist,
Gtk.PositionType.TOP, 1, 1)
self.grid.attach_next_to(self.paste_button, self.remove_button,
Gtk.PositionType.RIGHT, 1, 1)
self.set_default_size(800, 500)
self.show_all()
# Clipboard (to insert several rows)
self.clip = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)
self.clip2 = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
def on_column_clicked(self, col):
print(f'col.get_sizing()={col.get_sizing()}')
print(f'col.get_expand()={col.get_expand()}')
print(f'col.get_width()={col.get_width()}')
print(f'col.get_min_width()={col.get_min_width()}')
print(f'col.get_max_width()={col.get_max_width()}')
print(f'col.get_fixed_width()={col.get_fixed_width()}')
def on_remove_clicked(self, widget):
model, paths = self.treeview.get_selection().get_selected_rows()
refs = []
for path in paths:
refs.append(Gtk.TreeRowReference.new(model, path))
for ref in refs:
path = ref.get_path()
treeiter = model.get_iter(path)
model.remove(treeiter)
# print(f'AFTER REMOVAL, REMAINING ROWS={[str(r[0]) for r in model]}')
def on_paste_clicked(self, widget):
text = self.clip.wait_for_text()
if text is None:
text = self.clip2.wait_for_text()
if text is not None:
lines = text.split('\n') # separate the lines
lines = [tuple(L.split('\t')) for L in lines] # convert to tuples
print(f'PASTE LINES={lines}')
for line in lines:
if len(line) == 5:
line = tuple(int(value) for value in line)
self.store.append(line)
win = AppWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

How to get the QModelIndex associated with the QCheckbox placed in the QTableWIdget

I have the following code to place a CheckBox in the first column of a list of items in a QTableWidget.
checkboxWidget = QWidget()
checkBox = QCheckBox(checkboxWidget)
checkBox.clicked.connect(self._check_changed)
#
# If the variable is in the monitored list
# check the checkbox
#
isMonitored = False
if (self._monitored_variables != None):
if (self._monitored_variables[name]):
isMonitored = True
if (isMonitored):
checkBox.setCheckState(Qt.CheckState.Checked)
else:
checkBox.setCheckState(Qt.CheckState.Unchecked)
layoutCheckbox = QHBoxLayout(checkboxWidget)
layoutCheckbox.addWidget(checkBox)
layoutCheckbox.setAlignment(Qt.AlignCenter)
layoutCheckbox.setContentsMargins(0, 0, 0, 0)
self._variables_view.setCellWidget(row,0, checkboxWidget)
I started with the answer to this question:
How should I connect CheckBox clicked signals in Table Widgets in PyQt5?
The difference that I have is that I want the CheckBox centered in the table cell, hence the extra controls.
The click handler looks like this:
def _check_changed(self):
cb = self.sender()
print(cb.parent())
ix = self._variables_view.indexAt(cb.pos())
print(ix.row(), ix.column(), cb.isChecked())
The problem I am facing is that the row/column is not correct.
How can I recover the row/column of the CheckBox that was clicked?
It seems that my previous answer was not explicit but I am going to take this answer to take it in a generic way that will work for all the classes that inherit from QAbstractItemView and any type of widget placed through the setCellWidget or setIndexWidget methods.
General case:
The key to the solution is to obtain the position of the widget that emits the signal with respect to the viewport of the QAbstractItemView, and then use indexAt to get the QModelIndex. This can be obtained by following the following steps:
Map any relative internal position of the widget to global coordinates.
Map global coordinates to local coordinate relative to the viewport.
use indexAt() with local coordinate.
gl = widget.mapToGlobal(QtCore.QPoint())
lp = view.viewport().mapFromGlobal(gp)
ix = view.indexAt(lp)
Specific case:
In this case, just do the following:
def _check_changed(self):
widget = self.sender()
gl = widget.mapToGlobal(QtCore.QPoint())
lp = self._variables_view.viewport().mapFromGlobal(gp)
ix = self._variables_view.indexAt(lp)
It seems the answer to my issues was rather simple. In the "indexAt" method the "cb.pos()" needs to be changed to "cb.parent().pos()".

How to fixed image when moving item in QGraphicsViewer

I am developing an image viewer using pyqt.
I want the image to be fixed when the box moved.
However, the image is pushed when the box tries to reach the viewer's side like this.
It was implemented using QGraphicsView, QGraphicsScene, and QGraphicsitem.
This is part of main class
self.scene_r = GraphicsScene()
self.scene_r.addPixmap(pix_resized)
self.resizedView.setScene(self.scene_r)
self.resizedView.centerOn(128,128)
This is QGraphicScene Class
class GraphicsScene(QGraphicsScene):
def __init__(self, parent=None):
QGraphicsScene.__init__(self)
rect_item = recItem(QRectF(0, 0, 100, 100))
rect_item.setFlag(QGraphicsItem.ItemIsMovable, True)
rect_item.setZValue(1)
rect_item.setPen(Qt.green)
self.addItem(rect_item)
I tried to override mouseMoveEvent() of QGraphicsRectItem class, but it failed.
That happens for two reasons:
Since you are making the item movable, you can move it freely, anywhere you want.
When the scene rectangle is smaller than the one visible in the view, the view tries to ensure that the whole scene rectangle stays visible, possibly by scrolling its contents.
Note that, unless explicitly set using setSceneRect() (on the scene or on the view, the results might differ), Qt automatically sets the scene rect implicitly to the bigger QRect that contains all visible graphics items.
There at least two possible solutions to your problem, which one to choose depends on what you need, and you can also decide to use both of them.
Explicitly set the sceneRect
You can set the sceneRect to a specific rectangle, an item, or all existing items. Note that, while in your case it won't change much if you set the rectangle for the scene or the view, in more complex cases (for example, multiple views showing the same scene) the result might differ.
# on the view
self.setSceneRect(self.scene_r.itemsBoundingRect())
# alternativaly, on the scene
self.setSceneRect(self.itemsBoundingRect())
Limit the area in which the item can be moved
In this case you can intercept the itemChange ItemPositionChange (note that the ItemSendsGeometryChanges flag must be set) and return an adjusted value before it is actually applied:
class RecItem(QGraphicsRectItem):
def __init__(self, *args):
super().__init__(*args)
self.setFlags(self.ItemSendsGeometryChanges)
def itemChange(self, change, value):
if change == self.ItemPositionChange:
sceneRect = self.scene().sceneRect()
newGeometry = self.boundingRect().translated(value)
# the item pen must be taken into account
halfPen = self.pen().width() / 2
if value.x() < sceneRect.x():
value.setX(sceneRect.x() + halfPen)
if value.y() < sceneRect.y():
value.setY(sceneRect.y() + halfPen)
if newGeometry.right() + halfPen > sceneRect.right():
value.setX(sceneRect.right() - newGeometry.width())
if newGeometry.bottom() + halfPen > sceneRect.bottom():
value.setY(sceneRect.bottom() - newGeometry.height())
return value
return super().itemChange(change, value)

Controlling dynamic properties in Shady according to video frames not time

I am trying to use Shady to present a sequence of image frames. I'm controlling the flow from another machine, so that I first instruct the machine running Shady to present the first frame, and later on to run the rest of the frames.
I create a World instance, and attach to it an animation callback function. Within this callback I listen for communications from the other machine (using UDP).
First I receive a command to load a given sequence (stored as a numpy array), and I do
def loadSequence(self, fname):
yy = np.load(fname)
pages = []
sz = yy.shape[0]
for j in range(yy.shape[1]/yy.shape[0]):
pages.append(yy[:, j*sz:(j+1)*sz])
deltax, deltay = (self.screen_px[0] - sz) / 2, (self.screen_px[1] - sz) / 2
if (self.sequence is None):
self.sequence = self.wind.Stimulus(pages, 'sequence', multipage=True, anchor=Shady.LOCATION.UPPER_LEFT, position=[deltax, deltay], visible=False)
else:
self.sequence.LoadPages(pages, visible=False)
When I receive the command to show the first frame, I then do:
def showFirstFrame(self, pars):
self.sequence.page = 0 if (pars[0] == 0) else (len(self.sequence.pages) - 1)
self.sequence.visible = True
But what do I do now to get the other frames to be be displayed? In the examples I see, s.page is set as a function of time, but I need to show all frames, regardless of time. So I was thinking of doing something along these lines:
def showOtherFrames(self, pars, ackClient):
direction, ack = pars[0], pars[2]
self.sequence.page = range(1, len(self.sequence.pages)) if (direction == 0) else range(len(self.sequence.pages)-2, -1, -1)
But this won't work. Alternatively I thought of defining a function that takes t as argument, but ignores it and uses instead a counter kept in a global variable, but I'd like to understand what is the proper way of doing this.
When you make s.page a dynamic property, the function assigned to it must take one argument (t), but you can still just use any variables in the space when defining that function, and not even use the time argument at all.
So, for example, you could do something as simple as:
w = Shady.World(...)
s = w.Stimulus(...)
s.page = lambda t: w.framesCompleted
which will set the page property to the current frame count. That sounds like it could be useful for your problem.
Your global-variable idea is one perfectly valid way to do this. Or, since it looks like you're defining things as methods of an instance of your own custom class, you could use instance methods as your animation callbacks and/or dynamic property values—then, instead of truly global variables, it makes sense to use attributes of self:
import Shady
class Foo(object):
def __init__(self, stimSources):
self.wind = Shady.World()
self.stim = self.wind.Stimulus(stimSources, multipage=True)
self.stim.page = self.determinePage # dynamic property assignment
def determinePage(self, t):
# Your logic here.
# Ignore `t` if you think that's appropriate.
# Use `self.wind.framesCompleted` if it's helpful.
# And/or use custom attributes of `self` if that's
# helpful (or, similarly, global variables if you must).
# But since this is called once per frame (whenever the
# frame happens to be) it could be as simple as:
return self.stim.page + 1
# ...which is indefinitely sustainable since page lookup
# will wrap around to the number of available pages.
# Let's demo this idea:
foo = Foo(Shady.PackagePath('examples/media/alien1/*.png'))
Shady.AutoFinish(foo.wind)
Equivalent to that simple example, you could have the statement self.stim.page += 1 (and whatever other logic) inside a more-general animation callback.
Another useful tool for frame-by-frame animation is support for python's generator functions, i.e. functions that include a yield statement. Worked examples are included in python -m Shady demo precision and python -m Shady demo dithering.
It can also be done in a StateMachine which is always my preferred answer to such things:
import Shady
class Foo(object):
def __init__(self, stimSources):
self.wind = Shady.World()
self.stim = self.wind.Stimulus(stimSources, multipage=True)
foo = Foo(Shady.PackagePath('examples/media/alien1/*.png'))
sm = Shady.StateMachine()
#sm.AddState
class PresentTenFrames(sm.State):
def ongoing(self): # called on every frame while the state is active
foo.stim.page += 1
if foo.stim.page > 9:
self.ChangeState()
#sm.AddState
class SelfDestruct(sm.State):
onset = foo.wind.Close
foo.wind.SetAnimationCallback(sm)
Shady.AutoFinish(foo.wind)

Remove a specific QWidget from a QSplitter

I want to remove a specific Qwidget from a QSplitter (or theQSplitterwith it is the best choice i thing) after i create 2-3 of them.
To do this, i thing that i need to get the index of every widget that i create.
This is my code:
class Windows(self):
list1 = [widget1, widget2, widget3]
counter = 0
def __init__(self):
#A lot of stuff in here
self.splitter = QSplitter(Qt.Vertical)
self.spliiter_2 = QSplitter(Qt.Horizontal)
self.splitter_2.addwidget(self.splitter)
def addWidget(self):
if counter == 1:
self.splitter.addWidget(Windows.list1[0])
counter += 1
if counter == 2:
counter += 1
self.splitter.addWidget(Windows.list1[1])
if counter == 3:
self.splitter.addWidget(Windows.list1[2])
counter += 1
def deleteWidget(self):
??????????????
As you can see above, i create a QSplitter in the class Windows, and then i create another one (self.splitter_2) where i put the first one.
Every time that i press a combination of keys, i call the addWidget method, and it adds a widget (that depends of the variable counter) from the list1.
If i want to delete the 2nd widget(for example), how can i do it, without deleting the first widget?. I thing that i need the index, but i do not know how to get it.
I have created a button that calls the deleteWidgetmethod, by the way.
I read about the hide() form, but i do not know how to specified it. But, i would rather delete it completely.
Hope you can help me.
Your deleteWidget() function could look like this:
def deleteWidget(self, index):
delete_me = self.splitter.widget(index)
delete_me.hide()
delete_me.deleteLater()
We need hide() because according to the documentation
When you hide() a child, its space will be distributed among the other
children.

Categories