tkinter - resizing empty frame - python

How do you force a frame to get window_height 0?
the general case where my problem occurs:
import Tkinter as Tk
class App(Tk.Frame):
def __init__(self, master):
Tk.Frame(self, master)
self.place_holder = Tk.Frame(master=self)
self.content = Tk.Frame(master=self)
self.place_holder.pack()
self.content.pack(side=Tk.RIGHT)
Tk.Button(master=self,command=self.add_something).pack(side=Tk.TOP)
self.to_destroy = []
def add_something(self):
foo = Tk.button(master=self.place_holder, command=self.destroy_last)
self.too_destroy.append(foo)
def destroy_last(self):
self.to_destroy[-1].destroy()
the problem:
As I add more elements to the place_holder, it rescales nicely.
When I remove elements from the place_holder, it rescales nicely.
EXCEPT when I remove the last element.
Before i added anything, even when i do place_holder.pack(), it will not show. But after removing the last element, the place_holder will keep the size of this last element. Is there a way to hide the place_holder again untill i add content again?
example image
The empty container at the bottom left does not contain any elements, but still has the size of the last element in it, how can i get this to disappear without removing it (i want it again in the same place)?

What is happening is that when you remove the last widget, pack no longer is managing the frame so it isn't responsible for setting the frame size.
The simplest solution is just to temporarily pack a 1x1 pixel frame, which wil cause the placeholder frame to shrink.
There's no way to make a frame of zero pixels, so this method will always result in a one pixel tall/wide area for the placeholder. If you don't want that one pixel, you can install call pack_forget on the placeholder to completely remove it from the display, and then use pack with suitable options to re-add it when you put something in it.
Example:
def destroy_last(self):
self.to_destroy.pop().destroy()
if len(self.to_destroy) == 0:
tmp = Tk.Frame(self.place_holder, width=1, height=1, borderwidth=0)
tmp.pack()
self.place_holder.update()
tmp.destroy()

Related

Tkinter, update widgets real time if list is modified

Let say I've a list of widgets that are generated by tkinter uisng a loop (it's customtkinter in this case but since tkinter is more well known so I think it'd be better to make an example with it), each widgets lie in the same frame with different label text. Here is an example for the code:
x=0
self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame")
self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
self.scrollable_frame.grid_columnconfigure(0, weight=1)
self.scrollable_frame_switches = []
for i in range(x,100):
switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}")
switch.grid(row=i, column=0, padx=10, pady=(0, 20))
self.scrollable_frame_switches.append(switch)
My question is, if the list that help generated those widgets change (in this case it's just a loop ranging from 0-100, might change the widgets text, list size..), what would be the best way for real time update the tkinter window contents?
Ps: I've tried to look for my answer from many places but as of right now, the best answer I can come up with is to update the whole frame with same grid but changed list content, I'll put it bellow. Is there any way better than this? Thank you
Like I said before, while the existing answer might work, it might be inefficient since you are destroying and creating new widgets each time there is a change. Instead of this, you could create a function that will check if there is a change and then if there is extra or less items, the changes will take place:
from tkinter import *
import random
root = Tk()
def fetch_changed_list():
"""Function that will change the list and return the new list"""
MAX = random.randint(5, 15)
# Create a list with random text and return it
items = [f'Button {x+1}' for x in range(MAX)]
return items
def calculate():
global items
# Fetch the new list
new_items = fetch_changed_list()
# Store the length of the current list and the new list
cur_len, new_len = len(items), len(new_items)
# If the length of new list is more than current list then
if new_len > cur_len:
diff = new_len - cur_len
# Change text of existing widgets
for idx, wid in enumerate(items_frame.winfo_children()):
wid.config(text=new_items[idx])
# Make the rest of the widgets required
for i in range(diff):
Button(items_frame, text=new_items[cur_len+i]).pack()
# If the length of current list is more than new list then
elif new_len < cur_len:
extra = cur_len - new_len
# Change the text for the existing widgets
for idx in range(new_len):
wid = items_frame.winfo_children()[idx]
wid.config(text=new_items[idx])
# Get the extra widgets that need to be removed
extra_wids = [wid for wid in items_frame.winfo_children()
[-1:-extra-1:-1]] # The indexing is a way to pick the last 'n' items from a list
# Remove the extra widgets
for wid in extra_wids:
wid.destroy()
# Also can shorten the last 2 steps into a single line using
# [wid.destroy() for wid in items_frame.winfo_children()[-1:-extra-1:-1]]
items = new_items # Update the value of the main list to be the new list
root.after(1000, calculate) # Repeat the function every 1000ms
items = [f'Button {x+1}' for x in range(8)] # List that will keep mutating
items_frame = Frame(root) # A parent with only the dynamic widgets
items_frame.pack()
for item in items:
Button(items_frame, text=item).pack()
root.after(1000, calculate)
root.mainloop()
The code is commented to make it understandable line by line. An important thing to note here is the items_frame, which makes it possible to get all the dynamically created widgets directly without having the need to store them to a list manually.
The function fetch_changed_list is the one that changes the list and returns it. If you don't want to repeat calculate every 1000ms (which is a good idea not to repeat infinitely), you could call the calculate function each time you change the list.
def change_list():
# Logic to change the list
...
calculate() # To make the changes
After calculating the time for function executions, I found this:
Widgets redrawn
Time before (in seconds)
Time after (in seconds)
400
0.04200148582458496
0.024012088775634766
350
0.70701003074646
0.21500921249389648
210
0.4723021984100342
0.3189823627471924
700
0.32096409797668457
0.04197263717651367
Where "before" is when destroying and recreating and "after" is only performing when change is needed.
So I've decided that if I want to click a button, that button should be able to update the list. Hence, I bind a non-related buttons in the widget to this function:
def sidebar_button_event(self):
global x
x=10
self.scrollable_frame.destroy()
self.after(0,self.update())
Which will then call for an update function that store the change value, and the update function will just simply overwrite the grid:
def update(self):
self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame")
self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
self.scrollable_frame.grid_columnconfigure(0, weight=1)
self.scrollable_frame_switches = []
for i in range(x,100):
switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}")
switch.grid(row=i, column=0, padx=10, pady=(0, 20))
self.scrollable_frame_switches.append(switch)

Prevent ListBoxItems from being resized (specifically shrunk)

I have a Gtk Listbox to which I'm adding a large number of items (basically text labels). I've put the ListBox inside a ScrolledWindow but if I add too many items then the height of each item is reduced until the text on the label is no longer readable.
How can I prevent the ListBox items from being reduced in height as I add more of them?
The code I'm using to create the ListBox and add the items looks like this:
# Add the listbox
self.test_list_window = Gtk.ScrolledWindow();
self.test_list = Gtk.ListBox()
self.test_list.connect("row_activated", some_method)
self.test_list_window.add(self.test_list)
The adding of the items is done with this method (each ListBox item has a LHS and RHS label). I thought that the set_size_request would add a minimum size to the ListBox entries but it does not appear to do so (also setting a specific height in pixels feels like the wrong answer I just want to prevent the rows from shrinking).
def add_list_box_entry(self, lhs, rhs, lbox, set_min_size=False):
box = Gtk.Box()
if set_min_size:
box.set_size_request(10, 10)
box.pack_start(Gtk.Label(label=lhs, xalign=0), True, True, 1)
lab = Gtk.Label(label=f'({rhs})')
lab.set_halign(0.95)
box.pack_start(lab,False, True, 5)
lbox.add(box)
To resolve this behavior you need to call show_all() on the Listbox after adding the items.
If you don't there is a vertically tiny entry in the Listbox for each item you added. This can be clicked on but that doesn't display the widgets it contains and so can give the impression that the items have been scaled down from their normal height.

How to limit the rightest Gtk.TreeViewColumn's width inside a Gtk.ScrolledWindow?

I cannot prevent the rightest column of a Gtk.TreeView to expand.
As the real Gtk.TreeView may display a greater number of rows, making it usually somewhat greater than the screen's height, it is embedded in a Gtk.ScrolledWindow. This is required. Without it, attaching an empty grid at the right of the treeview, expanding itself horizontally, would fix the problem. Based on this idea, I've tried a workaround that introduces another difficulty (see below).
I have built a minimal working example from the example from https://python-gtk-3-tutorial.readthedocs.io/en/latest/treeview.html#filtering, without filtering nor buttons; and the columns are 80 px wide at least (this works) and their content is horizontally centered. This last detail makes the horizontal expansion of the rightest column visible. In the original example, it does expand too, but as everything is left aligned, this is not really visible. I'd liked to keep the columns' content centered, without seeing the rightest expanded.
This example is minimal, but contains some helping features: you'll find clickable column titles, that will display some information about the clicked column in the console; a remove button (works fine, remove the selected rows) and a paste button that allows to paste new rows from a selection (e.g. from selected lines from a spreadsheet, but there's nothing to check the data are correct, if you paste something that does not convert to int, it will simply crash).
Workaround
A workaround I've tried consist of gathering both the treeview and a horizontally expanding empty right grid at its right inside a grid that would be put inside the Gtk.ScrolledWindow. It works, but causes other subtle problems: in some situations, the treeview does not get refreshed (it happens after a while), yet nothing prevents the main loop to refresh the view (there's no other processing in the background, for instance). To experiment this workaround: comment and uncomment the lines as described in the code below; run the program via python script.py (if you need to install pygobject in a venv, see here), notice the rightest column does not expand to the right any longer, select the 3 first rows and press "remove", then from a spread sheet, select 3 lines of dummy integers as shown below and then press "paste". Scroll down to the last rows: you'll see most of the time that the 3 pasted lines do not show up, even if it is possible to scroll over the last row. Maybe one of them will show up after some time, then another... (or simply select a row, and they'll show up). Strangely, it happens if one has just removed as many lines as one wants to paste after the removal (3 removed, 3 pasted; or 4 removed, 4 pasted etc.).
Example spreadsheet selection:
Question
So, I'd prefer to avoid the workaround (I'm afraid I may find other situations triggering a bad refreshing of the treeview), that I could not fix itself (for instance, setting self.scrollable_treelist.set_propagate_natural_height(True) proved useless, maybe I'm not using it correctly though?) and only attach the treeview itself directly in the Gtk.ScrolledWindow. How to prevent the rightest column to expand, then?
(I've tried to use a fair amount of setters and properties of the cell renderers, the treeview, the treeview columns, the scrolled window, to no avail. Some of them are still in the code below.)
Any solution using and fixing the workaround above would be accepted though.
In any case, the treeview may be scrolled, and lines may be added and removed from it without any refreshing problem.
Source Code
import gi
try:
gi.require_version('Gtk', '3.0')
except ValueError:
raise
else:
from gi.repository import Gtk, Gdk
# ints to feed the store
data_list = [(i, 2 * i, 3 * i, 4 * i, 5 * i) for i in range(40)]
class AppWindow(Gtk.Window):
def __init__(self):
super().__init__(title="Treeview Columns Size Demo")
self.set_border_width(10)
# Setting up the self.grid in which the elements are to be positioned
self.grid = Gtk.Grid()
self.grid.set_column_homogeneous(True)
self.grid.set_row_homogeneous(True)
self.add(self.grid)
# Creating the ListStore model
self.store = Gtk.ListStore(int, int, int, int, int)
for data_ref in data_list:
self.store.append(list(data_ref))
# creating the treeview and adding the columns
self.treeview = Gtk.TreeView(model=self.store)
rend = Gtk.CellRendererText()
rend.set_alignment(0.5, 0.5)
for i, column_title in enumerate([f'nĂ—{p}' for p in [1, 2, 3, 4, 5]]):
column = Gtk.TreeViewColumn(column_title, rend, text=i)
column.set_min_width(80)
# column.set_max_width(80)
# column.set_fixed_width(80)
# column.set_sizing(Gtk.TreeViewColumnSizing(1))
column.set_alignment(0.5)
column.set_clickable(True)
column.connect('clicked', self.on_column_clicked)
self.treeview.append_column(column)
self.treeview.set_hexpand(False)
self.treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
# Put the treeview in a scrolled window
self.scrollable_treelist = Gtk.ScrolledWindow()
self.scrollable_treelist.set_vexpand(True)
self.grid.attach(self.scrollable_treelist, 0, 0, 8, 10)
self.scrollable_treelist.add(self.treeview)
# WORKAROUND
# Alternatively, embed the treeview inside a grid containing an
# empty grid to the right of the treeview
# To try it: comment out the previous line; uncomment next lines
# scrolled_grid = Gtk.Grid()
# empty_grid = Gtk.Grid()
# empty_grid.set_hexpand(True)
# scrolled_grid.attach(self.treeview, 0, 0, 8, 10)
# scrolled_grid.attach_next_to(empty_grid, self.treeview,
# Gtk.PositionType.RIGHT, 1, 1)
# self.scrollable_treelist.add(scrolled_grid)
# self.scrollable_treelist.set_propagate_natural_height(True)
# Buttons
self.remove_button = Gtk.Button(label='Remove')
self.remove_button.connect('clicked', self.on_remove_clicked)
self.paste_button = Gtk.Button(label='Paste')
self.paste_button.connect('clicked', self.on_paste_clicked)
self.grid.attach_next_to(self.remove_button, self.scrollable_treelist,
Gtk.PositionType.TOP, 1, 1)
self.grid.attach_next_to(self.paste_button, self.remove_button,
Gtk.PositionType.RIGHT, 1, 1)
self.set_default_size(800, 500)
self.show_all()
# Clipboard (to insert several rows)
self.clip = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)
self.clip2 = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
def on_column_clicked(self, col):
print(f'col.get_sizing()={col.get_sizing()}')
print(f'col.get_expand()={col.get_expand()}')
print(f'col.get_width()={col.get_width()}')
print(f'col.get_min_width()={col.get_min_width()}')
print(f'col.get_max_width()={col.get_max_width()}')
print(f'col.get_fixed_width()={col.get_fixed_width()}')
def on_remove_clicked(self, widget):
model, paths = self.treeview.get_selection().get_selected_rows()
refs = []
for path in paths:
refs.append(Gtk.TreeRowReference.new(model, path))
for ref in refs:
path = ref.get_path()
treeiter = model.get_iter(path)
model.remove(treeiter)
# print(f'AFTER REMOVAL, REMAINING ROWS={[str(r[0]) for r in model]}')
def on_paste_clicked(self, widget):
text = self.clip.wait_for_text()
if text is None:
text = self.clip2.wait_for_text()
if text is not None:
lines = text.split('\n') # separate the lines
lines = [tuple(L.split('\t')) for L in lines] # convert to tuples
print(f'PASTE LINES={lines}')
for line in lines:
if len(line) == 5:
line = tuple(int(value) for value in line)
self.store.append(line)
win = AppWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

Why does a frame keep a non null size when emptied?

I have a list of tasks to do. That list has several subgroups, that I want to display together. To do so I created a frame for each subgroup and display each tasks in the corresponding frame with grid(). Once a task is done, I destroy the corresponding label.
When a subgroup is empty from the start, tkinter reduce the size of the frame to 0, and I don't see it. But when a subgroup that had tasks becomes empty it seems to have a minimum size and does not disappear. Is there a way to prevent that?
Here is an example of my problem: the frame0 does not appear because it is empty (which is good). But even after destroying the labels of frame1 (with the buttons) the frame1 keeps one row for some reason.
import tkinter as tk
window=tk.Tk()
frame0=tk.Frame(window,bg='green')
frame1=tk.Frame(window,bg='red')
frame2=tk.Frame(window,bg='blue')
window.grid_columnconfigure(0,weight=1)
frame0.grid(sticky='ew')
frame1.grid(sticky='ew')
frame2.grid(sticky='ew')
labelList1=[]
for i in range(2):
labelList1.append(tk.Label(frame1,text='Task type 1'))
labelList1[-1].grid(sticky='ew',pady=5)
tk.Label(frame2,text='Task type 2: 1').grid(sticky='ew',pady=5)
tk.Label(frame2,text='Task type 2: 2').grid(sticky='ew',pady=5)
for i in range(2):
tk.Button(window,text='Destroy {}'.format(i),command=labelList1[i].destroy).grid()
window.mainloop()
I have found a way around it by using only one frame and using rows 1 to 100 for subgroup 0, 101 to 200 to subgroup 1, etc But I don't find this elegant.
Tkinter will not resize a Frame if it contains no child widgets. So if you had a callback that removed all your labels at once the Frame wouldn't resize at all. A simple (if somewhat kludgy) workaround is to add a dummy Frame widget to frame1. Eg, add this line
tk.Frame(frame1).grid()
before your labelList1 loop.
FWIW, here's a variation of your code that destroys both the label and the corresponding button.
import tkinter as tk
window=tk.Tk()
frame0=tk.Frame(window,bg='green')
frame1=tk.Frame(window,bg='red')
frame2=tk.Frame(window,bg='blue')
window.grid_columnconfigure(0,weight=1)
frame0.grid(sticky='ew')
frame1.grid(sticky='ew')
frame2.grid(sticky='ew')
tk.Frame(frame1).grid()
num_tasks = 3
labelList1=[]
for i in range(num_tasks):
labelList1.append(tk.Label(frame1,text='Task type 1: {}'.format(i)))
labelList1[-1].grid(sticky='ew',pady=5)
tk.Label(frame2,text='Task type 2: 1').grid(sticky='ew',pady=5)
tk.Label(frame2,text='Task type 2: 2').grid(sticky='ew',pady=5)
def kill_label_and_button(l, b):
l.destroy()
b.destroy()
for i in range(num_tasks):
b = tk.Button(window,text='Destroy {}'.format(i))
b.config(command=lambda l=labelList1[i], b=b: kill_label_and_button(l, b))
b.grid()
window.mainloop()

Python tkinter main window improper size when .grid() widgets

I have a game board which is rows x columns list.
Min size is 2x2 and max 10x10, with unequal rows:columns being okay (e.g. 2x3, 4x9).
Main window object has no predetermines geometry size setting, and widgets (buttons) are being .grid() in it for each list element in a was that creates a 2D map.
Ideally, given the method used this would lead to a nice, edge=to-edge map inside the main window.
Unfortunately, testing has shown that while this is true for maps with columns count > 3, when columns <= 3 then the window seems to default to a certain X-size, where this ugly free space is present at the right of the window.
This is not the case for Y-axis, which is defined by rows.
Note that buttons placed are fixed 32x32 px (determined by image inside).
def createMap (): #creates rows x columns 2D list - a map
global rowsEntryVar, columnsEntryVar, mapList
mapList = []
for row in range(rowsEntryVar):
tempList = []
for column in range(columnsEntryVar):
tempList.append(Button(root, bd=0, bg=redMagenta, activebackground=redMagenta))
mapList.append(tempList)
and then:
def drawMap ():
global mapList
for row in range(len(mapList)):
for column in range(len(mapList[row])):
mapList[row][column].grid(row=row, column=column)
Image:
Image showing the problem
Please go easy on me, I'm quite new to programming. :)
This appears to be a platform-specific limitation. I can't duplicate the problem on my Mac, but I can on a windows VM. Apparently, Windows won't allow the width of the window to be smaller than the space required for the buttons and icon on the titlebar.
My advice is to give the rows and columns a positive weight so that they will grow to fit the window, and then use the sticky option to cause the buttons to fill the space given to them.
when columns <= 3 then the window seems to default to a certain X-size,
Tkinter defaults to the size of the widgets so you must be setting the geometry for "root" somewhere. The following works fine on my Slackware box (and using a function as a function eliminates the globals). If you are just starting, then it is good to form good habits, like conforming to the Python Style Guide https://www.python.org/dev/peps/pep-0008/ (variables and functions are all lower case with underlines).
from Tkinter import *
def create_map (rowsEntryVar, columnsEntryVar): #creates rows x columns 2D list - a map
mapList = []
for row in range(rowsEntryVar):
tempList = []
for column in range(columnsEntryVar):
tempList.append(Button(root, text="%s-%s" % (row, column),
bd=0, bg="magenta2", activebackground=r"magenta3"))
mapList.append(tempList)
return mapList
def draw_map(mapList):
for row in range(len(mapList)):
for column in range(len(mapList[row])):
mapList[row][column].grid(row=row, column=column)
root = Tk()
map_list=create_map(4, 3)
draw_map(map_list)
root.mainloop()

Categories