How to fit Tkinter listbox to contents - python

I'm adding strings to a listbox using the code below. When I run the code and the window opens, the longer strings get clipped as the window is not large enough (see screenshot). I have tried making the window resizeable and adding scroll bars but I was wondering if there was a way to automatically size it to fit the content.
master = tk.Tk()
listbox = tk.Listbox(master, selectmode=tk.SINGLE)
games = ["Garry's Mod", "Mount and Blade: Warband", "Tekkit"]
for game in sorted(games):
listbox.insert(tk.END, game)
button = tk.Button(master, text="Execute", command=execute)
listbox.pack()
button.pack()
tk.mainloop()

Resetting the listbox width worked for me. I used the Oblivion's answer and noticed that the width is always zero.
listbox = tk.Listbox(master, selectmode=tk.SINGLE)
listbox.config(width=0)
I also recommend to reset the root window geometry after reloading a content of the list. Otherwise if user manually extends a window the window would stop accommodate size of its content.
root.winfo_toplevel().wm_geometry("")

just give width and height 0 as below
listbox.config(width=0,height=0)

tkListAutoWidth.py shows one way to do it.
Edit:
So you might have something along the lines of,
import tkinter as tk
from tkinter import font
class NewListbox(tk.Listbox):
def autowidth(self, maxwidth=100)
autowidth(self, maxwidth)
def autowidth(list, maxwidth=100):
f = font.Font(font=list.cget("font"))
pixels = 0
for item in list.get(0, "end"):
pixels = max(pixels, f.measure(item))
# bump listbox size until all entries fit
pixels = pixels + 10
width = int(list.cget("width"))
for w in range(0, maxwidth+1, 5):
if list.winfo_reqwidth() >= pixels:
break
list.config(width=width+w)
if __name__ == "__main__":
master = tk.Tk()
listbox = NewListbox(master, selectmode=tk.SINGLE)
# ...
# ...
keys = serverDict.keys()
for key in sorted(keys):
listbox.insert("end", key)
listbox.pack()
button = tk.Button(master, text="Execute", command=execute)
button.pack()
listbox.autowidth()
master.mainloop()

Related

Tkinter is there a way to resize the scroll bar in a canvas window

I have made most of this window already, and would prefer to not have to restart because of a hitch with a scrollbar not resizing properly. Problem being that the scrollbars appear way too small for the listboxes and I want them to span the whole height of each box respecitvely, but as of now they can only function if you spam the arrows as the actual scrolling bit can't move for lack of space. Any help would be appreciated, stuck on this for a while now. (Using python 3.8).
import tkinter as tk
from tkinter import *
setup = tk.Tk()
setup.title("Set Up Game")
setup.geometry("450x650")
setup.resizable(width=False, height=False)
select_Box = tk.Canvas(setup, width=450, height=496, bg="#cd3636")
select_Box.pack(padx=10)
listbox1 = Listbox(setup, width=33, height=30)
listbox1_win = select_Box.create_window(110,250, window=listbox1)
listbox2 = Listbox(setup, width=33, height=30)
listbox2_win = select_Box.create_window(320,250, window=listbox2)
scroll1 = Scrollbar(setup)
scroll1_win = select_Box.create_window(200,250, window=scroll1)
scroll2 = Scrollbar(setup)
scroll2_win = select_Box.create_window(410,250, window=scroll2)
listbox1.config(yscrollcommand = scroll1.set, selectmode=SINGLE)
scroll1.config(command = listbox1.yview)
listbox2.config(yscrollcommand = scroll2.set, selectmode=SINGLE)
scroll2.config(command = listbox2.yview)
nameArray = ["Bulbasaur", "Ivysaur", "Venasaur", "Charmander", "Charmelion", "Charazard", "Squirtle", "Wartortle", "Blastoise", "Lucario", "Garchomp", "Gengar", "Snorlax", "Reuniclus", "Joel","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder","placeholder"]
for item in nameArray:
listbox1.insert(END, item)
setup.mainloop()
If you want to use Canvas.create_window to place all of your widgets, all you have to do is define the height of your scrollbar (you may need to play around with the numbers a little to get it to the right size).
So the edited snippet from your code will be:
scroll1 = Scrollbar(setup)
scroll1_win = select_Box.create_window(200,
250,
height=480, # this is all you're missing!
window=scroll1)

How to properly use buttons in tkinter with same window

This is my first day using tkinter, and I wanted to know how to properly code what I have done.
What I wanted to make was a kind of diaporama so I wanted to find a way to display pictures in a canvas and have two buttons so I could go to the previous picture or the following one. The way I stored pictures is using a list lof numpy arrays, with l of size n, so I have n pictures and they are the same size. So I wrote the following code:
from tkinter import *
import numpy as np
from PIL import Image, ImageTk
import sys
sys.setrecursionlimit(10000) #I increased the recursion limit because I had some issues
l = [(255*np.random.rand(50,50)).astype(np.uint8) for i in range(105)] #this is a random list in case you want to test the code
def next(i,n):
if i == n-1:
return 0
else:
return i+1
def previous(i,n):
if i==0:
return n-1
else:
return i-1
window = Tk()
window.geometry("200x100+900+500")
def display(i):
#This is to clear my window at each iteration because I would like to keep the same window
for widget in window.winfo_children():
widget.destroy()
array = l[i] #this is the i-th picture
img = ImageTk.PhotoImage(image=Image.fromarray(array),master = window)
canvas = Canvas(window, width=48, height=48)
canvas.place(x=10, y=20)
canvas.create_image(0,0, anchor="nw", image=img)
Label(window, text="Picture n°"+str(i), fg="black").place(x=5, y=0)
Button(window, text ='Next',command=lambda: display(next(i,len(l)))).place(x=140, y=35)
Button(window, text ='Previous',command = lambda: display(previous(i,len(l)))).place(x=70, y=35)
window.mainloop()
display(0)
I know that is bad code and not the way it should be written. It is working fine but I need help to improve the code please.
You should only put the code of updating the image inside display() and create the interface outside the function. Then recursion is not needed.
Also a global variable is required to keep track the current index to the image list. This variable should be updated when Next or Previous button is clicked.
Below is a modified example:
from tkinter import *
import numpy as np
from PIL import Image, ImageTk
l = [(255*np.random.rand(50,50)).astype(np.uint8) for i in range(105)] #this is a random list in case you want to test the code
current = 0
def display(dir):
global current
# update "current" based on "dir"
current = (current + dir) % len(l)
# show the "current" image
image = ImageTk.PhotoImage(Image.fromarray(l[current]))
imgbox.config(image=image, text=f"Picture n°{current}")
imgbox.image = image # keep a reference to avoid "image" from being garbage collected
window = Tk()
window.geometry("200x100+900+500")
# use a label for showing image and text together
imgbox = Label(window, fg="black", compound="bottom", width=70)
imgbox.place(x=5, y=0)
Button(window, text ='Next', command=lambda: display(+1)).place(x=150, y=35)
Button(window, text ='Previous', command=lambda: display(-1)).place(x=80, y=35)
display(0) # show first image
window.mainloop()

Tkinter notebook - Too many tabs for window width

I am having a problem with my first tkinter (Python 3) notebook app.
The canvas on which the data is displayed only needs to be 775px wide, by 480px high. This is all very well until the number of tabs makes the window wider than that. All the data is placed on one side and the other is a sea of emptyness. I have tried to make the notebook widget scrollable but I cannot get it to work.
Any advice would be greatly received.
#!/usr/bin/python
# Try to work with older version of Python
from __future__ import print_function
import sys
if sys.version_info.major < 3:
import Tkinter as tk
import Tkinter.ttk as ttk
else:
import tkinter as tk
import tkinter.ttk as ttk
#============================================================================
# MAIN CLASS
class Main(tk.Frame):
""" Main processing
"""
def __init__(self, root, *args, **kwargs):
tk.Frame.__init__(self, root, *args, **kwargs)
self.root = root
self.root_f = tk.Frame(self.root)
self.width = 700
self.height = 300
# Create a canvas and scroll bar so the notebook can be scrolled
self.nb_canvas = tk.Canvas(self.root_f, width=self.width, height=self.height)
self.nb_scrollbar = tk.Scrollbar(self.root_f, orient='horizontal')
# Configure the canvas and scrollbar to each other
self.nb_canvas.config(yscrollcommand=self.nb_scrollbar.set,
scrollregion=self.nb_canvas.bbox('all'))
self.nb_scrollbar.config(command=self.nb_canvas.xview)
# Create the frame for the canvas window, and place
self.nb_canvas_window = tk.Frame(self.nb_canvas, width=self.width, height=self.height)
self.nb_canvas.create_window(0, 0, window=self.nb_canvas_window)
# Put the whole notebook in the canvas window
self.nb = ttk.Notebook(self.nb_canvas_window)
self.root_f.grid()
self.nb_canvas.grid()
self.nb_canvas_window.grid()
self.nb.grid(row=0, column=0)
self.nb_scrollbar.grid(row=1, column=0, sticky='we')
self.nb.enable_traversal()
for count in range(20):
self.text = 'Lots of text for a wide Tab ' + str(count)
self.tab = tk.Frame(self.nb)
self.nb.add(self.tab, text=self.text)
# Create the canvas and scroll bar for the tab contents
self.tab_canvas = tk.Canvas(self.tab, width=self.width, height=self.height)
self.tab_scrollbar = tk.Scrollbar(self.tab, orient='vertical')
# Convigure the two together
self.tab_canvas.config(xscrollcommand=self.tab_scrollbar.set,
scrollregion=self.tab_canvas.bbox('all'))
self.tab_scrollbar.config(command=self.tab_canvas.yview)
# Create the frame for the canvas window
self.tab_canvas_window = tk.Frame(self.tab_canvas)
self.tab_canvas.create_window(0, 0, window=self.tab_canvas_window)
# Grid the content and scrollbar
self.tab_canvas.grid(row=1, column=0)
self.tab_canvas_window.grid()
self.tab_scrollbar.grid(row=1, column=1, sticky='ns')
# Put stuff in the tab
for count in range(20):
self.text = 'Line ' + str(count)
self.line = tk.Label(self.tab_canvas_window, text=self.text)
self.line.grid(row=count, column=0)
self.root.geometry('{}x{}+{}+{}'.format(self.width, self.height, 100, 100))
return
# MAIN (MAIN) =======================================================
def main():
""" Run the app
"""
# # Create the screen instance and name it
root = tk.Tk()
# # This wll control the running of the app.
app = Main(root)
# # Run the mainloop() method of the screen object root.
root.mainloop()
root.quit()
# MAIN (STARTUP) ====================================================
# This next line runs the app as a standalone app
if __name__ == '__main__':
# Run the function name main()
main()
OK, so I think I understand now. The tabs are inside the notebook, and inseperable from the notebook. As such, the notebook will always be as wide as the frames within it. To get the effect I wanted I would need put a canvas into the notebook, and then add the tabs the the canvas. And that is not allowed. So back to the drawing board!
If the tabs are of 'constant' width and you know how many will fit the desired (fixed?)size of the window, you could create a "scrolling tabs" widget by hiding the ones that don't fit your width. Create two buttons, left and right that for example hides the one to the right and shows the next hidden one to the left.
If there a way to figure out the width of a tab (fontsize in the label, padding etc?) it could be done more 'dynamic'.
I would recommend combining the solutions from here: Is there a way to add close buttons to tabs in tkinter.ttk.Notebook? (to be able to close a tab) and here: https://github.com/muhammeteminturgut/ttkScrollableNotebook to use buttons instead of a scroll-bar to handle the width issue.
Two changes to get it to work are to load the "notebookTab" variable as the CustomNotebook and to put the closing icon on the left side by switching the order of the innermost children of style.layout in the first answer. This produces a slidable and closeable custom notebook type.

Python - TKinter - Destroying widgets not lowering frame height

I am having and issue where I have a frame in a game that displays the current progress of the game (let's call this frame; "results").
If the player chooses to start a new game all the widgets inside results get destroyed and the frame is forgotten to hide it until it is used again.
Now the issue I am having is; When results gets called back it is in-between two other frames. However, it has remained the size it was in the previous game when it has contained all the widgets, before the widgets were destroyed. The widgets are not shown in the frame but it's still the size it was when the widgets were there.
As soon as a new widget is placed in results the size is corrected but I can't figure out how to make the height = 0. I have tried results.config(height=0) but that hasn't worked.
Does anyone know how to "reset" the size of the frame to 0?
Sorry for the proverbial "wall-of-text" but I couldn't find a way to provide the code in a compact way.
Thanks
If I completely understand what you want, then this illustration is correct:
The blue is the results frame
The results removed, everything else resized:
And the corresponding code for this is something like:
import tkinter
RESULTS_WIDTH = 128
root = tkinter.Tk()
left_frame = tkinter.Frame(root, height=64, bg='#cc3399')
right_frame = tkinter.Frame(root, height=64, bg='#99cc33')
def rem_results(event):
# Remove widget
results.destroy()
# Resize other widthets
left_frame.config(width=128 + RESULTS_WIDTH/2)
right_frame.config(width=128 + RESULTS_WIDTH/2)
# Reposition other widgets
left_frame.grid(row=0, column=0)
right_frame.grid(row=0, column=1)
def add_results(event):
# Create results widget
global results
results = tkinter.Frame(root, width=RESULTS_WIDTH, height=64, bg='#3399cc')
results.grid(row=0, column=1)
# Resize other widgets
left_frame.config(width=128)
right_frame.config(width=128)
# Reposition other widgets
left_frame.grid(row=0, column=0)
right_frame.grid(row=0, column=2)
# Initialize results
add_results(None)
# Bind actions to <- and -> buttons
root.bind( '<Left>', rem_results )
root.bind( '<Right>', add_results )
#$ Enter eventloop
root.mainloop()

How do I create a Tiling layout / Flow layout in TkInter?

I want to to fill my window with, say, labels and I want them to wrap once the column would be bigger than the current window (or rather parent frame) size.
I've tried using the grid layout, but then I have to calculate the size of the content of each row myself, to know when to put the next element in the next row.
The reason I ask, is because I want to create some sort of tiled file icons.
Or asked differently, is there something like Swing's FlowLayout for TkInter?
What I do when I want something like this is use the text widget for a container. The text widget can have embedded widgets, and they wrap just like text. As long as your widgets are all the same height the effect is pretty nice.
For example (cut and pasted from the question at the author's request):
textwidget = tk.Text(master)
textwidget.pack(side=tk.LEFT, fill=tk.BOTH)
for f in os.listdir('/tmp'):
textwidget.window_create(tk.INSERT, window=tk.Label(textwidget, text=f))
Here is a way to make flow behavior inside a frame.
I wrote a function that will do this. Basically you pass a frame to the function (not root or top level) and the function will look at all the children of the frame, go through them measure their sizes and place them in the frame.
Here is the placement procedure
Place the first widget, and move x over an amount equal to its width.
Measure the next widget.
If placing the next widget would cause it to goes past the frame width, bump its x value to 0 and bump it down a y value equal to the largest widget in the current row (start a new row).
Reset the value of the largest widget since you are starting a new row.
Keep repeating until all widgets are placed.
Bind that procedure to the resizing of the frame event.
I used 3 functions to make this work:
The function that runs the procedure.
The function that binds the resizing of the frame to the function.
The function that unbinds the resizing of the frame.
Here are the functions:
from tkinter import *
def _reorganizeWidgetsWithPlace(frame):
widgetsFrame = frame
widgetDictionary = widgetsFrame.children
widgetKeys = [] # keys in key value pairs of the childwidgets
for key in widgetDictionary:
widgetKeys.append(key)
# initialization/priming loop
width = 0
i = 0
x = 0
y = 0
height = 0
maxheight = 0
# loop/algorithm for sorting
while i < len(widgetDictionary):
height = widgetDictionary[widgetKeys[i]].winfo_height()
if height > maxheight:
maxheight = height
width = width + widgetDictionary[widgetKeys[i]].winfo_width()
# always place first widget at 0,0
if i == 0:
x = 0
y = 0
width = widgetDictionary[widgetKeys[i]].winfo_width()
# if after adding width, this exceeds the frame width, bump
# widget down. Use maximimum height so far to bump down
# set x at 0 and start over with new row, reset maxheight
elif width > widgetsFrame.winfo_width():
y = y + maxheight
x = 0
width = widgetDictionary[widgetKeys[i]].winfo_width()
maxheight = height
# if after adding width, the widget row length does not exceed
# frame with, add the widget at the start of last widget's
# x value
else:
x = width-widgetDictionary[widgetKeys[i]].winfo_width()
# place the widget at the determined x value
widgetDictionary[widgetKeys[i]].place(x=x, y=y)
i += 1
widgetsFrame.update()
def organizeWidgetsWithPlace(frame):
_reorganizeWidgetsWithPlace(frame)
frame.bind("<Configure>", lambda event: _reorganizeWidgetsWithPlace(frame))
_reorganizeWidgetsWithPlace(frame)
def stopOrganizingWidgetsWithPlace(frame):
frame.unbind("<Configure>")
And here is an example of them in use:
def main():
root = Tk()
root.geometry("250x250")
myframe = Frame(root)
# make sure frame expands to fill parent window
myframe.pack(fill="both", expand=1)
buttonOrganize = Button(myframe, text='start organizing',
command=lambda: organizeWidgetsWithPlace(myframe))
buttonOrganize.pack()
buttonStopOrganize = Button(myframe, text='stop organizing',
command=lambda: stopOrganizingWidgetsWithPlace(myframe))
buttonStopOrganize.pack()
##### a bunch of widgets #####
button = Button(myframe, text="---a random Button---")
canvas = Canvas(myframe, width=80, height=20, bg="orange")
checkbutton = Checkbutton(myframe, text="---checkbutton----")
entry = Entry(myframe, text="entry")
label = Label(myframe, text="Label", height=4, width=20)
listbox = Listbox(myframe, height=3, width=20)
message = Message(myframe, text="hello from Message")
radioButton = Radiobutton(myframe, text="radio button")
scale_widget = Scale(myframe, from_=0, to=100, orient=HORIZONTAL)
scrollbar = Scrollbar(myframe)
textbox = Text(myframe, width=3, height=2)
textbox.insert(END, "Text Widget")
spinbox = Spinbox(myframe, from_=0, to=10)
root.mainloop()
main()
Notice:
That you do not need to grid, pack or place them. As long as you specify the frame, that will all be done at once when the function is called. So that is very convenient. And it can be annoying if you grid a widget, then try to pack another, then try to place another and you get that error that you can only use one geometry manager. I believe this will simply overwrite the previous choices and place them. I believe you can just drop this function in and it will take over management. So far that has always worked for me, but I think you should really not try to mix and match geometry managers.
Notice that initially the buttons are packed, but after pressing the button, they are placed.
I have added the "WithPlace" naming to the functions because I have a similar set of functions that do something very similar with the grid manager.

Categories

Resources