-2

Tkinter Requirements

diagram showing tkinter layout

So I am relatively new to using tkinter and I am struggling with a very specific doubt here. I tried finding solutions to this but as much as I find it obvious, the solution to this doesn't seem to be easy to understand. So if you can see the image above, I am trying to create a GUI for a particular project which requires multi-layer (I am calling it 3D array based) widgets. Let's say the variables used for this pointer system are i, j, and k.

I am creating individual layer widgets using for loop:

for n in range(i):
    frame_x[i] = Frame(root).grid(row = 1, column = i)
    entry_x[i] = Entry(frame_x[i]).grid(row = 2, column = i)
    button_x[i] = Button(frame_x[i]).grid(row=3, column = i)

Please note this is not a functional code, I have tried to keep it to the point just to give an idea of the method I am using. (Let me know if you want a more detailed code block.)

Now coming to the problem. I am able to do the basic part of this. But the problem is that I want it to work dynamically.

Let's say if the user enters j = 4 first. 4 blocks will be created. Later if he changes the value to j = 2 and the presses the button, ideally it should make the widgets at block j= 3 and 4 disappear. But I guess tkinter works on overlapping basis and doesn't change a grid element until something is specifically overlapped over it. How do I do that. I tried destroying the entire frame just after entering the for loop, but that doesn't work as for the first time no widget is created before destroying and python throws NameError saying I can't use a variable before assignment.

Anyways, please let me know how do I do this efficiently. And also in general, if there is a better way to go about the whole thing. Please refer the image above and let me know if it doesn't make sense.

I am not very comfortable with classes in general. I prefer the inefficient way by only using functions to do everything I have to. So it would be great if you can share me a framework without using classes. But its okay if you use them. I know I should start working with classes at some point.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • 2
    You need to provide much more code, see [How to create a Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/mcve). – martineau Nov 06 '18 at 12:09
  • I think you could handle the problem by creating separate pages for each of what you're calling "layers" which will allow you to easily switch from one to another as well as delete them. For starters see @Bryan Oakley's [answer](https://stackoverflow.com/questions/7546050/switch-between-two-frames-in-tkinter/7557028#7557028) to the question [Switch between two frames in tkinter](https://stackoverflow.com/questions/7546050/switch-between-two-frames-in-tkinter) for an example of creating them. It should relatively easy to figure out how to allow the deletion of individual pages. – martineau Nov 06 '18 at 12:52
  • With regards to `tkinter` throwing `NameError` before the loop. If you don't have a way of know whether it's the first time, then wrap the destroying the frame in a `try`/`except` block and ignoring it. You also need to get over your discomfort with classes—they're often the easiest and better way to do things—and are really aren't that hard to understand. Just think of them as reusable code "widgets". – martineau Nov 06 '18 at 13:17
  • @martineau: I humbly request that you do _not_ recommend that solution. That was never designed to be a general purpose starting point for beginners. That one answer has spawned literally hundreds of new questions. I wrote it assuming that the pages were static, and that the person using the pattern already understood tkinter. – Bryan Oakley Nov 06 '18 at 13:18
  • @Bryan: I see. Do you mean you don't want me to _ever_ recommend it, or just that it wasn't a good fit for this question? – martineau Nov 06 '18 at 13:23
  • 1
    @martineau: honestly? I would say almost never. I would delete that answer entirely if I could. Unfortunately, years ago someone copied the code and put it in a beginners tutorial on youtube without properly explaining it. There probably hasn't been a week go by in several years when there wasn't at least one question stemming from that code. :-\ – Bryan Oakley Nov 06 '18 at 13:26
  • @Bryan: Understood. Yeah, I've seen many of those other questions (as well as the youtube beginners tutorial and know exactly what you mean regarding how the [mostly plagiarized and unattributed] code in it differs from your own). – martineau Nov 06 '18 at 13:34
  • @martineau: originally the code was nearly verbatim from the original answer. I called the guy out on it and he eventually admitted he copied the code. He added a tiny attribution in one place, but it's hard to spot. I started adding small changes to my answer so that I could tell if the code was originally mine or if it came from the tutorial. Most questions come from the tutorial. – Bryan Oakley Nov 06 '18 at 13:41
  • Devang Savla: Here's a [good tutorial](https://www.tutorialspoint.com/python/python_classes_objects.htm) on Python OOP ([Object-Oriented Programming](https://en.wikipedia.org/wiki/Object-oriented_programming)) which is what using classes is all about. – martineau Nov 06 '18 at 14:32

1 Answers1

1

First off, I want to address this part of the question:

I guess tkinter works on overlapping basis and doesn't change a grid element until something is specifically overlapped over it.

I'm not entirely sure what you mean by that, but if it means what I think it means, it is a false statement. tkinter doesn't "work on an overlapping basis". If you destroy a widget, it is destroyed. It doesn't matter if it's overlapped or not.


Based on the tiny bit of code you posted, the main problem is that you aren't putting the entry and button in the frame. Because of that, they are not destroyed when you destroy the frame.

The reason you aren't putting the widgets into the frame is because of this line:

frame_x[i] = Frame(root).grid(row = 1, column = i)

In python, when you do x=y().z(), x has the value of z(). Thus, when you do frame_x[i] = Frame(...).grid(...), frame_x[i] has the value of .grid(...), and .grid(...) always returns None. Thus, frame_x[i] will be None.

When you next do entry_x[i] = Entry(frame_x[i]).grid(...), it's the same as doing entry_x[i] = Entry(None).grid(...). Because the master of the Entry is None, it becomes a child of the root window.

So, the first step is to separate the creation of the widget from the layout of the widget.

frame_x[i] = Frame(root)
frame_x[i].grid(row = 1, column = i)

Once you do that, the Entry and Button widgets will become a child of the frame, and you can remove widgets you don't want by destroying the frame (eg: frame_x[i].destroy()), since destroying a widget will also cause all children of the widget to be destroyed.

Once you have that in place, you can destroy unwanted widgets by simply calling .destroy() on the frame. For example, if you have previously created 10 groups and now need only 5, you can destroy the others and then remove them from the list like this:

# assume 'num' contains the number of frames that we want,
# and that it is smaller than the number of items in frames_x
for frame in frames_x[num:]:
    frame.destroy()
frames_x = frames_x[:num]

Here is a complete working program to illustrate. Enter a number and click the button. It will create that many frame+entry+button combinations. Enter a new number that is larger or smaller and it will either add or remove widgets.

This would be easier if you used classes, but you specifically asked for a solution that doesn't use classes. In your real code you probably need to also save the entry widgets in an array so that you can reference them later, but this example is focuses on the creation of the widgets rather than writing your whole program for you.

import tkinter as tk

frames_x = [] def create_widgets():
    global frames_x

    num = int(num_widgets.get())
    # if the number is less than the previous number of
    # widgets, delete the widgets we no longer want
    for frame in frames_x[num:]:
        frame.destroy()
    frames_x = frames_x[:num]

    # if the number is greater than the previous number of
    # widgets, create additional widgets
    for i in range(len(frames_x), num):
        # create new widget
        frame = tk.Frame(root, bd=1, relief="raised")
        entry = tk.Entry(frame)
        button = tk.Button(frame, text="click me")

        # pack entry and button in frame
        button.pack(side="right")
        entry.pack(side="left", fill="x", expand=True)

        # grid the frame in the parent
        frame.grid(row=i+1, column=0, columnspan=2)

        # save the new frame in the array
        frames_x.append(frame)

root = tk.Tk() num_widgets = tk.Entry(root) button = tk.Button(root, text="Create widgets", command=create_widgets)

button.grid(row=0, column=1) num_widgets.grid(row=0, column=0, sticky="ew")

root.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Great explanation of what's wrong with the all too common `mywidget = widget().grid()` tkinter mistake. Totally missed that aspect in the minimal code fragment in the OP's question... – martineau Nov 06 '18 at 13:52