Tkinter, saving functions to a list and then running them - python

I'm working on a GUI for a project in school. All the buttons that I have in my GUI are bound with functions that I have created. These functions call for already predefined functions. For some of the predefined functions, I need one or two arguments and I have solved that with entries. I type in the arguments in the right entries that are connected to the specific button and when I press the button, the function will run with the corresponding arguments.
The thing I want to do is to in some way when I press a button, the function should be saved to a list instead of being executed right away. And when I push the "run" button(a new button that I will create) everything in my list will be executed. I have been thinking about using a list box but I don't know exactly how they work or if its even possible to run a list box that contains a number of functions. Does someone have any ideas or solutions for me? Can I use the list box for this or is there something else that is better to use?
class App:
def __init__(self, master):
frame = Frame(master)
frame.pack()
self.entry1 = IntVar()
self.entry2 = IntVar()
def do_something():
value1 = self.entry1.get()
value2 = self.entry2.get()
self.listbox.insert(END, "predefined_function(value1, value2)")
def run_listbox_contents():
pass
self.button = Button(frame, text="Move", command=lambda: do_something())
self.button.pack(side=TOP)
self.entry1.set("value1")
self.entry = Entry(frame, textvariable=self.entry1)
self.entry.pack(side=TOP)
self.entry2.set("value2")
self.entry = Entry(frame, textvariable=self.entry2)
self.entry.pack(side=TOP)
self.listbox = Listbox(master)
self.listbox.pack(side=TOP)
root = Tk()
app = App(root)
root.title("Mindstorms GUI")
root.geometry("800x1200")
root.mainloop()
root.destroy()

Just use a standard list.
something like this
def hest(txt):
print "hest: " +txt
def horse(txt):
print "horse: " + txt
funcList = []
funcList.append(hest)
funcList.append(horse)
for x in funcList:
x("Wow")
This outputs
hest: Wow
horse: Wow
Was this what you wanted?

If I were you, I wouldn't want to save functions to a list. I would suggest another solution for you.
I suppose you have heard of the principle of MVC (Model-View-Controller). In your case, the list box is a part of view, and the process that saves functions and then calls them at once is a part of controller. Separate them.
You might want to save and display any string in the list box to let the users know that the corresponding functions have been enlisted and ready to run. For example, save a string "Function1 aug1 aug2 aug3" or "Funtion2 aug1 aug2" or whatever you like as a handle of the corresponding function.
And for the controller part, write a function (let's say conductor()). It reads the handle strings from the list, parses them and calls the corresponding functions. Where you want to run the enlisted functions, there you just call conductor().
Update:
Due to your comment I understand that you are pretty new to program. Let me show you how to write a simplest parser with your given variable names.
def run_listbox():
to_do_list = #get the list of strings
for handle_string in to_do_list:
#Let's say you got
#handle_string = "Predfined_function1 value1 value2"
#by here
handle = handle_string.split(" ")
#Split the string by space, so you got
#handle = ["Predfined_function1", "value1", "value2"]
#by here
if handle[0] == "Predfined_function1":
Predfined_function1(handle[1], handle[2]) #Call Predfined_function1(value1, value2)
elif handle[0] == "Predfined_function2":
Predfined_function2(handle[1], handle[2])
#elif ...
#...
#elif ...
#...
#elif ...
#...
This is not a perfect parser, but I hope it could let you know what does a parser look like.

Related

How to limit the number of characters for several Entry widgets

I am trying to limit the number of characters that can be input in a list of Entry widgets. I tried using the following:
def character_limit(entry_text):
if len(entry_text.get()) > 0:
entry_text.set(entry_text.get()[:10])
player_names = []
for i in range(num_players):
player_names.append(tk.StringVar())
player_names[i].trace("w", lambda *args: character_limit(player_names[i]))
player_name_entry = tk.Entry(top, textvariable=player_names[i])
player_name_entry.grid(row=i, column=0)
But this only limits the last Entry widget. How can I fix this?
The looping problem is a very commonly seen problem and to fix it, you have to store the current value of the iteration within the lambda itself:
def character_limit(*args, entry_text):
# if len(entry_text.get()) > 0: Can remove this line as it seems to not really be doing anything
entry_text.set(entry_text.get()[:10])
for i in range(num_players):
...
player_names[i].trace("w", lambda *args, i=i: character_limit(entry_text=player_names[i]))
The reason you use *args is because, trace passes 3 arguments to the function itself, that we don't need mostly.
But a more better method to do this will be to use validation for the entry widgets as this will prevent you needing to create a StringVar and trace its activity unnecessarily:
def character_limit(inp):
if len(inp) > 10:
return False
return True
player_names = []
vcmd = (root.register(character_limit), '%P')
for i in range(num_players):
player_name_entry = Entry(root, validate='key', validatecommand=vcmd)
player_name_entry.grid(row=i, column=0)
player_names.append(player_name_entry)
Read:
tkinter creating buttons in for loop passing command arguments
Interactively validating Entry widget content in tkinter
The problem is not related to the widget. Variable i is not local to the lambda functions, so the last value of i is used for every function.
To create local variables change your lambda into:
player_names[i].trace("w", lambda *args, n=i: character_limit(player_names[n]))
For a good description see https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result

How should be deleted widgets that were create by a loop, in python tkinter?

I created entries and labels by loops. If user click the button there will be new entries and lables in two-two seperated row. Now I want to delete them by other button clicking, but labels are not deletable -only entries are.
My other problem is I want to delete just only the last widgets that were added by one clicking. To make it clear: I want to delete the last 18-18 labels and entries by one clicking. Currently, all the added entries so far will be deleted. The labels are not at all.
Here is the relevant code:
self._1thLabel_list = ['Label1', 'Label2', 'Label3', 'Label4', 'Label5',
'Label6', 'Label7', 'Label8', 'Label9']
self._2thLabel_list = ['Label10', 'Label11', 'Label12', 'Label13', 'Label14',
'Label15', 'Label16', 'Label17', 'Label18']
nothingList2 = []
self.col = 4
for j in range(len(self._1thLabel_list)):
self.myLab = Label(root, text=self._1thLabel_list[j]).grid(row=0, column=j+1)
for k in range(1):
self.myEntry_loop = Entry(root)
self.myEntry_loop.grid(row=k + 1, column=j+1, pady=10, padx=10)
self.myEntry_loop_2 = Entry(root)
self.myEntry_loop_2.grid(row=k + 3, column=j + 1, pady=10, padx=10)
nothingList2.append(self.myEntry_loop)
nothingList2.append(self.myEntry_loop_2)
for l in range(len(self._2thLabel_list)):
self.myLab_2 = Label(root, text=self.mylist_2[l]).grid(row=2, column=l + 1)
self.myButton_newWidgets = Button(root, text="Add", command=self.Add)
self.myButton_newWidgets.grid(row=self.col, column=4)
self.myButton_deleteWidgets = Button(root, text="Delete", command=self.deleteThem)
self.myButton_deleteWidgets.grid(row=self.col, column=5)
And here is how try to delete them:
def deleteThem(self):
for v in range(18):
nothingList2.pop(0)
for dele in nothingList2:
dele.destroy() # Here, it is deleting entries but delete all of them. I want just delete the last 18 of them.
for w in range(9):
self._1thLabel_list.pop(0)
for delet in self._1thLabel_list:
delet.destroy() # I got "AttributeError: 'str' object has no attribute 'destroy'" error in this line
for x in range(9):
self._2thLabel_list.pop(0)
for delet2 in self._2thLabel_list:
delet2.destroy()
Here is a solution (explanation below):
from tkinter import Tk, Button
root = Tk()
removable_widget_dict = {}
class RemovableWidget(Button):
def __init__(self, parent, title, key):
Button.__init__(self, parent)
self.key = key
self.config(text=title)
self.pack()
def delete(self):
self.destroy()
removable_widget_dict.pop(self.key)
for i in range(10):
key = f'item{i}'
removable_widget_dict[key] = RemovableWidget(root, f'Button {i}', key)
for key in removable_widget_dict.keys():
if key == 'item5':
removable_widget_dict[key].delete()
break
root.mainloop()
Explanation:
First of I would like to mention that this is something I came up with (at least similar to this) when doing a personal project so all of this comes from experience in a sense that this may not be the best solution but it certainly works.
After doing the basic stuff with tkinter (root = Tk(), root.mainloop()) You create a dictionary which will sort of store classes. Now classes are an important factor here since each instance of a class can act independently which cannot really be achieved with functions at least not as easy.
So then You create a class for whatever widget You need, You can create multiple classes for multiple widgets or somehow merge them all into one (don't know how that would work) but lets stick to one class for one type of widget.
In this class You define all the basic stuff You would for a widget but also You add an important argument "key". This is not as important but certainly is necessary because You do not want to leave unused classes in memory (don't know about the technical aspects but imo it keeps everything more clean especially if You have to read it[dictionary] for some reason)
Then You define the deletion function and here is where the independence of class instances comes in: for each class instance You will be able to call this function which will only affect that class instance. So now in this function You will destroy the widget that class created (in this case the button in self.button) or multiple widgets. and then the cleanup part comes in: You globally define the dictionary and then delete the "key" from it. and the key also makes it easier to access the class instance for other reasons.
The final note. You can access the classes who are stored in dictionaries functions like this: dictionary[key].that_class_function

Returning PY_VARxxx instead of expected string

I'm currently creating a GUI in order to turn a lot of individual instruments into one complete system. In def smuSelect(self) I create a list self.smuChoices I can use to call individual choices such as smuChoices[0] and it will return "2410(1)".
Once I call def checkBoxSetup it returns PY_VARxxx. I've tried searching the different forums and everything. I've seen mentions using the .get() which just gives me the state of the individual choice. The reason I want the actual string itself is I would like to use it in def testSetup(self) for the user to assign specific names to the individual machine, for example, 2410 = Gate.
My initial attempt was to create another variable smuChoice2 but I believe this is still changing the original list self.smuChoices.
import tkinter as tk
import numpy as np
from tkinter import ttk
def checkBoxSetup(smuChoice2): #TK.INTVAR() IS CHANGING NAME OF SMUS NEED TO CREATE ANOTHER INSTANCE OF SELF.SMUCHOICES
for val, SMU in enumerate(smuChoice2):
smuChoice2[val] = tk.IntVar()
b = tk.Checkbutton(smuSelection,text=SMU,variable=smuChoice2[val])
b.grid()
root = tk.Tk()
root.title("SMU Selection")
"""
Selects the specific SMUs that are going to be used, only allow amount up to chosen terminals.
--> If only allow 590 if CV is picked, also only allow use of low voltage SMU (maybe dim options that aren't available)
--> Clear Checkboxes once complete
--> change checkbox selection method
"""
smuChoices = [
"2410(1)",
"2410(2)",
"6430",
"590 (CV)",
"2400",
"2420"
]
smuChoice2 = smuChoices
smuSelection = ttk.Frame(root)
selectInstruct = tk.Label(smuSelection,text="Choose SMUs").grid()
print(smuChoices[0]) #Accessing list prior to checkboxsetup resulting in 2410(1)
checkBoxSetup(smuChoice2)
print(smuChoices[0]) #Accessing list after check box setup resulting in PY_VAR376
variableSMUs = tk.StringVar()
w7_Button = tk.Button(smuSelection,text="Enter").grid()
w8_Button = tk.Button(smuSelection,text="Setup Window").grid()
root.mainloop()
I was able to solve the problem by changing my list, smuChoices, to a dictionary then modifying
def checkBoxSetup(smuChoice2):
for val, SMU in enumerate(smuChoice2):
smuChoice2[val] = tk.IntVar()
b = tk.Checkbutton(smuSelection,text=SMU,variable=smuChoice2[val])
b.grid()
to
def checkBoxSetup(self):
for i in self.smuChoices:
self.smuChoices[i] = tk.IntVar()
b = tk.Checkbutton(self.smuSelection,text=i,variable=self.smuChoices[i])
b.grid()
Previously I was replacing the variable with what I'm guessing is some identifier that tkinter uses to store the state which is why I was getting PYxxx.
First of all getting PY_VARXX instead of what's in a variable class indicates the lack of get().
replace:
print(self.smuChoices[0])
with:
print(self.smuChoices[0].get())
Secondly, if you want to display the value of a variable class on a label, button, etc. you could rather just use the textvariable option by simply assigning the variable class to it.
Replace:
tk.Label(self.smuName,text=SMU).grid()
with:
tk.Label(self.smuName, textvariable=self.smuChoices[val]).grid()
Your question is still a bit unclear to me but I will try to provide an answer to the best of my understanding.
As I understand it, you're trying to create a set of Checkbuttons for a given list of items. Below is an example of a method that takes items as an argument and returns a dictionary of checkboxes that have root as their parent:
import tkinter as tk
def dict_of_cbs(iterable, parent):
if iterable:
dict_of_cbs = dict()
for item in iterable:
dict_of_cbs[item] = tk.Checkbutton(parent)
dict_of_cbs[item]['text'] = item
dict_of_cbs[item].pack() # it's probably a better idea to manage
# geometry in the same place wherever
# the parent is customizing its
# children's layout
return dict_of_cbs
if __name__ == '__main__':
root = tk.Tk()
items = ("These", "are", "some", "items.")
my_checkboxes = dict_of_cbs(items, root)
root.mainloop()
Additionally note that I haven't used any variable classes (BooleanVar, DoubleVar, IntVar or StringVar) as they seem to be redundant in this particular case.

Text updation from a function in a Label in Tkinter

I am having a question with with four options and a timer.Now I had read a json file content and get it in questions list. After setting the GUI and shuffling the questions in question list , Now I want to update the questionText and options in Buttons. I called loadQuestion() function after all this, But my program stops abruptly after that.
from Tkinter import *
import json
from random import shuffle
import tkMessageBox
class ProgramGUI(Frame):
def __init__(self, master=None):
master.title('QuizBox')
master.update()
master.minsize(350, 150)
Frame.__init__(self, master)
try:
with open('questions.txt') as data_file:
try:
questions=json.load(data_file)
except ValueError,e:
tkMessageBox.showerror("Invalid JSON","Invlid JSON Format")
master.destroy()
questions=[]
data_file.close()
except (OSError, IOError) as err:
tkMessageBox.showerror("File Not Found","File Not Found!!")
master.destroy()
questionText = StringVar()
Label(master,textvariable=questionText,justify=CENTER,wraplength=200).pack()
questionText.set("Question text goes here")
timer = IntVar()
Label(master,textvariable=timer,justify=CENTER,fg="blue").pack()
timer.set("10")
buttonList = ["Answer 1", "Answer 2", "Answer 3", "Answer 4"]
self.rowconfigure(0, pad=3)
for i, value in enumerate(buttonList):
self.columnconfigure(i, pad=3)
Button(self, text=value).grid(row=0, column=i)
self.pack()
score = IntVar()
Label(master,textvariable=score,justify=CENTER).pack()
score.set("Score: 0")
shuffle(questions)
#print questions
loadQuestion(self)
master.mainloop()
def loadQuestion(self):
print questions
if len(questions) > 0:
# Modify the questionText StringVar with first question in question[] and delete it from questions[]
root = Tk()
gui = ProgramGUI(master=root)
root.destroy()
The loadQuestion() method is responsible for displaying the next question in the GUI and starting the timer. The method must first select a question (i.e. the dictionary containing the question and answers) from the questions list and then display the text of the question in the appropriate Label and the answers in the Buttons of the GUI in a random order. Also rather than trying to randomise the order of the answers, I need to shuffle the buttonList list to randomise the order of the buttons before assigning answers to them.
The question should be removed from the question list so that it is not selected again.We can use “pop()” list method to removes the last element in a list.
The timer IntVar is set to 11 and then “updateTimer()” is called to start the timer. Rather than trying to randomise the order of the answers, I am trying to shuffle the buttonList list to randomise the order of the buttons before assigning answers to them. Since the timer is immediately updated after being set, the first number the user sees is 10.
The updateTimer() method will first subtract one from the timer IntVar, and then check if the timer is 0. If it is, a messagebox with a “Game Over” message and the user’s score, then destroy the main window to end the program. Otherwise (if the timer is not 0), We need to call the “updateTimer()” method again in 1 second. I think to do we can use the “after()” method for this, and by storing the ID of the upcoming call in a variable we can cancel it as needed.
Note : questionList is json format of type :
[{
"question": "Example Question 1",
"wrong1": "Incorrect answer",
"wrong2": "Another wrong one",
"wrong3": "Nope, also wrong",
"answer": "Correct answer"
} ]
There are a few problems in your code that relate to your use of instance variables, these being variables unique to an instance of an object. Instance variables are accessible within methods of the same class, and this is what you need to do to access the question list within your loadQuestion() method. For example:
questions = json.load(data_file)
defines a local variable named questions in the __init__() method, however, this variable does not exist once the __init__() function terminates. You need to make it an instance variable with self like this:
self.questions = json.load(data_file)
Now this variable can be accessed with self.questions in methods of the same class such as loadQuestions(), which would be written like this (note use of self.):
def loadQuestion(self):
print self.questions
if len(self.questions) > 0:
# Modify the questionText StringVar with first question in question[] and delete it from questions[]
pass
Now, to update the value of the question label requires similar changes. Declare questionText as an instance variable in __init__():
self.questionText = StringVar()
and update it within loadQuestions():
def loadQuestion(self):
print self.questions
if len(self.questions) > 0:
# just take the first question and answers from the pre-shuffled list
q_and_a = self.questions.pop()
self.questionText.set(q_and_a['question'])
# update answer buttons too....
You will find that you will need to use a similar method for each of the answer buttons, i.e. make these instance variables and update the button's text in loadQuestions().

Problems with a bind function from tkinter in Python

I am working on an application that is supposed to support both running from a console and from a GUI. The application has several options to choose from, and since in both running modes the program is going to have the same options obviously, I made a generalisation:
class Option:
def __init__(self, par_name, par_desc):
self.name = par_name
self.desc = par_desc
class Mode():
def __init__(self):
self.options = []
self.options.append(Option('Option1', 'Desc1'))
self.options.append(Option('Option2', 'Desc2'))
self.options.append(Option('Option3', 'Desc3'))
self.options.append(Option('Option4', 'Desc4'))
self.options.append(Option('Option5', 'Desc5'))
#And so on
The problem is that in GUI, those options are going to be buttons, so I have to add a new field to an Option class and I'm doing it like this:
def onMouseEnter(par_event, par_option):
helpLabel.configure(text = par_option.desc)
return
def onMouseLeave(par_event):
helpLabel.configure(text = '')
return
class GUIMode(Mode):
#...
for iOption in self.options:
iOption.button = Button(wrapper, text = iOption.name, bg = '#004A7F', fg = 'white')
iOption.button.bind('<Enter>', lambda par_event: onMouseEnter(par_event, iOption))
iOption.button.bind('<Leave>', lambda par_event: onMouseLeave(par_event))
#...
There is also a "help label" showing the description of the option every time a mouse hovers over it, so there I am binding those functions.
What is happening is that while I am indeed successfully adding a new field with a button, the bind function seems to mess up and the result is this:
Help label is always showing the description of the last option added, no matter over which button I hover. The problem seems to go away if I directly modify the Option class instead, like this:
class Option:
def __init__(self, par_name, par_desc):
self.name = par_name
self.desc = par_desc
self.button = Button(wrapper, text = self.name, bg = '#004A7F', fg = 'white')
self.button.bind('<Enter>', lambda par_event: onMouseEnter(par_event, self))
self.button.bind('<Leave>', lambda par_event: onMouseLeave(par_event))
But I obviously can't keep it that way because the console mode will get those fields too which I don't really want. Isn't this the same thing, however? Why does it matter if I do it in a constructor with self or in a loop later? I therefore assume that the problem might be in a way I dynamically add the field to the class?
Here is the full minimal and runnable test code or whatever it is called, if you want to mess with it: http://pastebin.com/0PWnF2P0
Thank you for your time
The problem is that the value of iOption is evaluated after the
for iOption in self.option:
loops are complete. Since you reset iOption on each iteration, when the loop is completed iOption has the same value, namely the last element in self.options. You can demonstrate this at-event-time binding with the snippet:
def debug_late_bind(event):
print(iOption)
onMouseEnter(event, iOption)
for iOption in self.options:
iOption.button = Button(wrapper, text = iOption.name,
bg = '#004A7F', fg = 'white')
iOption.button.bind('<Enter>', debug_late_bind)
which will show that all events that iOption has the same value.
I split out the use of iOption to debug_late_bind to show that iOption comes in from the class scope and is not evaluated when the bind() call is executed. A more simple example would be
def print_i():
print(i)
for i in range(5):
pass
print_i()
which prints "4" because that is the last value that was assigned to i. This is why every call in your code to onMouseEnter(par_event, iOption) has the same value for iOption; it is evaluated at the time of the event, not the time of the bind. I suggest that you read up on model view controller and understand how you've tangled the view and the controller. The primary reason this has happened is that you've got two views (console and tk) which should be less coupled with the model.
Extracting the .widget property of the event is a decent workaround, but better still would be to not overwrite the scalar iOption, but instead use list of individual buttons. The code
for n, iOption in enumerate(self.options):
would help in creating a list. In your proposed workaround, you are encoding too much of the iOption model in the tkinter view. That's bound to bite you again at some point.
I don't know what the actual problem was with my original code, but I kind of just bypassed it. I added a dictionary with button as a key and option as a value and I just used the par_event.widget to get the option and it's description, which is working fine:
buttonOption = {}
def onMouseEnter(par_event):
helpLabel.configure(text = buttonOption[par_event.widget].desc)
return
def onMouseLeave(par_event):
helpLabel.configure(text = '')
return
class GUIMode(Mode):
def run(self):
#...
for iOption in self.options:
iOption.button = Button(wrapper, text = iOption.name, bg = '#004A7F', fg = 'white')
iOption.button.bind('<Enter>', lambda par_event: onMouseEnter(par_event))
iOption.button.bind('<Leave>', lambda par_event: onMouseLeave(par_event))
buttonOption[iOption.button] = iOption
#...

Categories