Passing data between windows in Tkinter using object-oriented approach - python-3.x

Working with tkinter, I have a main window that reads from a TSV file and creates a list of the rows and their values, and creates buttons based on each of the items. When you click a button, it opens a secondary TopLevel to edit the item you clicked. When completed, I want to pass the saved data back to the main window so it can be saved back to the list which can then be fully written back to the TSV.
I've searched and perused several SO and blog posts on this topic, the closest one being this:
How to pass data between top levels in tkinter
However, in this solution creates the widgets for the new (edit) window in the same class as the root and I'd like to separate the work for this into its own class. The problem is, when I pass the saved value back to the main window, I get an error (see below after code).
Here is a simplified version of the code I'm using (note, each of these classes will be in their own separate file, but for the sake of this demo, they are combined.
#!/usr/bin/python3
from tkinter import *
from tkinter import ttk
import os
from tkinter import messagebox
class MainApp:
def __init__(self, master):
self.testVar = "none"
self.master = master
ttk.Button(self.master, text = "Open Window", command=lambda: NewWindow(self.master)).pack()
class NewWindow(Toplevel):
def __init__(self, master = None):
super().__init__(master = master)
self.title("New Window")
self.geometry("200x200")
label = Label(self, text ="This is a new Window")
label.pack()
ttk.Button(self, text = "Save", command=lambda: self.SaveData()).pack()
def SaveData(self):
messagebox.showinfo(title=None, message="Save complete: " + self.master.testVar)
def main():
os.system('cls') # on windows
root = Tk()
MainApp(root)
root.mainloop()
if __name__ == "__main__": main()
Here is the error:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python39\lib\tkinter\__init__.py", line 1885, in __call__
return self.func(*args)
File "C:\repos\legend-bowl\test.py", line 22, in <lambda>
ttk.Button(self, text = "Save", command=lambda: self.SaveData()).pack()
File "C:\repos\legend-bowl\test.py", line 25, in SaveData
messagebox.showinfo(title=None, message="Save complete: " + self.master.testVar)
File "C:\Python39\lib\tkinter\__init__.py", line 2347, in __getattr__
return getattr(self.tk, attr)
AttributeError: '_tkinter.tkapp' object has no attribute 'testVar'
I would think that the instance of NewWindow should be able to access values off of its parent since the instance of the parent is passed into its constructor. I feel like I'm missing something simple that the other solutions haven't been able to answer for me.
Should I be able to access from the root on its children?
Obviously, there might be better ways to handle this that I'm not thinking of, so if you offer that as a solution, I'd still like to know the answer to #1

The problem is that self.master doesn't refer to the instance of MainApp, it refers to the root window. This is because you don't pass in the instance of MainApp to NewWindow, and self.master refers to a variable defined by tkinter.
You need to pass the instance of MainApp to NewWindow, save it, and then refer to it when trying to access testVar.
One way would be to do something like this (though personally I think a function would be better than lambda here):
ttk.Button(self.master, text = "Open Window", command=lambda: NewWindow(self, self.master)).pack()
Next, you have to define NewWindow to accept and save the parameter, and use it instead of self.master
class NewWindow(Toplevel):
def __init__(self, mainapp, master = None):
super().__init__(master = master)
self.mainapp = mainapp
...
def SaveData(self):
messagebox.showinfo(title=None, message="Save complete: " + self.mainapp.testVar)

Related

How to open two modules in one window, Python Tkinter

I have a question: i am tossing the code from 2ch files, i have already lost ideas. Calling fileA.py opens a window for me with two buttons exit and start. Exit works but when I click start I need to open the second window fileB.pt. (I want both windows to open in one window) Seemingly works only problem I have is it doesn't open "window on window" but "docks underneath" and I have the effect of two windows open :/. Please help, thank you in advance:) Python 3.10
fileA.py
import tkinter as tk
from GUI.module.scale_of_img import get_scale
class FirstPage:
def __init__(self, root):
self.root = root
def get_settings(self):
# Window settings
self.root.title('....')
self.root.resizable(False, False)
self.root.geometry("1038x900")
if __name__ == '__main__':
first = FirstPage(tk.Tk())
first.get_run_first_page()
fileB.py
import tkinter as tk
"importy..."
''' The second side of the application '''
class SecondPage:
def __init__(self, root=None):
self.root = root
self.my_canvas = tk.Canvas(self.root, width=1038, height=678)
self.my_canvas.pack(fill="both", expand=True)
if __name__ == '__main__':
second = SecondPage(tk.Tk())
second.get_run()
in order to put two "windows" in the same "window" you need to put all items inside a Frame, which is basically a container than you can simply pack when you want everything to show and unpack when you want everything in it to disapear.
all items in the first window will be children of a frame and all items in the second window will be children of another frame, and to switch you just need to call pack_forget() on one and pack() on another.
for the first file
class FirstPage:
def __init__(self, root):
self.root = root
self.frame = tk.Frame(root)
self.frame.pack(expand=True)
def get_picture(self):
# all items inside this window must be children of self.frame
self.my_canvas = tk.Canvas(self.frame, width=1038, height=500)
...
def get_second_page(self):
from GUI.module.second_page import SecondPage
self.frame.pack_forget() # to hide first page
# self.frame.destroy() # if you are never brining it back
SecondPage(self.root).get_run()
and for the second file
class SecondPage:
def __init__(self, root=None):
self.root = root
self.frame = tk.Frame(root) # new frame
self.frame.pack(expand=True)
self.my_canvas = tk.Canvas(self.frame, width=1038, height=678)
self.my_canvas.pack(fill="both", expand=True)
def get_button(self):
# Add buttons
# all here should be children of self.frame now
button1 = tk.Button(self.frame, text="...", )
...
you could destroy the first frame when you switch over to save some resources if you don't intend to return to it ever again, but the difference in memory is negligible.
assuming what you want is another Tk window to open, you shouldn't give it the same root, instead use an instance of Toplevel
from tkinter import Toplevel
# class definition here
def get_second_page(self):
from GUI.module.second_page import SecondPage
SecondPage(Toplevel(self.root)).get_run()
passing the Toplevel as a child of self.root is necessary, but note that the two windows have different roots.
Edit: turns out this wasn't what the OP ment by "window on window" -_-, but it am keeping it here for other readers.

How can I create a multiple buttons in tkinter that change the background image when hovering over them?

I am coding a GUI for a Mathematics Formula Calculator. I want to create multiple buttons that change the background image when hovering over them, and I don't really know how to go about doing that...
I have already tried creating a class for the button itself so that I can modify the behaviour of it, it did not work...
import tkinter as tk
class HoverButton(tk.Button):
def __init__(self, master, **kw):
tk.Button.__init__(self, master=master,**kw)
self.defaultbackground = tk.PhotoImage(file = "GeometricBackground.png")
self.currentbackground = tk.PhotoImage(file = "")
self.bind("<Enter>", self.on_enter)
self.bind("<Leave>", self.on_leave)
def on_enter(self, currentbackground):
image = tk.Label(self, image = currentbackground)
image.pack()
def on_leave(self):
image = tk.Label(self, image = self.defaultbackground)
image.pack()
root = tk.Tk()
classButton = HoverButton(root, currentbackground = "MainMenu.png")
classButton.grid()
root.mainloop()
I was really hoping this would cut it, but I got this error message when it executed:
_tkinter.TclError: unknown option "-currentbackground"
Any help would be appreciated :)
There are several issues with your code:
The error you get is because you are trying to pass the currentbackground option to your HoverButton but given the way your class is defined:
def __init__(self, master, **kw):
tk.Button.__init__(self, master=master,**kw)
currentbackground ends into the kw dictionary you pass in argument to the standard tkinter Button class which has no currentbackground option, hence the unknown option error. To fix it, you can define the options specific to your class like
def __init__(self, master, defaultbackground="", currentbackground="", **kw):
tk.Button.__init__(self, master=master, **kw)
so that defaultbackground and currentbackground won't end in the kw dictionary.
When an event occurs, the function you bound to this event is executed with one argument, the "event" object that contains information about the event (like the pointer coordinates, the widget in which the event happened ...) so you need to add this argument when you define on_enter() and on_leave().
You are creating a label to put the image inside then packing this label in the button. This is overly complicated (and probably will result in the button not reacting to click events). The button class has an image option to set the background image of the button, so you can change the image with button.configure(image=<image>).
Inserting all those changes in the code gives
import tkinter as tk
class HoverButton(tk.Button):
def __init__(self, master, defaultbackground="GeometricBackground.png", currentbackground="", **kw):
tk.Button.__init__(self, master=master, **kw)
self.defaultbackground = tk.PhotoImage(file=defaultbackground)
self.currentbackground = tk.PhotoImage(file=currentbackground)
self.configure(image=self.defaultbackground)
self.bind("<Enter>", self.on_enter)
self.bind("<Leave>", self.on_leave)
def on_enter(self, event):
self.configure(image=self.currentbackground)
def on_leave(self, event):
self.configure(image=self.defaultbackground)
root = tk.Tk()
classButton = HoverButton(root, currentbackground="MainMenu.png")
classButton.grid()
root.mainloop()

Python 3 Tkinter button command not working (very specific scenario)

I am using these calendar modules found in this post for my program, with some slight modifications to the imports to make it work for the latest python version.
I'll just show the snippets of my code that I feel does matter to this problem.
So I have this pop up window that I made that I use for alerts:
#class for pop-up windows for alerts, errors etc.
class PopUpAlert():
def __init__(self, alert='Alert!'):
self.root = tk.Tk()
tk.Label(self.root,
text=alert,
font="Verdana 15",
fg='red',
padx=10,
pady=5).pack(side=tk.TOP)
self.root.bind('<Return>', (lambda event: self.ok()))
tk.Button(self.root,
text='ok',
pady=10,
command=self.ok).pack(side=tk.TOP)
def ok(self):
print('ok clicked')
self.root.destroy()
The function ok was made just for me to test if the function is even being called. This window works completely fine in my code, except when I try to implement with the calendar, where the "ok" button of my PopUpAlert (which is supposed to destroy the window) stops working:
class CalendarDialog(tkSimpleDialog.Dialog):
"""Dialog box that displays a calendar and returns the selected date"""
def body(self, master):
self.calendar = ttkcalendar.Calendar(master)
self.calendar.pack()
def apply(self):
self.result = self.calendar.selection
def validate(self):
if self.calendar.selection == None:
PopUpAlert(alert='Please select a date or click cancel!')
return False
return True
The calendar has an "ok" button that is used to confirm selection of the date and close the calendar window. What I was trying to do is make it such that the user cannot click "ok" to close the window if he/she has not picked a date. For that, I used the function validate which is pre-defined in the class tkSimpleDialog.Dialog which my CalendarDialog inherits from. I overwrote the function in my CalendarDialog class to call up PopUpAlert, then returned False to the parent function ok (which is called when the "Ok" button is pressed on the calendar window):
def ok(self, event=None):
if not self.validate():
self.initial_focus.focus_set() # put focus back
return
self.withdraw()
self.update_idletasks()
self.apply()
self.cancel()
def cancel(self, event=None):
# put focus back to the parent window
self.parent.focus_set()
self.destroy()
(The whole thing can be found in the tkSimpleDialog file that's linked in the other SO page that I linked above.)
After commenting out lines one by one I found that the "ok" button on my PopUpAlert only didn't work when self.root.destroy() isn't called on the calendar. Why? How do I fix this?
I already tried changing my PopUpAlert to be a Toplevel window, which also didn't work.
It would be a lot nicer of you to provide a mcve instead of asking us to make it.
The problem is that a dialog by default disables clicks to other windows, including windows it spawns. To fix this you need to use a Toplevel instead of Tk (as mentioned) AND add this line of code to the end of PopUpAlert.__init__:
self.root.grab_set()
It would be a lot neater if you subclassed Toplevel rather than that weird wrapper. Here's a mcve:
try:
import Tkinter as tk
import tkSimpleDialog as sd
except:
import tkinter as tk
from tkinter import simpledialog as sd
#class for pop-up windows for alerts, errors etc.
class PopUpAlert(tk.Toplevel):
def __init__(self, master, alert='Alert!', **kwargs):
tk.Toplevel.__init__(self, master, **kwargs)
tk.Label(self,
text=alert,
font="Verdana 15",
fg='red',
padx=10,
pady=5).pack(side=tk.TOP)
self.bind('<Return>', self.ok)
tk.Button(self,
text='ok',
pady=10,
command=self.ok).pack(side=tk.TOP)
self.grab_set() # this window only gets commands
def ok(self, *args):
print('ok clicked')
self.destroy()
class CalendarDialog(sd.Dialog):
"""Dialog box that displays a calendar and returns the selected date"""
def body(self, master):
self.calendar = tk.Label(master, text="Whatever you do, don't click 'OK'!")
self.calendar.pack()
def validate(self):
PopUpAlert(self, alert='Please select a date or click cancel!')
def display():
CalendarDialog(root)
root = tk.Tk()
tk.Button(root, text='data data data', command=display).pack()
root.mainloop()
Note I also got rid of that useless lambda, which happens to be a pet peeve of mine. lambda is great in some cases, but it's very rarely needed.

Is there a way to stop a tkinter class object from opening until the button is opened

I have a little problem with my code. I am writing multiples classes with different GUI interfaces as a project. However, every time I import those classes the GUI window automatically opens the window and I want the window to open only when a button is clicked.
from FinalProject import addFlight
from FinalProject import reserveFlight
class ex:
def __init__(self,win):
self.win = win
...
...
def mainButtons(self):
look = Button(self.win, text="Add New Flight",command=lambda: self.reserveMenu(1))
look.place(relx="0.2", rely="0.3")
res = Button(self.win, text="Book A Flight",command=lambda: self.reserveMenu(2))
res.place(relx="0.4", rely="0.3")
...
...
def reserveMenu(self, options):
if options == 1:
self.flight = Toplevel(self.win)
self.flMenu = addFlight.AddFlights(self.flight)
self.flMenu.addingFlight()
# call(["python","addFlight.py"])
if options == 2:
pass
# self.flight = Toplevel(self.win)
# self.flMenu = reserveFlight.ReserveFlights(self.flight)
# self.flMenu.reserve()
# call(["python","reserveFlight.py"])
...
...
The "reserveMenu" function works fine but is there way to suppress those import statements or at least prevent the windows from opening until the button is clicked.
I know there are other methods of opening my python code but this HAS to be done using CLASSES. Trust me I have found way easier methods of doing this. FYI, there is more code but I only copied the more important parts.
Instead of using a method you could define your reserve option windows as classes, ReserveAdd, ReserveBook, that inherit from tkinter.Toplevel. And all a button would do is to call them. Here's an example:
import tkinter as tk
root = tk.Tk()
class ReserveAdd(tk.Toplevel):
def __init__(self, master):
super().__init__(master)
self.master = master
tk.Label(self, text="This is ReserveAdd window.").pack()
class ReserveBook(tk.Toplevel):
def __init__(self, master):
super().__init__(master)
self.master = master
tk.Label(self, text="This is ReserveBook window.").pack()
def res_one():
ReserveAdd(root)
def res_two():
ReserveBook(root)
tk.Button(root, text="Reserve Option 1", command=res_one).pack()
tk.Button(root, text="Reserve Option 2", command=res_two).pack()
root.mainloop()
In the above example Reserve Option 1 calls an instance of ReserveAdd class whereas Reserve Option 2 calls an instance of a ReserveBook class.
I'd define a single method for buttons but that's not exactly the scope here.

Python 3.5 tkinter confirmation box created progress bar and reads in csv not quite working

I'm realtively new to python and am making a GUI app that does a lot of file i/o and processing. To complete this i would like to get a confirmation box to pop-up when the user commits and actions. From this when clicking 'yes' the app then runs the i/o and displays a progress bar.
From other threads on here I have gotten as far as reading about the requirement to create an addtional thread to take on one of these processes (for example Tkinter: ProgressBar with indeterminate duration and Python Tkinter indeterminate progress bar not running have been very helpful).
However, I'm getting a little lost because I'm not activating the threaded process from the Main() function. So I'm still getting lost in how, and where, I should be creating the progress bar and passing of the i/o process to another thread (reading in a csv file here).
Here is my code and I would be very grateful for any help anyone can give me:
import tkinter as tk
import tkinter.messagebox as messagebox
import csv
import tkinter.ttk as ttk
import threading
class ReadIn(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.initUI()
def initUI(self):
self.parent.title("Read in file and display progress")
self.pack(fill=tk.BOTH, expand=True)
self.TestBtn = tk.Button(self.parent, text="Do Something", command=lambda: self.confirm_pb())
self.TestBtn.pack()
def confirm_pb(self):
result = messagebox.askyesno("Confirm Action", "Are you sure you want to?")
if result:
self.handle_stuff()
def handle_stuff(self):
nf = threading.Thread(target=self.import_csv)
nf.start()
self.Pbar()
nf.join()
def Pbar(self):
self.popup = tk.Tk()
self.popup.title('Loading file')
self.label = tk.Label(self.popup, text="Please wait until the file is created")
self.progressbar = ttk.Progressbar(self.popup, orient=tk.HORIZONTAL, length=200,
mode='indeterminate')
self.progressbar.pack(padx=10, pady=10)
self.label.pack()
self.progressbar.start(50)
def import_csv(self):
print("Opening File")
with open('csv.csv', newline='') as inp_csv:
reader = csv.reader(inp_csv)
for i, row in enumerate(reader):
# write something to check it reading
print("Reading Row " + str(i))
def main():
root = tk.Tk() # create a Tk root window
App = ReadIn(root)
root.geometry('400x300+760+450')
App.mainloop() # starts the mainloop
if __name__ == '__main__':
main()
The statement nf.join() in function handle_stuff() will block tkinter's main loop to show the progress bar window. Try modify handle_stuff() as below:
def handle_stuff(self):
nf = threading.Thread(target=self.import_csv)
nf.start()
self.Pbar()
#nf.join() # don't call join() as it will block tkinter's mainloop()
while nf.is_alive():
self.update() # update the progress bar window
self.popup.destroy()

Resources