How to dynamically control multiple Listboxes simultaneously in tkinter? - python

So, I have 5 listboxes in which I need to control at the same time, almost as if they were one listbox with columns.
I am trying to find a way in which when I select an item from any one of the listboxes and delete them, it will highlight and delete the other items in the corresponding index.
so far I am only able to delete the other indexed items only when I invoke curselection() on Listbox1, but if a user selects an item on listbox2 and calls the same, it'll throw an error because the variable is looking for listbox1.
I can't seem to find any documentation or examples of how to control multiple listboxes simultaneously anywhere.
Is it possible to have a self.listbox[0, 1, 2, 3].curselection() type of thing? or even an if statement that allows me to check if self.listbox1.curselection() == True: and then execute according.
This is the function anyway:
def removeSeq(self, event=None):
index = self.listbox1.curselection()[0]
print(index)
## self.listbox1.selection_set(1)
## selectedItem = self.listbox2.curselection()
## print(selectedItem)
## self.listbox1.delete(selectedItem)
## self.listbox2.delete(selectedItem)
## self.listbox3.delete(selectedItem)
## self.listbox4.delete(selectedItem)
## self.listbox5.delete(selectedItem)
pass
I've commented most of it out for test purposes, any help would be massively appreciated.

In your binding you can use event.widget to know which widget was clicked on. Then it's just a matter of getting the selection from that widget and applying it to the other listboxes.
Here's a simple example. To delete a row, double-click in any listbox:
import tkinter as tk
class MultiListbox(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
for i in range(5):
lb = tk.Listbox(self, height=10, exportselection=False)
lb.pack(side="left", fill="y")
for j in range(10):
lb.insert("end", f"Listbox {i+1} value {j+1}")
lb.bind("<Double-1>", self.removeSeq)
def removeSeq(self, event):
lb = event.widget
curselection = lb.curselection()
index = curselection[0] if curselection else None
for listbox in self.winfo_children():
listbox.delete(index)
root = tk.Tk()
mlb = MultiListbox(root)
mlb.pack(side="top", fill="both", expand=True)
root.mainloop()

Related

Why does my selection callback get called on my second Listbox?

from tkinter import *
from tkinter.ttk import *
root = Tk()
listbox = None
listboxMultiple = None
listboxStr = None
listboxMultipleStr = None
def main():
global root
global listboxStr
global listboxMultipleStr
global listbox
global listboxMultiple
root.protocol("WM_DELETE_WINDOW", exitApplication)
root.title("Title Name")
root.option_add('*tearOff', False) # don't allow tear-off menus
root.geometry('1600x300')
listboxStr = StringVar()
listboxStr.set("ABCD")
listbox = Listbox(root, name="lb1", listvariable=listboxStr, width=120)
listbox.pack(side=LEFT)
listbox.bind("<<ListboxSelect>>", selectListItemCallback)
listboxMultipleStr = StringVar()
listboxMultipleStr.set("")
listboxMultiple = Listbox(root, name="lb2", listvariable=listboxMultipleStr, width=120)
listboxMultiple.pack(side=LEFT)
root.mainloop()
def selectListItemCallback(event):
global listboxMultipleStr
global listbox
global listboxMultiple
print("event.widget is {} and listbox is {} and listboxMultiple is {}\n".format(event.widget, listbox, listboxMultiple))
selection = event.widget.curselection()
listboxMultipleStr.set("")
if selection:
index = selection[0]
data = event.widget.get(index)
newvalue = "{}\n{}".format(data,"SOMETHING")
print("selected \"{}\"\n".format( data ))
print("newvalue is \"{}\"\n".format( newvalue ))
listboxMultiple.insert(END, "{}".format(data))
listboxMultiple.insert(END, "SOMETHING")
#listboxMultipleStr.set( newvalue )
else:
pass
def exitApplication():
global root
root.destroy()
if __name__ == "__main__":
main()
Using python3 on Windows 7. I've setup a callback "selectListItemCallback" for one of my two listbox widgets. And yet, when I click on the text in "lb1" it works as expected, I update "lb2" with the same selected text plus I add another line to "lb2".
The issue is, when I then select the item in "lb2", it still calls the callback and the event.widget is "lb1" and not "lb2".
My intent is to have a list of items in 'lb1' and when I select any of them, then 'lb2' gets filled with info related to the selected 'lb1' item. And, I don't want a selection callback invoked in my 'lb2' widget.
Can you see what I'm doing wrong that could be causing this strange behavior?
Thank you.
I've posted the code; and this does run on my Windows 7 machine using python3. Python 3.8.6 to be exact.
It is because the event fires when the first list box loses the selection when you click on the other list box. The event fires whenever the selection changes, not just when it is set.
If you don't want the first listbox to lose its selection when you click in the second listbox, set exportselection to False for the listboxes. Otherwise, tkinter will only allow one to have a selection at a time.

How can I change the text of each button in a nested list?

I've looked around SO and tried the solutions offered, but I can't seem to change the text of any button I've made using a double for loop.
The loops are there so I can add them to a list of lists of buttons so I can (supposedly) access them conveniently through the nested list by calling board_button[2][3] or something. They are also dynamically created after the user inputs the board_size such that it generates a grid of nxn buttons so there's that. These are all done inside a class method, and there's another class method that should change the button's text when it's called by the button.
I've tried using the solutions offered here, but none of them actually worked for my problem.
Pardon the long block of code, but I honestly think the way I've made it may have contributed to the problem, and give more insight as result.
from tkinter import filedialog
from tkinter import *
class MainWindow(Frame):
board_size = None
file_input = None
board_buttons = None
board_strvars = None
row = []
def __init__ (self, parent):
Frame.__init__(self, parent)
# initialize widgets but don't show them yet
self.initWidgets()
# show the starting window
self.startWindow()
def generateBoard(self, to_forget=None):
# hides the groups of the method that called it
if to_forget != None:
for i in to_forget:
self.row[i].forget()
# get the board_size from user
self.board_size = int(self.size_entry.get())
# initialize text variables for each button
self.board_strvars = []
for i in range(self.board_size):
self.row_strvars=[]
for j in range(self.board_size):
var = StringVar()
var.set(" ")
self.row_strvars.append(var)
self.board_strvars.append(self.row_strvars)
# insert list of lists of buttons here
self.row[1].pack(fill=X)
self.board_buttons = []
for i in range(self.board_size):
self.row_buttons=[]
for j in range(self.board_size):
self.row_buttons.append(Button(self.row[1], textvariable=self.board_strvars[i][j], command=lambda:self.place(i, j)))
self.row_buttons[j].grid(row=i, column=j)
self.board_buttons.append(self.row_buttons)
# for solve and back button
self.row[2].pack(fill=X)
def initWidgets(self):
# create the rows or groups of widgets
for i in range(3):
self.row.append(Frame())
# row 0; startWindow
self.size_entry = Entry(self.row[0])
self.size_entry.pack(fill=X, side=LEFT)
self.size_button = Button(self.row[0], text="Enter", command=lambda:self.generateBoard([0]))
self.size_button.pack(fill=X, side=LEFT)
self.load_button = Button(self.row[0], text="Load", command=self.loadFile)
self.load_button.pack(fill=X, side=LEFT)
# row 2; generateBoard
self.solve_button = Button(self.row[2], text="Solve", command=self.showSolutions)
self.solve_button.pack(fill=X, side=LEFT)
self.back_button = Button(self.row[2], text="Back", command=lambda:self.startWindow(to_forget=[0,2], to_destroy=[1]))
self.back_button.pack(fill=X, side=RIGHT)
def loadFile(self):
print("file loaded!")
def place(self, i, j):
if self.board_strvars[i][j].get() == " ":
self.board_strvars[i][j].set("C")
else:
self.board_strvars[i][j].set(" ")
def showSolutions(self):
print("solutions shown!")
def startWindow(self, to_forget=None, to_destroy=None):
# hides the groups of the method that called it
if to_forget != None:
for i in to_forget:
self.row[i].forget()
# destroys the groups' child widgets and hides the group
if to_destroy != None:
for i in to_destroy:
for child in self.row[i].winfo_children():
child.destroy()
self.row[i].forget()
self.row[0].pack(fill=X)
if __name__ == "__main__":
root=Tk()
root.title("test")
app = MainWindow(root)
root.mainloop()
I originally wanted to define a function that will change the text of the button that called it. But so far I've found no way to do so.
Doing the solutions offered in the post I linked changes nothing to the buttons. In the code I provided though, I used the StringVar() to be assigned as textvariable of the button. However, it only changes the last row, last column button element no matter which button you click. It's supposed to work in a way that the button that was clicked, will get its text changed.
Thanks!
Change your lambda function to force a closure:
def generateBoard(self, to_forget=None):
...
for i in range(self.board_size):
self.row_buttons=[]
for j in range(self.board_size):
self.row_buttons.append(Button(self.row[1], textvariable=self.board_strvars[i][j], command=lambda i=i, j=j:self.place(i, j)))
self.row_buttons[j].grid(row=i, column=j)
self.board_buttons.append(self.row_buttons)
Also note that its better to not call your own method place since there is already a place method in Tk.

Identify button when generated in a for loop [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 6 months ago.
I am trying to program a minesweeper game on python using tkinter. I started off by creating a grid of buttons using a two dimensional list, and the generation of the button and everything works. The only issue I have is that I don't know how to determine which button in my grid is clicked. My goal is to be able to click on a button and through that I know the coordinates of that in my grid [row][col].
This is the code I have so far.
from tkinter import *
from functools import partial
from itertools import product
# Here, we are creating our class, Window, and inheriting from the Frame
# class. Frame is a class from the tkinter module. (see Lib/tkinter/__init__)
class Window(Frame):
# Define settings upon initialization. Here you can specify
def __init__(self, master=None):
# parameters that you want to send through the Frame class.
Frame.__init__(self, master)
#reference to the master widget, which is the tk window
self.master = master
#with that, we want to then run init_window, which doesn't yet exist
numRows = int(input("# of Rows: "))
numCols = int(input("# of Cols: "))
self.init_window(numRows, numCols)
#Creation of init_window
def init_window(self, rowNum, colNum):
# print(x, y)
# changing the title of our master widget
self.master.title("GUI")
# allowing the widget to take the full space of the root window
self.pack(fill=BOTH, expand=1)
# creating a button instance
#quitButton = Button(self, text="Exit",command=self.client_exit)
# placing the button on my window
#quitButton.place(x=0, y=0)
but = []
for row in range(0, rowNum):
curRow = []
for col in range(0, colNum):
curRow.append(Button(self, bg="gray", width=2,height=1, command=lambda: self.open_button(row, col)))
curRow[col].grid(row=row,column=col)
but.append(curRow)
#but[1][1].config(state="disabled")
#but[1][1]["text"] = "3"
#but[1][1]["bg"] = "white"
def open_button(self, r, c):
print(r, " : ", c)
# root window created. Here, that would be the only window, but
# you can later have windows within windows.
root = Tk()
root.geometry("600x600")
#creation of an instance
app = Window(root)
#mainloop
root.mainloop()
Whenever I click on the grid, it gives me the very last button...
For example, a 9x9 grid always gives me "9 : 9" whenever I click any button.
Solutions welcomed! I want an easy way to get the coordinates without changing too much of the code (if possible).
Thanks!
The row and col variables are assigned each value in the ranges. At the end of the loop that generates the buttons, the values for those variables are left at the last values in the ranges, e.g. "9 : 9".
Try replacing the line
curRow.append(Button(self, bg="gray", width=2,height=1, command=lambda: self.open_button(row, col)))
with
curRow.append(Button(self, bg="gray", width=2,height=1, command=lambda rw=row, cl=col: self.open_button(rw, cl)))
This assigns the values of row and col at the time the button is created to the variables rw and cl, which remain the same for each button as the for-loop iterates.
See this link:
Tkinter assign button command in loop with lambda

How to bind a "select all" event on a listbox?

I have two list boxes. When one listbox is selected, it triggers the end to update using the output from a function. This works fine when I click each option individually using the <<ListboxSelect>> event, however I don't know now to get it to work with a select all button. The select all button works in terms of highlighting items, but I can not get it to update the second list.
Comments are from a previous question.
from Tkinter import *
# dummy list so that the code does not relay on actually drives and files
rdrive = ['drive1','drive2','drive3']
sel_files = {'drive1': ['file1','file2'],
'drive2': ['file3','file4'],
'drive3': ['file6','file5']}
class Example(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.initUI()
def initUI(self):
self.parent.title("Listbox")
self.pack(fill=BOTH, expand=1)
# Drive Select List Box
# global rdrive
# rdrive = drive_ctypes.find_rmdrv()
# use dummy rdrive instead of physical drives. Otherwise,
# cant reproduce the problem.
self.lb = Listbox(self, height=10, selectmode=MULTIPLE)
for i in rdrive:
self.lb.insert(END, i)
self.lb.bind("<<ListboxSelect>>", self.onSelect)
self.lb.grid(row =3, column =2)
self.drives_select_b = Button(self, text = "Select All", command = self.select_all_drives)
#self.drives_select_b.bind("<Button-1>", PLACE HOLDER)
self.drives_select_b.grid(row =4, column =3)
## File Select List Box
self.flb = Listbox(self, height=10, selectmode=MULTIPLE)
self.flb.grid(row =3, column =4)
def onSelect(self, event):
# most changes are here. GUI programming is event driven, so you need
# to get the list of files for selected drive (i.e. when selection even occurs).
# Also here you respond the the even, so that the right list is populated.
# get widget (i.e. right listbox) and currently selected item(s)
widget = event.widget
selection=widget.curselection()
files_avalibe = []
# if something was selected, than get drives for which it was selected
# and retrieve files for each drive
if selection:
for drive_i in selection:
selected_drive = rdrive[drive_i]
files_avalibe += sel_files[selected_drive]
print(files_avalibe)
# once we have files from the selected drive, list them
# in the right list box
self.update_file_list(files_avalibe)
def update_file_list(self, file_list):
# updates right listbox
self.flb.delete(0, END)
for i in file_list:
self.flb.insert(END, i)
def select_all_drives(self):
self.lb.select_set(0, END)
root = Tk()
f = Example(root)
root.mainloop()
Your select_all_drives function can trigger the event:
def select_all_drives(self):
self.lb.select_set(0, END)
self.lb.event_generate("<<ListboxSelect>>")
You can reuse the code that you have in the onSelect method. All you need to do is replace event.widget with self.lb:
def select_all_drives(self):
self.lb.select_set(0, END)
selection=self.lb.curselection()
files_avalibe = []
if selection:
for drive_i in selection:
selected_drive = rdrive[drive_i]
files_avalibe += sel_files[selected_drive]
self.update_file_list(files_avalibe)
Of course, this is somewhat repetitive (both methods have identical code). It might be better to factor this out into a separate method:
def get_selected_files(self):
selection=self.lb.curselection()
files_avalibe = []
if selection:
for drive_i in selection:
selected_drive = rdrive[drive_i]
files_avalibe += sel_files[selected_drive]
return files_avalibe
and then call get_selected_files in the onSelect and select_all_drives methods:
def onSelect(self, event):
self.update_file_list(self.get_selected_files())
...
def select_all_drives(self):
self.lb.select_set(0, END)
self.update_file_list(self.get_selected_files())

Deleting widgets (involving tkinter module)

new guy here and I'm slowly getting the hang of python, but I have a question.
I have two files here
one is named first_file.py
from other_file import GameFrame
from Tkinter import Tk
def main():
tk = Tk()
tk.title("Game of Life Simulator")
tk.geometry("380x580")
GameFrame(tk)
tk.mainloop()
main()
and the other is other_file.py
from Tkinter import *
from tkFileDialog import *
class GameFrame (Frame):
def __init__(self, root):
Frame.__init__(self,root)
self.grid()
self.mychosenattribute=8
self.create_widgets()
def create_widgets(self):
for rows in range(1,21):
for columns in range(1,21):
self.columns = columns
self.rows = rows
self.cell = Button(self, text='X')
self.cell.bind("<Button-1>", self.toggle)
self.cell.grid(row=self.rows, column=self.columns)
reset = Button(self, text="Reset")
reset.bind("<Button-1>", self.reset_button)
reset.grid(row=22, column = 3, columnspan=5)
def reset_button(self, event):
self.cell.destroy()
for rows in range(1,21):
for columns in range(1,21):
self.columns = columns
self.rows = rows
self.cell = Button(self, text='')
self.cell.bind("<Button-1>", self.toggle)
self.cell.grid(row=self.rows, column=self.columns)
After I push the reset button what happens right now is one button gets destroyed and another set of buttons are made on top of the already present buttons, but I need to be able to destroy or atleast configure all buttons to be blank. So how would I do that for all the buttons since I used a for loop to generate them? (Is there a better way to generate the buttons besides using a for loop?) Thanks.
A common method is to save your objects in a list (or dictionary) in order to access them when needed. A simple example:
self.mybuttons = defaultdict(list)
for rows in range(1,21):
for columns in range(1,21):
self.mybuttons[rows].append(Button(self, text=''))
Then you can get buttons, this way:
abutton = self.mybuttons[arow][acolumn]
There are some problems with your code that prevent running it (indentation of the reset lines and the use of the undefined self.toggle), so I could not fix it, but this example should be enough for you to do it.

Categories

Resources