Tkinter grid system weird placement of buttons - python

I am trying to create Hangman game in Tkinter. I am using buttons for choosing the letters, row for the grid is 0, and column is 0,1,2,3 up to 26. For down rows I have label, picture, etc. Now, there is space between first couple of buttons, and when picture is displayed, there is even more spaces, so they even can fit on the window. Here is example code and I will also attach picture from the original program. How can I fix the buttons? picture 1 picture 2
from tkinter import *
from PIL import ImageTk, Image
window = Tk()
A = Button(text ="A").grid(row = 0, column = 0)
B = Button(text ="B").grid(row = 0, column = 1)
C = Button(text ="C").grid(row = 0, column = 2)
D = Button(text ="D").grid(row = 0, column = 3)
E = Button(text ="E").grid(row = 0, column = 4)
lbl = Label(text = "Hello").grid(row = 1, column = 0)
photo = ImageTk.PhotoImage(file='hang1.jpg')
def show_image():
pic = Label(window, image=photo).grid(row=3, column=2)
btn = Button(window, text = "Show image", command = show_image).grid(row = 2, column = 2)
window.mainloop()

It is not weird but standard behavior in all tools which use grid - ie. in Excel or in <table> in HTML .
If you put bigger element in the same column then other elements in then same column will use more space.
One method is to use span which inform then element/image should use more columns and then it will may not resize column - but if image will be too big then it may resize even all columns.
In tkinter in grid() you can use column=0, columnspan=26 to use all columns for image. The same you can use for word and button and they will be centered.
import tkinter as tk # PEP8: `import *` is not preferrred
from PIL import ImageTk, Image
import string
# --- functions --- # PEP8: all functions at the beginning
def show_image():
pic['image'] = photo
# --- main ---
span = len(string.ascii_uppercase)
print('span:', span)
window = tk.Tk()
for number, char in enumerate(string.ascii_uppercase):
b = tk.Button(window, text=char) # use `window` as parent
b.grid(row=0, column=number)
lbl = tk.Label(window, text="Hello")
lbl.grid(row=1, column=0, columnspan=span)
pic = tk.Label(window) # create empty label and later replace only image in this label
pic.grid(row=2, column=0, columnspan=span)
photo = ImageTk.PhotoImage(file='lenna.png')
btn = tk.Button(window, text="Show image", command=show_image)
btn.grid(row=3, column=0, columnspan=24)
window.mainloop()
Result:
Other method is to use Frame to group buttons and then you can use different layout (grid, pack, place) to put other elements
In this version I use frame as parent for buttons.
I can also use different layout to put other elements - pack() - but I could use also grid() but it would be independent of the grid inside the frame.
import tkinter as tk # PEP8: `import *` is not preferrred
from PIL import ImageTk, Image
import string
# --- functions --- # PEP8: all functions at the beginning
def show_image():
pic['image'] = photo
# --- main ---
span = len(string.ascii_uppercase)
print('span:', span)
window = tk.Tk()
frame = tk.Frame(window)
frame.pack()
for number, char in enumerate(string.ascii_uppercase):
b = tk.Button(frame, text=char) # use `frame` as parent
b.grid(row=0, column=number)
lbl = tk.Label(window, text="Hello")
lbl.pack()
pic = tk.Label(window)
pic.pack()
photo = ImageTk.PhotoImage(file='lenna.png')
btn = tk.Button(window, text="Show image", command=show_image)
btn.pack()
window.mainloop()
BTW:
Image Lenna from Wikipedia
PEP 8 -- Style Guide for Python Code

Related

python issue adds a new label

take a look at this code
well it happens to just add another label for no reason
import tkinter
from tkinter import *
clicks = 1
def click_count():
global clicks
# making the label that shows how many idk you have
label = Label(frame, text="you have " + str(clicks), font=(('Courrier'), 32))
label.pack()
clicks += 1
label.pack()
#generating the window
root = tkinter.Tk()
root.geometry('500x500')
#making the expandable frame
frame = Frame(root)
frame.pack(expand=YES)
#making the button
button = Button(frame, text= "click", font=(('Courrier'), 32), command=click_count)
button.pack()
root.mainloop()
and then i tried this
and i also tried to remove the label.pack at the end
but it still does the same thing which is adding another label
import tkinter
from tkinter import *
clicks = 1
def click_count():
global clicks
# making the label that shows how many idk you have
label = Label(frame, text="you have " + str(clicks), font=(('Courrier'), 32))
label.pack()
label.destroy()
clicks += 1
label.pack()
#generating the window
root = tkinter.Tk()
root.geometry('500x500')
#making the expandable frame
frame = Frame(root)
frame.pack(expand=YES)
#making the button
button = Button(frame, text= "click", font=(('Courrier'), 32), command=click_count)
button.pack()
root.mainloop()
i was expecting it to add a nmber to the label but it just shows another label
It does not add a Label for no reason. It adds the label because that's what your function tells it to do. Each time you click the button, the function is executed that creates and packs a new Label.
What you should do is create the label at the onset and link it to a variable. Then, you can change the value of this variable in the function.
Also, you don't have to import tkinter twice and it's sensible to update the clicker first and then display the result instead of showing the last value it had. Your approach works in such a small program but the value of clicks will always be one higher than displayed. So, you may get into problems when you use the value.
from tkinter import *
def click_count():
global clicks
clicks += 1
click_counter.set("you have " + str(clicks))
#Initiate root
root = Tk()
root.geometry('500x500')
#Set initial values for click counter
clicks = 0
click_counter = StringVar()
click_counter.set("you have 0")
#making the expandable frame
frame = Frame(root)
frame.pack(expand=YES)
# making the label that shows how many idk you have
label = Label(frame, textvariable=click_counter, font=(('Courrier'), 32)) ## The label gets updated, whenever the value of click_counter changes.
label.pack()
#making the button
button = Button(frame, text= "click", font=(('Courrier'), 32), command=click_count)
button.pack()
root.mainloop()

tkinter useing grid on a frame doesn't seem to work

Im trying to get a tkinter gui that lets the user sign in im really new to tkinter. I made a frame and when i use grid to put another frame widget inside of the frame it does it based off of the root and not the inner_frame thats what I think is happening. In the code I made a grey box to demonstrate and I dont understand why it is below the blue frame and not inside the yellow frame under the "sign in" text. Thanks for the help.
from tkinter import *
root = Tk()
root.title("sighn in test")
#colors
background = "#273E47"
accent = "#d8973c"
red = "#bb4430"
white = "#edf2f4"
#this creates and places the background frame
main_buttons_frame = Frame(root, height = 500, width = 400, bg = background).grid(row = 0, column = 0)
#this creates and places the inner frame
inner_frame = Frame(main_buttons_frame, height = 450, width = 300, bg = accent).grid(row = 0, column = 0)
#this creates and places the "sighn in text"
top_text = Label(inner_frame, text = "sign in", font = ("helvitica", 30, "bold"), bg = accent, fg =
background).grid(row = 0, column = 0)
#this is a test to demonstrate
test_frame = Frame(inner_frame, bg = "grey", height = 100, width = 100).grid(row = 1, column = 0)
root.mainloop()
You have very common mistake of beginners
inner_frame = Frame(...).grid...()
It assigns None to inner_frame because grid()/pack()/place() gives None.
So later Frame(inner_frame, ..) means Frame(None, ..) and it adds to root
You have to do it in two steps
inner_frame = Frame(...)
inner_frame.grid(...)
And now you have Frame assigned to inner_frame
EDIT:
With correctly assigned widgets I get
and now gray box is inside yellow frame but image shows different problem - grid()/pack() automatically calculate position and size and external frame automatically change size to fit to child's size.
Using .grid_propagate(False) you can stop it
But it shows other problem - cells don't use full size of parent so yellow frame is moved to left top corner, not centered :) Empty cells have width = 0 and heigh = 0 so moving all to next row and next column will not change it - it will be still in left top corner :)
You need to assign weight to column and/or row which decide how to use free space. All columns/rows has weight=0 and when you set weight=1 then this row and/or column will use all free space - (this would need better explanation) - and then element inside cell will be centered.
main_buttons_frame.grid_columnconfigure(0, weight=1)
main_buttons_frame.grid_rowconfigure(0, weight=1)
import tkinter as tk # PEP8: `import *` is not preferred
# --- colors ---
background = "#273E47"
accent = "#d8973c"
red = "#bb4430"
white = "#edf2f4"
# --- main ---
root = tk.Tk()
root.title("sighn in test")
main_buttons_frame = tk.Frame(root, height=500, width=400, bg=background) # PEP8: without spaces around `=` inside `( )`
main_buttons_frame.grid(row=0, column=0)
#main_buttons_frame = None
main_buttons_frame.grid_propagate(False)
main_buttons_frame.grid_columnconfigure(0, weight=1)
main_buttons_frame.grid_rowconfigure(0, weight=1)
inner_frame = tk.Frame(main_buttons_frame, height=450, width=300, bg=accent)
inner_frame.grid(row=0, column=0)
#inner_frame = None
inner_frame.grid_propagate(False)
inner_frame.grid_columnconfigure(0, weight=1)
inner_frame.grid_rowconfigure(0, weight=1)
top_text = tk.Label(inner_frame, text="sign in", font=("helvitica", 30, "bold"), bg=accent, fg=background)
top_text.grid(row=0, column=0,)
#top_text = None
#top_text.grid_propagate(False)
#top_text.grid_columnconfigure(0, weight=1)
#top_text.grid_rowconfigure(0, weight=1)
test_frame = tk.Frame(inner_frame, bg="grey", height=100, width=100)
test_frame.grid(row=1, column=0)
#test_frame = None
#test_frame.grid_propagate(False)
#test_frame.grid_columnconfigure(1, weight=1)
#test_frame.grid_rowconfigure(0, weight=1)
root.mainloop()
BTW: PEP 8 -- Style Guide for Python Code

How can I access dynamic labels in a labelframe, that is within a labelframe?

I am trying to access the text / values of children (labels) within a LabelFrame X, where X itself is within a LabelFrame Y and the number of children changes in tkinter.
Is there a somewhat easy way of solving this problem?
I provided a simplified version of my code below. Essentially, the number of labels changes based on certain inputs (in the example randomized).
What I want to do later on is to write a for-loop that will access each of the labels and then, based on the text/value either replace the label with a new one, or move on to the next.
import tkinter as tk
from tkinter import ttk
import numpy as np
win = tk.Tk()
LabelFrame_level_one = ttk.LabelFrame(text = " Level 1 ")
LabelFrame_level_one.grid(column = 0, row = 0, padx = 8, pady = 4)
def click_update_button():
delete_old_labels = LabelFrame_level_one.grid_slaves()
for labels_ in delete_old_labels:
labels_.destroy()
dynamic_number = max(round(np.random.rand() * 10), 1)
LabelFrame_level_two = ttk.LabelFrame(LabelFrame_level_one, text=" Level 2 ")
LabelFrame_level_two.grid(column=1, row=2, padx=4, pady=4, sticky="W")
for i in range(dynamic_number):
ttk.Label(LabelFrame_level_two, text="Dynamic entry" + str(i)).grid(column=0, row= i, pady=3)
Update_Button = ttk.Button(win, text="Update", command=click_update_button)
Update_Button.grid(column=1, row=0)
win.mainloop()

How to add a specific spacing in pixels between tkinter buttons?

I have written some code for some buttons. However, I am not sure how to add a specific number of pixels of spacing for each button. So far is the code I have written. However, I have not yet figured out a reliable way to add spacing between the buttons in pixel sizes.
import tkinter as tk
#from tkinter import PhotoImage
def banana():
print ("Sundae")
def tomato():
print ("Ketchup")
def potato():
print ("Potato chips")
root = tk.Tk()
root.geometry("960x600")
f1 = tk.Frame(root, width=70, height=30)
f1.grid(row=3, column=0, sticky="we")
button_qwer = tk.Button(f1, text="Banana", command=banana)
button_asdf = tk.Button(f1, text="Tomato", command=tomato)
button_zxcv = tk.Button(f1, text="Potato", command=potato)
button_qwer.grid(row=0, column=0)
button_asdf.grid(row=0, column=1)
button_zxcv.grid(row=0, column=2)
root.mainloop()
Adding space between widgets depends on how you are putting the widgets in the window. Since you are using grid, one simple solution is to leave empty columns between the buttons, and then give these columns a minsize equal to the space you want.
Example:
f1.grid_columnconfigure((1, 3), minsize=10, weight=0)
button_qwer.grid(row=0, column=0)
button_asdf.grid(row=0, column=2)
button_zxcv.grid(row=0, column=4)
Using a specific number of pixels of spacing between each Buttondoesn't sound to me like such as good idea because it isn't very flexible nor easily portable to devices with different resolutions.
Nevertheless I've figured-out a way of doing it—namely by putting a do-nothing invisible button between of the each real ones. This got somewhat involved, mostly because it requires putting an image on each Button used this way so its width option argument will be interpreted as number of pixels instead of number of characters (here's some documentation describing the various Button widget configuration options).
import tkinter as tk
# Inline XBM format data for a 1x1 pixel image.
BITMAP = """
#define im_width 1
#define im_height 1
static char im_bits[] = {
0x00
};
"""
root = tk.Tk()
root.geometry("960x600")
bitmap = tk.BitmapImage(data=BITMAP, maskdata=BITMAP)
f1 = tk.Frame(root, width=70, height=30)
f1.grid(row=3, column=0, sticky=tk.EW)
def banana():
print ("Sundae")
def tomato():
print ("Ketchup")
def potato():
print ("Potato chips")
def layout_buttons(parent, buttons, spacing):
if buttons:
first, *rest = buttons
first.grid(row=0, column=0) # Position first Button.
for index, button in enumerate(rest, start=1):
col = 2*index
# Dummy widget to separate each button from the one before it.
separator = tk.Button(parent, relief=tk.FLAT, state=tk.ACTIVE,
image=bitmap, borderwidth=0, highlightthickness=0,
width=spacing)
separator.grid(row=0, column=col-1)
button.grid(row=0, column=col)
buttons = (
tk.Button(f1, text="Banana", command=banana),
tk.Button(f1, text="Tomato", command=tomato),
tk.Button(f1, text="Potato", command=potato),
)
layout_buttons(f1, buttons, 30)
root.mainloop()
Result:
Here's a blow-up showing that the spacing is exactly 30 pixels (as counted in my image editor and indicated by the thin horizontal black line between the adjacent edges of the two Buttons).

How to change the colour of everything in a tkinter GUI at once

I have some code (as shown below) which prompts the user to select which colour to change the GUI to. But my problem is that it only changes the background. I'd like to know if there's a way to change the background of every label and button at once or do I have to change each label/button individually.
import tkinter
window = tkinter.Tk()
colour_frame = tkinter.Frame(window)
options_frame = tkinter.Frame(window)
def colours():
options_frame.pack_forget()
red.pack()
orange.pack()
back_button.pack()
colour_frame.pack()
def back():
options_frame.pack()
colour_frame.pack_forget()
def make_red():
window.configure(background="red")
def make_orange():
window.configure(background="orange")
colour_button = tkinter.Button(options_frame, text="Appearance", command=colours)
red = tkinter.Button(colour_frame, text="RED", command=make_red)
red.configure(bg = "red")
orange = tkinter.Button(colour_frame, text="ORANGE", command=make_orange)
orange.configure(bg = "orange")
back_button = tkinter.Button(colour_frame, text="Back", command=back)
window.mainloop()
You can make a list containing all your widgets you want to change
myWidgets = [button1, label1, ... ] # List of widgets to change colour
for wid in myWidgets:
wid.configure(bg = newColour)
Here's an example code of changing the background colour of multiple labels at once.
import tkinter as tk
# Change all label backgrounds
def change_colour():
c = user.get() #Get the entered text of the Entry widget
for wid in widget_list:
wid.configure(bg = c)
# Create GUI
root = tk.Tk()
tk.Label(root, text='Enter a colour').pack()
user = tk.Entry(root)
user.pack()
label_frame = tk.Frame(root)
label_frame.pack()
btn = tk.Button(root, text='Change Colour', command = change_colour)
btn.pack()
widget_list = [user, btn] # Add defined widgets to list
#Dynamicly create labels for example
for x in range(10):
lbl = tk.Label(label_frame, text='Label '+str(x))
lbl.pack(side = tk.LEFT)
widget_list.append(lbl) #Add widget object to list
root.mainloop()
Or if you have a Frame already containing all the widgets you want to change, then you can use this instead.
parent_widget.winfo_children() will return a list containing all the widgets stored inside the parent widget
def change_colour():
c = user.get()
for wid in label_frame.winfo_children():
wid.configure(bg = c)
Try using ttk for some of your GUI elements. ttk allows you to create styles for widgets and update the style to all widgets at once (at least for those that have the same style). You may need to mix the usage of ttk and tkinter, but it should make things a bit easier in the long run. Here is an example I made:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
# Creating a style for the buttons
color_style_button = ttk.Style()
color_style_button.configure("color.TButton", foreground="red")
def change_color(color):
# This function changes the style to all buttons using the "color.Button style"
if color == "red":
color_style_button.configure("color.TButton", foreground="red")
elif color == "blue":
color_style_button.configure("color.TButton", foreground="blue")
elif color == "green":
color_style_button.configure("color.TButton", foreground="green")
frame_a = ttk.Frame(root)
frame_a.pack()
red_button = ttk.Button(frame_a, text="Red", command=lambda: change_color("red"), style="color.TButton")
red_button.pack()
blue_button = ttk.Button(frame_a, text="Blue", command=lambda: change_color("blue"), style="color.TButton")
blue_button.pack()
green_button = ttk.Button(frame_a, text="Blue", command=lambda: change_color("green"), style="color.TButton")
green_button.pack()
root.mainloop()
I recommend checking out this site to learn more about ttk and styles.

Categories