Python: How to dynamically display image in label widget (tkinter) - python-3.x

I am stuck trying to dynamically display a specific image on a tk page based on button clicked on a previous page. PageOne has 5 images with 5 buttons below each. Clicking on specific button should take the user to the second page and display image 3 if the 3rd button is clicked.
I have figured out how to pass a value of 1 to 5 depending on which button is clicked, and the images are saved pic1.gif,...pic5.gif so to return the correct image I just need to append the value to the file location.
I am struggling to figure out how to refresh PageTwo when it is accessed.
Any help would be greatly appreciated, thanks.
TITLE_FONT = ("Helvetica", 18, "bold")
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
self.attributes("-fullscreen", False)
self.geometry('{}x{}'.format(1000, 1000))
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (PageOne, PageTwo):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(PageOne)
def show_frame(self, c):
frame = self.frames[c]
frame.tkraise()
class PageOne(tk.Frame):
praiseid = 0
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
def PraiseClick(button_id):
PageOne.praiseid = button_id
controller.show_frame(PageTwo)
users= [1,2,3,4,5]
for i in users:
location = str('C:/Users/XXXXXXX/Documents/pic'+str(i)+'.gif')
icon = tk.PhotoImage(file=location)
IDlabel = tk.Label(self,image=icon)
IDlabel.image = icon
IDlabel.place(x=i*100,y=200,width=100,height=100)
for j in users:
praisebutton = tk.Button(self,text="Click",width=10,command=lambda x=j: PraiseClick(int(x)))
praisebutton.place(x=j*100,y=300,width=100,height=44)
backbutton = tk.Button(self, text="Go to Start Page",
command=lambda: controller.show_frame(StartPage))
backbutton.place(x=100,y=50,width=200,height=44)
class PageTwo(tk.Frame):
def get_id(self):
return(PageOne.praiseid)
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.location = 'C:/Users/XXXXXXX/Documents/pic'+str(self.get_id())+'.gif'
icon = tk.PhotoImage(file=self.location)
self.IDlabel = tk.Label(self,image=icon)
self.IDlabel.image = icon
self.IDlabel.place(x=0,y=200,width=100,height=100)

The selected image from the PageOne is not drawn on the PageTwo because the drawing function is localized in the __init__() function and no event is raise when the PageTwo is redraw by calling the function tkraise().
Problem 1 - generate an event when calling tkraise() of the PageTwo.
Here, the OOP of Python will be the answer by overriding the function
tkraise() in the class PageTwo.
class PageTwo(tk.Frame):
...
def tkraise(self):
print('PageTwo.tkraise()')
tk.Frame.tkraise(self)
# then call the drawing icon
self.refresh_icon() # see Problem 2
Problem 2 - localize the drawing of the icon in a function of the class PageTwo.
To take into account of the new selected icon, create a function
refresh_icon() in the class PageTwo and call it from both
__init__() and tkraise() functions.
class PageTwo(tk.Frame):
...
def refresh_icon(self):
self.location = 'C:/Users/XXXXXXX/Documents/pic'+str(self.get_id())+'.gif'
icon = tk.PhotoImage(file=self.location)
self.IDlabel = tk.Label(self,image=icon)
self.IDlabel.image = icon
self.IDlabel.place(x=0,y=200,width=100,height=100)
Add at the end of the __init__() function.
class PageTwo(tk.Frame):
...
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
...
self.refresh_icon()
Bonus 1 - to prevent an Exception in case of missing image, add a check before.
Create a file_exists() function, then check before loading in
PhotoImage().
def file_exists(filepath):
try:
fp_file = open(filepath)
return (True)
except IOError:
return (False)
And in the function refresh_icon() of the class PageTwo:
self.location = 'C:/Users/XXXXXXX/Documents/pic'+str(self.get_id())+'.gif'
if (file_exists(self.location)):
icon = tk.PhotoImage(file=self.location)
...
Bonus 2 - clear the current IDlabel in case of image not loaded.
when creating a new Label in the PageTwo, the variable
self.IDlabel will store the new Image without deleting the old one.
Before creating a new one, call the destroy() function.
Add a declaration of the variable self.IDlabel and assign it to None in the __init__() function. Then call destroy() in the
class PageTwo(tk.Frame):
...
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
...
self.refresh_icon()
self.IDlabel = None
...
def refresh_icon(self):
self.location = 'C:/Users/XXXXXXX/Documents/pic'+str(self.get_id())+'.gif'
if (self.IDlabel): # check label and destroy it
self.IDlabel.destroy()
if (file_exists(self.location)):
...

Related

How to pass variables from one class to another after a class has been instantiated?

I have created a tkinter application which uses a framework very similar to the one shown in this video https://www.youtube.com/watch?v=jBUpjijYtCk
Essentially there is a main class which uses other classes below it as pages. These pages are created as soon as the program is ran.
My problem is that I need data from one of these pages to be sent to another page so that it can display the appropriate data depending on the data that is sent from the previous page.
I have simplified my program down to showcase the issue
from tkinter import *
class TkinterApp(Tk):
def __init__(self):
Tk.__init__(self)
container = Frame(self)
container.grid(row=1, column=0, sticky="nesw")
self.frames = {}
for p in (ClassA, ClassB):
frame = p(parent = container, controller = self)
self.frames[p] = frame
frame.grid(row=0, column=0, sticky="nesw")
self.ShowFrame(ClassA)
def ShowFrame(self, page_name):
page = self.frames[page_name]
page.tkraise()
def PassText(self, text):
self.frames[ClassB].GetData(text)
class ClassA(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.controller = controller
button1 = Button(self, text="button1", command=lambda: self.SubmitInfo("button1"))
button1.grid(row=0, column=0)
def SubmitInfo(self, data):
self.controller.ShowFrame(ClassB)
self.controller.PassText(data)
class ClassB(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.controller = controller
self.label = Label(self, text=self.data)
self.label.grid(column=0, row=0)
def GetData(self, data):
self.data = data[0]
app = TkinterApp()
app.mainloop()
Desired outcome:
the text shown on the ClassB page being "button1"
Current outcome:
AttributeError: 'ClassB' object has no attribute 'data'
to my knowledge, this is happening as the class TkinterApp is causing ClassB to be ran before the value of data exists.
Any help would be greatly appreciated
In line 44, remove Label widget parameter text=self.data
Add .configure in line 49, self.label.configure(text=self.data)
snippet:
class ClassB(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.controller = controller
self.label = Label(self) #<-- line 44
self.label.grid(column=0, row=0)
def GetData(self, data):
self.data = data[0]
self.label.configure(text=self.data) #Add in <---line 49

Calling a Method of another class with a lambda expression in python

I am trying to create a little GUI with multiple pages. The First page has a button which raises the second page and changes the label text on the second page. However, I fail to call the method of the second page which is supposed to change the text. Can somebody tell me, why I get the following error when calling the method: SecondPage.changeLabel()?
TypeError: changeLabel() missing 1 required positional argument: 'self'
UPDATE:
import tkinter as tk
class ExampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
container.pack()
self.frames = {}
for F in (FirstPage, SecondPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(FirstPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
def get_page(self, frame_class):
return self.frames[frame_class]
class FirstPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
page = controller.get_page(SecondPage)
self.buttonFP = tk.Button(self, text="Next Page",
command=lambda : [f for f in [page.changeLabel(),
controller.show_frame(SecondPage)]])
self.buttonFP.pack()
class SecondPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.label = tk.Label(self, text="Test")
self.label.pack()
def changeLabel(self):
"""change text of label"""
newLabel = "changed"
self.label.configure(text=newLabel)
if __name__ == "__main__":
app = ExampleApp()
app.mainloop()
You get the error because you are trying to call the method on the class rather than on the instance of the class.
Since your architecture has a controller, you can modify the controller to have a function that returns the instance of a page. You can then use the instance to call methods on that page.
Example:
class ExampleApp(tk.Tk):
...
def get_page(self, frame_class):
return self.frames[frame_class]
Later, you can use this method to get the instance of the page, and use the instance to call the method:
page = self.controller.get_page(SecondPage)
page.changeLabel()

Is there a way to grid a tkinter element that is in a different class from the frame that i want to grid it on to

I am writing a D&D simulator using tkinter as a gui, however because I want to make an array where each index corresponds to the row that the element is on, for the option Menu's I have created my own class that includes the information that they need such as the list of options and the stringVar. However when I run the program it does not grid the option menu onto the frame even though the object has been created.
Full code if you need it: https://pastebin.com/2FxBcUb2
main class that is run:
class dndSimulator(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.iconbitmap(self, default="Ampersand_on_Black (1).ICO")
tk.Tk.wm_title(self, "D&D simulator")
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (StartGame,MainMenu):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(StartGame)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
My frame (the part where it calls the dropdown box class):
def addCharactersection(self, firstColumn):
teamid = (firstColumn//4)
self.removeDelete(firstColumn)
self.placeholders = self.moveelements(firstColumn, 1, len(self.teamUiItems[teamid])-1)
self.placeholders = self.placeholders[0]
self.charactersection = []
self.charactersection.append(dropdownMenu("",lstoffiles))
self.charactersection.append(tk.Entry(self))
self.charactersection.append(ttk.Button(self, text = "Commit", command = lambda: self.commitValues(firstColumn)))
self.teamUiItems[teamid].insert(self.placeholders, self.charactersection)
self.teamUiItems[teamid][self.placeholders][0].dropdowngrid(self.placeholders+2, 0,1)
self.teamUiItems[teamid][self.placeholders][1].grid(row = self.placeholders+2,column = 1, columnspan = 1)
self.teamUiItems[teamid][self.placeholders][2].grid(row = self.placeholders+2, column = 2, columnspan = 1)
#grid these
self.addDelete(firstColumn)
print(self.teamUiItems[teamid][self.placeholders])
and the dropdown menu class:
class dropdownMenu(tk.Frame):
'''
create a dropdown menu where this class includes all the vars and stuff
'''
def __init__(self,default,list):
super(dropdownMenu, self).__init__()
self.default = default
self.list = list
self.menuvar = tk.StringVar()
self.menuvar.set(default)
self.menu = tk.OptionMenu(self,self.menuvar, *self.list)
def dropdowngrid(self,prow,pcolumn,pcolumnspan):
self.menu.grid(row = prow, column = pcolumn, columnspan = pcolumnspan)
def dropdownforget(self):
self.menu.grid_forget()
def dropdowninfo(self):
return {'row':self.menu.grid_info()['row'], 'column':self.menu.grid_info()['column'], 'columnspan':self.menu.grid_info()['columnspan']}
I expect it to grid onto the frame that is shown in the correct position however It does not appear even though the rest of the elements that stay within the frames class do appear.
Thank you if you can help because I have been trying to fix this for the past hour or so!
EDIT: after testing code I can confirm you forget to use .pack() to show frame. (First I tried with .grid() but your frame is in window which uses .pack())
You create your frame dropdownMenu
self.charactersection.append(dropdownMenu("",lstoffiles))
and you use
self.teamUiItems[teamid][self.placeholders][0].dropdowngrid(self.placeholders+2, 0,1)
which runs .grid() for OptionMenu
def dropdowngrid(self, ...):
self.menu.grid(...)
but optionmenu self.menu is inside frame self and you forgot self.pack()
After I add this I can see it in window.
def dropdowngrid(self, ...):
self.menu.grid(...)
self.pack()

switching windows in tkinter with classes

in a [former question][1] received a perfect script from #acw1668 for creating popup-windows (see below).
How can this be rewritten in a form that the new windows are not popups but just a switch from one page to the next (the listboxes/candvas are not necessarily needed here)?
Edit: tried to amend the code according to #Bryan Oakley's suggestions.
My issue here: I do not manage to pass the list lst from the GUI class to the other page classes without an error message:
File "/.spyder-py3/temp.py", line 25, in __init__
frame = F(parent=container, controller=self)
TypeError: __init__() missing 1 required positional argument: 'lst'
What am I missing here?
And I do not understand what's happening here:
for F in (StartPage, PageOne, PageTwo):
page_name = F.__name__
frame = F(parent=container, controller=self,)
self.frames[page_name] = frame
If somebody could explain, please?
import tkinter as tk
from tkinter import ttk
class GUI(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.lst = ['a', 'b', 'c']
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (StartPage, PageOne, PageTwo):
page_name = F.__name__
frame = F(parent=container, controller=self,)
self.frames[page_name] = frame
# put all of the pages in the same location;
# the one on the top of the stacking order
# will be the one that is visible.
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("StartPage", self.lst)
def show_frame(self, page_name):
'''Show a frame for the given page name'''
frame = self.frames[page_name]
frame.tkraise()
def show_popup(self, page, lst):
win = page(self, lst)
win.grab_set() # make window modal
self.wait_window(win) # make window modal
class StartPage(tk.Frame):
def __init__(self, parent, controller, lst):
tk.Frame.__init__(self, parent)
self.controller = controller
self.lst = lst
# ------------------------------------------------------------------- #
label = tk.Label(self, text="Check this out")
label.pack(pady=10,padx=10)
# ------------------- create buttons ---------------------------------
button1 = ttk.Button(self, text="show all",
width = 25, command=lambda:
controller.show_popup(App, self.lst))
button1.pack(pady=10, padx=10)
button2 = ttk.Button(self, text="show page one",
width = 25, command=lambda:
controller.show_frame(PageOne))
button2.pack(pady=10, padx=10)
class PageOne(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is page 1")
label.pack(side="top", fill="x", pady=10)
button = tk.Button(self, text="Go to the start page",
command=lambda: controller.show_frame("StartPage"))
button.pack()
class PageTwo(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is page 2")
label.pack(side="top", fill="x", pady=10)
button = tk.Button(self, text="Go to the start page",
command=lambda: controller.show_frame("StartPage"))
button.pack()
class App(tk.Toplevel):
def __init__(self, parent, lst):
tk.Toplevel.__init__(self, parent)
self.lst = lst
self.title('This is the pop up window')
self.geometry('400x200')
label = tk.Label(self, text=self.lst)
label.pack(side="top", fill="x", pady=10)
parent.grid_rowconfigure(0, weight = 1)
parent.grid_columnconfigure(0, weight = 1)
if __name__ == '__main__':
app = GUI()
app.mainloop()
[1]: http://stackoverflow.com/questions/41181809/how-to-open-and-close-another-window-with-scrollbar-in-tkinter-for-python-3-5/41182843?noredirect=1#comment69580999_41182843
Your class initializers are defined like this:
class StartPage(tk.Frame):
def __init__(self, parent, controller, lst):
In order to create an instance of this class it requires three arguments (plus self): parent, controller, and lst.
Now, let's look at how you're creating the instance:
frame = F(parent=container, controller=self,)
Notice how you have the parent and you have the controller, but you haven't passed in anything for lst. That is why the error states "missing 1 required positional argument: 'lst'" -- because you are literally missing one required argument named "lst".
To fix this problem, you simply need to provide this extra argument. For example:
frame = F(parent=container, controller=self, lst=self.lst)
HOWEVER, you probably shouldn't do that. The architecture of this little block of code you copied makes it possible to access values on the GUI class from any of the "page" classes without having to do any extra work.
Because this variable is an attribute of the GUI class, and you are passing a reference to the instance of the GUI class to each "page" (the controller attribute), you can access this data any time you want without having to pass it in at construction time. You can remove it from __init__ and from where you're creating the pages (ie: go back to the original code before your modifications), and then just use self.controller.lst whenever you need the value.
For example:
class SomePage(tk.Frame):
def __init__(self, parent, controller):
self.controller = controller
...
def some_function(self):
print("The value of 'lst' is:", self.controller.lst)

Tkinter bind inconsistency

This problem appears to be hard to duplicate -- as I am able to correctly do it in a shorter program. What I'm hoping for is maybe some guidance on what could be going wrong. I have posted the version where it works correctly:
import tkinter as tk
from tkinter import *
from tkinter import ttk
class DIS(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.wm_title(self, "program")
container = tk.Frame(self)
container.pack(side="top", fill="both", expand = True)
container.grid_rowconfigure(0, weight = 1)
container.grid_columnconfigure(0, weight = 1)
self.usernameVar = StringVar()
self.frames = {}
for F in (StartPage, contactQues, nowTry, next):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row = 0, column = 0, sticky = "nsew")
self.show_frame(StartPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
button2 = ttk.Button(self, text = "Here's a Button", command= lambda: controller.show_frame(nowTry))
button2.pack()
class nowTry(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.entry1 = Entry(self)
self.entry1.pack()
self.button1 = ttk.Button(self, text = "Yes", command = self.go)
self.button1.pack()
self.entry1.bind("<Return>", self.go)
def go(self, event=None):
print (self.entry1.get())
self.controller.show_frame(contactQues)
class contactQues(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.entry1 = Entry(self)
self.entry1.pack(pady=10, padx=10)
self.button1 = ttk.Button(self, text = "Submit", command= self.UsernameSubmit)
self.button1.pack()
self.entry1.bind("<Return>", self.UsernameSubmit)
def UsernameSubmit(self, event=None):
UsernameEntryGet = self.entry1.get()
self.controller.usernameVar.set(UsernameEntryGet)
self.controller.show_frame(next)
class next(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, textvariable = self.controller.usernameVar)
label.pack() ###Label is posted with input correctly
The issue I'm having with my main program is that the self.controller.usernameVar label does not post like it does in this example (nothing shows up at all) when the Return key is pressed to submit the input. However, when the submit button is clicked with the mouse, the label appears properly.
So, given this information, I feel as if my bind("<Enter>"... command is being managed wrong. I've tried self.bind..., self.controller.bind..., self.entryX.bind... without success.
Any ideas with this framework what could be wrong?
I believe I figured it out. The issue was that in my full program, I had multiple bind commands. While I was trying to solve this issue, I had some entry prompts bound to the controller and others to the entry itself (e.g,. self.controller.bind in a few classes and self.entry.bind in others).
I changed them all to self.entry.bind and it apparently fixed it -- which is why this code snippet worked as expected.

Resources