tkinter, "refresh" the mainwindow - python

Though I think that the solution might be similar to this one: tkinter, display a value from subwindow in mainwindow , I still decided to ask this question since I have trouble to figure it out on my own.
I have the list "fields" with which I am creating any given number of rows, with two labes inside of them. After opening a subwindow, I want to be able to manipulate that list (in my example simply just append at the moment) and after clicking on a button (here "ADD"), I want the mainwindow to update, so that it shows the rows of the manipulated list. It works fine for the most part but I dont what is the best way to update the mainwindow in this example.
The only solution I was able to come up with, is to destroy the mainwindow and recreate it but I have the feeling that this might not be the best solution. Is there a better one?
import tkinter as tk
a=0
fields=[("a",1),("c",2),("e",3)]
class clsApp(object):
def __init__(self):
self.root=tk.Tk()
self.root.title("MainWindow")
##Labels##
self.rootLabel=tk.Label(self.root, text="WindowAppExperiment", padx=100)
self.aLabel=tk.Label(self.root, text=a, padx=100)
##Buttons##
self.BtExit=tk.Button(self.root, text="Quit", fg="red", command=self.root.quit)
###self.BtNewWindow=tk.Button(self.root, text ="Edit", command=lambda:self.clsNewWindow(self.root, self.aLabel).run())
self.BtNewField=tk.Button(self.root, text ="New Field", padx=30, command=lambda:self.clsNewFields(self.root).run())
def grid (self):
self.rootLabel.pack()
self.aLabel.pack()
self.fckPackFields()
self.BtNewField.pack()
self.BtExit.pack(side="left")
###self.BtNewWindow.pack(side="right")
def fckPackFields(self):
if fields:
for field in fields:
##create labels##
row=tk.Frame(self.root)
nameLabel=tk.Label(row, text =field[0], width=20, anchor="w")
valueLabel=tk.Label(row, text =field[1], width=5)
##pack labels##
row.pack(side="top", fill="x", padx=5, pady=5)
nameLabel.pack(side="left")
valueLabel.pack(side="right", expand=True, fill="x")
def run(self):
self.grid()
self.root.mainloop()
self.root.destroy()
class clsNewFields(object):
def __init__(self, Parent):
self.parent=Parent
##Window##
self.top=tk.Toplevel()
self.top.title("Add Fields")
##Labels##
self.enterNameLabel=tk.Label(self.top, text ="Enter fieldname", padx=10)
self.enterValueLabel=tk.Label(self.top, text ="Enter value", padx=10)
##Entryfields##
self.EntryName=tk.Entry(self.top)
self.EntryValue=tk.Entry(self.top)
##Buttons##
self.BtADD=tk.Button(self.top, text ="ADD", command=lambda:self.fckAddField(self.EntryName, self.EntryValue))
self.BtClose=tk.Button(self.top, text ="Close", command=self.top. quit)
def grid(self):
self.enterNameLabel.pack()
self.enterValueLabel.pack()
self.EntryName.pack()
self.EntryValue.pack()
self.BtADD.pack()
self.BtClose.pack()
def fckAddField(self, Name, Value):
self.name=Name.get()
self.value=Value.get()
global fields
fields.append((self.name, self.value))
print(fields)
self.parent.update
def run(self):
self.grid()
self.top.mainloop()
self.top.destroy()
clsApp().run()

Welcome to StackOverflow.
First of all - do you really want to declare the clsNewFields inside your clsApp ? Yes, the Fields should be used inside App, but i do not see a need for using class-in-class-declaration.
Second - you are packing the Fields in def fckPackFields(self):. This is not automatically called when you update it.
You are not calling update function by using self.parent.update.
You are using global variable for fields, what does not really suit your needs. Why not having a list inside your App-class like:
def __init__(self):
self.__fields=[]
def __set_fields(self, value):
self.__fields=value
def __get_fields(self):
return self.__fields
Fields = property(__get_fields, __set_fields)
def __loadUI(self, event=None):
# This function should be called at the end of __init__
self.fieldFrame=tk.Frame(self.root)
self.fieldFrame.pack(side="top")
def fckPackFields(self):
#First clean area
[...]
#Then add fields
for field in self.__fields:
# create the row, etc.
# !!! but do it inside self.fieldFrame !!!
[...]
I would prefer using grid instead of pack over here, because there I think it is easier to place a frame at a certain position, then you could just destroy self.fieldFrame and recreate it at the same position for placing the fields in it.
UPDATE:
Just checked your code again. With some simple tricks your can tweak your GUI to do what you want:
def __init__(self):
self.fieldFrame=None #this line has been added
#completely reworked this function
def grid(self):
self.rootLabel.grid(row=1, column=0, columnspan=2, sticky=tk.NW+tk.SE)
self.fckPackFields()
self.BtNewField.grid(row=3, column=0, sticky=tk.NW+tk.SE)
self.BtExit.grid(row=3, column=1, sticky=tk.NW+tk.SE)
#Only one line changed / one added
def fckPackFields(self):
self.__cleanFields() #added this line, function beyond
if fields:
for field in fields:
##create labels##
row=tk.Frame(self.fieldFrame) #add the row to the fieldFrame
[...]
#In here we destroy and recreate fieldFrame as needed
def __cleanFields(self):
if self.fieldFrame:
self.fieldFrame.destroy()
##FieldFrame##
self.fieldFrame=tk.Frame(self.root)
self.fieldFrame.grid(row=2, column=0, columnspan=2)
In clsNewFields:
def fckAddField(self, Name, Value):
[...]
self.parent.fckPackFields() # instead of self.parent.update
EDIT:
Have a look at these two questions:
Class inside a Class
Benefit of nested Classes
I did not mean to point out that nested classes are to be avoided in general but I do want to focus you into the thought of "is there a real necessity or benefit of it for my use-case".

Related

python Tkinter grid method not working as should be for some reason

i am trying to get my listbox to move to the bottom of the gui but no matter how high of a row value i give it it wont budge. you can see my listbox in the Creat_Gui method in my code. im not sure why this is happpening it cant be the button because the button is in row 1 so im not sure whats causing this.
i tried using sticky='s' that didnt work i tried changing the rows multiple times didnt work. i tried using the root.rowconfigure(100,weight=1) this worked kind of but messes with thte grid which is annoying
import tkinter as tk
class Manager:
def __init__(self):
self.root=tk.Tk()
self.root.title('password_manager')
self.root.geometry('500x600')
self.create_GUI()
self.storage = {}
self.root.mainloop()
def create(self):
pass
def open_page(self):
print('openpage')
def add_new_button(self):
pass
def add_new(self):
self.app_title=tk.Label(text='test')
self.app_title.grid(row=2,column=50)
self.application_name=tk.Entry(self.root,width=20,font=('arial',18))
self.username=tk.Entry(self.root,width=20,font=('arial',18))
self.password=tk.Entry(self.root,width=20,font=('arial',18))
self.application_name.grid(row=2, column=1)
self.username.grid(row=3, column=2)
self.password.grid(row=4, column=3)
self.app_name_label = tk.Label(self.root, text='Application Name:')
self.username_label = tk.Label(self.root, text='Username:')
self.password_label = tk.Label(self.root, text='Password:')
self.app_name_label.grid(row=2, column=0)
self.username_label.grid(row=3, column=1)
self.password_label.grid(row=4, column=2)
self.password.bind('<Return>',self.hide)
def hide(self,thing):
#store user info to pass onto dictionary and hide textboxes
username=self.username.get()
password=self.password.get()
app_name=self.application_name.get()
self.application_name.grid_forget()
self.username.grid_forget()
self.password.grid_forget()
self.add_to_memory(username,password,app_name)
def add_to_memory(self,username,password,app_name):
#store username password and application name in dictionary
if app_name in self.storage.keys():
return
else:
self.storage[app_name]=(username,password)
print(self.storage)
def create_GUI(self):
#create initial interface
#self.root.columnconfigure(100, weight=1)
#self.root.rowconfigure(100, weight=1)
self.listbox=tk.Listbox(self.root,width=100)
self.listbox.grid(row=200,column=0)
self.button=tk.Button(self.root,text='add new',font=('arial',18),command=self.add_new)
self.button.grid(row=1,column=0)
Manager()
If you want to use grid, and you want a widget to be at the bottom of it's parent, you need to designate some row above that widget to take all of the extra space, forcing the widget to the bottom. Here's one example:
def create_GUI(self):
self.listbox=tk.Listbox(self.root,width=100)
self.expando_frame = tk.Frame(self.root)
self.button=tk.Button(self.root,text='add new',font=('arial',18),command=self.add_new)
self.root.grid_rowconfigure(2, weight=1)
self.button.grid(row=1,column=0)
self.expando_frame.grid(row=2, sticky="nsew")
self.listbox.grid(row=3,column=0)
With that, the listbox will be at the bottom, with extra space above it. If you want to add more widgets, you can add them to self.expando_frame rather than self.root and they will appear above the listbox.
Using frames in this way is a valuable technique. You could, for example, use three frames in root: one for the top row of buttons, one for the listbox on the bottom, and one for everything in the middle. You could then use pack on these frames and save a line of code (by virtue of not having to configure the rows). You can then use grid for widgets inside the middle frame.

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)

Adding a standard object as a Frame class attribute with Tkinter

I am trying to create a GUI using Tkinter that will allow me to interface with some lab equipment.
I would like to have a combo box reference the handles of various pieces of equipment in my "hardware manager".
Currently, in the top-level frame (that is, one below Root()), I instantiate the hardware manager as a class atribute. However, I am having some trouble accessing that information from nested frames.
Below is the test code (to simplify things):
from Tkinter import *
import ttk
class TestClass():
def __init__(self):
self.test_array = [1, 2, 3]
class TestFrame(Frame):
def __init__(self, frMaster):
Frame.__init__(self, frMaster, bg='gray', padx=5, pady=5)
self.frMaster = frMaster
self.testClass = TestClass()
self.subFrame = SubFrame(self.frMaster)
self.subFrame.grid(row=0, column=0, padx=10, pady=10)
class SubFrame(Frame):
def __init__(self, frMaster):
Frame.__init__(self, frMaster)
self.frMaster = frMaster
self.sHandle = StringVar()
self.cbTest = ttk.Combobox(self.frMaster, width=20, justify='left',
values=[0, 1, 2], textvariable=self.sHandle)
self.cbTest.grid(row=0, column=0, padx=10, pady=10, sticky='nesw')
root = Tk()
test = TestFrame(root)
root.mainloop()
What I would like to happen is to use the test_array attribute from the test_class as the value list for the combo box. So ideally, the line instantiating the combo box would actually look something like this:
self.cbTest = ttk.Combobox(self.frMaster, width=20, justify='left',
values=self.frMaster.testClass.test_array,
textvariable=self.sHandle)
This obviously won't work, though, as self.frMaster in that instance actually refers to the root window, not the test_frame instance. So I get an attribute error.
I also tried instantiating the SubFrame simply with "self" as the master. This technically works, but it fails to display the combo box.
So, is there a better way to access the data in the TestClass instance? Or, alternatively, how can I get the combo box to display if I pass the TestFrame instance as the master to the SubFrame instance?
Thanks!
In the simplest terms possible, if you need the attributes of an object (eg: test_array from TestClass), you need a reference to that object (eg: the instance of TestClass).
There are several ways to do it. The simplest is to pass the reference down to the code that needs it. For example:
class TestFrame(Frame):
def __init__(self, frMaster):
...
self.testClass = TestClass()
self.subFrame = SubFrame(self.frMaster, self.testClass)
...
class SubFrame(Frame):
def __init__(self, frMaster, testclass):
...
self.cbTest = ttk.Combobox(..., values=testclass.test_array, ...)

How to automatically insert several tkinter items in Tk() window using classes

I apologize in advance if this is a stupid simple question, but i am really bad att python classes and can't seem to get it to work!
Here is my code:
from tkinter import *
a = Tk()
class toolsGUI():
def __init__(self, rootWin):
pass
def frame(self):
frame = Frame(rootWin)
frame.configure(bg = 'red')
frame.grid()
def button(self, binding, text):
btn = Button(rootWin, text=text)
btn.configure(bg = 'orange', fg = 'black')
btn.bind('<'+binding+'>')
btn.grid(row=1, sticky = N+S+E)
I simply want the button() or frame() to understand that rootWin is the same as in __init__, in this case rootWin should be variable a, thus placing the button in the Tk() window. After looking around, I understand that this is not the way to do it. Do anyone have another suggestion that might work?
You're pretty close. You are passing a to the toolsGUI initializer which is the right first step. You simply need to save this as an instance variable, then use the variable whenever you need to reference the root window:
def __init__(self, rootWin):
...
self.rootWin = rootWin
...
def frame(self):
frame = Frame(self.rootWin)
...
An alternative is to have toolsGUI inherit from Frame, in which case you can put all of the widgets in the frame instead of the root window. You then need the extra step of putting this frame inside the root window.
class toolsGUI(Frame):
def __init__(self, rootWin):
Frame.__init__(self, rootWin)
def frame(self):
frame = Frame(self)
...
a = Tk()
t = toolsGUI(a)
t.pack(fill="both", expand=True)
a.mainloop()
As a final bit of advice: don't user variables that are the same name as methods if you can avoid it. "frame" is a poor choice of function names. Instead, call it "create_frame" or something, otherwise it could be confused with class Frame and the local variable frame

how to stack two widgets and switch between them?

I want to script two widgets which would be stacked and I would switch from one to another with a key. Each widget would be a Frame containing several Labels. I have the following code so far (only one Label per Frame):
import Tkinter as tk
import ttk
import datetime
def main():
# initialize root
root = tk.Tk()
# initialize widgets
dash = Dashboard(root)
notepad = Notepad(root)
# set key actions
root.bind('<F11>', root.lift)
root.bind('<F1>', dash.raiseme)
root.bind('<F2>', notepad.raiseme)
root.mainloop()
class Dashboard(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # voodoo
self.dashframe = tk.Frame(parent)
self.labone = tk.Label(self.dashframe, text="lab1", fg='black', bg='blue')
self.labone.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme dash"
self.labone.configure(text=datetime.datetime.now())
self.dashframe.lift()
class Notepad(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # also voodoo
self.noteframe = tk.Frame(parent)
self.laboneone = tk.Label(self.noteframe, text="lab11", fg='white', bg='red')
self.laboneone.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme notepad"
self.laboneone.configure(text=datetime.datetime.now())
self.noteframe.lift()
if __name__ == '__main__':
main()
Pressing F1 and F2 reach the correct routines but the only thing I get is the main window, empty. There are no errors displayed so I guess that the code runs fine (just not the way O would like to :)).
Can I achieve the switch using the skeleton above?
There are at least two big problems with your code.
First, you're creating all these new frames, but not placing them anywhere, so they will never show up anywhere. If you have a main window with nothing placed on it, of course you will just "get the main window, empty". You need to call pack or some other layout method on any widget to get it to show up on its parent. In this case, it sounds like you want to put them both in the exact same place, so grid or place is probably what you want.
Second, your Dashboard and Notepad classes are themselves Frames, but they don't do any Frame-ish stuff; instead, they each create another, sibling Frame and attach a label to that sibling. So, even if you packed the Dashboard and Notepad, they're just empty frame widgets, so that wouldn't do any good.
If you fix both of those, I think your code does what you want:
class Dashboard(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # voodoo
self.labone = tk.Label(self, text="lab1", fg='black', bg='blue')
self.labone.grid(row=0, column=0)
self.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme dash"
self.labone.configure(text=datetime.datetime.now())
self.lift()
class Notepad(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # also voodoo
self.laboneone = tk.Label(self, text="lab11", fg='white', bg='red')
self.laboneone.grid(row=0, column=0)
self.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme notepad"
self.laboneone.configure(text=datetime.datetime.now())
self.lift()
However, you might also want to set a fixed size for everything; otherwise you could end up lifting the red widget and, e.g., only covering 96% of the blue one because the current time is a bit narrower than the previous oneā€¦
The code you linked to for inspiration attempted to do this:
newFrame = tkinter.Frame(root).grid()
newFrame_name = tkinter.Label(newFrame, text="This is another frame").grid()
That won't work, because grid returns None, not the widget. But at least it calls grid; yours doesn't even do that.

Categories

Resources