Positioning widgets on frames with Tkinter - python

So, I'm fairly new to Python, and Tkinter.
I'm in the process of creating a Battleship game. So far I've succesfully created a grid of buttons. I am now trying to create a sort of a menu to the right of this game pad. I'm trying to do this by having two different Frames in my window - one where my game pad is and one where I have the menu (which is below a Label).
Whatever I put inside cheat_button.grid() I can't seem to change the position of this button. It just stays in the top-left corner of the menu_frame Frame. I also want to add anothern button beneath this one, as well as a message box a bit further down, on the same frame. Any suggestions? I have linked the part of the code I find relevant.
Thanks.
col_tot = 10
row_tot = 10
self.grid_frame = tk.Frame(self)
self.grid_frame.grid(row=0, column=0, rowspan=row_tot, columnspan=col_tot, sticky = "WENS")
self.menu_frame = tk.Frame(self, bg="grey", bd=1, relief="ridge")
self.menu_frame.grid(row=1, column=col_tot, rowspan=row_tot-1, columnspan=4, sticky = "WENS")
button_no = 0
self.button_list = []
for x in range(row_tot):
self.button_list.append([""] * col_tot)
for i in range(row_tot):
for j in range(col_tot):
self.button_list[i][j] = (tk.Button(self.grid_frame, height = 3, width = 6, text = str(button_no + 1),
activebackground = "yellow", relief = "groove"))
self.button_list[i][j]["command"] = lambda i=i, j=j:self.update_colour(i, j)
self.button_list[i][j].grid(row = i, column = j, sticky = "NW")
button_no += 1
self.welcome = tk.Label(self, text = "Welcome to Battleship!",
fg = "red", bg = "grey", font = ("Helvetica", 12), relief="ridge")
self.welcome.grid(row = 0, column = col_tot, columnspan = 4, sticky = "WENS")
self.cheat_button = tk.Button(self.menu_frame, text = "Cheat")
self.cheat_button.grid()
self.cheat_button.config(bg = "grey")

Rows and columns have a size of zero if they are empty and haven't configured to have a minimum size. It's also important to know that each frame has it's own "grid", so even though the frame on the left has 10 rows and ten columns, the frame on the right starts out completely empty.
If you want the button centered in the frame, one solution is to leave an empty row above and below it, and configure those rows to be given any extra space. For example:
self.welcome.grid(row = 0, column = 0, sticky = "WENS")
# notice we've put nothing in rows 1 or 3
self.cheat_button.grid(row=2, column=0)
self.menu_frame.grid_rowconfigure(1, weight=1)
self.menu_frame.grid_rowconfigure(3, weight=1)

Related

Python how to use .pack for an other monitor resolution?

Hi i can't solve this problem, i created a program in a 1920x1080 resolution monitor and when i run it on purpose on an other pc with 1366x768 resolution the position of the Labels is different, i picked the height that should be 768 in one case and 1080 in the other case with:
larghezza = window.winfo_screenheight()
and used in the pack placements:
bottone_reset.pack(side=BOTTOM,anchor=S,pady=(0,altezza*0.1))
bottone_premi.pack(side=BOTTOM,anchor=S,pady=(0,altezza*0.2))
etichetta_click.pack(side=BOTTOM,anchor=S,pady=(0,altezza*0.1))
etichetta_titolo.pack(side=BOTTOM,anchor=S,pady=(0,altezza*0.1))
Because in this way the formula changes according to the current monitor resolution but the Labels are still in a different position, how to handle this ? Thanks
So, the problem being that the location of the buttons and label changes based on window size. You're trying to modify the positioning manually based on the window size. While ingenious, it's obviously not working as you intend. There are, however, different options other than pack(). These are grid() and place().
(Links go to "geeksforgeeks" website, which is what I'm currently using as reference.)
'grid()' places elements in a grid using columns and rows, and is best used for elements that have the same dimensions (at least in the direction you're placing them, so if you want them all aligned on the X direction, they should have similar length to not look weird).
bottone_reset.grid(column = 0, row = 0, padx = 5, pady = 5)
bottone_premi.grid(column = 1, row = 0, padx = 5, pady = 5)
etichetta_click.grid(column = 2, row = 0, padx = 5, pady = 5)
etichetta_titolo.grid(column = 3, row = 0, padx = 5, pady = 5)
'place()' lets you determine the explicit location of something, which can be useful but in case of your choice of element placement would likely not help, since if the window shortens, place will not change location.
I would suggest creating a frame for the elements listed, then depending on what is most useful to you, use pack() or grid() to place the elements within that frame. In turn, place that frame where you want the buttons to end up.
frame.pack(side=BOTTOM, anchor = S)
bottone_reset = Button(frame,text = "reset")
bottone_premi = Button(frame, text = "premi")
etichetta_click = Label(frame, text = "click")
etichetta_titolo = Label(frame, text = "Title")
Ofc, your definition of the buttons and labels will look different, these are just placeholders.
The following code will always place the buttons in the bottom center of the window, regardless of what size that screen is (unless the total elements no longer fit). It's closest to what I could see you were trying to attempt. of course, you'll have to replace my manual input of window width and height with your screen size detection. I could not immediately get that to work with just tkinter, so I decided to change values manually.
from tkinter import *
window = Tk()
window.title("Pack Problem")
windowWidth = 1000
windowHeight = 768
windowSize = str(windowWidth) + "x" + str(windowHeight)
window.geometry(windowSize)
frame = Frame()
frame.pack(side=BOTTOM, anchor = S)
bottone_reset = Button(frame,text = "reset")
bottone_premi = Button(frame, text = "premi")
etichetta_click = Label(frame, text = "click")
etichetta_titolo = Label(frame, text = "Title")
bottone_reset.grid(column = 0, row = 0, padx = 5, pady = 5)
bottone_premi.grid(column = 1, row = 0, padx = 5, pady = 5)
etichetta_click.grid(column = 2, row = 0, padx = 5, pady = 5)
etichetta_titolo.grid(column = 3, row = 0, padx = 5, pady = 5)
window.mainloop()
The layout and makeup are obviously just placeholders, but this should function as a framework for what you want to do.
EDIT: With a better understanding of the problem, different approach. Leaving the original comment for those that could use it.
In order to change padding size based upon resolution, the fastest way I can think of is to bind a resize function to window configuration.
window.bind('<Configure>', function)
Sadly, attempts to make it a smooth adjustment have not been successful so far. I'll leave that up to you to attempt if you wish.
The solution I've gotten to work is this:
def padsize(e):
if e.height <= 1080 and e.height > 768:
bottone_reset.grid(column=0, row=0, padx=25, pady=25)
bottone_premi.grid(column=1, row=0, padx=25, pady=25)
etichetta_click.grid(column=2, row=0, padx=25, pady=25)
etichetta_titolo.grid(column=3, row=0, padx=25, pady=25)
#print("window height is larger than 768")
elif e.height <768 and e.height > 640:
bottone_reset.grid(column=0, row=0, padx=15, pady=15)
bottone_premi.grid(column=1, row=0, padx=15, pady=15)
etichetta_click.grid(column=2, row=0, padx=15, pady=15)
etichetta_titolo.grid(column=3, row=0, padx=15, pady=15)
#print("window height is larger than 768")
elif e.height <640 and e.height > 250:
bottone_reset.grid(column=0, row=0, padx=5, pady=5)
bottone_premi.grid(column=1, row=0, padx=5, pady=5)
etichetta_click.grid(column=2, row=0, padx=5, pady=5)
etichetta_titolo.grid(column=3, row=0, padx=5, pady=5)
#print("window height is smaller than 768")
window.bind('<Configure>', padsize)
As you can see, I made the pad size differences really drastic, in order to be able to see the change rather than needing a ruler to check. It should be far less noticeable at smaller size differences.

How to control label positions and spacing in Tkinter grid

Now I want to make the food price at the same row therefore I change that to row 2 as well but then the output is this
the food goes back to row 0, why does it do that? I am trying to get something like this:
foodprice=['3','2','1.50']
drinks = ['Water','Hot water','Medium water']
drinksprice = ['1','2','3']
myFrame3 = Frame(root, bg = '')
myFrame3.pack()
myFrame3.columnconfigure(0, weight=1)
myFrame3.columnconfigure(1, weight=2)
for x in range (len(food)):
foodop = Label(myFrame3, font=("Impact", "15"), text = food[x])
foodop.grid(row = 2+x, column = 4) # <--
for x in range (len(foodprice)):
fprice = Label(myFrame3, font=("Impact", "15"), text = foodprice[x])
fprice.grid(row = 2+x, column = 8) # <--
for x in range (len(drinks)):
drinksop = Label(myFrame3, font=("Impact", "15"), text = drinks[x])
drinksop.grid(row = 4+(len(food))+x, column = 4) # <--
for x in range (len(drinksprice)):
drinksp = Label(myFrame3, font=("Impact", "15"), text = drinksprice[x])
drinksp.grid(row = 4+(len(food))+x, column = 8) # <--
Creating white space around text does not work like in spreadsheet programs.
Tkinter collapses grid rows which are completely empty if there are no weights set by rowconfigure() to prevent this. The same is true for columns.
A layout like this:
import tkinter as tk
root = tk.Tk()
def place(col, row, text='Label', color='white'):
label = tk.Label(root, text=text, bg=color)
label.grid(column=col, row=row,
ipadx=10, ipady=10, padx=1, pady=1, sticky="NSEW")
def make_layout(cols, rows):
for c in cols:
place(c, 0, c, 'lightgrey')
for r in rows:
place(0, r, r, 'lightgrey')
for r in rows:
for c in cols:
place(c, r, 'Label')
make_layout((1,2),(1,2))
root.mainloop()
... would look exactly like this:
without configuring weights in columns 8 to 29 and rows 5 to 49.
(To get this, replace make_layout((1,2),(1,2)) by make_layout((7,30),(4,50)) in code!)
And even if you added weights, Labels are still higher than weighted empty rows, and thus the layout would maybe not look as clean as you might imagine.
Tkinter's Grid Geometry Manager is a good tutorial I read the other day that goes into this detail. It also introduces the optional columnspan and rowspan parameters of the grid() function.
Last but not least, you should have a look into How to justify text in label in Tkinter

how to format 2 check buttons in same row and column python

I have created two check buttons and I want them both in row 5 and column 1. But I want the 'YES' check button to be formatted on the left side and the 'NO' check button to be on the right.
I have used sticky but it will not work. I also tried changing the width of the check buttons but this did not work either.
Do you have any suggestions?
Thank you!
self.yes_checkbtn = Checkbutton(self.entry_frame, width = 20, variable = checkbutton1,
anchor = W, text = "YES")
self.no_checkbtn = Checkbutton(self.entry_frame, width = 20, variable = checkbutton2,
anchor = E, text = "NO")
self.yes_checkbtn.grid(row = 5, column = 1, sticky = W)
self.no_checkbtn.grid(row = 5, column = 1, sticky = E)
Outcome:
I dont have enough points yet to show the image without a link - sorry
Put both check buttons under another new frame, then you can grid this new frame to column 1 of row 5, also change option width of Checkbutton to a small number, like 5.
Example code
from tkinter import *
root = Tk()
entry_frame = Frame(root)
entry_frame.grid(row=0, column=0)
for i in range(4):
for j in range(2):
label = Label(entry_frame, text=f'Row {i} Column {j}')
label.grid(row=i, column=j)
frame = Frame(entry_frame)
frame.grid(row=4, column=1)
checkbutton1 = BooleanVar()
checkbutton2 = BooleanVar()
yes_checkbtn = Checkbutton(frame, width = 5, variable = checkbutton1,
anchor = W, text = "YES", bg='green')
no_checkbtn = Checkbutton(frame, width = 5, variable = checkbutton2,
anchor = E, text = "NO", bg='blue')
yes_checkbtn.grid(row = 0, column = 1, sticky = W)
no_checkbtn.grid(row = 0, column = 2, sticky = W)
root.mainloop()

Tkinter issue with using wrap length on a Label widget organised within a grid

So I have 2 rows dedicated to a messaged label widget to display any successful/unsuccessful messages to the user while they're using the tkinter GUI.
Some of the messages are to long to fit within the column width, so I have used the wraplength feature for the Label widget to wrap the text to the next line.
The issue that I'm having, is that because of this feature, it shifts the placements of the widgets underneath this widget, by 1 row for every new row it wraps the text onto.
So I was wondering if there's any way to have the text wrap, without moving the lower widgets.
How the message Label looks within the GUI with height = 1:
How the message Label looks when it wrap's the text to a newline with height = 1:
How the message Label looks within the GUI with height = 2:
How the message Label looks when it wrap's the text to a newline with height = 1:
I would like for the message Label in the 2nd image link to display the way it does, but keeping the vertical layout of the widgets as seen in the 1st image link.
The following code is for defining the widgets:
Choice_list = Listbox(window, selectmode=SINGLE, width = 17, height = 9,
justify = CENTER)
image = PhotoImage(file = 'Dummy_Logo.gif')
Image_label = Label(window, image = image)
extract = Button(window, text = "Archive News from Webpage",
command = func1, width = 20)
archive = Button(window, text = "Extract News from Archive",
command = func2, width = 22)
display = Button(window, text = "Display the News",
command = func3, width = 14)
Message_Widget = Label(window, text = '', fg = 'black', justify = CENTER,
height = 2, wraplength = 300)
Log_Event = Checkbutton(window, text = 'Log Event', variable = logState,
command = logEvent)
The following code is the grid alignment for the widgets:
Image_label.grid(row = 1, column = 1)
Choice_list.grid(row = 1, column = 2, rowspan = 9, sticky = W)
Message_Widget.grid(row = 2, column = 1, rowspan = 2)
Log_Event.grid(row = 12, column = 2)
archive.grid(row = 13, column = 1, rowspan = 2, sticky = W)
extract.grid(row = 13, column = 1, rowspan = 2, sticky = E)
display.grid(row = 13, column = 2, rowspan = 2, sticky = W)
Give the label a height of 2 or 3. The problem is simply that it wants to be one character tall by default. Tkinter will allocate only one character worth of height when laying out all the widgets. When the text wraps, the label simply must become taller so that the text will fit.
By giving it a height of 2 or 3 to start out, that extra space is built-in to the GUI. When the text wraps, the label doesn't have to grow to accommodate the new text.
This might not be a proper method of solving this problem, however I have managed to get the outcome I was looking for by only adjusting the rowspan parameter in the Message_Widget to rowspan = 11.
Message_Widget.grid(row = 2, column = 1, rowspan = 11)
This produced the following results:
With a short text:
https://i.stack.imgur.com/XpGLq.png
With a long text:
https://i.stack.imgur.com/iXwlR.png
Have you considered using a text widget for the readout?
For example this below code will wrap the text if it is to long and it will not resize the widgets. This is just an example but should be enough to help you with you problem.
from tkinter import *
import random
root = Tk()
text1 = Text(root, height=1, width=40, wrap="word")
text1.grid(row=0, column=0, sticky="ew")
def update_lbl1():
x = ["Click and drag to see some much longer random text for updating label!!!","some short data"]
text1.delete("1.0", "end")
text1.insert("1.0", random.choice(x))
Button(root, text="Change label from list", command = update_lbl1).grid(row=1, column=0)
root.mainloop()

How to place widgets next to each other?

I am building a simple GUI program to manage priorities. I am having trouble placing button widgets next to each other. It is somehow logical to me that if I want three buttons (Add, Remove, Edit) to place next to each other, I should use column = 0 for Add, column = 1 for Remove and column = 2 for Edit. Anyway, this is what I get:
Here is the createWidgets function:
def createWidgets(self):
listBox = Listbox(width = 30).grid(row=1, column = 0)
Button(self.root,
text = "Add",
command = self.addItem).grid(row = 2, column = 1, sticky = W)
Button(self.root,
text="Remove",
command = self.removeItem).grid(row = 2, column = 2, sticky = W)
Button(self.root,
text="Edit",
command = self.editItem).grid(row = 2, column = 3, sticky = W)
textBox = Text(height = 10, width = 30).grid(row = 3)
Use the columnspan option for:
textBox = Text(height = 10, width = 30).grid(row = 3, column=0, columnspan=3) # specify the column also
and
listBox = Listbox(width = 30).grid(row=1, column = 0, columnspan=3)
My advice is to break your UI down into regions, and manage each region independently. It's much easier than trying to cram everything into one big grid, especially when you have widgets with very different sizes.
Create three frames: one for the top, one for the group of buttons, and one for the bottom. You can then use pack to place them top-to-bottom:
top_frame.pack(side="top", fill="both", expand=True)
button_frame.pack(side="top", fill="x")
bottom_frame.pack(side="bottom", fill="both", expand=True)
(the use of expand depends on if you want that frame to expand when the window is made larger)
Once you've done that, you can tackle each region separately. For example, you can make the buttons children of the button frame, and use .pack(side='left') to get them to be left-aligned. The other widgets can also use pack if they are the only widgets in each of the other frames, or you can use grid.
You will find that just a few minutes organizing your UI before starting to code will make a big difference in how easy it is to create and maintain.
Example:
def createWidgets(self):
topFrame = tk.Frame(self)
buttonFrame = tk.Frame(self)
bottomFrame = tk.Frame(self)
topFrame.pack(side="top", fill="both", expand=True)
buttonFrame.pack(side="top", fill="x")
bottomFrame.pack(side="bottom", fill="both", expand=True)
listBox = tk.Listbox(topFrame, width=30)
listBox.pack(side="top", fill="both", expand=True)
tk.Button(buttonFrame, text="Add").pack(side="left")
tk.Button(buttonFrame, text="Remove").pack(side="left")
tk.Button(buttonFrame, text="Edit").pack(side="left")
textBox = tk.Text(bottomFrame, height=10, width=30)
textBox.pack(fill="both", expand=True)
When you do
textBox = Text(height = 10, width = 30).grid(row = 3)
tkinter automatically sets column = 0 and because textBox has a width of 30 the first column is stretched to a width of 30. It is possible to place textBox so that it occupies all columns by using the columnspan parameter:
textBox = Text(height = 10, width = 30).grid(row = 3, column = 0, columnspan = 3)
Because listBox also has a width of 30, you should also use columnspan here:
listBox = Listbox(width = 30).grid(row=1, column = 0, columnspan = 3)
A comprehensive guide for the grid method is here.

Categories