Tkinter Listbox not deleting items correctly - python

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.

Related

How can I display information on separate lines in my GUI? [duplicate]

I have a working Tkinter.Listbox object, but I want to set it up so that its elements can have carriage returns without having to somehow set up multiple linked items.
For instance, if I want to generate a selection pane with items that look like this..
# Here are four elements for the selector Listbox..
lb_items = ('mama', 'luigi', 'my birds', \
'this is a single element\n spanning two lines!')
# This generates and displays the selector window..
tk_selector = SingleSelect(lb_items, "TEST SELECTOR")
tk_selector.run_selector()
..it would be great if I could get the output to look like this mockup..
..instead of what it actually generates, which is this..
Listboxes seem to ignore '\n' and triple-quote strings with line-returns entirely; if \n is used, neither the characters nor the line break appears.
Is it possible to have individual, selectable Listbox elements that appear with line breaks?
I would also be satisfied with a word-wrap option, but after some looking, I couldn't find any such option in Listbox or Tk in general.
I could probably fake the effect by making multiline strings into multiple elements then setting it up to return the whole line if any of them are called, but it feels like an ordeal for something that could have a simple solution.
Like Bryan Oakley said, there is no native support in Listbox for carriage returns, so I tried building the 'fake' version I mentioned in the question, and it turns out that it isn't really that hard.
My solution is to parse each 'raw' string before inserting them into the Listbox, breaking strings into individual lines using splitlines, recording the number of lines and which indices in the Listbox's element roll correspond to which unbroken input string, and then selecting all the parts whenever the Listbox's selection changes using Listbox.bind('<<ListboxSelect>>', self._reselection_fxn).
There's an abridged and annotated sample below, or you can see my complete, working, even-more-heavily annotated code here.
class Multiline_Single_Selector(object):
## Go ahead and choose a better class name than this, too. :/
def __init__(self, itemlist, ..):
# ..
lb_splitlines = self._parse_strings(itemlist)
# ^ splits the raw strings and records their indices.
# returns the split strings as a list of Listbox elements.
self.my_Listbox.insert(0, *lb_splitlines)
# ^ put the converted strings into the Listbox..
self.my_Listbox.bind('<<ListboxSelect>>', self._reselect)
# ^ Whenever the Listbox selection is modifed, it triggers the
# <<ListboxSelect>> event. Bind _reselect to it to determine
# which lines ought to be highlighted when the selection updates.
# ..
def _parse_strings(self, string_list):
'''Accepts a list of strings and breaks each string into a series of lines,
logs the sets, and stores them in the item_roster and string_register attributes.
Returns the split strings to be inserted into a Listbox.'''
self.index_sets = index_sets = []
# ^ Each element in this list is a tuple containing the first and last
# Listbox element indices for a set of lines.
self.string_register = register = {}
# ^ A dict with a whole string element keyed to the index of the its
# first element in the Listbox.
all_lines = []
# ^ A list of every Listbox element. When a string is broken into lines,
# the lines go in here.
line_number = 0
for item in string_list:
lines = item.splitlines()
all_lines.extend(lines) # add the divided string to the string stack
register[line_number] = item
# ^ Saves this item keyed to the first Listbox element it's associated
# with. If the item is selected when the Listbox closes, the original
# (whole) string is found and returned based on this index number.
qty = len(lines)
if qty == 1: # single line item..
index_sets.append((line_number, line_number))
else: # multiple lines in this item..
element_range = line_number, line_number + qty - 1
# ^ the range of Listbox indices..
index_sets.extend([element_range] * qty)
# ^ ..one for each element in the Listbox.
line_number += qty # increment the line number.
return all_lines
def _reselect(self, event=None):
"Called whenever the Listbox's selection changes."
selection = self.my_Listbox.curselection() # Get the new selection data.
if not selection: # if there is nothing selected, do nothing.
return
lines_st, lines_ed = self.index_sets[selection[0]]
# ^ Get the string block associated with the current selection.
self.my_Listbox.selection_set(lines_st, lines_ed)
# ^ select all lines associated with the original string.
def _recall(self, event=None):
"Get the complete string for the currently selected item."
selection = self.my_Listbox.curselection()
if selection: # an item is selected!
return self.string_register[selection[0]]
return None # no item is selected.
If you adjust this code to match your own and drop it into an existing Listbox setup, it ought to let you simulate carriage returns. It's not the same as a native line-wrap or \n-parsing function directly in Listbox, but it does basically the same thing.
To get the whole string corresponding to the current selection, just bind _recall to whatever input or event you want to have return it. In this case, it returns None when no item is selected.
It's also kind of a lot of work considering the sophistication of the desired effect and it may not be appropriate to every situation. But at least you can do it.
It is not possible for a listbox item to be spread across more than one line or row.
This solution seems to me much easier:
Instead of trying:
lb_items = ('mama', 'luigi', 'my birds', \
'this is a single element\n spanning two lines!')
make two elements:
lb_items = ('mama', 'luigi', 'my birds', \
'this is a single element', 'spanning two lines!')

How to get state of each checkbox generated through a loop, as soon as it is checked/unchecked in tkinter?

I'm trying add/remove an item (text of checkbox) to/from a list whenever a checkbox is checked/unchecked in tkinter.
My idea was to add a command to the checkbutton, like:
cb = Checkbutton(master,...,command=some_fun)
but I cannot think of a way to define the function. I was thinking the function should contain the widget attribute cget('text'), but the problem is I have many checkboxes made with the help of a loop.
I guess the question is: how can I reference the checkbox whose state got changed and is therefore calling the function some_fun?
The way I generated the checkboxes is:
cb_identities = []
for i in range(cb_max_num):
cb = Checkbutton(frame_data,bg="white")
cb_identities.append(cb)
And then I'm dynamically changing them depending on some radiobuttons:
def fun_chck(): #shows or hides checkbuttons based on radiobutton input
data = read_data(rb_var.get())
for i in range(cb_max_num):
cbname = (cb_identities[i])
if len(data)-1 < i:
cbname.grid_forget()
else:
cbname.config(text=data[i]) #I would place some_fun here, which gets text option of checked box
cbname.grid(row=i,column=1,sticky=W)
Update! I managed with the following code for anyone interested:
cb_var_init = [0] * cb_max_num #create the initial list of inactive checkbuttons, all 0
input_params=[] #list which needs to be populated/depopulated based on checkbutton state
def get_data(data): #populates a list with parameter from checked checkbuttons,
global cb_var_init
cb_var_list = list(map(lambda var: var.get(),list(cb_var.values())))
for i in range(len(data)):
if cb_var_list[i] > cb_var_init[i]:
input_params.append(data[i])
elif cb_var_list[i] < cb_var_init[i]:
input_params.remove(data[i])
cb_var_init = cb_var_list
return(input_params)
cb_var is a dictionary of IntVars, and data is a list of checkbuttons' names.
As for the command on each checkbutton, I used cbname.config(text=data[i],command=lambda: get_data(data)) as suggested in another topic for functions with arguments.
Now each time I check a checkbutton, I immediately get a list of parameters which should show in the next Frame, which is dynamically updated.

Tkinter - Removing n nameless Labels

I will try to exemplify as much as possible.
I am developing a program in which I have data recorded in a database.db and access it through the SQLite3 library. I created a function to return the data for a specific column that I want and it will be several Labels with the data returned.
In this program I use the .pack() method and I know that if I use the pack_forget() command, the Label will be removed from the window with every new query I make.
The problem is that as there are several Labels for each data returned, I do not create names for each Label and now I have this problem. For each search, I want to remove all Labels for the newest search. How can I remove Labels from the window if they do not have a variable name. Below is an example:
def clear():
# ???
Label.pack_forget()
def search(v):
# 'v' cames from Entry's textvariable
tmp4Label = []
var4Len = list(c.execute(
f'SELECT * FROM table WHERE element = "{v.get()}"').fetchall())
for i in range(len(var4Len)):
# Getting just the first column data
#'c' is the cursor
a.append(list(c.execute(
f'SELECT * FROM table WHERE element = "{v.get()}"').fetchall())[i][0])
for i in tmp4Label:
# Labels for each data, removing '[' and ']' characters
Label(root, text=f"{str([i]).replace('[', '').replace(']', '')}",font=('Arial 18 bold')).pack()
One solution is to store the Labels in a list:
list_of_labels = []
def clear():
while list_of_labels:
list_of_labels.pop().pack_forget() # remove from the list
def search(v):
for i in tmp4Label:
lbl = Label(root, text=i)
lbl.pack()
list_of_labels.append(lbl) # add to the list
Another solution is to use a single label instead of a stack of them.
def clear():
data_label.pack_forget()
def search(v):
global data_label
data_label = Label(root, text='\n'.join(map(str, tmp4Label)))
data_label.pack()
Whatever you do it's extremely important to use 2 lines to define the Label. The first to define it, the second to lay it out (pack it). You cannot combine those 2 lines.

How do I check if any entry boxes are empty in Tkinter?

I am making a GUI in Tkinter with Python 2.7. I have a frame with about 30 entry boxes among other things. I don't have access to the code right now, but is there a way to check if any or several of the boxes are empty so I can warn the user and ask them if they want to proceed?
Is there a way to go through each of them with a 'for' loop and check the length of each entry. Or is there a command to check if any box is empty?
You can get the content of the entry using Tkinter.Entry.get method.
Check the length of the entry using len function and get method:
if len(entry_object.get()) == 0: # empty!
# do something
or more preferably:
if not entry_object.get(): # empty! (empty string is false value)
# do something
Use for loop to check several entry widgets:
for entry in entry_list:
if not entry_object.get():
# empty!
You should populate entry_list beforehand.
Manually:
entry_list = []
entry = Entry(...)
...
entry_list.append(entry)
entry = Entry(...)
...
entry_list.append(entry)
or, using winfo_children method (to get all entries):
entry_list = [child for child in root_widget.winfo_children()
if isinstance(child, Entry)]

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.

Categories