Python Tkinter- Remember State of Created New Window (Checkboxes) - python

I have a button called view and when it is clicked on it will create a new window with a list in a listbox and a list of checkboxes that correspond to the listbox. When switching through the items in the listbox you will see that the listbox items are independent of each other and will have their own check box values. As of right now it will remember the checkbox values for each listbox item except when you close the created window. When you close the second window that was created all of that information disappears when you try and open the window back up again. I need a way to create a second window, do everything it does now with the listbox and checkboxes, but when closed it can be opened again and pick up where you left off.
For example, if I highlight the first item in the listbox and check the first checkbox, I should be able to close that window and open it again and when the first item in the listbox is highlighted, I see that there is a check in the first checkbox.
import tkinter
from tkinter import *
def myfunction(event):
canvas1.configure(scrollregion=canvas1.bbox("all"))
def onselect(evt):
# Note here that Tkinter passes an event object to onselect()
w = evt.widget
x = 0
index = int(w.curselection()[0])
value = w.get(index)
print('You selected item %d: "%s"' % (index, value))
for y in enable:
for item in list_for_listbox:
checkbuttons[item][y][1].grid_forget()
checkbuttons[value][y][1].grid(row=x, column=0)
# Label(frame2, text="some text").grid(row=x, column=1)
x += 1
def printcommand():
for item in list_for_listbox:
for y in enable:
print(item + " [" + y + "] " + str(checkbuttons[item][y][0].get()))
def create_new_window():
global new_window
new_window = tkinter.Toplevel()
new_window.geometry("750x500")
new_window_commands()
master = tkinter.Tk()
master.title("Checkboxes test")
master.geometry("750x500")
button1 = Button(master, command =create_new_window,text="View")
button1.place(x=50,y=250)
def new_window_commands():
# enable = ['button 1', 'button 2', 'button 3', 'button 4', 'button 5', 'button 6', 'button 7']
global list_for_listbox
global enable
global checkbuttons
global canvas1
enable = []
for x_number_of_items in range(1, 15):
enable.append("button " + str(x_number_of_items))
list_for_listbox = ["one", "two", "three", "four"]
listbox = Listbox(new_window)
listbox.place(x=5, y=5, width=100, height=10 + 16*len(list_for_listbox))
listbox.update()
frame1 = Frame(new_window, borderwidth=1, relief=GROOVE, highlightthickness=1, highlightbackground="black",
highlightcolor="black")
frame1.place(x=listbox.winfo_width() + 10, y=5, width=300, height=listbox.winfo_height())
canvas1 = Canvas(frame1)
frame2 = Frame(canvas1, height=500)
scrollbar1 = Scrollbar(frame1, orient="vertical", command=canvas1.yview)
canvas1.configure(yscrollcomman=scrollbar1.set)
scrollbar1.pack(side="right", fill="y")
canvas1.pack(side="left")
canvas1.create_window((0, 0), window=frame2, anchor='nw')
frame2.bind("<Configure>", myfunction)
printbutton = Button(new_window, text="Print", command=printcommand)
printbutton.place(x=100, y=250)
checkbuttons = {}
for item in list_for_listbox:
listbox.insert(END, item)
checkbuttons[item] = (dict())
for y in enable:
temp_var = BooleanVar()
checkbuttons[item][y] = [temp_var, Checkbutton(frame2, text=y, variable=temp_var)]
listbox.bind('<<ListboxSelect>>', onselect)
print(enable)
mainloop()
printcommand()

With your current structure, the simplest fix would be:
Only create the new_window once.
withdraw() new_window instead of letting it close each time.
Open the same instance of new_window again when called upon.
You'll need to implement the following:
# Default your new_window to None
new_window = None
def create_new_window():
global new_window
# If new_window doesn't exist, create a new one
if not new_window:
new_window = tkinter.Toplevel()
new_window.geometry("750x500")
# add a new protocol to redirect on closing the window.
new_window.protocol("WM_DELETE_WINDOW", hide_window)
new_window_commands()
else:
# if new_window already exist, just unhide it
new_window.deiconify()
# add a new function for when window is closing
def hide_window():
global new_window
new_window.withdraw()
You might also want to add the same protocol method under master so that when it closes, destroy both master and new_window object:
master.protocol('WM_DELETE_WINDOW', destroy_all)
def destroy_all():
global master
global new_window
master.destroy()
new_window.destroy()
If possible, for your next tkinter code I would suggest considering an object oriented approach though. I will see if I can provide a short sample here later.
As a side note, while I understand a lot of documentations in tkinter uses the from tkinter import * approach, I would discourage this practice and advise to import tkinter as tk instead (or as you already did, import tkinter, which accomplishes the same thing). See relevant answer here
Here's a quick sample of OOP approach in a similar vein:
import tkinter as tk
# Here the main window can be called upon as its own instance with its own instance attributes.
class Window(tk.Tk):
def __init__(self):
super().__init__()
self.main_button = tk.Button(self, text="Creat a sub window", command=self.open_sub_window)
self.main_button.pack()
# define the things you wish to retain under the main window as an instance attribute
self.sub_check_var = tk.BooleanVar()
self.sub_entry_var = tk.StringVar()
# when creating a new window, just reference back to the main attributes you've already created.
def open_sub_window(self):
self.sub_window = tk.Toplevel()
tk.Checkbutton(self.sub_window, text="I'm a checkbox!", variable=self.sub_check_var).pack()
lbl_frm = tk.LabelFrame(self.sub_window, text="I am an entry!")
lbl_frm.pack()
tk.Entry(lbl_frm, text=self.sub_entry_var).pack()
gui = Window()
gui.mainloop()
Note this is but one way to do it. You just need to feel around to get comfortable with your implementation, there's no right/wrong way to do things.

Related

how to change the color of PanedWindow upon hovering over it, for multiple PanedWindow in tkinter?

I am trying to make PanedWindow change color when I hover mouse over it in tkinter.
now this works for a single iteration.
but when i try to do it for multiple panedwindows it only changes color of the last window.
import tkinter as tk
root = tk.Tk()
for i in range(10):
m1 = tk.PanedWindow(root, bd=4, relief="flat", bg="blue")
m1.pack()
def on_enter(e):
m1.config(background='OrangeRed3', relief="flat")
def on_leave(e):
m1.config(background='SystemButtonFace', relief="flat")
# Create a Button
button1 = tk.Button(m1, text=f"{i}")
button1.pack(pady=20)
# Bind the Enter and Leave Events to the Button
m1.bind('<Enter>', on_enter)
m1.bind('<Leave>', on_leave)
m1.add(button1)
tk.mainloop()
Since at each iteration of the loop all variables are overwritten, the functions are bound to the last created element. It is necessary to pass the desired element to the function. It is even better to collect everything created in dictionaries, so that in the future you can easily change them.
import tkinter as tk
from functools import partial
ms = {}
btns = {}
root = tk.Tk()
def on_enter(m, e):
m.config(background='OrangeRed3', relief="flat")
def on_leave(m, e):
m.config(background='SystemButtonFace', relief="flat")
for i in range(10):
ms[i] = tk.PanedWindow(root, bd=4, relief="flat", bg="blue")
ms[i].pack()
# Create a Button
btns[i] = tk.Button(ms[i], text=f"{i}")
btns[i].pack(pady=20)
# Bind the Enter and Leave Events to the Button
ms[i].bind('<Enter>', partial(on_enter, ms[i]))
ms[i].bind('<Leave>', partial(on_leave, ms[i]))
ms[i].add(btns[i])
tk.mainloop()

Tkinter destroy button create by a loop

I want to create a reminder to remind me of the time I have chosen in the combobox by pressing the confirmed button. I put the time in a label and also create a delete button that can delete the label and button itself in the same row by a loop. It works if there's only one label, but if I increased the number of it it can only destroy the last label and button.
below is my code:
class final:
def __init__(self,app):
self.savelist=[]
self.time= StringVar()
self.timecombo = ttk.Combobox(app,textvariable=self.time)
self.timecombo.grid(row=0,column=1)
self.timecombo.config(value =('1:00','2:00','3:00','4:00','5:00','6:00','7:00','8:00','9:00','10:00','11:00','12:00'))
self.button1=Button(app,text='confirmed',command=self.save)
self.button1.grid(row=3,column=2)
***def save(self):
savetext = self.time.get()
self.savelist.append(savetext)
self.deletebutton_list = []
self.savelabel_list = []
for i in range(len(self.savelist)):
savelabel = Label(app, text=self.savelist[i])
savelabel.grid(row=4 + i, column=0)
self.savelabel_list.append((savelabel))
deletebutton = Button(app, text='delete' , command=functools.partial(self.deletelabel,idx=i))
deletebutton.grid(row=4 + i, column=1)
self.deletebutton_list.append(deletebutton)
def deletelabel(self, idx):
self.savelabel_list[idx].destroy()
self.deletebutton_list[idx].destroy()
self.savelist.remove(self.savelist[idx])
self.savelabel_list.remove(self.savelabel_list[idx])
self.deletebutton_list.remove(self.deletebutton_list[idx])***
app = Tk()
a = final(app)
app.title('things to do')
app.geometry("500x300+200+200")
app.mainloop()
I believed that there must be something wrong in the loop or the function deletelabel but I still can't fix it.
self.savelabel_list.remove(self.savelabel_list[idx])
Do not change the list. If you delete label/button #1, then label #2 becomes #1, and so when you press the button to delete label #2, it deletes label #3 because everything has moved up. Also, note that every time you call "save()" it creates a new set of widgets that overlay the old widgets, which will eventually slow down the computer. Create and grid the new time label only. Keep track of the row with a self.next_row variable (or whatever) and increment it by one each time.
This is a question that points out how usable classes are. Create a new class, with label and a close button, for each reminder.
from tkinter import *
from tkinter import ttk
class NewButton:
def __init__(self, master, label_text, this_row):
## put everything in a new frame so destroying
## one frame will destroy everything in it
self.fr=Frame(master)
self.fr.grid(row=this_row, column=1)
Label(self.fr, text=label_text).grid(row=0, column=1)
Button(self.fr, text="Close This",
command=self.fr.destroy).grid(row=0, column=2)
class Final:
def __init__(self,app):
self.app=app
self.this_row=4
self.time_var= StringVar()
self.timecombo = ttk.Combobox(app,textvariable=self.time_var)
self.timecombo.grid(row=0,column=1)
self.button1=Button(app,text='confirmed',command=self.save)
self.button1.grid(row=3,column=2)
def save(self):
save_text = self.time_var.get()
self.this_row += 1
next_button=NewButton(self.app, save_text, self.this_row)
self.time_var.set("")
app = Tk()
a = Final(app)
app.title('things to do')
app.geometry("500x300+200+200")
app.mainloop()

Update label of tkinter menubar item?

Is it possible to change the label of an item in a menu with tkinter?
In the following example, I'd like to change it from "An example item" (in the "File" menu) to a different value.
from tkinter import *
root = Tk()
menu_bar = Menu(root)
file_menu = Menu(menu_bar, tearoff=False)
file_menu.add_command(label="An example item", command=lambda: print('clicked!'))
menu_bar.add_cascade(label="File", menu=file_menu)
root.config(menu=menu_bar)
root.mainloop()
I found the solution myself in the Tcl manpages:
Use the entryconfigure() method like so, which changes the value after it has been clicked:
The first parameter 1 has to be the index of the item you want to change, starting from 1.
from tkinter import *
root = Tk()
menu_bar = Menu(root)
def clicked(menu):
menu.entryconfigure(1, label="Clicked!")
file_menu = Menu(menu_bar, tearoff=False)
file_menu.add_command(label="An example item", command=lambda: clicked(file_menu))
menu_bar.add_cascade(label="File", menu=file_menu)
root.config(menu=menu_bar)
root.mainloop()
I do not know if that used to be different on 2.7, but it does not work on 3.4 anymore.
On python 3.4 you should start counting entries with 0 and use entryconfig.
menu.entryconfig(0, label = "Clicked!")
http://effbot.org/tkinterbook/menu.htm
Check this dynamic menu example. The main feature here is that you don't need to care about a serial number (index) of your menu item. No index (place) of your menu is needed to track. Menu item could be the first or the last, it doesn't matter. So you could add new menus without index tracking (position) of your menus.
The code is on Python 3.6.
# Using lambda keyword and refresh function to create a dynamic menu.
import tkinter as tk
def show(x):
""" Show your choice """
global label
new_label = 'Choice is: ' + x
menubar.entryconfigure(label, label=new_label) # change menu text
label = new_label # update menu label to find it next time
choice.set(x)
def refresh():
""" Refresh menu contents """
global label, l
if l[0] == 'one':
l = ['four', 'five', 'six', 'seven']
else:
l = ['one', 'two', 'three']
choice.set('')
menu.delete(0, 'end') # delete previous contents of the menu
menubar.entryconfigure(label, label=const_str) # change menu text
label = const_str # update menu label to find it next time
for i in l:
menu.add_command(label=i, command=lambda x=i: show(x))
root = tk.Tk()
# Set some variables
choice = tk.StringVar()
const_str = 'Choice'
label = const_str
l = ['dummy']
# Create some widgets
menubar = tk.Menu(root)
root.configure(menu=menubar)
menu = tk.Menu(menubar, tearoff=False)
menubar.add_cascade(label=label, menu=menu)
b = tk.Button(root, text='Refresh menu', command=refresh)
b.pack()
b.invoke()
tk.Label(root, textvariable=choice).pack()
root.mainloop()

Tkinter - How would I go about resetting list of objects?

Yesterday I asked this question Creating elements by loop Tkinter to find out how to dynamically create some bullet points. Now I'm looking to add a clear button so when pressed, will reset the entire form. I have tried setting the list back to [] but it didn't work.
edit - So basically when I press reset I'd like it to look exactly like it did when the form was loaded.
The buttons are removed with the destroy method:
for button in self.button:
button.destroy()
import Tkinter as tk
class ButtonBlock(object):
def __init__(self, master):
self.master = master
self.button = []
self.button_val = tk.IntVar()
entry = tk.Entry()
entry.grid(row=0, column=0)
entry.bind('<Return>', self.onEnter)
entry.focus()
clear_button = tk.Button(master, text='Clear', command=self.onClear)
clear_button.grid(row=0, column=1)
def onClear(self):
for button in self.button:
button.destroy()
def onEnter(self, event):
entry = event.widget
num = int(entry.get())
self.onClear()
for i in range(1, num+1):
self.button.append(tk.Radiobutton(
self.master, text=str(i), variable=self.button_val, value=i,
command=self.onSelect))
self.button[-1].grid(sticky='WENS', row=i, column=0, padx=1, pady=1)
def onSelect(self):
print(self.button_val.get())
if __name__ == '__main__':
root = tk.Tk()
ButtonBlock(root)
root.mainloop()
Setting the list back (i.e. using self.button = []) just clears the data stored in the button variable. That action alone is not connected to the user interface (UI). You have to explicitly remove the widget objects which were created (by the onEnter method).
So the clearing feature you are looking for should be feasible by extending the answer from your previous question. Add an onClear method to the ButtonBlock class so that when your "Clear" control (i.e. using a button widget) is selected its callback function calls ButtonBlock.onClear(), similar to how your Entry widget invokes the onEnter method.
EDIT: See unutbu's answer to this question. When selected, the clear_button control calls ButtonBlock.onClear(). The for loop in onClear gets a reference to each button ojbect from the button list and calls the object's destroy method, which removes it from the UI.

How to delete Tkinter widgets from a window?

I have a list of tkinter widgets that I want to change dynamically.
How to delete the widgets from the window?
You can call pack_forget to remove a widget (if you use pack to add it to the window).
Example:
from tkinter import *
root = Tk()
b = Button(root, text="Delete me", command=lambda: b.pack_forget())
b.pack()
root.mainloop()
If you use pack_forget, you can later show the widget again calling pack again. If you want to permanently delete it, call destroy on the widget (then you won't be able to re-add it).
If you use the grid method, you can use grid_forget or grid_remove to hide the widget.
One way you can do it, is to get the slaves list from the frame that needs to be cleared and destroy or "hide" them according to your needs. To get a clear frame you can do it like this:
from tkinter import *
root = Tk()
def clear():
list = root.grid_slaves()
for l in list:
l.destroy()
Label(root,text='Hello World!').grid(row=0)
Button(root,text='Clear',command=clear).grid(row=1)
root.mainloop()
You should call grid_slaves(), pack_slaves() or slaves() depending on the method you used to add the widget to the frame.
You simply use the destroy() method to delete the specified widgets like this:
lbl = tk.Label(....)
btn = tk.Button(....., command=lambda: lbl.destroy())
Using this you can completely destroy the specific widgets.
You say that you have a list of widgets to change dynamically. Do you want to reuse and reconfigure existing widgets, or create all new widgets and delete the old ones? It affects the answer.
If you want to reuse the existing widgets, just reconfigure them. Or, if you just want to hide some of them temporarily, use the corresponding "forget" method to hide them. If you mapped them with pack() calls, you would hide with pack_forget() (or just forget()) calls. Accordingly, grid_forget() to hide gridded widgets, and place_forget() for placed widgets.
If you do not intend to reuse the widgets, you can destroy them with a straight destroy() call, like widget.destroy(), to free up resources.
clear_btm=Button(master,text="Clear") #this button will delete the widgets
clear_btm["command"] = lambda one = button1, two = text1, three = entry1: clear(one,two,three) #pass the widgets
clear_btm.pack()
def clear(*widgets):
for widget in widgets:
widget.destroy() #finally we are deleting the widgets.
Today I learn some simple and good click event handling using tkinter gui library in python3, which I would like to share inside this thread.
from tkinter import *
cnt = 0
def MsgClick(event):
children = root.winfo_children()
for child in children:
# print("type of widget is : " + str(type(child)))
if str(type(child)) == "<class 'tkinter.Message'>":
# print("Here Message widget will destroy")
child.destroy()
return
def MsgMotion(event):
print("Mouse position: (%s %s)" % (event.x, event.y))
return
def ButtonClick(event):
global cnt, msg
cnt += 1
msg = Message(root, text="you just clicked the button..." + str(cnt) + "...time...")
msg.config(bg='lightgreen', font=('times', 24, 'italic'))
msg.bind("<Button-1>", MsgClick)
msg.bind("<Motion>", MsgMotion)
msg.pack()
#print(type(msg)) tkinter.Message
def ButtonDoubleClick(event):
import sys; sys.exit()
root = Tk()
root.title("My First GUI App in Python")
root.minsize(width=300, height=300)
root.maxsize(width=400, height=350)
button = Button(
root, text="Click Me!", width=40, height=3
)
button.pack()
button.bind("<Button-1>", ButtonClick)
button.bind("<Double-1>", ButtonDoubleClick)
root.mainloop()
Hope it will help someone...
You can use forget method on the widget
from tkinter import *
root = Tk()
b = Button(root, text="Delete me", command=b.forget)
b.pack()
b['command'] = b.forget
root.mainloop()
I found that when the widget is part of a function and the grid_remove is part of another function it does not remove the label. In this example...
def somefunction(self):
Label(self, text=" ").grid(row = 0, column = 0)
self.text_ent = Entry(self)
self.text_ent.grid(row = 1, column = 0)
def someotherfunction(self):
somefunction.text_ent.grid_remove()
...there is no valid way of removing the Label.
The only solution I could find is to give the label a name and make it global:
def somefunction(self):
global label
label = Label(self, text=" ")
label.grid(row = 0, column = 0)
self.text_ent = Entry(self)
self.text_ent.grid(row = 1, column = 0)
def someotherfunction(self):
global label
somefunction.text_ent.grid_remove()
label.grid_remove()
When I ran into this problem there was a class involved, one function being in the class and one not, so I'm not sure the global label lines are really needed in the above.

Categories