Identify button when generated in a for loop [duplicate] - python

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

Related

How to dynamically control multiple Listboxes simultaneously in tkinter?

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

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.

Inserting new rows in Tkinter grid

Let's say I have a Tkinter app with 2 rows displaying 2 widgets:
from tkinter import *
from tkinter.ttk import *
root = Tk()
Label(root, text="Some Data").grid(row=0)
Label(root, text="Some Data").grid(row=1)
root.mainloop()
Now this will display two widgets on row0 and row1.
Now if I want to insert another (one or more) widget between these two rows at a later stage (say as a response to a button click event), what would be the best way to do that.
Current output:
Some Data
Some Data
Expected output:
Some Data
<<New data>>
Some Data
<<New Data>> will be inserted at a later stage as a response to a button click.
<<New Data>> may be one or more rows.
I do have a simple solution for you.
If you are expecting to insert a widget later and you know you will be then you can simply place your 2nd label on grid row 2 and then place your new widget on grid row 1 later. If you need to have more than one row you could place your 2nd label even further down the line.
from tkinter import *
from tkinter.ttk import *
root = Tk()
def add_new_data():
Label(root, text="<<New Data>>").grid(row=1)
Label(root, text="Some Data").grid(row=0)
Label(root, text="Some Data").grid(row=2)
Button(root, text="Add New Data", command=add_new_data).grid(row=3)
root.mainloop()
Results:
The reason this works is because Tkinter's geometry manager will collapse rows and columns to nothing if there is nothing in them so you can use this behavior to your advantage when working with something like this.
Now if you wanted something that could work with any number of label then we can use a list to help us accomplish that.
My next example with be written in class and will show the use of a list to do what we want.
We can store widgets in a list and because we can do this we are also able to decide where in that list to put stuff and use the lists index to our advantage when setting up the grid.
First thing is to create our Some Data labels and append them to a list. The next is the add the button to that list at the end of the list. This button we will used to call a class method that will insert() a new label into our list.
Next that same method will forget the grid for all widgets inside of our label frame and then it will perform a for loop over the list and re add all the old widgets and the new one in the correct order.
Take a look at the below example.
import tkinter as tk
class App(tk.Frame):
def __init__(self, master, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.master = master
self.label_frame = tk.Frame(self.master)
self.label_frame.grid()
self.label_list = []
for i in range(2):
self.label_list.append(tk.Label(self.label_frame, text="Some Data"))
self.label_list[i].grid(row=i)
self.label_list.append(tk.Button(self.label_frame, text="Add new data", command=self.add_new_data))
self.label_list[2].grid(row=2)
def add_new_data(self):
self.label_list.insert(1, tk.Label(self.label_frame, text="<<New Data>>"))
for widget in self.label_frame.children.values():
widget.grid_forget()
for ndex, i in enumerate(self.label_list):
i.grid(row=ndex)
if __name__ == "__main__":
root = tk.Tk()
my_app = App(root)
root.mainloop()
Results:
We can add as many new labels as we like.

Is there a way to clear all widgets from a tkinter window in one go without referencing them all directly?

I'm trying to clear a tkinter window completely. However, I need a way to clear every widget on the window all at once without using pack.forget().
You could use a simple recursive loop to list all the children wigets of your main window :
def all_children (window) :
_list = window.winfo_children()
for item in _list :
if item.winfo_children() :
_list.extend(item.winfo_children())
return _list
Then just use this list :
widget_list = all_children(window)
for item in widget_list:
item.pack_forget()
What you need to do is set a frame to your main window and place everything that is to be cleared out at some point inside that frame. Then you simply do frame_name.destroy()
The following example has a button that creates a frame with several label widgets and a button.
The button calls a method that will destroy the frame and everything in it.
Then you can create the frame again with the first button.
Let me know if you have any question:
import tkinter as tk
class ExampleApp(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.some_frame = None
tk.Button(self.master, text="Create new frame with widgets!", command = self.create_stuff).pack()
def create_stuff(self):
if self.some_frame == None:
self.some_frame = tk.Frame(self.master)
self.some_frame.pack()
for i in range(5):
tk.Label(self.some_frame, text = "This is label {}!".format(i+1)).pack()
tk.Button(self.some_frame, text="Destroy all widgets in this frame!",
command= self.destroy_some_frame).pack()
def destroy_some_frame(self):
self.some_frame.destroy()
self.some_frame = None
root = tk.Tk()
my_example = ExampleApp(root)
root.mainloop()
You can use destroy method for each widget for example if it's a button you write btn1.destroy() and do it for all widgets. The forget method isn't recommended for it only remove the widgets from appearance.

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