I want the whole thing to expand, as the user expands it, without widgets, like the listbox, reshaping. I want it to be exactly like expanding a still image which maintains its aspect ratio. How do I do this?
from tkinter import *
import random
from random import *
import threading
selected = 0
contained = {}
contained[0] = []
name = {}
maxEventItems = 100
i = 0
while i < 10 ** 3:
contained[0].append(i)
name[i] = 'Event {}'.format(i)
i += 1
# Beginning of thread part, in the future.
def eventViewerListboxItems():
i = 0
while i < len(contained[selected]):
eventNumber = contained[selected][i]
eventViewerListbox.insert(END,'{}'.format(name[eventNumber]))
i += 1
master = Tk()
masterCanvas = Canvas(master)
masterCanvas.grid(row = 0,column = 0,sticky = N + S + E + W)
masterFrame = Frame(masterCanvas)
masterFrame.grid(row = 0,column = 0)
main = Frame(masterFrame)
main.grid(row = 0,column = 0)
topButtons = Frame(main)
topButtons.grid(row = 0, column = 0)
saveButton = Button(topButtons,text = 'Save')
saveButton.grid(row = 0,column = 0,sticky = W)
loadButton = Button(topButtons,text = 'Load')
loadButton.grid(row = 0,column = 1,sticky = W)
createEventButton = Button(topButtons,text = 'Create event')
createEventButton.grid(row = 0,column = 2,sticky = W)
eventViewer = Frame(main)
eventViewer.grid(row = 1, column = 0)
eventViewerListboxScrollbar = Scrollbar(eventViewer)
eventViewerListboxScrollbar.grid(row = 1,column = 1, sticky = W + N + S)
eventViewerListbox = Listbox(eventViewer)
eventViewerListbox.grid(row = 1,column = 0,sticky = W)
eventViewerListbox.config(yscrollcommand = eventViewerListboxScrollbar.set)
eventViewerListboxScrollbar.config(command = eventViewerListbox.yview)
bottomButtons = Frame(main)
bottomButtons.grid(row = 2, column = 0,sticky = E)
simulateButton = Button(bottomButtons,text = 'Simulate')
simulateButton.grid(row = 0,column = 0,sticky = E)
callEventViewerListboxItems = threading.Thread(target = eventViewerListboxItems)
callEventViewerListboxItems.start()
partial_contained = {}
partial_contained[selected] = []
i = 0
while i < maxEventItems and i < len(contained[selected]):
partial_contained[selected].append(contained[selected][i])
i += 1
print('I started putting the items from contained[{}] into the listbox in the event viewer.'.format(contained[selected][0]))
print()
print('Below, I will show the first {} items that are in contained:'.format(i,contained[selected]))
print(partial_contained[selected])
print()
master.mainloop()
The problem is that you are using grid, but you haven't given a weight to any rows or columns. The weight determines which rows and columns are given extra space such as when a user resizes the window. By default the weight of every row and column is zero, meaning they get no extra weight.
As a rule of thumb, you need to call grid_rowconfigure and grid_columnconfigure for at least one row and one column, and give that row or column a positive weight. You will need to do this for every widget that is using grid to manage its children.
You also aren't using the sticky attribute consistently, so even if you get the columns to grow and shrink, your widgets won't expand to fill the area they've been given.
You need to be methodical about creating a complex layout. My advice is to start over, and just get masterCanvas to grow and shrink like you expect. Give it a bold, distinctive cover so you can see it (ie: if it has the same color as the background, it's impossible to know if it's growing or shrinking to fill the area)
Once you get masterCanvas where it is growing and shrinking properly, move on to masterFrame, and then main, and then eventViewer, and finally the listbox. At each step be sure to give the widget a distinctive color so you can visualize it.
You might also want to consider removing so many layers of nested frames. The more you have, the more difficult it becomes to get them to all work together. However, multiple areas are easy if you take a methodical approach, and don't try to get half a dozen different frames working together all at once. Get one working, then focus on its immediate children. When they work, focus on their children, and so on.
Related
I am trying to make a shopping-inspired vending machine UI. The goal is to fill up my frame with frames using for loop and 2D list, then pack on buttons and labels onto it. Codes are as followed:
frames = []
frame_order = []
num = 0
for x in range(5):
frames.append([])
for y in range(5):
frames[x].append(5)
for x in range(5):
for y in range(5):
frames[x][y] = Frame(store_canvas, width=1520 / 5, height=1030 / 5, bd = 2, relief = SOLID)
frames[x][y].grid(row=y, column=x)
frames[x][y].pack_propagate(False)
frame_order.append(frames[x][y])
This will reun an SQL query database that will return with an item list as a list[].Then fill up the frames inside the 2D list
retrieve_tiem()
I have also set up another frame on the side that has other filters. the problem is that if the filter query SQL and returns with an item list less than the grid size (5x5, a total of 25 items) then the for loop won't run as it cannot populate. to avoid this, I tried to use Try and Except but the loop would just fill the remaining space with buttons instead.
for frame in frame_order:
try:
Button(frame, anchor='nw', height = 9, width = 35, font = 20).pack()
Label(frame, text=str(Item_list[num]), anchor='nw', font = 20, width = 35, bg = 'darkgreen', fg = 'yellow' ).pack()
num += 1
except:
pass
Is there a way to avoid this? Like not creating the button when the Item list run out off item or fill the rest of the list with '*blank*' until the list reach the total amount of item that the frame can hold? I am also open to other methods too if it works. All answer are appreciated, please go easy on me as I am still learning python and SQL:)
You can actually use 1-D list instead of 2-D list. Below is an example based on your code:
from tkinter import *
import random
root = Tk()
store_canvas = Frame(root)
store_canvas.pack()
# create the 25 frames
ROWS = COLS = 5
MAX_ITEMS = ROWS * COLS
frames = []
for i in range(MAX_ITEMS):
frames.append(Frame(store_canvas, width=1520/COLS, height=1030/ROWS, bd=2, relief=SOLID))
frames[-1].grid(row=i//COLS, column=i%COLS)
frames[-1].pack_propagate(False)
# function to simulate retrieving data from database table
def retrieve_tiem():
return [f"Item #{i+1}" for i in range(random.randint(1,MAX_ITEMS))]
# function to show the retrieved items
def update_list():
Item_list = retrieve_tiem()
label_font = ("Arial", 20)
for i, frame in enumerate(frames):
for w in frame.winfo_children():
w.destroy()
if i < len(Item_list):
item = Item_list[i]
Button(frame).pack(fill="both", expand=1)
Label(frame, text=item, font=label_font, bg="darkgreen", fg="yellow").pack(fill="x")
update_list()
root.bind("<F5>", lambda e: update_list())
root.mainloop()
The problem is with your try statement. Here's an example:
a = 5
try:
a += 7
int("str") #This is just to raise an exception
except:
pass
After running this code, the value of a will be 12, even though an error occured. This is the same thing that is happening in your code. The line that creates the button is run successfully, but the creating the label raises an exception. This is why you get buttons in the remaining space. This can be resolved using else.
If we try this instead:
a = 5
try:
int("str") #This is just to raise an exception
except:
pass
else:
a += 7
The a += 7 line will only be run if there is not exception in the try statement, so the value will remain 5. For your code, this will be
try:
Item_list[num] #This is what causes the error
except:
pass
else:
Button(frame, anchor='nw', height = 9, width = 35, font = 20).pack()
Label(frame, text=str(Item_list[num]), anchor='nw', font = 20, width = 35, bg = 'darkgreen', fg = 'yellow' ).pack()
num += 1
Alternatively, you could have an if statement to check if num is larger than the length of the data returned, but there's not enough information in the question for me to be sure that will work.
I have created a simple loop within a combobox which creates a dynamic number of entries - this in fact depends on the number of filters the user wants. Ideally I'd like to store all of the user-made choices through the 'Submit' Button but I can't seem to be able to pass the variables into the 'callback' function within the class module. As a result, I am only able to store the last combobox. I have created a 'n' variable which would allow for easy retrieval of each combobox. Essentially I then want to be storing all those selections in the variable 'user_selections'. Ideally this code would then be re-used as template when facing user-selection choices.
For future reference I'd also like to explore whether there is the possibility of having multiple user selections within each of the comboboxes, rather than one single dropdown selection. I am rather new to python coding so struggling to put things together.
Any help is massively appreciated! Code below:
import tkinter as tk
from tkinter import *
from tkinter import ttk
class User_ComboBox(Tk):
def __init__(self, s, options):
Tk.__init__(self)
self.title("User Selections: ")
x = s*100
y = s*50
self.geometry(str(x) + "x" + str(y) + '+350+350')
self.labelTop = tk.Label(self,text = "Data Slicing Options: ")
self.labelTop.grid(row = 0, column = 0, sticky = W, pady = 2)
for i in range(1, s+1):
n = "combobox_" + str(i)
self.label = Label(self,text="Select Criteria " + str(i))
self.label.grid(row = i, column = 0, sticky = W, pady = 2)
self.n = ttk.Combobox(self,values=options[i - 1])
self.n.grid(row = i, column = 1, sticky = W, pady = 2)
self.okButton = tk.Button(self, text='Submit',command = self.callback)
self.okButton.grid(row = i + 1, column = 0, sticky = W, pady = 2)
def callback(self):
""" Get the contents of the Entry and exit """
self.comboBox_contents = {'a':self.n.get()}
self.destroy()
def ComboboxSelection():
options = [['Layer 1','Layer 2','Layer 3'],['Americas','APAC','EMEA'],['Bank','Institution','Fund']]
n_comboboxes = 3
selection = User_ComboBox(n_comboboxes, options)
selection.mainloop()
return selection.comboBox_contents
user_selections = ComboboxSelection()
I've edited your code below, this should work:
(note that I've removed a few things and cleaned it a bit). Basically the problem was that you were trying to reassign to n every time, whereas you needed a list container to which to append each n.
from tkinter import *
from tkinter import ttk
class User_ComboBox(Tk):
def __init__(self, s, options):
Tk.__init__(self)
self.title("User Selections: ")
self.comboBox_contents = []
self.comboBoxes = [[] for n in range(s)]
x = (s+1)*100
y = (s+1)*50
self.geometry(str(x) + "x" + str(y) + '+350+350')
self.labelTop = Label(self,text = "Data Slicing Options: ")
self.labelTop.grid(row = 0, column = 0, sticky = W, pady = 2)
for i in range(s):
self.label = Label(self,text="Select Criteria " + str(i))
self.label.grid(row = i+1, column = 0, sticky = W, pady = 2)
self.comboBoxes[i] = ttk.Combobox(self, values = options[i])
self.comboBoxes[i].grid(row = i+1, column = 1, sticky = W, pady = 2)
self.okButton = Button(self, text='Submit',command = self.callback)
self.okButton.grid(row = i + 2, column = 0, sticky = W, pady = 2)
def callback(self):
""" Get the contents of the Entry and exit """
self.comboBox_contents = [x.get() for x in self.comboBoxes]
self.destroy()
def ComboboxSelection():
options = [['Layer 1','Layer 2','Layer 3'],['Americas','APAC','EMEA'],['Bank','Institution','Fund']]
n_comboboxes = 3
selection = User_ComboBox(n_comboboxes, options)
selection.mainloop()
return selection.comboBox_contents
user_selections = ComboboxSelection()
print(user_selections)
also, if I understand your second question correctly, I think what you are looking for is a ListBox?
As a general comment, try keeping text and code to a minimum, the shorter the question the more people are gonna read it!
I am trying to create a menu program in tkinter, where check boxes are created from items in a dictionary, then the total price of selected items is calculated when a button is clicked.
menu_items = {"Spam - £3" : 3, "Eggs - £7" : 7, "Chips - £1" : 1, "Beer - £2" : 2}
def widgets(self):
# create menu list
row = 1
for item in menu_items:
self.item = BooleanVar()
Checkbutton(self,
text = item,
variable = self.item
).grid(row = row, column = 0, sticky = W)
row += 1
calc_but = Button(self,
text = "Click to calculate",
command = self.calculate
).grid(row = row + 1, column = 0, sticky = W)
self.results_txt = Text(self, width = 20, height = 4, wrap = WORD)
self.results_txt.grid(row = row + 2, column = 0, columnspan = 2)
This creates check boxes, button and text display just fine, but my problem comes with my calculate method.
def calculate(self):
bill = 0
for item in menu_items:
if self.item.get():
bill += menu_items.get(item)
msg = "Total cost - £" + str(bill)
self.results_txt.delete(0.0, END)
self.results_txt.insert(0.0, msg)
It will add up everything (ticked or not), but only when the final check box is ticked. It displays 0 if the final item is not ticked.
I am not sure what my problem is, or if I am approaching this in the wrong way.
What's happening here
The way you create your buttons - by looping through each key in you dictionary - your program only references the last one you created, which it saves as self.item. When you call calculate(), it only checks and adds up this button's value.
One way around this is to save all the references to these Buttons in a table:
menu_items = {"eggs":7, "chips":1, "beer":2}
selected = {}
def calculate():
bill = 0
for item in menu_items:
if selected[item].get():
bill += menu_items[item]
results.delete(1.0, END)
results.insert(END, "Total cost - £" + str(bill))
for item in menu_items:
is_selected = BooleanVar()
button = Checkbutton(master, text=item, variable=is_selected)
button.grid(sticky="w")
selected[item] = is_selected
purchase_btn = Button(master, text="Calculate", command=calculate)
purchase_btn.grid()
results = Text(master, wrap=WORD)
results.grid(columnspan=2)
(I've omitted your class structure here. It is easy enough to re-incorporate it)
Now we're keeping a is_selected dictionary, so that we can track whether a button has been selected or not.
Its easy to reference this table, because the keys are the items themselves! Neat, not?
A couple more tips on tkinter:
If you're gridding in the next row every time, just call grid() with no col and row arguments - tkinter adds the row for you, and keeps the column.
It's best always to create a widget, store it as a variable, then grid this variable. Some people try to do this all in one, and then get confused when the try to reference their widget by this variable. They will find nothing, because the grid method returns nothing!
I hope this helped. Good luck!
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)
I'm trying to create multiple entry boxes with a for loop, so i don't have to make all the different boxes manually in case i want to increase the amount of entries, but this way I can't get my entry value via .get(). If i print L1 the output is a list of three empty strings, so no value was added after I typed them into the entry boxes. How can I make a list containing all the entry values as floats?
from tkinter import *
window = Tk()
window.geometry("450x450+200+200")
def do():
print(L1)
L1 = []
for i in range(3):
labelx = Label(window, text = str(i)).grid(row = i, column = 0)
v = StringVar()
num = Entry(window, textvariable = v).grid(row = i, column = 1)
num1 = v.get()
L1.append(num1)
button1 = Button(window, text = 'OK', command = do).grid(column = 1)
Your original code is storing the value in a list. Instead, store a reference to the widget. With this, there's no reason to create the StringVar objects.
Note that to do this you must create the widget and call grid as two separate statements. It not only allows this technique to work, it's generally considered a best practice to separate widget creation from widget layout.
L1 = []
for i in range(3):
labelx = Label(window, text = str(i))
num = Entry(window, textvariable = v)
labelx.grid(row = i, column = 0)
num.grid(row = i, column = 1)
L1.append(num)
...
for widget in L1:
print("the value is", widget.get())
Use the list, L1, to store the id of the Tkinter StringVar(). Use the get method in the function called by the button. Otherwise, how is the program to know when the data is ready to be retrieved. A StringVar returns a string that will have to be converted to a float. Also, it's a bad habit to use i, l, or o as single digit variable names as they can look like numbers.
window = Tk()
window.geometry("450x450+200+200")
def do():
for var_id in L1:
print(var_id.get())
var_id.set("")
L1 = []
for ctr in range(3):
## grid() returns None
Label(window, text = str(ctr)).grid(row = ctr, column = 0)
var_id = StringVar()
ent=Entry(window, textvariable = var_id)
ent.grid(row = ctr, column = 1)
##num1 = v.get()-->nothing entered when program starts
if 0==ctr:
ent.focus_set()
L1.append(var_id)
button1 = Button(window, text = 'OK', command = do).grid(column = 1)
window.mainloop()