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()
Related
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
I am trying to call the instance variable of my inherited class account_validation from my PageTwo class. I think i may be getting the error because of my inhertied class selfService, which main functions is to display various tkinter windows. Credit to #bryan oakely for the interface
class selfService(tk.Tk, Toplevel):
def __init__(self, *args, **kwargs):
selfService.database = Database()
self.employeeWindow = None
tk.Tk.__init__(self, *args, **kwargs)
self.container = tk.Frame(self)
self.container.pack(side="top", fill="both", expand = True)
self.container.grid_rowconfigure(0, weight=1)
self.container.grid_columnconfigure(0, weight=1)
self.frames = {}
selfService.restart = False
for F in (StartPage, PageOne, PageTwo):
self.frame = F(self.container, self)
self.frames[F] = self.frame
self.frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(StartPage)
def show_frame(self, cont):
self.frame = self.frames[cont]
self.frame.tkraise()
class PageTwo(tk.Frame, account_validation):
def __init__(self, parent, controller):
account_validation.__init__(self)
print(self.test)
tk.Frame.__init__(self, parent)
class account_validation():
def __init__(self):
self.test = 'test'
I am subject to:
print(self.test)
account_validation.init(self)
TypeError: init() missing 2 required positional arguments: 'parent' and 'controller'
Furthermore, I want to understand why people would rather inherit a class and access its properties that way instead of instantiating that class in another class and accessing that class properties that way? What is the best scenario for both? If I am using multiple class methods from the inherited class should I keep it inherited?
I need the buttons within LeftFrame to change its appearance when clicked. In the class AccOne, I tried to do left_frame.acc1.config(releif='SUNKEN'), but I get NameError: name 'left_frame' not defined. I tried making left_frame global, but no luck.
Here's the script.
class MainApp(Tk):
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
container = Frame(self)
container.pack()
container.rowconfigure(4, weight=1)
container.columnconfigure(2, weight=1)
right_frame = RightFrame(container, self)
left_frame = LeftFrame(container, right_frame)
left_frame.pack(side=LEFT)
right_frame.pack()
class RightFrame(Frame):
def __init__(self, parent, controller, *args, **kwargs):
Frame.__init__(self, parent, *args, **kwargs)
self.frames = {}
for F in (Welcome, AccOne, AccTwo, AccThree, AccFour, AccFive):
frame = F(self, self)
self.frames[F] = frame
self.show_frame(Welcome)
def show_frame(self, cont):
frame = self.frames[cont]
frame.grid(row=0, column=0)
frame.tkraise()
class LeftFrame(Frame):
def __init__(self, parent, controller, *args, **kwargs):
Frame.__init__(self, parent)
acc1 = Button(self, text="Account 1", width=12, height=3, command=lambda: controller.show_frame(AccOne))
acc1.pack()
I figured it would make sense to configure the button under def show_frame(self,cont): but I have no idea where to start since that method isn't under LeftFrame.
When creating tkinter windows with classes, try and think about creating a 'widget tree', this being a path through which you can access all of your widgets. In this simple example, MainWindow and SubWindow can access all of eachother's widgets:
class MainWindow(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
# widget
self.lbl = tk.Label(self, text='Title')
self.lbl.pack()
# create child window, as class attribute so it can access all
# of the child's widgets
self.child = SubWindow(self)
self.child.pack()
# access child's widgets
self.child.btn.config(bg='red')
class SubWindow(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
# can use this attribute to move 'up the tree' and access
# all of mainwindow's widgets
self.parent = parent
# widget
self.btn = tk.Button(self, text='Press')
self.btn.pack()
# access parent's widgets
self.parent.lbl.config(text='Changed')
Things to change in your code
Firstly, every time you create a widget that you might want to access later, assign it to a class variable. For example (this is part of the cause of your problem):
self.left_frame
self.acc1
not
left_frame
acc1
Secondly, make proper use of your parent and controller arguments. You're doing these, but you never use them or assign them to an atribute, so they may as well not be there. Assign them to a self.parent or self.controller attribute, so if you need to access them in a method later, you can.
I don't know exactly what you're trying to do and I can't see your AccOne class, but you should be able to find a way to access that button by making these changes.
Good luck!
I have this following code where I am calling the function from the button which takes input from the widgets. It's a function which takes about 4 minutes to run and for solving the 'not responding' problem of tkinter window, I want to get the func process running on the different core and terminate as it can be called again via the application running on mainloop with a different argument. I read the documentation of multiprocessing and Pool seemed to be the choice here but I don't know how to frame it here. Tried a few things with error.
class database(tk.Tk):
def __init__(self, *args, *kwargs):
tk.Tk.__init__(self, *args, **kwargs):
container= tk.Frame(self, width=1000, height=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 ( msPage): #many more pages here
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(msPage)
def show_frame(self,cont):
frame = self,frames[cont]
frame.tkraise()
def MarkS(msVar):
ms.func(msVar.get()) # func takes about 4 mins
class msPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
msVar = StringVar()
msCal = Calendar(self, selectmode='day'
textvariable=msVar).pack(fill="both", expand=True)
# button2 calls MarkS
button2 = ttk.Button(self,
text="Get Data",
command=lambda: MarkS(msVar)).pack(pady=30, padx=10)
app = database()
app.geometry("1066x568")
app.minsize(width=670, height=550)
app.mainloop()
This is a standalone example that might help you get started:
from multiprocessing import Process
class ms_var_class():
value = None
def __init__(self, value):
self.value = value
def get(self):
return self.value
# This is to simulate type(ms)
class ms_class():
process = None
# This is for simulating your long running function
def func(self, ms_var):
print(ms_var)
def MarkS(msVar):
ms.process = Process(target=ms.func, args=(msVar.get(),))
ms.process.start()
# Call ms.process.join() later
ms_var = ms_var_class("bogus")
ms = ms_class()
MarkS(ms_var)
if ms.process is not None:
ms.process.join()
Thank you Mike for commenting and helping me get towards the solution
Here's how I did it
Create a global queue
q= queue.Queue()
Defined the function MarkS within the class msPage
def MarkS(msVar):
q.put([7 , datetime.datetime.strptime(msVar.get(), '%x').strftime('%d-%b-%Y').upper(),0])
ThreadedTask(q).start()
7 is the unique number of the 9 functions that I had for each page (class)
Made another class of ThreadedTask as in the link below
Tkinter: How to use threads to preventing main event loop from "freezing"
class ThreadedTask(threading.Thread):
def __init__(self,queue):
threading.Thread.__init__(self)
q = queue
def run(self):
items = q.get()
if items[0]==7:
ms.func(items[1])
ThreadedTask(q).start() creates a constructor object and the run function automatically starts depending on the argument here which is of course the 1st element of q which is a list of 3 in my case. It is detected and the desired function is run in another thread which prevents the tkinter window from closing.
Hope this helps! :)
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)):
...