Remove a specific QWidget from a QSplitter - python

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.

Related

QTreeWidget clear empty space at the bottom of widget

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

How to destroy comboboxes if they are available in python gui?

I have a function, which based on the count generates the comboboxes. I want to destroy any combobox which is available already whenever my count variable changes. I used winfo_exists to do this...but it throws an attribute error every time. Please help me with this.
Here is the code of that function:
def create(event):
count = combo.current()
print ("count")
print(count)
for i in range(1,count+2):
if (create_combo[i].winfo_exists()):
create_combo[i].destroy()
for i in range (1,count+2):
create = tk.StringVar()
create_combo[i]= ttk.Combobox(new_window_2,width = 15,textvariable = create, values = sheets)
#create_combo.set("Sheet " + str(i))
create_combo[i].grid(column = i, row =4, padx=10,pady=10)
To delete the widgets which are created in loop, can be deleted by using the method available in this link
Python Tkinter :removing widgets that were created using a for loop
This worked for me... I dont understand why winfo_exists didn't work.
Anyway Thanks!!
list_of_owner_widgets = []
def create(event):
count = combo.current()
print(count)
for widget in list_of_owner_widgets:
widget.destroy()
for i in range (1,count+2):
create = tk.StringVar()
create_combo[i]= ttk.Combobox(new_window_2,width = 15,textvariable = create, values = sheets)
list_of_owner_widgets.append(create_combo[i])
create_combo[i].grid(column = i, row =4, padx=10,pady=10)
If you wish to destroy a Python widget, be it a Checkbox in your case, you use the following code.
It is a lot easier to remove and show widgets using the .grid method!
Your code:
create_combo[i].destroy()
I assume (as I can see further down the code file) that you used the grid method. In which case I would simply change the code to:
create_combo[i].grid_forget()
Hope This Helps!
From your post:
for i in range(1,count+2):
if (create_combo[i].winfo_exists()):
create_combo[i].destroy()
And the error:
AttributeError: 'str' object has no attribute 'winfo_exists'
I can infer that:Your create_combo must be a list full of string(Instead of Combobox widget).
You could add print(create_combo) before the first for loop to check the value in create_combo.It must be a list full of string.
And it seems that your problem is not here,you should check the way how you create the create_combo.
lets assume create_combo = ['a','b','c']. So I am creating three comboboxes create_combo[0...2]. So name of the comboboxes(widgets) is a, b, c.
No,you couldn't.
If really want to get a list of comboboxes you create,you should use:
create_combo = []
for i in range(3):
t = ttk.Combobox(xxxxx)
t.grid(xxxxxx)
create_combo.append(t) # append it to your create_combo
And then,you could use:
for i in create_combo:
if i.winfo_exists(): # i should be a widget,not string
xxxxxxx # your job

changing order of items in tkinter listbox

Is there an easier way to change the order of items in a tkinter listbox than deleting the values for specific key, then re-entering new info?
For example, I want to be able to re-arrange items in a listbox. If I want to swap the position of two, this is what I've done. It works, but I just want to see if there's a quicker way to do this.
def moveup(self,selection):
value1 = int(selection[0]) - 1 #value to be moved down one position
value2 = selection #value to be moved up one position
nameAbove = self.fileListSorted.get(value1) #name to be moved down
nameBelow = self.fileListSorted.get(value2) #name to be moved up
self.fileListSorted.delete(value1,value1)
self.fileListSorted.insert(value1,nameBelow)
self.fileListSorted.delete(value2,value2)
self.fileListSorted.insert(value2,nameAbove)
Is there an easier way to change the order of items in a tkinter listbox than deleting the values for specific key, then re-entering new info?
No. Deleting and re-inserting is the only way. If you just want to move a single item up by one you can do it with only one delete and insert, though.
def move_up(self, pos):
""" Moves the item at position pos up by one """
if pos == 0:
return
text = self.fileListSorted.get(pos)
self.fileListSorted.delete(pos)
self.fileListSorted.insert(pos-1, text)
To expand on Tim's answer, it is possible to do this for multiple items as well if you use the currentselection() function of the tkinter.listbox.
l = self.lstListBox
posList = l.curselection()
# exit if the list is empty
if not posList:
return
for pos in posList:
# skip if item is at the top
if pos == 0:
continue
text = l.get(pos)
l.delete(pos)
l.insert(pos-1, text)
This would move all selected items up 1 position. It could also be easily adapted to move the items down. You would have to check if the item was at the end of the list instead of the top, and then add 1 to the index instead of subtract. You would also want to reverse the list for the loop so that the changing indexes wouldn't mess up future moves in the set.

Separating Progress Tracking and Loop Logic

Suppose i want to track the progress of a loop using the progress bar printer ProgressMeter (as described in this recipe).
def bigIteration(collection):
for element in collection:
doWork(element)
I would like to be able to switch the progress bar on and off. I also want to update it only every x steps for performance reasons. My naive way to do this is
def bigIteration(collection, progressbar=True):
if progressBar:
pm = progress.ProgressMeter(total=len(collection))
pc = 0
for element in collection:
if progressBar:
pc += 1
if pc % 100 = 0:
pm.update(pc)
doWork(element)
However, I am not satisfied. From an "aesthetic" point of view, the functional code of the loop is now "contaminated" with generic progress-tracking code.
Can you think of a way to cleanly separate progress-tracking code and functional code? (Can there be a progress-tracking decorator or something?)
It seems like this code would benefit from the null object pattern.
# a progress bar that uses ProgressMeter
class RealProgressBar:
pm = Nothing
def setMaximum(self, max):
pm = progress.ProgressMeter(total=max)
pc = 0
def progress(self):
pc += 1
if pc % 100 = 0:
pm.update(pc)
# a fake progress bar that does nothing
class NoProgressBar:
def setMaximum(self, max):
pass
def progress(self):
pass
# Iterate with a given progress bar
def bigIteration(collection, progressBar=NoProgressBar()):
progressBar.setMaximum(len(collection))
for element in collection:
progressBar.progress()
doWork(element)
bigIteration(collection, RealProgressBar())
(Pardon my French, er, Python, it's not my native language ;) Hope you get the idea, though.)
This lets you move the progress update logic from the loop, but you still have some progress related calls in there.
You can remove this part if you create a generator from the collection that automatically tracks progress as you iterate it.
# turn a collection into one that shows progress when iterated
def withProgress(collection, progressBar=NoProgressBar()):
progressBar.setMaximum(len(collection))
for element in collection:
progressBar.progress();
yield element
# simple iteration function
def bigIteration(collection):
for element in collection:
doWork(element)
# let's iterate with progress reports
bigIteration(withProgress(collection, RealProgressBar()))
This approach leaves your bigIteration function as is and is highly composable. For example, let's say you also want to add cancellation this big iteration of yours. Just create another generator that happens to be cancellable.
# highly simplified cancellation token
# probably needs synchronization
class CancellationToken:
cancelled = False
def isCancelled(self):
return cancelled
def cancel(self):
cancelled = True
# iterates a collection with cancellation support
def withCancellation(collection, cancelToken):
for element in collection:
if cancelToken.isCancelled():
break
yield element
progressCollection = withProgress(collection, RealProgressBar())
cancellableCollection = withCancellation(progressCollection, cancelToken)
bigIteration(cancellableCollection)
# meanwhile, on another thread...
cancelToken.cancel()
You could rewrite bigIteration as a generator function as follows:
def bigIteration(collection):
for element in collection:
doWork(element)
yield element
Then, you could do a great deal outside of this:
def mycollection = [1,2,3]
if progressBar:
pm = progress.ProgressMeter(total=len(collection))
pc = 0
for item in bigIteration(mycollection):
pc += 1
if pc % 100 = 0:
pm.update(pc)
else:
for item in bigIteration(mycollection):
pass
My approach would be like that:
The looping code yields the progress percentage whenever it changes (or whenever it wants to report it). The progress-tracking code then reads from the generator until it's empty; updating the progress bar after every read.
However, this also has some disadvantages:
You need a function to call it without a progress bar as you still need to read from the generator until it's empty.
You cannot easily return a value at the end. A solution would be wrapping the return value though so the progress method can determine if the function yielded a progress update or a return value. Actually, it might be nicer to wrap the progress update so the regular return value can be yielded unwrapped - but that'd require much more wrapping since it would need to be done for every progress update instead just once.

Tkinter Listbox not deleting items correctly

I have a method that is suppose to take a search parameter and remove everything from the list that does not meet the parameter. But when it runs it removes list items at almost random. I've debugged it and it correctly determines if an item needs to be removed but it doesn't remove the right one. I think it has something to do with when I remove one item it messes up the indexes of the rest of the list, which doesn't with with my method of tracking the index.
I posted the whole class but the relevant code is towards the bottom
class StudentFinderWindow(Tkinter.Toplevel):
def __init__(self):
Tkinter.Toplevel.__init__(self) # Create Window
##### window attributes
self.title('Edit Students') #sets window title
##### puts stuff into the window
# text
editStudentInfoLabel = Tkinter.Label(self,text='Select the student from the list below or search for one in the search box provided')
editStudentInfoLabel.grid(row=0, column=0)
# entry box
self.searchRepositoryEntry = Tkinter.Entry(self)
self.searchRepositoryEntry.grid(row=1, column=0)
# list box
self.searchResults = Tkinter.Listbox(self)
self.searchResults.grid(row=2, column=0)
# search results initial updater
self.getStudentList()
for student in self.studentList:
self.searchResults.insert(Tkinter.END, student)
##### event handler
self.searchRepositoryEntry.bind('<KeyRelease>', self.updateSearch)
This is the relevant code
def updateSearch(self, event):
parameters = self.searchRepositoryEntry.get()
int = 0
currentList = self.searchResults.get(0, Tkinter.END)
length = len(parameters)
print(parameters)
print(length)
for i in currentList:
if not i[0:length] == parameters:
self.searchResults.delete(int)
print(i[0:length] == parameters)
print(i[0:length])
print(int)
int += 1
def getStudentList(self):
global fileDirectory # gets the directory that all the files are in
fileList = listdir(fileDirectory) # makes a list of files from the directory
self.studentList = [] # makes a new list
for file in fileList: # for loop that adds each item from the file list to the student list
self.studentList.append(file[:-4])
When you delete an item, everything below it moves up causing the index of all following items to change. The simplest solution to this sort of a problem (it's also common when deleting words from a text widget) is to delete backwards, starting at the end.
I think you already know the problem. When you delete an item, the index for the rest of the items change. For example, if you delete the 4th item, then the 5th item becomes the "new" 4th item. So you don't want to increment int whenever you delete an item. You can implement that with continue:
for i in currentList:
if not i[0:length] == parameters:
self.searchResults.delete(int)
continue # <-- Use continue so `int` does not increment.
int += 1
PS. It's not good coding style to use int as a variable name -- in Python it masks the built-in function of the same name.

Categories