Python: Implementing a series of functions with each one calling the next - python

programming isn't my field, but I'm trying to learn.
I've been writing a program that works something like this:
from Tkinter import *
root=Tk()
def Secondwindow():
firstframe.destroy()
secondframe = Frame(root)
secondframe.pack()
secondcontent = Label(secondframe, text = 'second window content').pack()
def Thirdwindow():
secondframe.destroy()
thirdframe = Frame(root)
thirdframe.pack()
thirdcontent = Label(thirdframe, text = 'third window content').pack()
def Fourthwindow():
thirdframe.destroy()
fourthframe = Frame(root)
fourthframe.pack()
fourthcontent = Label(fourthframe, text = 'fourth window content').pack()
thirdbutton = Button(thirdframe, text = 'Next ->', command = Fourthwindow).pack()
secondbutton = Button(secondframe, text = 'Next ->', command = Thirdwindow).pack()
firstframe = Frame(root)
firstframe.pack()
firstcontent = Label(firstframe, text = 'first window content').pack()
firstbutton = Button(firstframe, text = 'Next ->', command = Secondwindow).pack()
root.mainloop()
Now, this works perfectly, but as my program gets larger and more complicated I am starting to see that this is neither elegant nor easy to maintain. I would like to simply write each function in (more or less) sequence, but that causes namerrors when the program reads a reference to a function that hasn't been defined yet (it seems like the program shouldn't worry about it until it has to run the function, by which time it would have already seen the function definition, but oh well).
What is the simplest way to have this functionality (functions called from within functions) without having to stick the next function definition in the middle of the first function definition? Thanks in advance!

I un-nested the functions to see what the error was. The problem you have is that the functions try to access variables defined in the scope of another function. That won't work. You either have to nest functions so that their scopes overlap, as you did -- which is awkward -- or you have to use global variables -- which is less awkward, but still awkward -- or you have to pass variable names from function to function.
However, because you're using callbacks here -- which are quite advanced! -- executing the third option is more complicated. If you really want to get this working, I would suggest an object-oriented approach. But frankly I would suggest starting with something simpler than this for a beginning programmer.
The most important thing is that you get used to scoping rules. That, at least, I can explain with your code. Here's an explanation of the NameErrors you were getting.
def Secondwindow():
firstframe.destroy()
secondframe = Frame(root)
secondframe.pack()
secondcontent = Label(secondframe, text = 'second window content').pack()
secondbutton = Button(secondframe, text = 'Next ->', command = Thirdwindow).pack()
def Thirdwindow():
secondframe.destroy()
thirdframe = Frame(root)
thirdframe.pack()
thirdcontent = Label(thirdframe, text = 'third window content').pack()
thirdbutton = Button(thirdframe, text = 'Next ->', command = Fourthwindow).pack()
These two functions look like they do almost the same thing. But they don't! Here's why:
def Secondwindow():
firstframe.destroy()
This line refers to firstframe, which was defined in the global scope (i.e. at the 'lowest level' of the program. That means it can be accessed from anywhere. So you're ok here.
secondframe = Frame(root)
secondframe.pack()
secondcontent = Label(secondframe, text = 'second window content').pack()
secondbutton = Button(secondframe, text = 'Next ->', command = Thirdwindow).pack()
These variables are all defined within the scope of Secondwindow. That means they only exist within Secondwindow. Once you leave Secondwindow, they cease to exist. There are good reasons for this!
def Thirdwindow():
secondframe.destroy()
Now you run into your problem. This tries to access secondframe, but secondframe is only defined within Secondwindow. So you get a NameError.
thirdframe = Frame(root)
thirdframe.pack()
thirdcontent = Label(thirdframe, text = 'third window content').pack()
thirdbutton = Button(thirdframe, text = 'Next ->', command = Fourthwindow).pack()
Again, these are all defined only within the scope of ThirdWindow.
Now, I can't explain everything you need to know to make this work, but here's a basic hint. You can create a global variable within a function's namespace by saying
global secondframe
secondframe = Frame(root)
Normally python assumes that variables defined in a function are local variables, so you have to tell it otherwise. That's what global secondframe does. Now you really shouldn't do this very often, because as the global scope fills up with more and more variables, it becomes harder and harder to work with them. Functions create smaller scopes (or 'namespaces' as they're called in some contexts) so that you don't have to keep track of all the names (to make sure you don't use the same name in two places, or make other even more disastrous mistakes).
Normally, to avoid creating a global variable, you would have each function return the frame it defines by calling return secondframe. Then you could add a function argument to each function containing the previous frame, as in def Thirdwindow(secondframe). But because you're using callbacks to call Secondwindow, etc., this method gets knotty. Here's some code that works around the problem by using lambda statements.
from Tkinter import *
root=Tk()
def Secondwindow(firstframe):
firstframe.destroy()
secondframe = Frame(root)
secondframe.pack()
secondcontent = Label(secondframe, text = 'second window content').pack()
secondbutton = Button(secondframe, text = 'Next ->', command = lambda: Thirdwindow(secondframe)).pack()
def Thirdwindow(secondframe):
secondframe.destroy()
thirdframe = Frame(root)
thirdframe.pack()
thirdcontent = Label(thirdframe, text = 'third window content').pack()
thirdbutton = Button(thirdframe, text = 'Next ->', command = lambda: Fourthwindow(thirdframe)).pack()
def Fourthwindow(thirdframe):
thirdframe.destroy()
fourthframe = Frame(root)
fourthframe.pack()
fourthcontent = Label(fourthframe, text = 'fourth window content').pack()
firstframe = Frame(root)
firstframe.pack()
firstcontent = Label(firstframe, text = 'first window content').pack()
firstbutton = Button(firstframe, text = 'Next ->', command = lambda: Secondwindow(firstframe)).pack()
root.mainloop()
But the best way to fix this is to use object-oriented code. Unfortunately that's just too complex a topic to get into; it would just add more verbiage to an already long post. I honestly think you should spend some time getting used to functions and scoping first.
That said, I found a moment to fiddle with an object-oriented variation. Here it is:
from Tkinter import *
root=Tk()
class FrameRepeater(object):
def __init__(self, start=0, end=4):
self.frame = None
self.number = start
self.end = end
def new_frame(self):
if self.frame:
self.frame.destroy()
self.frame = Frame(root)
self.frame.pack()
self.content = Label(self.frame, text = 'window ' + str(self.number) + ' content')
self.content.pack()
self.button = Button(self.frame, text = 'Next ->', command = self.replace)
self.button.pack()
self.number += 1
def replace(self):
if self.number < self.end:
self.new_frame()
elif self.number >= self.end:
self.content.config(text='Press button again to quit')
self.button.config(command=self.quit)
def quit(self):
self.frame.destroy()
root.destroy()
exit()
FrameRepeater().new_frame()
root.mainloop()
A couple of things to note. First, in those lines that read like this, there's a subtle error:
thirdcontent = Label(thirdframe, text = 'third window content').pack()
You were storing None in thirdcontent, because the pack() method has no return value. If you want to preserve a reference to the Label, you have to save the reference first, then pack() it separately, as I did in new_frame above.
Second, as you can see from my replace method, you don't actually have to destroy the frame to change the text of the label or the button command! The above still destroys the first three frames just to show how it would work.
Hope this gets you started! Good luck.

You can add a parent variable to each function, as that is more or less the only dynamic part of your recursion:
def RecursiveWindow(parent):
parent.destroy()
frame = Frame(root)
frame.pack()
framContent = Label(frame, text = 'second window content').pack()
if foo: # This won't go on forever, will it?
RecursiveWindow(self)
It looks like you're coding an application with frames and a forward button, like a Windows installer or a slideshow.
Instead of having many frames, each differing only by the text they contain, why not just have one master frame object and the text separate? I don't use Tk for my GUIs, but here's what I mean (might work):
from Tkinter import *
slides = ['Text one', 'Text two', 'Text three', 'cow']
number = 0
root = Tk()
frame = Frame(root).pack()
button = Button(frame, text = 'Next ->', command = NextFrame).pack()
def NextFrame(number):
frameContent = Label(frame, text = slides[number]).pack()
number += 1

If you can copy&paste code you can factor it out:
from Tkinter import *
root=Tk()
messages = ['first window content', 'second window content', 'third window content', 'fourth window content' ]
def nextframe(current, messages):
# what happens when you click the button
def command():
current.destroy()
makeframe(messages)
return command
def makeframe(messages):
frame = Frame(root)
frame.pack()
# take the first message
next_content = Label(frame, text=messages.pop(0)).pack()
if messages: # if there are more make the button
next_button = Button(frame, text = 'Next ->', command = nextframe(frame, messages)).pack()
makeframe(messages)
root.mainloop()

Related

Tkinter: Problems having access to text input in text widget

I tried creating a program that will take in the symptoms of a person and return the disease they have. This is the GUI part of the project.
from tkinter import *
root = Tk()
root.title("Health GUI")
root.geometry("1000x625")
symptoms_list = []
def print_symptoms():
print(symptoms_list)
def typeSymptoms():
gap3 = Label(text="").pack()
symptoms_entry = Text(width=50, height=20)
symptoms_entry.pack()
symptoms_list.append(symptoms_entry.get(1.0, END))
done_symptoms = Button(text="I have written my symptoms", width=25, height=5, command=lol)
done_symptoms.pack()
gap1 = Label(text="").pack()
title = Label(text="HEALTH GUI", font=30).pack()
gap2 = Label(text="").pack()
start_button = Button(text="Click here to start", width=30, height=5, command=typeSymptoms, font=20).pack()
root.mainloop()
Just for simplicity, I tried printing out the symptoms given by the user to the console but it gives me a list with '\n'. Please help. Thanks!(PS: I lerned Tkinter day before yesterday so I don't know much)
At the moment, your variable symptoms_list just holds the contents of the newly created Text widget, since you append this content at startup.
If you want to add the symptoms to the list, you need to have your function lol() that you call when pressing the button.
This function should look something like:
def lol():
symptoms_text = symptoms_entry.get(1.0, END)
symptoms_list = symptoms_text.split('\n')
print_symptoms()
However, your widgets and the symptoms_list would have to be global variables in order for this program to work. It would probably be better, while you are getting acquainted with Tkinter, to learn how to create a dialog as Class with attributes. That makes sharing values between methods so much easier.

How to have script wait until user interacts in Tkinter?

I'm learning Python on my own and now I'm trying to learn some GUI with Tkinter. If anyone can help, I'm having trouble having the program show a greeting message and then offering options that should be picked by clicking a button. Actually, I think I've sorted out how to build it, but there's a key piece missing: I don't know how to have the program wait until the user has interacted by clicking one of the buttons.
Let's say, for instance, I create a file with a 'Window' Class (for the interface) and some functions along (Window.py):
import time
from tkinter import *
# Here I'll set each one of the the buttons' commands.
# I've referenced it before to avoid triggering a reference problem, but when I tried, I actually broke the code in three parts and used imports to link all of them.
# For simplicity's sake, however, I'll present everything in a single file in this question.
Choice = ''
def Choose1():
Choice = 1
def Choose2():
Choice = 2
def Choose3():
Choice = 3
# Here I create the object 'Window':
class Window:
def __init__(self):
self.Window = Tk()
self.Window.title = 'Interact'
self.Window.minsize = (500, 300)
# Here I'll add a label to display the messages that I want to show the user:
self.Text = Label(self.Window, text = '')
self.Text.pack()
# Aqui uma série de botões:
self.B1 = Button(self.Window, text = 'Option 1', command = Choose1)
self.B1.pack()
self.B2 = Button(self.Window, text = 'Option 2', command = Choose2)
self.B2.pack()
self.B3 = Button(self.Window, text = 'Option 3', command = Choose3)
self.B3.pack()
# Here I'll create an instance of the 'Window' object:
Example = Window()
# Here I'll create a function so that certain messages will be displayed to the user:
def Say(X):
Example.Text.configure(text = X)
Example.Text.update()
time.sleep(3) # Please ignore this. I inserted this delay so there's time for the user to read the message. I actualy have a better way to do it, but to keep it simple, let's leave it like this.
# Finally, the main part of the program:
Say('Welcome!')
Say('Which option would you like to choose?')
WaitInput() # I haven't figured out how this function would work, and that's my issue. I'd like the program to wait for the option to be chosen and only then print the following message:
Say('You've chosen option {}!'.format(Choose))
Text.mainloop()
Can anybody by any chance tell me how can I create this 'WaitInput()' function, or if something of the sort already exists in Python?
Appreciate it!
In all GUIs (in all languages) you use button to wait for input and execute some function when there are input data.
In tkinter you have to use lambda to assing function with arguments to button
self.b1 = tk.Button(self.window, text='Option 1', command=lambda:self.choose('1'))
I used after() to change text with delay and also to add buttons with delay - I used callback in say_later() to execute it with delay.
import time
import tkinter as tk
# --- classes ---
class Window:
def __init__(self):
self.window = tk.Tk()
self.window.title = 'Interact'
self.window.geometry('500x300')
#self.window.minsize = (500, 300)
self.text = tk.Label(self.window, text='')
self.text.pack()
def add_buttons(self):
self.b1 = tk.Button(self.window, text='Option 1', command=lambda:self.choose('1'))
self.b1.pack()
self.b2 = tk.Button(self.window, text='Option 2', command=lambda:self.choose('2'))
self.b2.pack()
self.b3 = tk.Button(self.window, text='Option 3', command=lambda:self.choose('3'))
self.b3.pack()
def say(self, message, callback=None):
self.text.configure(text=message)
if callback:
callback()
def say_later(self, delay, message, callback=None):
self.window.after(delay, lambda:self.say(message, callback))
def choose(self, value):
self.say("You've chosen option {}!".format(value))
# --- functions ---
# empty
# --- main ---
example = Window()
example.say('Welcome!')
example.say_later(3000, 'Which option would you like to choose?', example.add_buttons)
example.window.mainloop()

Python/Tkinter dynamically update label

I am currently trying to make a GUI to an existing python program using Tkinter. The program gives the user two options from which the user must choose to either accept or decline. Before using Tkinter the options were placed in the terminal and awaited for a raw_input. (y/n). How can I make this so the canvas text updates with the new data and awaits for the users button click?
To make my question more specific: How can I run another programs code while the Tkinter mainloop is running and make these two interact?
Example code below.
from Tkinter import *
root = Tk()
root.resizable(width=False, height=False)
root.geometry('{}x{}'.format(500,550))
root.wm_title("Tkinter test")
BtnFrame = Frame (root)
BtnFrame.pack(side = BOTTOM)
BtnFrame.place(y=450, x=20)
canvas_1 = Canvas(root, width = "200", height ="300")
canvas_2 = Canvas(root, width = "250", height ="300")
canvas_1.pack(side = LEFT)
canvas_2.pack(side = RIGHT)
textfield_1 = canvas_1.create_text(100,50)
textfield_2 = canvas_2.create_text(100,50,)
def update_textfiel_1(text):
global textfield_1
canvas_1.delete(textfield_1)
textfield = canvas.create_text(100,50,text = text)
def update_textfiel_2(text):
global textfield_2
canvas_2.delete(textfield_2)
textfield1 = canvas1.create_text(100,50,text = text)
Accept = Button(BtnFrame, text="Accept", width=25)
Decline = Button(BtnFrame, text="Decline", width=25)
Accept.pack(side = LEFT)
Decline.pack(side = RIGHT)
root.mainloop()
First off you have some inconsistent variable names in your update_textfiel functions, you can greatly simplify it by using .itemconfigure (documentation for methods on canvas widget)
def update_textfiel_1(new_text):
canvas_1.itemconfigure(textfield_1, text=new_text)
def update_textfiel_2(new_text):
canvas_2.itemconfigure(textfield_2, text=new_text)
If I understand correctly you want a way to have a function that will simply wait for the user to press one of the buttons and then return the result, this is very easy with tkMessageBox:
question = """Do you accept {}?
if you say no you will instead get {}"""
#this message can GREATLY be improved
# But I really don't understand what you are using it for...
def user_confirmation(name1, name2):
response = tkMessageBox.askyesno("Accept or Decline",question.format(name1,name2))
print(response)
if response: # == True
return name1
else:
return name2
I have not yet found a way to make a blocking function that works with the window you have currently...

How to open another window and get data from it, then save to a file?

I'm trying to create a program in python using tkinter, and this program is supposed to have a list of books created by the user. On the main window (the one with the list), there should be a menubar with the option to add a book to the list. When clicked, this option should open another window, this time with one entrybox, where the user should enter the book's title and an add button, to add the button to the list.
The list is saved in a .txt file.
This is the program I wrote so far:
import sys
from tkinter import *
def newBook():
def add():
BookTitle = v.get()
bookTitle = '\n' + BookTitle
books = open('c:/digitalLibrary/books.txt', 'a')
books.write(bookTitle)
books.close()
addWindow = Tk()
v = StringVar()
addWindow.geometry('250x40+500+100')
addWindow.title('digitalLibrary - Add Book')
newBookEntry = Entry(addWindow,textvariable=v)
newBookEntry.pack()
addButton = Button(addWindow, text='ADD', command=add)
addButton.pack()
def refresh():
books = open('c:/digitalLibrary/books.txt', 'r')
bookList = books.readlines()
books.close()
for i in range (0, len(bookList)):
bookOne = Label(text=bookList[i])
bookOne.grid(row=i, column=0, sticky=W)
def quitProgram():
tfQuit = messagebox.askyesno(title='Close Program', message='Are you sure?')
if tfQuit:
window.destroy()
window = Tk()
menubar = Menu(window)
window.geometry('400x400+200+100')
window.title('digitalLibrary')
booksmenu = Menu(menubar, tearoff=0)
booksmenu.add_command(label='Add Book', command=newBook)
booksmenu.add_command(label='Delete Book')
booksmenu.add_command(label='Close Program', command=quitProgram)
menubar.add_cascade(label='digitalLibrary', menu=booksmenu)
books = open('c:/digitalLibrary/books.txt', 'r')
bookList = books.readlines()
books.close()
for i in range (0, len(bookList)):
bookOne = Label(window, text=bookList[i])
bookOne.grid(row=i, column=0, sticky=W)
refreshButton = Button(window, text='Refresh', command=refresh)
refreshButton.grid(row=0, column=1)
window.config(menu=menubar)
window.mainloop()
It seems logical to me that this should work, but it just doesn't. When I click the ADD button on the Add Book window, all it does is add the line break to the .txt file.
I know that it works if I use the OS library and create a separate python file for the add book window, but I'd rather put it all in one code, if possible.
I've tried many things, and tried searching it in the web, but I got nowhere.
The root cause of your problem is that you are creating more than once instance of Tk. You cannot do this. If you want to create a popup window, create an instance of Toplevel. A proper Tkinter application creates exactly once instance of Tk with exactly one invocation of mainloop.
If your main goal is to simply get input from the user (versus learning how to write your own dialog), you might want to consider using one of the built-in dialogs.
For example:
import tkinter.simpledialog as tkSimpleDialog # python 3.x
...
def newBook():
BookTitle = tkSimpleDialog.askstring("Add Book","What is the name of the book?")
if BookTitle is not None:
bookTitle = '\n' + BookTitle
books = open('/tmp/books.txt', 'a')
books.write(bookTitle)
books.close()

Simple gui that display a repeated set of images when a button is pushed

I made a very simple gui that has a button and shows an image(.gif). My goal is to output another .gif whenever you press the button. There are 2 .gif files in my file directory and the point is to keep switching between these two whenever you press the button.
#Using python2.7.2
import Tkinter
root = Tkinter.Tk()
try:
n
except:
n = 0
def showphoto(par):
if par%2 == 0:
try:
label2.destroy()
except:
pass
photo = Tkinter.PhotoImage(file="masc.gif")
label2 = Tkinter.Label(image=photo)
label2.image = photo
label2.pack()
else:
try:
label2.destroy()
except:
pass
photo = Tkinter.PhotoImage(file="123.gif")
label2 = Tkinter.Label(image=photo)
label2.image = photo
label2.pack()
myContainer1 = Tkinter.Frame(root, width = 100, height = 100)
myContainer1.pack()
def callback(event):
global n
showphoto(n)
n = n + 1
button1 = Tkinter.Button(myContainer1)
button1["text"]= "Next pic"
button1["background"] = "green"
button1.bind("<Button-1>", callback(n))
button1.pack()
root.mainloop()
The current code just outputs the first image (masc.gif) but when I press the button it doesn't switch to the other image(123.gif). What am I doing wrong?
This can achieved much easier with classes as the class holds all the data necessary without the use of global variables.
import Tkinter as tk
from collections import OrderedDict
class app(tk.Frame):
def __init__(self,master=None, **kwargs):
self.gifdict=OrderedDict()
for gif in ('masc.gif','123.gif'):
self.gifdict[gif]=tk.PhotoImage(file=gif)
tk.Frame.__init__(self,master,**kwargs)
self.label=tk.Label(self)
self.label.pack()
self.button=tk.Button(self,text="switch",command=self.switch)
self.button.pack()
self.switch()
def switch(self):
#Get first image in dict and add it to the end
img,photo=self.gifdict.popitem(last=False)
self.gifdict[img]=photo
#display the image we popped off the start of the dict.
self.label.config(image=photo)
if __name__ == "__main__":
A=tk.Tk()
B=app(master=A,width=100,height=100)
B.pack()
A.mainloop()
Of course, this could be done more generally ... (the list of images to cycle through could be passed in for example), and this will switch through all the images in self.gifs ...
This approach also removes the necessity to destroy and recreate a label each time, instead we just reuse the label we already have.
EDIT
Now I use an OrderedDict to store the files. (keys=filename,values=PhotoImages). Then we pop the first element out of the dictionary to plot. Of course, if you're using python2.6 or earlier, you can just keep a list in addition to the dictionary and use the list to get the keys.
button1 = Tkinter.Button(myContainer1)
button1["text"]= "Next pic"
button1["background"] = "green"
button1.bind("<Button-1>", callback(n))
First, you bind the <Button-1> event to None (that's what callback(n) evaluates to). You should bind it to callback (no parentheses a.k.a the call operator).
Second, I suggest you change callback to not accept any arguments, remove the bind call and create your button as:
button1 = Tkinter.Button(myContainer1, command=callback)

Categories

Resources