Setting a tkinter variables using a generic set_value() method - python

I'd like to have a generic method that will change the value of a tkinter variable. I want to be able to do this from a menu option that brings up a new dialog box which is from a generic method that can be used for different variables. the code I have so far is below:
import sys
from tkinter import *
from tkinter import ttk
from tkinter import filedialog
import sequencer as seq
class View(ttk.Frame):
"""Main Gui class"""
def __init__(self, master = None):
ttk.Frame.__init__(self, master, borderwidth=5, width=450, height=500)
self.master = master
self.grid(column=0, row=0, sticky=(N, S, E, W))
self.columnconfigure(0, weight=1)
###############################
### User editable variables ###
self.precision = IntVar(value=4, name='precision')
self.sensitivity = IntVar(value = 50, name='sensitivity')
### User editable variables ###
###############################
self.create_menus()
def create_menus(self):
"""Produces the menu layout for the main window"""
self.master.option_add('*tearOff', FALSE)
self.menubar = Menu(self.master)
self.master['menu'] = self.menubar
# Menu Variables
menu_file = Menu(self.menubar)
menu_edit = Menu(self.menubar)
# Add the menus to the menubar and assign their variables
self.menubar.add_cascade(menu=menu_file, label="File")
self.menubar.add_cascade(menu=menu_edit, label = "Edit")
### ADD COMMANDS TO THE MENUES ###
### File ###
menu_file.add_command(label="Quit", command=self.master.destroy)
### Edit ###
menu_edit.add_command(label="Backbone", command=lambda : self.edit_backbone())
menu_edit.add_command(label="Precision", command=lambda : self.precision.set(self.set_value_int("Precision")))
menu_edit.add_command(label="Sensitivity", command=lambda : self.sensitivity.set(self.set_value_int("Sensitivity")))
menu_edit.add_command(label="print precision", command=lambda : print(self.precision.get()))
menu_edit.add_command(label="print sensitivity", command=lambda : print(self.sensitivity.get()))
def set_value_int(self, name):
"""Standards dialog that return a user define value of a specific type"""
t = Toplevel(self)
t.title("Set " + name)
label = ttk.Label(t, text="Set "+name)
label.grid(row=0)
entry = ttk.Entry(t)
entry.grid(row=1)
cancel = ttk.Button(t, text="Cancel", command=lambda : t.destroy())
cancel.grid(column=0, row=2)
okey = ttk.Button(t, text="Okey", command=lambda : okey(entry.get()))
okey.grid(column=1, row=2)
def okey(value):
"""return value according to type"""
try:
t.destroy()
return int(value)
except:
self.error_box("value must be and integer")
def error_box(self, error_message="Unknown error"):
"""(Frame, String) -> None
Opens an window with an Okey button and a custom error message"""
t=Toplevel(self)
t.title("Error")
label = ttk.Label(t, text=error_message)
label.grid(row=0)
okey = ttk.Button(t, text="Okey", command=lambda : t.destroy())
okey.grid(row=1)
if __name__ == "__main__":
root = Tk()
root.title("Sequencer")
view = View(root)
root.mainloop()
print("End")
The Edit-> print xxxxxx commands are purely for testing purposes to check if the values have changed. If these are executed before trying to change the values of precision or sensitivity then they work as expected.
If you try to change either of the tkinter variables in the way I have tried to do they become None types and I can't see why. I can only assume that you are not allowed to change them in the way that I have but I can't think of another way to do it without having a separated method for each variable which I'd like to avoid.
Baicly I'd like the user to be able to customise the variables precision and sensitivity and use the same method in the code to change the values.
Extra but not necessarily vital:- If there is a way to define which type the variable should be in the methods arguments as well that would be even better as I will have other variables for the user to change later and they will be of different types.
Any help would be much appreciated. I have tried to be as clear as I can but let me know if anything is not.
Thanks

self.set_value_int always returns None, so it's always going to set your variable to none.
Instead of trying to write a complex lambda that is hard to debug, put all of your logic inside the function. Have the function set the value. All you need to do is tell it what variable to set:
menu_edit.add_command(label="Precision",
command=lambda name="Precision", var=self.precision: self.set_value_int(name, var))
...
def set_value_int(self, name, var):
...
def okey():
s = entry.get()
try:
var.set(int(s))
...

Related

How to set focus with multiple class instances for key binding python tkinter

What am I doing wrong in python focus for binding?
I'm trying to bind the button widget to keyboard enter and nothing happens, or the focus ends on the last instance of the class to be loaded.
I added self.bind('<Return>', lambda e: self.convert) to the base code after the button.nothing.
Tried container.bind('<Return>', lambda e: self.convert) and nothing.
I'm at a loss as to why the code isn't binding the key event other than the focus is ending up in the wrong location (focus always ends up on the last instance of the class).
This is the code I'm working from https://github.com/photodude/Tkconverter
Link in the readme to the source non-OOP tutorial and all files to the source tutorial before I modified the code to be more OOP
How do I get the focus on the "active" user visible instance of the class?
Minimal, Reproducible Example
a link to the code is provided above.
Run the python code from the App.py file
In the GUI select "F to C"
enter a numeric value (i.e. like 98 or 100) in the entry box
press the enter key.
When you are on "F to C" and you press enter you get a popup error "could not convert string to float" but when you are on "C to F" the correct operation (temperature conversion) occurs. The "error" that pops up on "F to C" is the same as when you click the "convert" button and the entry field is blank or has something other than a numeric value. This popup error occurs on "C to F" and press enter key with a blank entry or non-numeric value.
If you are on "F to C" or "C to F", enter a numeric value (i.e. like 98 or 100) in the entry box, and click the "convert" button everything works as expected.
Expected binding behavior is for the active instance visible to the user to have the active focus for the key binding like the "convert" button has.
Code posted per community request. See Github for current code and history of attempted fixes.
App.py
# source https://www.pythontutorial.net/tkinter/tkraise/
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showerror
from ControlFrame import ControlFrame
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title('Temperature Converter')
self.geometry('300x120')
self.resizable(False, False)
if __name__ == "__main__":
app = App()
ControlFrame(app)
app.mainloop()
ControFrame.py
# source https://www.pythontutorial.net/tkinter/tkraise/
import tkinter as tk
from tkinter import ttk
from ConverterFrame import ConverterFrame
from TemperatureConverter import TemperatureConverter
class ControlFrame(ttk.LabelFrame):
def __init__(self, container):
super().__init__(container)
self['text'] = 'Options'
# radio buttons
self.selected_value = tk.IntVar()
ttk.Radiobutton(
self,
text='F to C',
value=0,
variable=self.selected_value,
command=self.change_frame).grid(column=0, row=0, padx=5, pady=5)
ttk.Radiobutton(
self,
text='C to F',
value=1,
variable=self.selected_value,
command=self.change_frame).grid(column=1, row=0, padx=5, pady=5)
self.grid(column=0, row=1, padx=5, pady=5, sticky='ew')
# initialize frames
self.frames = {}
self.frames[0] = ConverterFrame(
container,
'Fahrenheit',
TemperatureConverter.fahrenheit_to_celsius
)
self.frames[1] = ConverterFrame(
container,
'Celsius',
TemperatureConverter.celsius_to_fahrenheit
)
self.change_frame()
def change_frame(self):
for frame in self.frames.values():
frame.reset()
frame.grid_remove()
frame = self.frames[self.selected_value.get()]
frame.reset()
frame.tkraise()
frame.grid()
frame.focus_set()
ConverterFrame.py
# source https://www.pythontutorial.net/tkinter/tkraise/
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showerror
class ConverterFrame(ttk.Frame):
def __init__(self, container, unit_from, converter):
super().__init__(container)
self.unit_from = unit_from
self.converter = converter
self.master.bind('<Return>', lambda event=None: self.enter_key())
# field options
options = {'padx': 5, 'pady': 0}
# temperature label
self.temperature_label = ttk.Label(self, text=self.unit_from)
self.temperature_label.grid(column=0, row=0, sticky='w', **options)
# temperature entry
self.temperature = tk.StringVar()
self.temperature_entry = ttk.Entry(self, textvariable=self.temperature)
self.temperature_entry.grid(column=1, row=0, sticky='w', **options)
self.temperature_entry.focus()
# button
self.convert_button = ttk.Button(self, text='Convert')
self.convert_button.grid(column=2, row=0, sticky='w', **options)
self.convert_button.configure(command=self.convert)
# self.convert_button.bind('<Return>', lambda event=None: self.convert)
# result label
self.result_label = ttk.Label(self)
self.result_label.grid(row=1, columnspan=3, **options)
# add padding to the frame and show it
self.grid(column=0, row=0, padx=5, pady=5, sticky="nsew")
def convert(self, event=None):
""" Handle button click event
"""
try:
input_value = float(self.temperature.get())
result = self.converter(input_value)
self.result_label.config(text=result)
except ValueError as error:
showerror(title='Error', message=error)
def reset(self):
self.temperature_entry.delete(0, "end")
self.result_label.text = ''
def enter_key(self):
self.convert()
TemperatureConverter.py
# source https://www.pythontutorial.net/tkinter/tkraise/
class TemperatureConverter:
#staticmethod
def fahrenheit_to_celsius(f, format=True):
result = (f - 32) * 5/9
if format:
return f'{f} Fahrenheit = {result:.2f} Celsius'
return result
#staticmethod
def celsius_to_fahrenheit(c, format=True):
result = c * 9/5 + 32
if format:
return f'{c} Celsius = {result:.2f} Fahrenheit'
return result
Tried solution from Python Tkinter: Binding Keypress Event to Active Tab in ttk.Notebook as below simplified example but was unsuccessful
class ConverterFrame(ttk.Frame):
def __init__(...):
...
tag = str(self)
self._add_bindtag(self, tag)
self.bind_class(tag, '<Return>', lambda event=None: self.convert())
def _add_bindtag(self, widget, tag):
bindtags = widget.bindtags()
if tag not in bindtags:
widget.bindtags((tag,) + bindtags)
for child in widget.winfo_children():
self._add_bindtag(child, tag)
The solution here was to correct ControlFrame.py to initialize each instance of the frames on change_frame(). the original error in the source tutorial instantiated the ConverterFrame() class in a way that the last one would replace all previous ones inrelationship to focus and bindings.
by moving self.frames[0] = ConverterFrame(...)... into specific functions and calling those on change_frame() the frame replacement issue was corrected.
Full Modified code is on GitHub https://github.com/photodude/Tkconverter

Override class where mutliple kwargs are used in different functions

I am looking to override the Tkinter Frame, Button, Lable and Entry widgets to have them creted and packed in the same line of code. (IMO 2 lines to do this and get a reference for the obj is ugly, inefficient and harder to read even if it provides more flexability).
My previous code was this:
def cFrame(self, element, bg="white", borderwidth=0, relief="groove", side=None, padx=0, pady=0, height=0, width=0, expand=0, fill=None, image=None, highlightbackground=None, highlightcolor=None, highlightthickness=0, ipadx=0, ipady=0):
f = self.TkUtil.Frame(element, bg=bg, borderwidth=borderwidth, relief=relief, height=height, width=width, image=image, highlightbackground=highlightbackground, highlightcolor=highlightcolor, highlightthickness=highlightthickness)
f.pack(side=side, padx=padx, pady=pady, ipadx=ipadx, ipady=ipady, expand=expand, fill=fill)
return f
My proposed class would look like this:
class cFrame(Frame):
def __init__(self, master, **kwargs):
Frame.__init__(*(self, master), **kwargs)
self.pack(**kwargs)
The issue with this being **kwargs does not care if the keyword is valid and returns 'bad option -"kwarg"'.
I have tried expanding the function a bit more but I keep having issues:
class cButton(Button):
def __init__(self, master, **kwargs):
Button.__init__(*(self, master))
self.conf(**kwargs)
def conf(self, **kwargs ):
__pk_ops = {}
for k, v in kwargs.items():
try:
self.configure({k:v})
except:
__pk_ops[k] = v
self._pk(__pk_ops)
def _pk(self, __ops):
self.pack(__ops)
In this code, I have to loop through all kwargs before and indervidually configure them which is a lot of wasted time over the 1000's of widgets needed. Additionally, I have issues where self.configure(text="sometext") errors so 'text' is passed through to the self.pack() method because for some magic reason it doesn't like that option in particular (same for compound in this example too).
My end goal is to have x = Frame(root, bg="black") \n x.pack(side=TOP) be replaced with x = cFrame(root, bg="black", side=TOP) without default any of the options like my current code making any changes difficult and any sort of theam almost impossible.
You can create a custom class to override the pack() as below:
def cWidget:
def pack(self, **kw):
super().pack(**kw) # call original pack()
return self # return widget itself
# can do the same for grid() and place()
Then create custom widget class as below:
import tkinter as tk
class cButton(cWidget, tk.Button): pass
# do the same for other widgets you want
Now you can use the custom button class as below:
...
root = tk.Tk()
def clicked():
print('Button clicked')
btn['text'] = 'Clicked' # can use 'btn' to change its text as well
btn = cButton(root, text='Click me!', command=clicked, bg='gold').pack(padx=10, pady=10)
print(f'{btn=}') # here you won't get None
print(isinstance(btn, tk.Button)) # here you get True
...
Note that for Python 3.8+, you can simply use the Walrus Operator (:=):
(btn := tk.Button(root, text='Click me!')).pack(padx=5, pady=5)

NameError: name 'top' is not defined

I am trying to use Tkinter to create a GUI, the code is:
from tkinter import *
class LoginFrame(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.initUI()
# initialize the login screen UI
def initUI(self):
self.parent.title("Login Screen")
# create a menu bar
menubar = Menu(top)
# create a help menu
helpmenu = Menu(menubar, tearoff=0)
helpmenu.add_command(label="About", command=about)
menubar.add_cascade(label="Help", menu=helpmenu)
# display the menu
self.parent.config(menu=menubar)
#----------------------------------------------------------------------
def about():
"""about info"""
print("This is a Tkinter demo")
# create a button
#----------------------------------------------------------------------
def make_button(parent, command, caption=NONE, side=top, width=0, **options): # name error 'top' is not defined
"""make a button"""
btn = Button(parent, text=caption, command=command)
if side != top:
btn.pack(side=side)
else:
btn.pack()
return btn
def main():
top = Tk()
# Set up login frame properties
top.title("Login Screen")
# create a login button
login_btn = make_button(top, about, "Login")
top.mainloop()
if __name__ == '__main__':
main()
I tried to run the code, python gave me the following error:
builtins.NameError: name 'top' is not defined
You only define top in main, not at global scope, and even if it was at global scope, you defined it after make_button; default arguments in Python are evaluated once, at definition time, not looked up at call time.
The best approach would probably be to make most of your functions into class methods, and have the class itself create a top attribute.
But for the time being, you could do a minimalist change:
# Use None as a default at definition time, since top doesn't exist yet
def make_button(parent, command, caption=NONE, side=None, width=0, **options):
"""make a button"""
if side is None: # Convert None to top at call time
side = top
btn = Button(parent, text=caption, command=command)
if side is not top: # Minor tweak: Use identity test over equality
btn.pack(side=side)
else:
btn.pack()
return btn
def main():
global top # Make top a global then define it
top = Tk()
... rest of main ...
Note that this is still not great code; without main being executed, there is no top global defined, so your code is only usable as the main program, not as an importable module without a lot of hackery.
You are referring to top in the make_button parameter list - where you say side=top, but haven't actually defined top before that function. There is no global called top.
You can't set it as the default for a parameter until it is defined.
I also got same error, but I realized, I need to use upper case for "TOP" not "Top", after I used uppercase, it worked for me.
frame = Frame(root)
frame.pack()
root.title("Calcu_Displayframe")
num_1=StringVar()
topframe = Frame(root)
topframe.pack(side=TOP)
txtDisplay=Entry(frame, textvariable=num_1, bd=20, insertwidth=1, font=30)
txtDisplay.pack(side=TOP)
root.mainloop()
if you import tkinter like this : import tkinter as tk
then the pack gonna be test.pack(tk.TOP)
if you import tkinter like this : from tkinter import *
then the gonna be test.pack(TOP)

Allow user to change default text in tkinter entry widget.

I'm writing a python script that requires the user to enter the name of a folder. For most cases, the default will suffice, but I want an entry box to appear that allows the user to over-ride the default. Here's what I have:
from Tkinter import *
import time
def main():
#some stuff
def getFolderName():
master = Tk()
folderName = Entry(master)
folderName.pack()
folderName.insert(END, 'dat' + time.strftime('%m%d%Y'))
folderName.focus_set()
createDirectoryName = folderName.get()
def callback():
global createDirectoryName
createDirectoryName = folderName.get()
return
b = Button(master, text="OK and Close", width=10, command=callback)
b.pack()
mainloop()
return createDirectoryName
getFolderName()
#other stuff happens....
return
if __name__ == '__main__':
main()
I know next to nothing about tkInter and have 2 questions.
Is over-riding the default entry using global createDirectoryName within the callback function the best way to do this?
How can I make the button close the window when you press it.
I've tried
def callback():
global createDirectoryName
createDirectoryName = folderName.get()
master.destroy
but that simply destroys the window upon running the script.
I don't know how experienced are you in Tkinter, but I suggest you use classes.
try:
from tkinter import * #3.x
except:
from Tkinter import * #2.x
class anynamehere(Tk): #you can make the class inherit from Tk directly,
def __init__(self): #__init__ is a special methoed that gets called anytime the class does
Tk.__init__(self) #it has to be called __init__
#further code here e.g.
self.frame = Frame()
self.frame.pack()
self.makeUI()
self.number = 0 # this will work in the class anywhere so you don't need global all the time
def makeUI(self):
#code to make the UI
self.number = 1 # no need for global
#answer to question No.2
Button(frame, command = self.destroy).pack()
anyname = anynamehere() #remember it alredy has Tk
anyname.mainloop()
Also why do you want to override the deafult Entry behavior ?
The solution would be to make another button and bind a command to it like this
self.enteredtext = StringVar()
self.entry = Entry(frame, textvariable = self.enteredtext)
self.entry.pack()
self.button = Button(frame, text = "Submit", command = self.getfolder, #someother options, check tkitner documentation for full list)
self.button.pack()
def getfolder(self): #make the UI in one method, command in other I suggest
text = self.enteredtext.get()
#text now has whats been entered to the entry, do what you need to with it

Move tkinter label using button command

I am trying to recreate the boardgame monopoly using python and tkinter. I know how to place a label on a canvas or a frame, but how should I do this command is being run from another function in the class? I tried it using some function within the class Board, but then the error rises that the label, canvas, etc. are not defined as this happens in __init__(self,parent). How can I solve these errors? Or should I take a different approach to this? Hope I made my problem clear.
import tkFileDialog
from random import randint
class Board(Frame):
def __init__(self,parent):
##create the board
frame = Frame(parent)
frame.pack()
Frame.__init__(self,parent)
frame2 = Frame(frame)
frame2.pack()
c=Canvas(frame2,width=480,height=480)
c.pack(expand=YES,fill=BOTH)
c.background=PhotoImage(file='Board.gif')
c.create_image(0,0,image=c.background,anchor='nw')
##Add player 1
player1=PhotoImage(file='plane.gif')
label_player1 = Label(c,image=player1)
label_player1.image=player1
label_player1.place(x=430,y=420)
##Add player 2
player2=PhotoImage(file='car.gif')
label_player2 = Label(c,image=player2)
label_player2.image=player2
label_player2.place(x=430,y=450)
button = Button(frame, text="Next turn", command=self.next_turn)
button.pack()
button = Button(frame, text="Roll the dice", command=self.roll)
button.pack()
def roll(self):
number=randint(2,12)
if b==0:
self.place_player_down()
return number
def place_player_down(self):
for i in range(number+1):
h=int(430-i*30)
while h>=0:
player2=PhotoImage(file='car.gif')
label_player2 = Label(c,image=player2)
label_player2.image=player2
label_player2.place(x=h,y=420)
root = Tk()
board = Board(root)
board.pack()
root.mainloop()
The approach is correct (wrap your Tkinter widgets in a class with the event handler functions as methods), but you forgot to set the widgets as attributes of the class using the reference to self:
class Board(Frame):
def __init__(self,parent):
# ...
self.c = Canvas(frame2,width=480,height=480)
self.c.pack(expand=YES,fill=BOTH)
# ...
def place_player_down(self):
# Use 'self.c', not just 'c'
I think you want to do something similar with the value number, but it that case I would send it as an argument to place_player_down:
def roll(self):
number=randint(2,12)
if b==0:
self.place_player_down(number)
return number # Keep in mind that this value is returned but not used anymore
def place_player_down(self, number):
# Use 'number'

Categories

Resources