Tkinter: nested LabelFrame (s) not shown - python

I want to split a LabelFrame into two label frames. So first, I created an other LabelFrame and tested if it displays well. But no, it is not displayed.
But when I change childLabelFrame to a simple Label or a simple Frame I see it displayed well.
I read some similar questions such as this one, but I did not do those errors in my case.
mainLabelFrame=LabelFrame(parent,text="Description:",padx=20,pady=20,200, width=400,relief=RIDGE)
childLabelFrame=LabelFrame(mainLabelFrame,text="Help",relief=RIDGE)
childLabelFrame.grid(row=0,column=0)
mainLabelFrame.grid(row=3,column=0,columnspan=3,sticky=E+W)
How to resolve this ?

It seems like childLabelFrame has zero size and thus is not drawn. Indeed, both childLabelFrame.winfo_width() and childLabelFrame.winfo_height() return 1.
It is drawn correctly if
you specify a size, like childLabelFrame = LabelFrame(mainLabelFrame, text="Help", height=100, width=200), or
you add something inside the child label frame, e.g. Label(childLabelFrame, text="label").grid().

Related

How to update layers of tkinter widgets dynamically?

Tkinter Requirements
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.
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()

How to prevent floating widgets in tkinter python

I have the following UI for my tkinter application. My problem is that the OptionsMenu with long text moves all other widgets to the right and eventually out of frame.
Solutions that I can think of:
1) Wrap text to next row.
2) A way that StringVar can truncate the selected text of the dropdown up until certain characters but does not change the original value (selected in dropdown) it stores.
3) Stop making them float and overlap over other widgets.
Here is the image of the erratic behavior.
Here is my code: for the Finding Category dropdown.
if finding_names != []:
finding_names.insert(0,'All')
finding_type_select.set(finding_names[0])
finding_type_dropdown = OptionMenu(tab3_project_reports,finding_type_select,*finding_names)
finding_type_dropdown.configure(font='helvetica 12')
finding_type_dropdown.grid(row=5, column=1,padx=10, pady=10,sticky=W+E+N+S)
finding_type_dropdown.grid_columnconfigure(0, weight=1)
I have also tried to use the grid_columnconfigure but I did not really see any change.
I would appreciate any help. Thanks in advance.
The only thing I can see here that could be the problem (without seeing more code) is how you are using grid_columnconfig(). You can only apply a column/row config to a container. These would be the root window, a Toplevel() window or a Frame. You are currently using grid_columnconfigure() and that will work but note you can also just do columnconfigure() without the grid_ portion. Same goes for rows.
Instead of this:
finding_type_dropdown.grid_columnconfigure(0, weight=1)
Do this:
tab3_project_reports.grid_columnconfigure(0, weight=1)

ttk.Separator appearing as dot "." when using .pack() layout manager

My question is similar to this one, but I'm using the layout manager pack rather than grid so the answer in the alternate thread doesn't work for me.
Code:
iconLabelImage = ttk.Label(labelFrame)
self.iconImage = PhotoImage(file='images\icon.png')
iconLabelImage['image'] = self.iconImage
iconLabelImage.pack(anchor='w')
sep = ttk.Separator(parameterFrame, orient=VERTICAL)
sep.pack(side="right", fill="y")
The LabelFrame is a child of the parameterFrame.
It doesn't matter what parameters I change I can't seem to get the separator to extend more than a pixel even though it exists in a larger frame.
Any ideas?
Actually the idea is same with the question you have provided above. That means:
The expand option tells the manager to assign additional space to the widget box. If the parent widget is made larger than necessary to hold all packed widgets, any exceeding space will be distributed among all widgets that have the expand option set to a non-zero value.
-effbot
The point here you should focus on is: non-zero value/weight.
So to solve this problem using pack method add expand=True option.

Python - Moving an object\label\button and changing it's attribute

So I'm learning python and the book that is teaching me gives me two ways to create a label using tkinker:
self.canvas.create_text(30,10,text="Welcome",tags="text")
&
self.lbl = Label(frame1, text = "Welcome")
In the former example, moving it is easy:
self.canvas.move("text", 1, 0)
In the latter example, changing it's background color is easy:
self.lbl["bg"] = "red"
However I do not know how to both move it AND change it's background color in either example, at least not how to move it incrementally. I can do this:
self.lbl.place(x=2)
But unless I can get the x coordinate ahead of time, I can only move it once. I could set it ahead of time, but I'd like to avoid that option if possible.
There are ways to do both.
Firstly, Canvas text does not have a background, but you can create your own with a rectangle.
text = self.canvas.create_text(30, 10, text="Welcome", tags="text")
# The canvas.bbox method returns the corner coordinates of the provided item id.
rect = self.canvas.create_rectangle(self.canvas.bbox(text), fill='red')
# Then you need to reposition the rectangle so that it is behind the text.
self.canvas.lower(rect, text)
From there you just move them the same way as you mentioned in your question. Adding a group tag to both the text and rectangle would save you from having to move both items separately.
Secondly, you can get the current x, y coordinates of a widget with the .winfo_x() and .winfo_y() methods. So moving the Label becomes a simple matter of addition/subtraction:
self.lbl.place(x=self.lbl.winfo_x()+2)
I do not know of a method that moves a widget in increments as the move method does for the canvas.
As for which is best, I can't think of much between them. I suppose using a Canvas would mean you couldn't overlap any other widgets that may be in the window, since the text would just scroll out of view, and if you start using the ttk version of Label then styling isn't quite as straight forward, although it's not difficult.

How do you create a LabelFrame with a scrollbar in Tkinter?

I'm using Python and Tkinter to create a GUI for a program I'm writing, and I'm having a couple of problems.
I have three objects descended from LabelFrame in an object descended from Frame. One of the LabelFrame descendants is two columns of corresponding Label and Entry objects.
The problem is that there are a varying number of Label and Entry pairs, and there can be more than fit on the screen. I need a way to make a scrollbar for this LabelFrame so that everything fits on the screen. I've tried various ways of making a Scrollbar object, but nothing seems to work. How can I bind a scrollbar to this frame?
Also, I need to be able to refresh or reload this LabelFrame when the load_message() method is called, but it just redisplays the new pairs on top of the old ones (so when there are less pairs in the new set, the old set is still visible at the bottom). I've tried using grid_forget() but either nothing changes or the whole frame doesn't display. How can I forget this display and then redisplay it?
Here is the code for this class:
class freq_frame(LabelFrame):
def __init__(self, master = None, text = 'Substitutions'):
LabelFrame.__init__(self, master, text = text)
self.grid()
def load_message(self):
self.frequency = get_freq(message)
self.create_widgets()
def create_widgets(self):
self.label_list = [Label(self, text = get_label(char, self.frequency[char]), justify = LEFT) for char in self.frequency.keys()]
self.entry_list = [Entry(self, width = 1) for char in self.frequency.keys()]
for n in range(len(self.label_list)):
self.label_list[n].grid(column = 0, row = n)
for n in range(len(self.entry_list)):
self.entry_list[n].grid(column = 1, row = n)
If anyone can help with either of these problems, I'd appreciate it.
Also, this question seems like it might be a little thin, but I don't know what to add. Don't hesitate to ask for more information (but be specific).
Thanks!
Labelframes don't support scrolling. So the short answer to your question is "you can't". It sounds obvious, but if the documentation for a widget doesn't say it supports scrolling, it doesn't support scrolling.
However, there is a simple solution. First, add a canvas as a child to the labelframe and pack it so that it fills the labelframe. Attach scrollbars to the canvas and add them to the labelframe too. Then embed a frame within the canvas, add your widgets to that inner frame, and then adjust the scrollregion of the canvas to match the size of the frame after you've added all the inner labels and entries.
It sounds complicated, but it's really very straight-forward.
As for re-creating the widgets when you call load_message, calling grid_forget only removes them from view, it doesn't actually destroy the widgets. Over time you could potentially end up with hundreds of non-visible widgets which is almost certainly not what you want.
Instead, you want to first destroy all the existing widgets. That's pretty easy if they all are in the same parent, since you can ask the parent for a list of all its children. Just iterate over that list to delete each child, then add any new children. An even easier solution is to destroy and recreate that inner frame that contains the labels and entries. When you delete a widget, all child widgets get automatically destroyed. So, delete that inner frame, create a new one, and add your labels and entries again.

Categories