I think there's something fundamental I don't understand about tkinter but having read loads of tutorials and other answers, I still don't get it. I'm trying to organise a simple GUI. It should have a left hand pane with a game board (the game is othello) which has active buttons (I've left out the action function as that works ok), then a right hand pane which has 3 panes, top to bottom: one with radio buttons to toggle between 1 or 2 player, one with the current score, and one with a game evaluation. For now these latter 2 are just lines of text.
I thought I could set up a grid structure in a parent frame, then have 4 frames inside that grid, and then widgets inside them. Here's the code (you can ignore Board class unless you want to run it: the bit I'm struggling with is in Master)
from tkinter import *
from collections import defaultdict
from PIL import Image as PIL_Image, ImageTk
class Master:
def __init__(self):
self.board = Board()
self.display = Tk()
self.f = Frame(self.display, width=1050, height=700)
self.f.grid(row=0, column=0, rowspan=8, columnspan=8)
self.frame2 = Frame(self.f)
self.frame2.grid(row=0, column=8, rowspan=4, columnspan=4)
self.frame3 = Frame(self.f)
self.frame3.grid(row=4, column=8, rowspan=2, columnspan=4)
self.frame4 = Frame(self.f)
self.frame4.grid(row=6, column=8, rowspan=2, columnspan=4)
self.text1 = Text(self.frame3)
self.text1.pack()
self.text2 = Text(self.frame4)
self.text2.pack()
self.square = defaultdict(Button)
self.images = [ImageTk.PhotoImage(PIL_Image.open(f)) for f in ['white.png', 'empty.png', 'black.png']]
modes = [('{} vs {}'.format(i,j), (i, j)) for i in ['human','computer']
for j in ['human', 'computer']]
v = StringVar()
v.set(modes[0][1])
for text, mode in modes:
b = Radiobutton(self.frame2, text=text, variable=v, value=mode, command=lambda mode=mode: self.cp_set(mode))
b.pack(anchor=W)
self.text1.insert(END, 'score')
self.text2.insert(END, 'evaluation')
self.draw_board()
self.display.mainloop()
def draw_board(self):
for i, j in [(x,y) for x in range(8) for y in range(8)]:
self.square[i,j] = Button(self.f, command=lambda i=i, j=j: self.press(i,j), image=self.images[1 + self.board.square[i,j]])
self.square[i,j].image = 1 + self.board.square[i,j]
self.square[i,j].grid(column=i, row=j)
def cp_set(self, m):
self.pb, self.pw = m
return
def press(self, a, b):
# make it do something
return
class Board:
def __init__(self, parent=None):
self.parent = parent
if parent:
self.square = parent.square.copy()
self.black_next = not parent.black_next
self.game_over = parent.game_over
else:
self.square = defaultdict(int)
for square in [(x,y) for x in range(8) for y in range(8)]:
self.square[square] = 0
for square in [(3,3), (4,4)]:
self.square[square] = 1
for square in [(3,4), (4,3)]:
self.square[square] = -1
self.black_next = True
self.game_over = False
This gives a picture in which the top 4 rows of the game board line up with the radiobuttons but the bottom 4 rows get split up, with both text lines sitting in rows of their own, not opposite a row of the game board.
Seeing that this didn't work, I read something about the problem being grid not preserving column and rows between parent and child frames, so I tried a different approach in which the parent frame splits into 2 columns, and then the child frame sits underneath that with its own row/column definitions. But that didn't work either:
self.board = Board()
self.display = Tk()
self.f = Frame(self.display, width=1050, height=700)
self.f.grid(row=0, column=0, rowspan=1, columnspan=2)
self.frame1 = Frame(self.f, width=700, height=700)
self.frame1.grid(row=0, column=0, rowspan=8, columnspan=8)
self.f2 = Frame(self.f)
self.f2.grid(row=0, column=1, rowspan=2, columnspan=1)
self.frame2 = Frame(self.f2, width=350, height=350)
self.frame2.grid(row=0, column=0)
self.frame3 = Frame(self.f2, width=350, height=350)
self.frame3.grid(row=1, column=0)
self.text1 = Text(self.frame2)
self.text1.pack()
self.text2 = Text(self.frame3)
self.text2.pack()
# in this version, Radiobuttons are children of self.frame2,
# and Buttons in draw_board() are children of self.frame1
I really liked this second version until I saw the results in which the board has disappeared altogether. Any pointers would be much appreciated.
You might want to try something like this:
from tkinter import *
from collections import defaultdict
from PIL import Image as PIL_Image, ImageTk
class Master:
def __init__(self):
self.board = Board()
self.display = Tk()
self.left = Frame(self.display)
self.left.grid(row=0, column=0, sticky="new")
self.right = Frame(self.display)
self.right.grid(row=0, column=1)
self.right_top = Frame(self.right)
self.right_top.grid(row=0, column=0, sticky="nsw")
self.right_mid = Frame(self.right)
self.right_mid.grid(row=1, column=0)
self.right_bottom = Frame(self.right)
self.right_bottom.grid(row=2, column=0)
self.text1 = Text(self.right_mid)
self.text1.pack()
self.text2 = Text(self.right_bottom)
self.text2.pack()
self.square = defaultdict(Button)
self.images = [ImageTk.PhotoImage(PIL_Image.open(f)) for f in ['white.png', 'empty.png', 'black.png']]
modes = [('{} vs {}'.format(i,j), (i, j)) for i in ['human','computer'] for j in ['human', 'computer']]
v = StringVar()
v.set(modes[0][1])
for text, mode in modes:
b = Radiobutton(self.right_top, text=text, variable=v, value=mode, command=lambda mode=mode: self.cp_set(mode))
b.pack(anchor=W)
self.text1.insert(END, 'score')
self.text2.insert(END, 'evaluation')
self.draw_board()
self.display.mainloop()
def draw_board(self):
for i, j in [(x,y) for x in range(8) for y in range(8)]:
self.square[i,j] = Button(self.left, command=lambda i=i, j=j: self.press(i,j), image=self.images[1 + self.board.square[i,j]])
self.square[i,j].image = 1 + self.board.square[i,j]
self.square[i,j].grid(column=i, row=j)
The layout of the frames will be like this:
Related
I have a hard time implementing a scrollbar into my Tkinter project. I've been through numerous articles and answered questions on how to implement a scrollbar, but I'm just unable to implement a working solution after an entire day of researching this one 'simple' matter.
My current code looks like this:
import tkinter as tk
from tkinter import Button, ttk
from PIL import ImageTk, Image
from functools import partial
import queue as qu
import math
import re
import os
window = tk.Tk()
queue = qu.Queue()
#Basic values
#the window size
windowSize = "700x1000"
#picture and container size
x, y = 200, 300
#tmp
sidepanelsize = 200
window.geometry(windowSize)
#button identifier
def change(i):
print(I)
#temporary content generator
for g in range(12):
for item in os.listdir("."):
if re.search(r"\.(jpg|png)$", item):
queue.put(item)
n = queue.qsize()
#other panels that are going to be used later
frameLeft = tk.Frame(master=window, width=sidepanelsize, relief=tk.RIDGE)
frameLeft.pack(fill=tk.Y, side=tk.LEFT)
label1 = tk.Label(master=frameLeft, text="Left Panel")
label1.pack()
buttonLeft1 = tk.Button(master=frameLeft, text="Button 1", command=lambda: print("I'm a side button!"))
buttonLeft1.pack()
frameMain = tk.Frame(master=window, relief=tk.GROOVE, borderwidth=1)
frameMain.pack(side=tk.TOP, fill=tk.X, expand=1)
# SCROLLBAR IF YOU DISABLE THIS SECTION AND PUTS SOME PICTURES IN THE FOLDER WHITH THE FILE THE CODE WORKS #
myCanvas = tk.Canvas(master=frameMain)
myCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
myScrollbar = ttk.Scrollbar(master=frameMain, orient=tk.VERTICAL, command=myCanvas.yview)
myScrollbar.pack(side=tk.RIGHT, fill=tk.Y)
myCanvas.configure(yscrollcommand=myScrollbar.set)
myCanvas.bind('<Configure>', lambda e: myCanvas.configure(scrollregion=myCanvas.bbox("all")))
secondFrame = tk.Frame(master=myCanvas)
myCanvas.create_window((0, 0), window=secondFrame, anchor=tk.NW)
############################ END OF SCROLLBAR ############################
noOfImgPerRow = math.floor((int(windowSize.split("x")[0])-sidepanelsize+100)/x)
imgs = []
#generates the grid
for i in range(n):
o = i
i = (o % noOfImgPerRow) + 1
j = math.floor(o/noOfImgPerRow) + 1
frameMain.columnconfigure(i, weight = 1, minsize=x+15)
frameMain.rowconfigure(i, weight = 1, minsize=y+50)
frameBox = tk.Frame(
master=frameMain,
relief=tk.RAISED,
borderwidth=1,
width = x,
height = y
)
# here the error references to
frameBox.grid(row=j, column=i, padx=5, pady=5)
img = Image.open(queue.get()).convert("RGBA")
width, height = img.size
if width/x >= height/y:
left = width/2-(round((height*x)/y))/2
right = width/2+(round((height*x)/y))/2
upper = 0
lower = height
else:
left = 0
right = width
upper = height/2-(round((width*y)/x))/2
lower = height/2+(round((width*y)/x))/2
img2 = img.crop([left, upper, right, lower])
img2 = img2.resize((x, y), Image.Resampling.LANCZOS)
imgs.append(ImageTk.PhotoImage(img2))
label = tk.Label(master = frameBox, image = imgs[-1])
label.pack()
mainButton = Button(master=frameBox, text="Start", command=partial(change, o))
mainButton.pack()
window.mainloop()
I've tried to highlight the only thing of concern, that being the scrollbar, everything else is working at the moment, I just wanted to post the whole code for better understanding if it would help in any way.
My problem is whenever I implement the scrollbar, it throws back an error stating:
Traceback (most recent call last):
File "e:\Python\starter\main.py", line 85, in <module>
frameBox.grid(row=j, column=i, padx=5, pady=5)
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.1264.0_x64__qbz5n2kfra8p0\lib\tkinter\__init__.py", line 2522, in grid_configure
self.tk.call(
_tkinter.TclError: cannot use geometry manager grid inside .!frame2 which already has slaves managed by pack
This error seems pretty self-explanatory, just grid the canvas instead of packing it, but when after a lot of small tweaking and doing things a roundabouts things
My second thought was if it has a problem with the grid to wrap the gridded frame in another bigger packed frame, like so:
yetAnotherFrame = tk.Frame(frameMain)
yetAnotherFrame.pack()
noOfImgPerRow = math.floor((int(windowSize.split("x")[0])-sidepanelsize+100)/x)
imgs = []
for i in range(n):
o = i
i = (o % noOfImgPerRow) + 1
j = math.floor(o/noOfImgPerRow) + 1
yetAnotherFrame.columnconfigure(i, weight = 1, minsize=x+15)
yetAnotherFrame.rowconfigure(i, weight = 1, minsize=y+50)
frameBox = tk.Frame(
master=yetAnotherFrame,
relief=tk.RAISED,
borderwidth=1,
width = x,
height = y
)
frameBox.grid(row=j, column=i, padx=5, pady=5)
This actually runs to my surprise, but the scrollbar still isn't working and the layout is broken again.
Solution
In your code frameBox's parent is frameMain. Instead you need to have the canvas as parent or the secondFrame which have the canvas as its parent.
Example
This is basically your code with fixes, but some of the unnecessary parts are removed.
import tkinter as tk
from tkinter import ttk
window = tk.Tk()
window.geometry("400x400")
frameLeft = tk.Frame(master=window, width=400, height=400, relief=tk.RIDGE)
frameLeft.pack(fill=tk.Y, side=tk.LEFT)
myCanvas = tk.Canvas(master=frameLeft)
myCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
myScrollbar = ttk.Scrollbar(master=frameLeft, orient=tk.VERTICAL, command=myCanvas.yview)
myScrollbar.pack(side=tk.RIGHT, fill=tk.Y)
myCanvas.configure(yscrollcommand=myScrollbar.set)
myCanvas.bind('<Configure>', lambda e: myCanvas.configure(scrollregion=myCanvas.bbox("all")))
secondFrame = tk.Frame(master=myCanvas)
myCanvas.create_window((0, 0), window=secondFrame, anchor=tk.NW)
for i in range(100):
lbl = tk.Label(secondFrame, text=f"Label {i}")
lbl.grid(column=0, row=i, sticky=tk.W)
window.mainloop()
Current behavior, adding LabelFrame widgets (with their own label and picture children) into another Labelframe named "Test putting buffs..." The LabelFrame widgets are being added into the left side of the LabelFrame and "grow" to the right. Wanted behavior is for the widgets to appear on the right side of the frame and "grow" left.
Cannot seem to get there with anchor or sticky settings. How can this "grow-left" be done and still preserve the ability to sort by name or time remaining?
Current behavior gif:
Wanted behavior (mocked up with paint):
Code (took out the image stuff so files aren't needed to run):
import tkinter as tk
from tkinter import ttk
from PIL import ImageTk, Image
import time
class MainFrame(tk.Frame):
def __init__(self, container):
super().__init__(container)
self.grid(column=0, row=0)
self.buffs_list_frames = []
self.button1 = ttk.Button(self)
self.button1['text'] = "Simulate frame list appended True Strike"
self.button1['command'] = lambda: self.buffs_frame_list_is_appended(["True Strike", time.time() + 9])
self.button1.grid(column=0, row=0)
self.button2 = ttk.Button(self)
self.button2['text'] = "Simulate frame list appended Bulls"
self.button2['command'] = lambda: self.buffs_frame_list_is_appended(["Bulls", time.time() + 1080])
self.button2.grid(column=0, row=1)
self.button0 = ttk.Button(self)
self.button0['text'] = "Simulate frame list appended Endurance"
self.button0['command'] = lambda: self.buffs_frame_list_is_appended(["Endurance", time.time() + 1080])
self.button0.grid(column=0, row=2)
self.button3 = ttk.Button(self)
self.button3['text'] = "Simulate frame list put into .grid() and displayed"
self.button3['command'] = lambda: self.buffs_display_nicely()
self.button3.grid(column=0, row=3)
self.button4 = ttk.Button(self)
self.button4['text'] = "START loops of time passing"
self.button4['command'] = lambda: self.buffs_loop_time_passing()
self.button4.grid(column=0, row=4)
self.test_label_frame = ttk.LabelFrame(self, text="Testing putting buffs into a frame with grid")
self.test_label_frame.grid(column=1, row=0)
def buffs_loop_time_passing(self):
for x in self.buffs_list_frames:
x.buff_timer.set(f"{x.buff_birthday - time.time():.1f}s")
if x.buff_birthday < time.time() + 6:
x['background'] = 'red'
if x.buff_birthday < time.time():
self.buffs_list_frames.remove(x)
x.destroy()
self.after(1000, self.buffs_loop_time_passing)
def buffs_frame_list_is_appended(self, added_buff):
""" makes the buff frame and adds to the list of frame widgets
"""
self.buff_frame = tk.LabelFrame(self.test_label_frame, borderwidth=1, text=added_buff[0][0:4], labelanchor="n")
# self.buff_frame.buff_image_reference = ImageTk.PhotoImage(Image.open(added_buff[2]))
# self.buff_frame.buff_image_label = ttk.Label(self.buff_frame, image=self.buff_frame.buff_image_reference)
# self.buff_frame.buff_image_label.image_keep = self.buff_frame.buff_image_reference
# self.buff_frame.buff_image_label.grid(column=0, row=0)
self.buff_frame.buff_birthday = added_buff[1]
self.buff_frame.buff_timer = tk.StringVar()
self.buff_frame.buff_timer.set(f"{self.buff_frame.buff_birthday - time.time():.1f}s")
self.buff_frame.buff_label = ttk.Label(self.buff_frame, textvariable=self.buff_frame.buff_timer)
self.buff_frame.buff_label.grid(column=0, row=1)
self.buffs_list_frames.append(self.buff_frame)
self.buffs_display_nicely()
def buffs_display_nicely(self):
""" takes the list of frames, sorts by name, and .grids()s them into the test frame
"""
self.buffs_list_frames = sorted(self.buffs_list_frames, key=lambda z: z['text'])
print(f"sorted? {self.buffs_list_frames}")
j = 0
for x in self.buffs_list_frames:
x.grid(row=0, column=j)
j += 1
class App(tk.Tk):
def __init__(self):
super().__init__()
# configure the root window
self.title('NWN Buff Watcher')
self.geometry('300x50')
if __name__ == "__main__":
app = App()
main_frame = MainFrame(app)
app.mainloop()
Try this (using #acw1668's suggestion):
import tkinter as tk
from tkinter import ttk
from PIL import ImageTk, Image
import time
class MainFrame(tk.Frame):
def __init__(self, container):
super().__init__(container)
self.buffs_list_frames = []
self.buttons_frame = tk.Frame(self)
self.buttons_frame.pack(fill="both", side="left")
self.button1 = ttk.Button(self.buttons_frame, text="Simulate frame list appended True Strike",
command=lambda: self.buffs_frame_list_is_appended("True Strike", 9))
self.button1.grid(column=0, row=0)
self.button2 = ttk.Button(self.buttons_frame, text="Simulate frame list appended Bulls",
command=lambda: self.buffs_frame_list_is_appended("Bulls", 1080))
self.button2.grid(column=0, row=1)
self.button0 = ttk.Button(self.buttons_frame, text="Simulate frame list appended Endurance",
command=lambda: self.buffs_frame_list_is_appended("Endurance", 1080))
self.button0.grid(column=0, row=2)
#self.button3 = ttk.Button(self.buttons_frame, text="Order items", command=self.order_items)
#self.button3.grid(column=0, row=3)
self.button4 = ttk.Button(self.buttons_frame, text="START loops of time passing",
command=self.buffs_loop_time_passing)
self.button4.grid(column=0, row=4)
self.test_label_frame = ttk.LabelFrame(self, text="Testing putting buffs into a frame with grid")
self.test_label_frame.pack(side="right")
def buffs_loop_time_passing(self):
for x in self.buffs_list_frames:
x.buff_timer.set(f"{x.buff_birthday - time.time():.1f}s")
time_now = time.time()
if x.buff_birthday < time_now + 6:
x.config(bg="red")
if x.buff_birthday < time_now:
self.buffs_list_frames.remove(x)
x.destroy()
self.after(100, self.buffs_loop_time_passing)
def buffs_frame_list_is_appended(self, added_buff, time_alive):
""" makes the buff frame and adds to the list of frame widgets
"""
buff_frame = tk.LabelFrame(self.test_label_frame, borderwidth=1,
text=added_buff[:4], labelanchor="n")
# buff_frame.buff_image_reference = ImageTk.PhotoImage(Image.open(added_buff[2]), master=self)
# buff_frame.buff_image_label = ttk.Label(buff_frame, image=buff_frame.buff_image_reference)
# buff_frame.buff_image_label.image_keep = buff_frame.buff_image_reference
# buff_frame.buff_image_label.grid(column=0, row=0)
buff_frame.buff_birthday = time.time() + time_alive
buff_frame.buff_timer = tk.StringVar(master=self)
buff_frame.buff_timer.set(f"{buff_frame.buff_birthday - time.time():.1f}s")
buff_frame.buff_label = ttk.Label(buff_frame,
textvariable=buff_frame.buff_timer)
buff_frame.buff_label.grid(column=0, row=1)
self.buffs_list_frames.append(buff_frame)
self.order_items()
def order_items(self):
self.buffs_list_frames = sorted(self.buffs_list_frames, key=lambda z: z['text'], reverse=True)
for x in self.buffs_list_frames:
x.pack_forget()
x.pack(side="right")
class App(tk.Tk):
def __init__(self):
super().__init__()
# configure the root window
self.title("NWN Buff Watcher")
# self.geometry("300x50")
if __name__ == "__main__":
app = App()
main_frame = MainFrame(app)
main_frame.pack()
app.mainloop()
I made a few changes to your code. The main thing is that I removed buffs_display_nicely and instead added buff_frame.pack(side="right"). The side="right" tells tkinter to add the widgets from the right to the left.
Also you can improve your code by a lot if you add a class instead of using buff_frame.buff_birthday, buff_frame.buff_timer, ... Also it will make your life a lot easier
class calculate:
def __init__(self):
self.window = tk.Tk()
self.numbers = tk.Frame(master = self.window)
self.signs = tk.Frame(master = self.window)
self.lst, self.lst2 = (1,2,3,4,5,6,7,8,9,0,".","="),("C","+","-","*","/")
def buttons(self):
val = 0
for i in range(4):
for j in range(3):
self.numbutton = tk.Button(master = self.numbers,text = self.lst[val])
self.numbutton.grid(row = i,column = j)
val += 1
for i in range(5):
self.signbutton = tk.Button(master = self.signs,text = self.lst2[i])
self.signbutton.grid(row = i)
def packing(self):
self.numbers.grid(column= 0,row = 0,rowspan = 4)
self.signs.grid(column = 1,row = 0,rowspan = 4)
self.window.mainloop()
calculator = calculate()
calculator.buttons()
calculator.packing()
I am trying to put some buttons by using gird in python tkinter.
and I want that numbers and signs have same height.
but the signs is of larger height.
please help.
Thanks for help in advance.
You can force the size of a frame by giving the frame an height:
self.numbers = tk.Frame(master = self.window, height=200)
self.signs = tk.Frame(master = self.window, height=200)
Both frames will have a height of 200 but you can change it to whatever you want
for i in range(4):
for j in range(3):
self.numbutton = tk.Button(master = self.numbers,text = self.lst[val],width=5,height=5)
self.numbutton.grid(row = i,column = j)
val += 1
for i in range(5):
self.signbutton = tk.Button(master = self.signs,text = self.lst2[i],width=5, height=4)
self.signbutton.grid(row = i)
Add a padding in y of some 0.1 or 0.2 to make it look good.
You can add sticky="nsew" to all .grid(...) and add self.numbers.rowconfigure("all", weight=1) to make the number buttons to use all the available space inside self.numbers frame.
Below is modified code:
import tkinter as tk
class calculate:
def __init__(self):
self.window = tk.Tk()
self.numbers = tk.Frame(master=self.window)
self.signs = tk.Frame(master=self.window)
self.lst, self.lst2 = (1,2,3,4,5,6,7,8,9,0,".","="), ("C","+","-","*","/")
def buttons(self):
val = 0
for i in range(4):
for j in range(3):
self.numbutton = tk.Button(master=self.numbers, text=self.lst[val])
self.numbutton.grid(row=i, column=j, sticky="nsew") # added sticky
val += 1
for i in range(5):
self.signbutton = tk.Button(master=self.signs, text=self.lst2[i])
self.signbutton.grid(row=i, sticky="nsew") # added sticky
def packing(self):
self.numbers.grid(column=0, row=0, sticky="nsew") # added sticky
self.signs.grid(column=1, row=0, sticky="nsew") # added sticky
self.numbers.rowconfigure("all", weight=1) # use all available vertical space
self.window.mainloop()
calculator = calculate()
calculator.buttons()
calculator.packing()
I have this simple 5x5 button grid:
from tkinter import *
class App():
def __init__(self, root):
self.root = root
self.TopFrame = Frame(root)
self.BottomFrame = Frame(root)
self.TopFrame.grid(row=0)
self.BottomFrame.grid(row=6)
buttonQ = Button(self.BottomFrame, text="Quit", command=quit)
buttonS = Button(self.BottomFrame, text="Save", command=self.saveToFile)
buttonS.grid(row=0, column=0, padx=10)
buttonQ.grid(row=0, column=1, padx=10)
def Function(self):
self.grid = []
for i in range(5):
row = []
for j in range(5):
row.append(Button(self.TopFrame,width=6,height=3,command=lambda i=i, j=j: self.getClick(i, j),background='gray'))
row[-1].grid(row=i,column=j)
self.grid.append(row)
def getClick(self, i, j):
orig_color = self.grid[i][j].cget('bg')
if orig_color=="red":
self.grid[i][j]["bg"]="gray"
else:
self.grid[i][j]["bg"]="red"
def saveToFile(self):
myFile=open("example.txt", 'w')
for line in range(5):
for column in range(5):
bg_color = self.grid[line][column].cget('bg')
if bg_color == "red":
myFile.write("1 ")
else:
myFile.write("0 ")
myFile.write("\n")
#myFile.flush()
myFile.close()
myFile = open("example.txt",'r')
print(myFile.read())
myFile.close()
root = Tk()
app = App(root)
app.Function()
root.mainloop()
which I want to multiply 16 times (arranged in a matrix with 4 rows and 4 columns - one cell having a 5x5 button matrix) with space between them and the 'Quit' and 'Save' button bellow all. Can I achieve that only by using frames? Is there a way to multiply the 5x5 button grid 16 times and arrange it as a 4x4 matrix?
I'm new in Python programming so please be gentle :)
To follow the abstraction principle of programming, you could define your 5-by-5 button cell as a class (outside your App class) like below:
class Cell5by5():
def __init__(self, parent):
self.frame = Frame(parent)
self.buttons = []
for i in range(5):
row = []
for j in range(5):
row.append(Button(self.frame,width=6,height=3,
command=lambda i=i, j=j: self.getClick(i, j),
background='gray'))
row[-1].grid(row=i,column=j)
self.buttons.append(row)
def getClick(self, i, j):
orig_color = self.buttons[i][j].cget('bg')
if orig_color=="red":
self.buttons[i][j]["bg"]="gray"
else:
self.buttons[i][j]["bg"]="red"
Then in your App class, you can create as many Cell5by5 instances as you need:
class App():
def __init__(self, root):
self.root = root
...
self.create_cells()
def create_cells(self):
self.cells = []
for i in range(4):
row = []
for j in range(4):
row.append(Cell5by5(self.root))
row[-1].frame.grid(row=i, column=j, padx=2, pady=2)
self.cells.append(row)
def saveToFile(self):
# Each button is now obtainable by calling:
# self.cells[line][column].buttons[subline][subcolumn]
root = Tk()
app = App(root)
root.mainloop()
Also, I renamed your grid of buttons as self.buttons to prevent confusion with tkinter's .grid() method
I'm a car park admittance thingy for college. Basically what is does when it's run, a window comes up, asks the user to enter make, model, colour, and reg plate. Then it saves this data to a list, array or whatever. The user presses a button to enter their car into the car park, and also to see what cars are currently in the car park. When I press admit vehicle, I need the data to be saved to this list/array/tree, and also for a integer variable to decrease by one. Here's the relevant code. There is more, but this is the relevant bits.
# Admit Button
btn_admit = ttk.Button(bottom_frame)
btn_admit.config(text='Admit Vehicle')
btn_admit.bind('<Button-1>', self.admit) # I need this to reduce the variable as well
...
def admit(self, event):
self.useful_msg.set("Vehicle Admitted") # This is only here to show a message currently it does nothing else
This is the 'spaces available' variable:
self.num_spaces = IntVar(mid_frame)
self.num_spaces.set = 0
lbl_num_spaces = Label(mid_frame)
lbl_num_spaces.config(textvariable=self.num_spaces, bg='yellow')
Finally, this is the code for the window that shows the tree of cars that are in the car park (with some example cars hard-coded for now):
class ShowCarsGui:
def __init__(self, master):
self.master = master
self.master.geometry('1200x600+100+100')
# Frames
top_frame = tk.Frame(self.master)
tree_container = tk.Frame(self.master)
bottom_bar = tk.Frame(self.master)
# Widgets:
# Logo
carpark_icon = tk.PhotoImage(file='car.gif')
lbl_carpark_icon = tk.Label(top_frame)
lbl_carpark_icon.config(image=carpark_icon)
lbl_carpark_icon.image = carpark_icon
# Header
lbl_header = tk.Label(top_frame)
lbl_header.config(text="Vehicles in car park", font='helvetica 32 bold')
# Tree(ttk)
self.tree = ttk.Treeview(tree_container)
self.tree["columns"] = ("Make", "Model", "Colour", "Registration")
self.tree["height"] = 10
self.tree["show"] = 'headings' # Gets rid of default first column
vsb = ttk.Scrollbar(tree_container)
vsb.configure(orient='vertical', command=self.tree.yview)
hsb = ttk.Scrollbar(tree_container)
hsb.configure(orient='horizontal', command=self.tree.xview)
self.tree.configure(yscroll=vsb.set, xscroll=hsb.set)
self.tree_populate()
# Button
quit_button = tk.Button(bottom_bar)
quit_button.config(text='Quit', width=25)
quit_button.bind('<Button-1>', self.close_window)
# Positioning frames
top_frame.grid_rowconfigure(0, minsize=150) # Make row 150 pixels high
top_frame.grid(row=0)
tree_container.grid(row=1)
bottom_bar.grid(row=2)
# Top
lbl_carpark_icon.grid(row=0, column=0, padx=10, sticky='w')
lbl_header.grid(row=0, column=1, padx=20)
# Middle
self.tree.grid(column=0, row=0, sticky='nsew')
vsb.grid(column=1, row=0, sticky='ns')
hsb.grid(column=0, row=1, sticky='ew')
# Bottom
quit_button.grid(row=0, column=0)
def close_window(self, event):
self.master.destroy()
def tree_populate(self):
# Eventually this needs to come from car park object
tree_columns = ("Make", "Model", "Colour", "Registration")
tree_data = [
("Ford", "Ka", "Blue", "FD54 2WE"),
("Vauxhall", "Corsa", "Green", "KJ61 9YH"),
("VW", "Polo", "Silver", "AA54 9TQ"),
("Nissan", "Qashqai", "Red", "YRE 456W"),
("Toyota", "Starlet", "Gold", "J234 WYE"),
]
for col in tree_columns:
self.tree.heading(col, text=col, anchor='w')
for country_data in tree_data:
self.tree.insert("", 0, values=country_data)
Finally here is the code for the entire program:
import tkinter as tk
from tkinter import *
from tkinter import ttk
class CarParkGui:
def __init__(self, master):
self.master = master
self.master.configure(bg='light cyan')
self.master.title("Collyer's Car Park")
self.master.option_add('*Font', 'Georgia 12') # Font for all widgets
self.master.option_add('*Font', 'helvetica 20 bold')
self.master.option_add('*Background', 'light cyan') # background of all widgets
self.master.geometry('1200x500+100+100') # w,h,x,y (top left corner)
self.top() # Build top bar
self.middle() # Define middle frame
self.bottom() # Define Bottom Frame
def top(self):
# Frame for top section
top_frame = Frame(self.master)
# Logo
carpark_icon = PhotoImage(file='car.gif')
lbl_carpark_icon = Label(top_frame) # Instance of tkinter label (parent is frame)
lbl_carpark_icon.config(image=carpark_icon)
lbl_carpark_icon.image = carpark_icon # Have to have this as well as previous one
# Header
lbl_header = Label(top_frame)
lbl_header.config(text='Admit Vehicle', font='helvetica 32 bold')
# Grid positioning for top frame
top_frame.grid_rowconfigure(0, minsize=150) # Make row 150 pixels high
top_frame.grid(row=0)
# Within Frame
lbl_carpark_icon.grid(row=0, column=0, padx=10)
lbl_header.grid(row=0, column=1, padx=20)
def middle(self):
# Frame to contain other widgets
mid_frame = Frame(self.master)
# Label - Car Make
lbl_make = Label(mid_frame)
lbl_make.config(text='Make') # Presentation
# Label - Car Model
lbl_model = Label(mid_frame)
lbl_model.config(text='Model')
# Label - Colour
lbl_colour = Label(mid_frame)
lbl_colour.config(text='Colour')
# Label - Registration
lbl_reg = Label(mid_frame)
lbl_reg.config(text='Registration')
# Label - Spaces
lbl_spc = Label(mid_frame)
lbl_spc.config(text='Spaces')
# Text Entry - Make
self.make = StringVar(mid_frame)
txt_make = Entry(mid_frame)
txt_make.config(textvariable=self.make, width=20)
# Text Entry - Model
self.model = StringVar(mid_frame)
txt_model = Entry(mid_frame)
txt_model.config(textvariable=self.model, width=20)
# Text Entry - Colour
self.colour = StringVar(mid_frame)
txt_colour = Entry(mid_frame)
txt_colour.config(textvariable=self.colour, width=20)
# Text Entry - Registration
self.reg = StringVar(mid_frame)
txt_reg = Entry(mid_frame)
txt_reg.config(textvariable=self.reg, width=20)
# Label for number of space available (WILL BE UPDATED)
self.num_spaces = IntVar(mid_frame)
self.num_spaces.set = 0
lbl_num_spaces = Label(mid_frame)
lbl_num_spaces.config(textvariable=self.num_spaces, bg='yellow')
# Display
mid_frame.grid_columnconfigure(0, minsize=100) # Make row 150 pixels high
mid_frame.grid(row=1, sticky='w')
# Row 0
lbl_make.grid(row=1, column=0)
txt_make.grid(row=1, column=1)
lbl_spc.grid(row=1, column=2)
lbl_num_spaces.grid(row=1, column=3, sticky='w')
# Row 1
lbl_model.grid(row=2, column=0)
txt_model.grid(row=2, column=1, padx='10')
# Row 2
lbl_colour.grid(row=3, column=0)
txt_colour.grid(row=3, column=1, padx='10')
# Row 3
lbl_reg.grid(row=4, column=0)
txt_reg.grid(row=4, column=1, padx='10')
def bottom(self):
# Frame for bottom section
bottom_frame = Frame(self.master)
# Grid reference for bottom frame
bottom_frame.grid(row=2)
# Guidance message (WILL BE UPDATED)
self.useful_msg = StringVar(bottom_frame)
self.useful_msg.set("Enter your vehicle details")
self.lbl_msg = Label(bottom_frame) # Use self so we can change config at
self.lbl_msg.config(textvariable=self.useful_msg, fg='red', width=20)
# Admit Button
btn_admit = ttk.Button(bottom_frame)
btn_admit.config(text='Admit Vehicle')
btn_admit.bind('<Button-1>', self.admit)
# Show Vehicles Button
btn_show = ttk.Button(bottom_frame)
btn_show.config(text='Show Vehicles In Car Park')
btn_show.bind('<Button-1>', self.show)
# Within bottom_frame
# row 0
self.lbl_msg.grid(row=0, column=0)
# row 1
btn_admit.grid(row=1, column=0, sticky='e')
btn_show.grid(row=1, column=1, sticky='e')
def admit(self, event):
self.useful_msg.set("Vehicle Admitted")
def show(self, event):
self.new_window = Toplevel(self.master)
self.app = ShowCarsGui(self.new_window)
___ This is where ShowCarsGui() is ____
def main():
root = Tk()
CarParkGui(root)
root.mainloop()
main()
To do "two things with one button", the correct solution is to create a function that does these "two things", and then associate that function with the button.
btn_admit = ttk.Button(bottom_frame)
btn.configure(command=self.admit)
def admit(self):
self.num_spaces.set(self.num_spaces.get()-1)
self.useful_msg.set("Vehicle Admitted")
Note: the above example uses the command attribute rather than a binding. Using the command attribute is preferable because it has built-in support for keyboard traversal. And, of course, you can do as much or as little as you want in admit -- you aren't limited to one or two things.