Checkbutton command binding to wrong values when instantiated in loop - python-3.x

When I click on a checkbutton in my project, it is not executing the correct functionality. The project can be found at https://github.com/shitwolfymakes/Endless-Sky-Mission-Builder/ (indev branch)
I am building an application using tkinter, and am working on a function to dynamically place ttk.Entry objects next to ttk.Checkbutton objects, and then link them together.
I have already rewritten this function a few times, and even added a special case for when self.numMandatory is 0, but nothing has worked.
This is taken from guiutils.py, line 323.
# add the optional fields
for i in range(self.numMandatory, self.numFields):
print(self.rowNum)
self.listEntryStates.append(BooleanVar())
self.listEntryData.append(StringVar())
self.listEntryData[-1].set(self.listDefaultEntryData[i])
self.listEntries.append(ttk.Entry(self, textvariable=self.listEntryData[-1], state=DISABLED, style="D.TEntry"))
self.listEntries[-1].grid(row=self.rowNum, column=1, sticky="ew")
#print(self.listEntryStates[-1])
#print(self.listEntries)
self.listCheckbuttons.append(ttk.Checkbutton(self, onvalue=1, offvalue=0, variable=self.listEntryStates[-1],
command=lambda: self.cbValueChanged(self.listEntryStates[-1],
[self.listEntries[-1]])))
self.listCheckbuttons[-1].grid(row=self.rowNum, column=2, sticky="e")
print(self.listCheckbuttons[-1].__str__(), end=" is bound to: ")
print(self.listEntries[-1].__str__(), self.listEntryStates[-1])
self.rowNum += 1
# end for
This is taken from guiutils.py, line 349
def cbValueChanged(self, entryState, modifiedWidgets):
for widget in modifiedWidgets:
print("The value of %s is:" % widget, end="\t\t")
print(entryState.get())
if type(widget) is str:
break
elif entryState.get() is True:
widget.config(state='enabled', style='TEntry')
elif entryState.get() is False:
widget.config(state='disabled', style='D.TEntry')
#end for
#end cbValueChanged
In the main window, when I scroll down and click "add trigger", the new window appears properly. But when I click on the checkbutton next to the Entry that says "[<base#>]", that entry should be enabled by cbValueChanged.
For some reason, when the loop to add the optional fields runs, the command= section binds only the last entry in self.listEntries (but the entry it's binding each checkbutton to isn't created until the very last time through the loop)
I'm not sure where else I could ask a question like this, and I know this is asking more than most questions. If there is any more information you need, I'll be happy to provide it.

You ~~can't~~ edit: shouldn't use lambda in a loop. Frankly you shouldn't use it at all. Use functools.partial or make a real closure.
from functools import partial
self.listCheckbuttons.append(ttk.Checkbutton(self, onvalue=1, offvalue=0, variable=self.listEntryStates[-1],
command=partial(self.cbValueChanged,self.listEntryStates[-1],[self.listEntries[-1]])))

Related

Passing value to button click event

The code below creates 10 buttons. When user clicks the button, we need to know which button was clicked.
import tkinter as t
def click_button(number):
print("you clicked %d" % number)
root = t.Tk()
for number in range(0, 10):
btn = t.Button(root, text=number, padx=15, command=lambda: click_button(number))
btn.grid(row=0, column=number)
root.mainloop()
When I click any button, I always get 9. Why is that?
Is there a better/alternative way to address this problem?
It appears that the closure over number is by reference given a simple loop test, however I'm not sure this is correct behavior.
This works using partial which I have no doubt captures the value of number at the time of constructing the partial:
import functools as fc
import tkinter as t
def click_button(number):
print("you clicked %d" % number)
root = t.Tk()
for number in range(0, 10):
btn = t.Button(
root,
text=number,
padx=15,
command=fc.partial(click_button, number))
btn.grid(row=20, column=number)
root.mainloop()
You need to pass number as a lambda parameter.
Try:
command=lambda param = number : click_button(param)
Some clarification about the way lambda function "captures" local variable can be found here:
What do (lambda) function closures capture?
and here:
https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result
In a nutshell, by passing number as a parameter to the lambda, you ensure a copy of number will be performed for iterated number value. If you do not pass it as parameter, all lambda functions "number" point to the same variable ( loop number) which is evaluated only when the lambda function is called. At the time it is called number value is "9". This is very different from C++ where the scope of a "for loop" variable is the loop itself. In python the variable of the loop exists as long as someone (here the lambdas) references it.

Is there a way I can make buttons in Tkinter with a for loop, while also giving each one a different command?

I'm making a revision system for school, and I want it to be able to use a modular amount of subjects just in case a subject is added to the system, so I need a way to make a number of buttons with different subject names, and be able to differentiate between those buttons using tkinter. So for example, if they were to click the Mathematics button, it would take them to another bit of code specially suited for mathematics(Although, it can't be solely for Mathematics, since then I would need definitions for subjects that haven't even been added yet)
First I simply tried setting the command to "print(subjectnames[subcount-1])", thinking it would print the name of the button, but that just printed both names out straight away without even pressing a button. Then I tried changing the variable name subject to the name of the button, which I didn't expect to work, I was just stumped and desperate
Here I started setting up the definition
def chooseQuiz():
clearWindow()
subjectnames=[]
button=[]
This was probably unimportant, just labels for the title and spacing
Label(mainWindow, text="Which quizzes would you like to take?", bg='purple3', font=('constantia',25,"bold")).grid(row=0, column=0, padx=100, pady=0)
Label(mainWindow, bg='purple3').grid(row=1, column=0, padx=0, pady=15)
Here I extract data from an SQL table to get all subject names from all topics, again probably unimportant but here is where most of the variables are made
c.execute("SELECT Subject_name FROM topics")
for row in c.fetchall():
if row[0] in subjectnames:
pass
elif row[0] not in subjectnames:
subjectnames.append(row[0])
else:
messagebox.showerror("Error", "subjectnames are not appending")
chooseQuiz()
This is the main part of this question, where I tried to form a fluid number of buttons all with different commands, but to no avail
for subcount in range(len(subjectnames)):
button.append(Button(mainWindow, text=str(subjectnames[subcount-1]), bg='grey', fg='black', font=('cambria',15), width=25, command=(subject==subjectnames[subcount-1])))
button[-1].grid(row=subcount+2,column=0, padx=0, pady=15)
I expected the subject variable to be the same as the button I pressed, but it remained at 0(original value). I think this is due to wrong use of the command function in tkinter on my part. The buttons still showed up fine(only 2 subjects currently, Mathematics and Physics, and both showed up fine).
Yes, it is possible.
The following example creates a window with a reset button; upon clicking reset, a frame is populated with buttons corresponding to a random number of buttons chosen randomly from possible subjects. Each button has a command that calls a display function that redirects the call to the proper topic, which, in turn prints the name of its topic to the console, for simplicity of the example. You could easily create functions/classes corresponding to each topic, to encapsulate more sophisticated behavior.
Adding subjects, is as simple as adding a key-value pair in SUBJECTS
Pressing reset again, removes the current button and replaces them with a new set chosen randomly.
import random
import tkinter as tk
from _tkinter import TclError
SUBJECTS = {'Maths': lambda:print('Maths'),
'Physics': lambda:print('Physics'),
'Chemistry': lambda:print('Chemistry'),
'Biology': lambda:print('Biology'),
'Astronomy': lambda:print('Astronomy'),
'Petrology': lambda:print('Petrology'),}
topics = []
def topic_not_implemented():
print('this topic does not exist')
def get_topics():
"""randomly creates a list of topics for this example
"""
global topics
topics = []
for _ in range(random.randrange(1, len(SUBJECTS))):
topics.append(random.choice(list(SUBJECTS.keys())))
return topics
def reset_topics():
global topics_frame
try:
for widget in topics_frame.winfo_children():
widget.destroy()
topics_frame.forget()
topics_frame.destroy()
except UnboundLocalError:
print('error')
finally:
topics_frame = tk.Frame(root)
topics_frame.pack()
for topic in get_topics():
tk.Button(topics_frame, text=topic, command=lambda topic=topic: display(topic)).pack()
def display(topic):
"""redirects the call to the proper topic
"""
SUBJECTS.get(topic, topic_not_implemented)()
root = tk.Tk()
reset = tk.Button(root, text='reset', command=reset_topics)
reset.pack()
topics_frame = tk.Frame(root)
topics_frame.pack()
root.mainloop()

Tkinter Entry validatation

I am writing a program in Python 3.6 using Tkinter where a customer has multiple(11) entry fields. I want these entry fields to only accept integers and also be able to define the maximum amount of characters.
I already have a function that does this. But this function only works for one entry field. I have tried entering variables with calling the function so it changes another entry field for example. I was not able to do this.
This is the function I have that works with 1 entry field.
def limitMuntgeld(self, *args):
value = self.invoerM.get()
if len(value) > 5:
self.invoerM.set(value[:5])
if value.lower() in "abcdefghijklmnopqrstuvwxyz-=[];/":
self.invoerM.set(value[:0])
This is the example entry field code that works with the function
self.invoerMuntgeld = Entry(self, font=('Arial', 14), textvariable=self.invoerM)
This is combined with a trace on the entry field posted below.
self.invoerM = StringVar()
self.invoerM.trace('w', self.limitMuntgeld)
I have also tried it with vcmd and validatecommand. However, no good results.
My endresult would be one function working with all entry fields. If anyone has any suggestions, I am all ears!
The proper way to do entry validation is with the validatecommand option rather than using trace. With the validation feature built into the widget you don't need a reference to the widget itself (though you can use it if you want).
When the validatecommand is run, you can have it pass in what the new value will be if the input is valid. You only need to check this value and then return True or False, without having to know which widget it applies to.
For example:
import tkinter as tk
def validate_input(new_value):
valid = new_value .isdigit() and len(new_value) <= 5
return valid
root = tk.Tk()
validate = root.register(validate_input)
for i in range(10):
entry = tk.Entry(root, validate="key", validatecommand=(validate, "%P"))
entry.pack(side="top", fill="x")
root.mainloop()
For information about what %P represents, and what else can be used as arguments to the command, see this question: Interactively validating Entry widget content in tkinter

Python 3.5.1 Tkinter // Entry widget/Frame management issue

So I want my program to be able to reset the password. The problem is that everything works just fine with the first reset process, but if you wanted to reset the password one more time it gets wicked and I can't find a mistake or solution. What the program does the first time and what it should do:
When button3 is pressed show the first frame where you have to enter you current pw. If the enter key on the keyboard is pressed and the pw is right show the second frame. There, set a new pw and by hitting enter show the third frame. Type in the same pw you used in the second frame, the pw set frame. If both pw are the same go back to the title screen.
Second+ time and the PROBLEM: button3 is pressed, it shows the first frame, but the cursor is not in the entry field like it is the first time (entry.focus_set() is in the code), BUT the cursor is in the third entry field (of the last, the pw confirm, frame) without it even being .pack()ed or .raised or anything beyond. If I click the entry of the first frame (where I actually would be at this point) and confirm it shows the second frame like it is supposed to be, but the cursor now is in the entry of the first frame which is not even active anymore because it just .pack_forget(). If I proceed to the third frame the cursor is in the entry of the second frame.
from tkinter import *
root = Tk()
pw_actual = ""
pw_first = ""
def ssc_title():
sc_pw.pack_forget()
sc_pw_second.pack_forget()
sc_title.pack()
def ssc_pw_change():
sc_title.pack_forget()
sc_pw_testing.pack()
def ssc_pw_first():
sc_pw_testing.pack_forget()
sc_pw_first.pack()
def ssc_pw_second():
sc_pw_first.pack_forget()
sc_pw_second.pack()
#-----------------------------------
def ev_pw_testing(event):
pw_proof = en_pw_testing.get()
if pw_proof == pw_actual:
en_pw_testing.delete(0, END)
ssc_pw_first()
else:
en_pw_testing.delete(0, END)
def ev_pw_first(event):
global pw_first
pw_first = en_pw_first.get()
if pw_first == "":
errormessage("Please enter a password")
else:
en_pw_first.delete(0, END)
ssc_pw_second()
def ev_pw_second(event):
global pw_first
pw_second = en_pw_second.get()
if pw_first == pw_second:
write(pw_second)
en_pw_second.delete(0, END)
ssc_title()
else:
errormessage("Passwords don't match")
en_pw_second.delete(0, END)
#------------------
sc_pw_testing = Frame(root, bg=bgclr)
lab_pw_testing = Label(sc_pw_testing, text="Please enter the password", bg=bgclr, fg=fontclr, font=font_12).pack(ipady=10)
en_pw_testing = Entry(sc_pw_testing, show="X")
en_pw_testing.pack()
en_pw_testing.focus_set()
en_pw_testing.bind("<Return>", ev_pw_testing)
sc_pw_first = Frame(root, bg=bgclr)
lab_pw_first = Label(sc_pw_first, text="Set new password", bg=bgclr, fg=fontclr, font=font_12).pack(ipady=10)
en_pw_first = Entry(sc_pw_first, show="X")
en_pw_first.pack()
en_pw_first.focus_set()
en_pw_first.bind("<Return>", ev_pw_first)
sc_pw_second = Frame(root, bg=bgclr)
lab_pw_second = Label(sc_pw_second, text="Confirm password", bg=bgclr, fg=fontclr, font=font_12).pack(ipady=10)
en_pw_second = Entry(sc_pw_second, show="X")
en_pw_second.pack()
en_pw_second.focus_set()
en_pw_second.bind("<Return>", ev_pw_second)
sc_title = Frame(root, bg=bgclr)
bttn3 = Button(sc_title, text="Password reset", bg=bgclr, fg=fontclr, font=font_12, relief=FLAT, command=ssc_pw_change).pack()
#------------
root.mainloop()
(Here I didn't include irrelevant functions etc like the errormessage toplevel funtion which justs create a window or the pickle stuff, because then it'll be too long and the issue does not seem to be laying there.)
Like I was saying - the first time changing the password works just fine. Every other time after the first time the same problem occures every time. The only way to get it to work again is to restart the program. I was trying following solutions:
resetting the global variables before entering the reset process again
resetting the global variables after switching the frame and being used
putting each frame (or page if you will) into a function
putting each frame (or page) into a class following the Switch between two frames in tkinter tutorial
leaving the frame/page switcing functions out and put the .pack()s and .pack_forget()s right in the pw checking (event) functions
instead of packing using a grid
using ("", lambda event: function())
Each solution worked just like the code right now, no changes at all, and the problem stays just as well.
I mean I could work around it, like disable the button so you can only reset the pw one time each session, but there is no reason why it should be like that, or even omit the entry.focus_set(), so you have to click the entry field no matter what and the problem never has a chance to occur (which is seriously annoying to click each entry field before entering characters each time).
I ran out of ideas what I could try to do differently. I feel like missing the forest for the trees. Come on guys, gimme a kick in my butt, what am I missing out here!?

Stopping, restarting and changing variables in a thread from the main program (Python 3.5)

I'm very new to threading am and still trying to get my head around how to code most of it. I am trying to make what is effectively a text editor-type input box and so, like every text editor I know, I need a cursor-bar thing to indicate the location at which the text is being typed to. Thus I also want to be able to flicker/blink the cursor, which i thought would also prove good practice for threading.
I have a class cursor that creates a rectangle on the canvas based on the bounding box of my canvas text, but I then need to change it's location as more characters are typed; stop the thread and instantaneously hide the cursor rectangle when the user clicks outside of the input box; and lastly restart the thread/a loop within the thread (once again, sharing a variable) - the idea here being that the cursor blinks 250 times and after then, disappears (though not necessary, I thought it would make a good learning exercise).
So assuming that I have captured the events needed to trigger these, what would be the best way to go about them? I have some code, but I really don't think it will work, and just keeps getting messier. My idea being that the blinking method itself was the thread. Would it be better to make the whole class a thread instead? Please don't feel restricted by the ideas in my code and feel free to improve it. I don't think that the stopping is working correctly because every time I alt+tab out of the window (which i have programmed to disengage from the input box) the Python shell and tkinter GUI stop responding.
from tkinter import *
import threading, time
class Cursor:
def __init__(self, parent, xy):
self.parent = parent
#xy is a tuple of 4 integers based on a text object's .bbox()
coords = [xy[2]] + list(xy[1:])
self.obj = self.parent.create_rectangle(coords)
self.parent.itemconfig(self.obj, state='hidden')
def blink(self):
blinks = 0
while not self.stop blinks <= 250:
self.parent.itemconfig(self.obj, state='normal')
for i in range(8):
time.sleep(0.1)
if self.stop: break
self.parent.itemconfig(self.obj, state='hidden')
time.sleep(0.2)
blinks += 1
self.parent.itemconfig(self.obj, state='hidden')
def startThread(self):
self.stop = False
self.blinking = threading.Thread(target=self.blink, args=[])
self.blinking.start()
def stopThread(self):
self.stop = True
self.blinking.join()
def adjustPos(self, xy):
#I am not overly sure if this will work because of the thread...
coords = [xy[2]] + list(xy[1:])
self.parent.coords(self.obj, coords)
#Below this comment, I have extracted relevant parts of classes to global
#and therefore, it may not be completely syntactically correct nor
#specifically how I initially wrote the code.
def keyPress(e):
text = canvas.itemcget(textObj, text)
if focused:
if '\\x' not in repr(e.char) and len(e.char)>0:
text += e.char
elif e.keysym == 'BackSpace':
text = text[:-1]
canvas.itemconfig(textObj, text=text)
cursor.adjustPos(canvas.bbox(textObj))
def toggle(e):
if cursor.blinking.isAlive(): #<< I'm not sure if that is right?
cursor.stopThread()
else:
cursor.startThread()
if __name__=="__main__":
root = Tk()
canvas = Canvas(root, width=600, height=400, borderwidth=0, hightlightthickness=0)
canvas.pack()
textObj = canvas.create_text(50, 50, text='', anchor=NW)
root.bind('<Key>', keyPress)
cursor = Cursor(canvas, canvas.bbox(textObj))
#Using left-click event to toggle thread start and stop
root.bind('<ButtonPress-1', toggle)
#Using right-click event to somehow restart thread or set blinks=0
#root.bind('<ButtonPress-3', cursor.dosomething_butimnotsurewhat)
root.mainloop()
If there is a better way to do something written above, please also tell me.
Thanks.

Resources