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()
Related
So I have a class that just creates a gui with a text box to type into. I would like to store multiple of these objects and their content that is entered by the user. I have a button (on a seperate gui) that creates the new object and spawns the gui and then stores that into a simple list listOobjects = []. My thought process was that I could simply just call listOobjects[i] when a button was pressed to bring that gui back with its contents still in place. That didn't work so then I thought maybe I could use the listOobjects[i].withdraw() and listOobjects[i].deiconify() to hide and recall what gui I want. Not only did that not work but I also feel that isn't the best course of action with possibly 12 gui's practically 'minimized'. I also looked into pickle to save and recall objects. I haven't tried it yet but was curious of my best course of action here?
Although I don't know what is considered best practice in this case, here is one possible way that avoids keeping track of 'minimized' widgets that aren't being used.
This solution deletes widgets which are not currently being displayed by calling the destroy method, so the program isn't doing extra work maintaining objects that aren't being used.
In order to be able to recall the deleted widgets when the button is pressed, all relevant information from a widget is recorded and stored in a tuple before the widget is deleted. This way, no information is lost, but only simple values are being stored instead of tkinter widgets.
These tuples of values are then used as parameters to instantiate new widgets which exactly replicate the old, deleted ones.
Here is a simple demo that toggles between three different Entry widgets:
import tkinter as tk
window = tk.Tk()
window['width'] = 500
window['height'] = 300
textBox1 = tk.Entry(master=window, bg='darkblue', fg='yellow', text="First",
relief=tk.RAISED, bd=3)
textBox2 = tk.Entry(master=window, bg='purple', fg='white', text="Second",
relief=tk.RAISED, bd=3)
textBox3 = tk.Entry(master=window, bg='darkgreen', fg='yellow', text="Third",
relief=tk.RAISED, bd=3)
textBox1.place(x=50, y=100, width=100, height=30)
textBox2.place(x=200, y=100, width=100, height=30)
textBox3.place(x=350, y=100, width=100, height=30)
# Will store all information necessary to reconstruct and place previously displayed
# text boxes which have been removed.
storage = [None, None, None]
# Store a reference to the text box which is currently displayed
activeTextBox = {'textBox': textBox1, 'index': 0}
# After recording all information to be used in 'storage', call 'destroy()' to delete
# the widget instead of hiding it (for more efficiency)
def sendToStorage(textBox, index):
parameters = (textBox['bg'], textBox['fg'], textBox['relief'], textBox['bd'], textBox.get())
storage[index] = parameters
textBox.destroy()
# Using the stored information, construct a new text box (tk.Entry widget) which is
# identical to the old one that was deleted.
def retrieveFromStorage(index):
txtB = storage[index]
storage[index] = None
activeTextBox['textBox'] = tk.Entry(window, bg=txtB[0], fg=txtB[1], relief=txtB[2], bd=txtB[3])
activeTextBox['textBox'].insert(0, txtB[4])
activeTextBox['textBox'].place(x=50+150*index, y=100, width=100, height=30)
# Put the old text box in storage and retrieve the one after it. Increment the index
# of the text box that is currently active (loop around once you get to the end of 'storage').
def toggleTextBox():
sendToStorage(activeTextBox['textBox'], activeTextBox['index'])
activeTextBox['index'] += 1
if activeTextBox['index'] == len(storage):
activeTextBox['index'] = 0
retrieveFromStorage(activeTextBox['index'])
window.update()
# DEMO: CALL FUNCTION TO STORE AND DELETE ALL BUT 1 TEXT BOX
sendToStorage(textBox2, 1)
sendToStorage(textBox3, 2)
# THIS IS THE BUTTON THAT WILL CYCLE BETWEEN THE TEXT BOXES
toggleButton = tk.Button(master=window, text='TOGGLE ACTIVE TEXT BOX',
command=toggleTextBox)
toggleButton.place(x=100, y=200, width=300, height=50)
window.mainloop()
It will keep track of the text boxes' current text (that the user has entered) as well as their formatting options. Try it out and see if it does what you're looking for!
I'm attempting to create a level selection menu of sorts for a terminal-based game I'm making. I had it sorting levels into columns of ten, but it occured to me that the menu would get very wide as more levels are added. Instead, I'm trying to make an alphabetical list that can be scrolled through.
I've followed several posts' advice about making a scrollable frame, and I've run into a problem I haven't seen before: The window keeps flashing and spazzing out. It only occurs one I add the line indicated below, but that line also is the one that makes the level buttons appear.
def show_levels(self, frames):
self.menu_title = tk.Label(self,
text=" Nonstop Robot ",
fg="blue"
)
if frames:
self.frame_levels = tk.Frame(self,
relief="sunken",
width=50,
height=100
)
self.level_canvas = tk.Canvas(self.frame_levels)
self.level_list = tk.Frame(self.level_canvas)
self.level_scroll = tk.Scrollbar(self.frame_levels,
orient="vertical",
command=self.level_canvas.yview
)
self.level_canvas.configure(yscrollcommand=self.level_scroll.set)
self.frame_menu = tk.Frame(self)
def level_bind(event):
self.level_canvas.configure(
scrollregion=self.level_canvas.bbox("all"),
width=70,
height=200
)
self.level_buttons = []
"""
Code that adds the buttons to all the frames.
"""
for b in self.level_buttons:
b.pack()
if frames:
self.level_scroll.pack(side="right", fill="y")
self.level_canvas.pack(side="left")
self.level_canvas.create_window((0, 0),
window=self.frame_levels,
anchor="nw"
)
self.frame_levels.bind("<Configure>", level_bind)
self.frame_levels.pack(side="bottom")
self.level_list.pack() # This is the line in question
self.frame_menu.pack(side="top")
self.menu_title.pack()
I removed the code that actually creates all the buttons, since it's very long. If needed, I can add it.
You can't use both create_window and pack on self.frame_levels, and if you use self.frame_levels.pack, it will not scroll along with the rest of the canvas.
You also seem to have the problem that you create a canvas that is a child of self.frame_levels, and then later you try to add self.frame_levels to the canvas.
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.
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()
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.