WCTech
WCTech

Reputation: 174

Tkinter dynamically create widgets from button

I'm attempting to make a dynamic GUI where clicking a button causes the creation of a new frame that is placed above the button with 3 entry widgets (user options) inside of it, and I need to be able to read the user input from the 3 entry widgets & possibly alter them. Each time the button is pressed, three new callable entry widgets should appear.

I know that this is wrong because it has been giving me errors, but could something similar to the lists be used to create the widgets dynamically?

from Tkinter import *

app = Tk()

frameNames = []
widgetNames = []

def createwidgets():
    global widgetNames
    global frameNames
    frameNames += (str("g"+str(len(frameNames))))  #why does the letter & number get added as seperate elements?
    widgetNames += [(str("w"+str(len(widgetNames)-1))), 
                    (str("w"+str(len(widgetNames)))),    
                    (str("w"+str(len(widgetNames)+1)))]

    frameNames[len(frameNames) - 1] = Frame(app)
    frameNames[len(frameNames) - 1].pack()

    widgetNames[len(widgetNames) - 3] = Entry(frameNames[len(frameNames) - 1])
    widgetNames[len(widgetNames) - 3].pack()
    widgetNames[len(widgetNames) - 2] = Entry(frameNames[len(frameNames - )- 1])
    widgetNames[len(widgetNames) - 2].pack()
    widgetNames[len(widgetNames) - 1] = Entry(frameNames[len(frameNames) - 1])
    widgetNames[len(widgetNames) - 1].pack()

createWidgetButton = Button(app, text="createWidgets", command=createwidgets())
createWidgetButton.grid(sticky=S)

app.mainloop()

Upvotes: 0

Views: 7599

Answers (2)

Bryan Oakley
Bryan Oakley

Reputation: 385800

The main problem is these four lines of code:

frameNames[len(frameNames) - 1] = Frame(app)
frameNames[len(frameNames) - 1].pack()
...
createWidgetButton = Button(app, text="createWidgets", command=createwidgets())
createWidgetButton.grid(sticky=S)

You are creating both the frame and button as a child of app, but you are using grid for one and pack for the other. You must be consistent with all direct descendants of app - they must all use pack or they must all use grid.

The second problem is this line:

frameNames += (str("g"+str(len(frameNames))))  #why does the letter & number get added as seperate elements?

Here, frameNames is a list and you are trying to add it with a string. Adding is not the same as appending. You need to append the new name, or put the new name in a temporary list before adding it.

frameNames.append(str(...))

The third problem is this line:

createWidgetButton = Button(app, text="createWidgets", command=createwidgets())

The above is exactly the same as this:

result = createWidgets()
createWidgetButton = Button(app, text="createWidgets", command=result)

You must pass a reference to a function, not call the function. Change the line to this (notice the lack of parenthesis after createWidgets):

createWidgetButton = Button(app, text="createWidgets", command=createwidgets)

Unrelated to the problem, but your code would be much easier to read if you used temporary variables instead of repeating the pattern (str("w"+str(len(widgetNames)-1). As written, your code is almost impossible to read. Also, you don't want to be storing widget names, you need to store the actual widgets themselves.

And finally, don't do a wildcard import. There is simply no good reason to do it.

Here is how I would rewrite your code:

import Tkinter as tk

app = tk.Tk()

frames = []
widgets = []

def createwidgets():
    global widgetNames
    global frameNames

    frame = tk.Frame(app, borderwidth=2, relief="groove")
    frames.append(frame)

    frame.pack(side="top", fill="x")

    for i in range(3):
        widget = tk.Entry(frame)
        widgets.append(widget)

        widget.pack(side="left")

createWidgetButton = tk.Button(app, text="createWidgets", command=createwidgets)
createWidgetButton.pack(side="bottom", fill="x")

app.mainloop()

Upvotes: 3

Ethan Field
Ethan Field

Reputation: 4730

This is relatively simple.

We can do this by creating a Frame widget but not packing it, filling it with whatever we need and then having a Button call the pack on the Frame widget.

Much like the below:

from tkinter import *

class App:
    def __init__(self, root):
        self.root = root
        self.create = Button(self.root, text="Create", command=self.draw)
        self.create.pack(side="bottom")
        self.frame = Frame(self.root)
        self.entry1 = Entry(self.frame)
        self.entry2 = Entry(self.frame)
        self.entry3 = Entry(self.frame)
        self.entry1.pack()
        self.entry2.pack()
        self.entry3.pack()
        self.submit = Button(self.frame, text="Submit", command=self.command)
        self.submit.pack()
    def draw(self):
        self.frame.pack(side="top")
    def command(self):
        print(self.entry1.get())
        print(self.entry2.get())
        print(self.entry3.get())

root = Tk()
App(root)
root.mainloop()

If you need to add multiple of these forms you can do something like the below which makes use of anonymous functions (lambda) in order to have "self aware" buttons which "know" which frame they're in:

from tkinter import *

class App:
    def __init__(self, root):
        self.frames = []
        self.entries = []
        self.count = 0
        self.root = root
        self.create = Button(self.root, text="Create", command=self.draw)
        self.create.pack(side="bottom")
    def draw(self):
        self.frames.append(Frame(self.root, borderwidth=1, relief="solid"))
        self.frames[self.count].pack(side="top")
        self.entries.append([Entry(self.frames[self.count]), Entry(self.frames[self.count]), Entry(self.frames[self.count])])
        for i in self.entries[self.count]:
            i.pack()
        Button(self.frames[self.count], text="Submit", command=lambda c=self.count: self.submit(c)).pack()
        self.count += 1
    def submit(self, c):
        for i in self.entries[c]:
            print(i.get())

root = Tk()
App(root)
root.mainloop()

Upvotes: 0

Related Questions