How to place widgets next to each other? - python

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.

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.

Creating and Getting values from tkinter check boxes using for loop

This code is a part of my project in which I have to manage the attendance of 50 (or more) students.
The thing I want is that all the checkboxes should initially be 'checked' (showing the present state) and when I uncheck random checkboxes (to mark the absent) and click the Submit button (yet to be created at the bottom of the window), I should get a list with 'entered date' as first element and the roll numbers i.e. 2018-MC-XX as other elements.
For example: ['01/08/2020', '2018-MC-7', '2018-MC-11', '2018-MC-23', '2018-MC-44']
Actually my plan is when I will get a list I will easily write it to a text file. Also, if there is another way of creating multiple scrollable checkboxes without packing them inside a canvas then please do tell!
from tkinter import *
from tkcalendar import DateEntry
root = Tk()
root.geometry('920x600+270+50')
root.minsize(920,600)
Attendance_frame = Frame(root) ### Consider it a Main Frame
Attendance_frame.pack()
attendaceBox = LabelFrame(Attendance_frame, text = 'Take Attendance', bd = 4, relief = GROOVE, labelanchor = 'n',font = 'Arial 10 bold', fg = 'navy blue', width = 850, height = 525) # A Label Frame inside the main frame
attendaceBox.pack_propagate(0)
attendaceBox.pack(pady = 15)
dateFrame = Frame(attendaceBox) # A small frame to accommodate date entry label & entry box
dateFrame.pack(anchor = 'w')
font = 'TkDefaultFont 10 bold'
date_label = Label(dateFrame, text = 'Enter Date : ', font = font).grid(row = 0, column = 0, sticky = 'w', padx = 10, pady = 10)
date_entry = DateEntry(dateFrame, date_pattern = 'dd/mm/yyyy', showweeknumbers = FALSE, showothermonthdays = FALSE)
date_entry.grid(row = 0, column = 1, sticky = 'w')
noteLabel = Label(attendaceBox, text = 'Note: Uncheck the boxes for absentees').pack(anchor = 'w', padx = 10, pady = 5)
canvas = Canvas(attendaceBox, borderwidth=0, background="#ffffff")
checkFrame = Frame(canvas, width = 100, height = 50)
vsb = Scrollbar(canvas, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
canvas.pack_propagate(0)
canvas.create_window((4,4), window=checkFrame, anchor="nw")
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
checkFrame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
for i in range(0,51): # A loop to create Labels of students roll numbers & names
c = Checkbutton(checkFrame, text = f"{'2018-MC-'+str(i+1)} Student {i+1}")
c.grid(row = i, column = 0, padx = 10, sticky = 'w')
mainloop()
First you need StringVar for each Checkbutton in order to get the state of the Checkbuttons later. Then you can use a list to hold the StringVars so that you can access them later. You can set the onvalue of the checkbuttons to the roll number associated to them.
Also you can use Text widget instead of Canvas+Frame. Below is an example:
import tkinter as tk
root = tk.Tk()
frame = tk.Frame(root)
frame.pack()
text = tk.Text(frame, width=40, height=20)
text.pack(side=tk.LEFT, fill=tk.BOTH)
vars = []
for i in range(51):
rollnum = '2018-MC-'+str(i+1)
var = tk.StringVar(value=rollnum)
cb = tk.Checkbutton(text, text=rollnum, variable=var, onvalue=rollnum, offvalue='', bg='white')
text.window_create('end', window=cb)
text.insert('end', '\n')
vars.append(var)
vsb = tk.Scrollbar(frame, orient=tk.VERTICAL, command=text.yview)
vsb.pack(side=tk.RIGHT, fill=tk.Y)
text.config(yscrollcommand=vsb.set)
def submit():
# extract roll numbers for checked checkbuttons
result = [var.get() for var in vars if var.get()]
print(result)
tk.Button(root, text='Submit', command=submit).pack()
root.mainloop()

Button not showing up in Tkinter, even w/ geometry manager

I'm designing an app in tkinter, but there is a button that won't show up. The button is crucial to the program's operation.
import tkinter as tk
global field
root = tk.Tk()
root.resizable(0,0)
root.geometry('368x200')
header = tk.Label(root, text = 'Header Text', pady=20)
header.config(font = ('Tahoma', 24))
header.grid(row = 0, columnspan=2)
enter_here = tk.Label(root, text = 'Question: ')
enter_here.grid(row = 1, column = 0, pady = 50)
field = tk.Entry(root, width = 50)
field.grid(row = 1, column = 1, pady = 50)
answer = tk.Button(root, text = 'Answer', command = answerf, width=10)
answer.grid(row=2, column=2)
root.mainloop()
Title, header text and the letters are all placeholders. I just need to figure out how to use the button. I've looked around and couldn't find any answers; most people had just forgot a geometry manager.
You have to be careful what values you are passing to various parameters of tkinter widgets. In above case, this is the reason why you are not able to see button.
Change
field = tk.Entry(root, width = 50)
field.grid(row = 1, column = 1, pady = 50)
to
field = tk.Entry(root, width = 25)
field.grid(row = 1, column = 1, pady = 30)
And,
answer = tk.Button(root, text = 'Answer', command = answerf, width=10)
answer.grid(row=2, column=2)
to
answer = tk.Button(root, text = 'Answer', command = answerf, width=10)
answer.grid(row=1, column=2)
output:
The problem is simply that you're forcing the window to be a size that is too small for the objects inside it.
A simple fix is to remove this line:
root.geometry('368x200')
If you insist on keeping this line, then you need to adjust the parameters of the other widgets so that they fit in the constrained space. For example, you could reduce the size of the widgets or reduce the padding.

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()

Positioning widgets on frames with Tkinter

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)

Categories