Compacting my class for my tkinter window with buttons - python

I have an issue in that I can compact this code (I've been told I can) but did not receive any help in doing so and don't have a clue how to do it.
Tried putting this into a for loop, but I want a 3x3 grid of buttons with the middle one being a listbox instead, dead centre of the buttons.
I have looked around, and after an hour, I got no answer.
Here I tried appending each button to a list and packing them in a for loop, but is it possible to do them each in a for loop and the Listbox done and packed separately after?
class MatchGame(Toplevel):
def __init__(self,master):
self.fr=Toplevel(master)
self.GameFrame=Frame(self.fr)
self.AllButtons=[]
self.AllButtons.append(Button(self.GameFrame,bg="red",height=5,width=15,text=""))
self.AllButtons.append(Button(self.GameFrame,bg="green",height=5,width=15,text=""))
self.AllButtons.append(Button(self.GameFrame,bg="dark blue",height=5,width=15,text=""))
self.AllButtons.append(Button(self.GameFrame,bg="turquoise",height=5,width=15,text=""))
self.AllButtons.append(Listbox(self.GameFrame,bg="grey",height=5,width=15))
self.AllButtons.append(Button(self.GameFrame,bg="yellow",height=5,width=15,text=""))
self.AllButtons.append(Button(self.GameFrame,bg="pink",height=5,width=15,text=""))
self.AllButtons.append(Button(self.GameFrame,bg="orange",height=5,width=15,text=""))
self.AllButtons.append(Button(self.GameFrame,bg="purple",height=5,width=15,text=""))
for x in range(0,len(self.AllButtons)):
AllButtons[x].grid(row=int(round(((x+1)/3)+0.5)),column=x%3)
self.GameFrame.grid(row=0,column=0)
Quit=Button(self.fr, text="Destroy This Game", bg="orange",command=self.fr.destroy)
Quit.grid(row=1,column=0)
It needs to have individual colours, same size and all that, but I don't know what to do. I'm fairly new to classes, and I can't work out for the life of me how to make this window with compact code (not 9 lines for each object, then packing them all.)

If you want to dynamically create a 3x3 grid of buttons. Then a nested for-loop seems to be your best option.
Example:
import tkinter as tk
root = tk.Tk()
# List of your colours
COLOURS = [['red', 'green', 'dark blue'],
['turquoise', 'grey', 'yellow'],
['pink', 'orange', 'purple']]
# Nested for-loop for a 3x3 grid
for x in range(3):
for y in range(3):
if x == 1 and y == 1: # If statement for the Listbox
tk.Listbox(root, bg = COLOURS[x][y], height=5, width=15).grid(row = x, column = y)
else:
tk.Button(root, bg = COLOURS[x][y], height=5, width=15).grid(row = x, column = y)
root.mainloop()

Related

Tkinter Grid Spacing Issue

So I'm trying to create a basic text-entry thing to understand the basics of Tkinter, but when I try and grid something, if the column or row is greater than 1, it will act as if it is 1
Here is my code:
from tkinter import *
window = Tk()
window.minsize(width=500, height=300)
# label
my_label = Label(text="Text", font=("Consolas", 24, "bold"))
my_label["text"] = "New Text"
my_label.config(text="New Text")
# my_label.pack() # adding keyword parameters such as "expand=True" or "side='left'" can affect where it is positioned
# my_label.place(x=100, y=40)
my_label.grid(column=10, row=15)
# Button
def button_clicked():
my_label["text"] = input.get()
button = Button(text="Click Me", command=button_clicked)
button.grid(row=1, column=1)
# Entry
input = Entry(width=10)
window.mainloop()
Now I want the label to be about 3/4 of the way across the screen, and as you see, I use (for the label) column 10 and row 15. Still, when I try and run this, the label will only go diagonally down from the button. Is this how Tkinter is supposed to work, or am I just doing it wrong? I would appreciate any help.
yes, that's basically how tkinter works, it is at column 10, it's just that columns 2-9 all have zero width as they contain nothing, so you end up seeing only 2 columns.
to make something "centered" you need to have its "bounding box" scale with the containing "frame" using grid_columnconfigure from the Tk official grid tutorial
my_label.grid(column=10, row=15)
window.grid_columnconfigure(10,weight=1) # column 10 will grow
window.grid_rowconfigure(15,weight=1) # row 15 will grow
this allows the bounding box to grow and the label will remain centered as it is not set to stick to any corner, but it is offset by the button.
but if you really wanted it to be in the exact center then you can make the label span more columns than the button.
my_label.grid(column=1,columnspan=2, row=2) # label spans columns 1 and 2
window.grid_columnconfigure(2,weight=1) # column 2 will grow
window.grid_rowconfigure(2,weight=1) # row 2 will grow.
you get the required result.
but now we want both the button and the label to be centered, we can set them both in the same column that is set to grow with the size of the window, and make both rows grow to fit the entire window.
my_label.grid(column=1,columnspan=2, row=2, sticky="N")
window.grid_columnconfigure(1,weight=1)
window.grid_rowconfigure([1,2],weight=1)
simply playing with grid_columnconfigure and column and columnspan and sticky and their row counterparts is usually enough to get any shape you want on any screen scale if you add some padding.

How to delete a shape from canvas created by a function in tkinter python?

New to ktinker and I've met a problem that I can't find a solution to.
My goal is to animate a shape and allow it to move using a function inside a while loop, and the function generates the shape while the while loop deletes and refreshes the canvas.
My code look something like this:
def shape():
global a
a = screen.create_rectangle(x,100,x+50,200,fill = 'white')
while True:
shape(x,y)
x+=10
screen.update()
screen.delete(a)
time.sleep(0.03)
the code successfully creates a rectangle and it moves, but the code isn't deleting the rectangles. However, the code works fine and deletes the rectangles if I'm not using a function.
The proper way to animate objects on a tk.Canvas is different from pygame and other GUI frameworks.
tk.Canvas does not require a "blitting" process; the items do not need to be deleted, and recreated each frame.
The proper approach is to move existing items, either using the tk.Canvas.move method, or the tk.Canvas.coord method. The first moves the item by a provided δx and δy, whereas the second re-positions the item to the new coordinates passed to it.
Here is an example with tk.Canvas.move:
import tkinter as tk
def shape(x):
return screen.create_rectangle(x, 100, x+50 , 200, fill='white')
def animate(rect, dx=0, dy=0):
screen.move(rect, dx, dy)
screen.after(100, animate, rect, dx)
if __name__ == '__main__':
root = tk.Tk()
screen = tk.Canvas(root, width=400, height=400, bg='cyan')
screen.pack()
rect = shape(50)
animate(rect, dx=10)
root.mainloop()
Notice that we make use of the tk.mainloop provided by the framework, instead of a clumsy while True loop. The tk.after method is the correct approach to call a function (here animate) at regular intervals.
We also avoid the use of time.sleep which always results to problems and blocking the GUI.
Try updating the screen after you delete the shape.
def shape():
global a
a = screen.create_rectangle(x,100,x+50,200,fill = 'white')
while True:
shape(x,y)
x+=10
screen.update()
screen.delete(a)
screen.update()
time.sleep(0.03)

Creating multiple differently colored shapes using tkinter and classes

I'm trying to create a program that will draw 16 different squares, in four different colors. So for example, the code for a red piece looks like this:
redSquare = Label(window,bg="red",width=2,height=2)
redSquare.place(x=5,y=5)
Now, instead of copy and pasting this multiple time in my code, would there be a way to create a class, where the changeable attribute is the color and position? And if so, how would that code look?
I wrote this code now using the best practices that I have learned so far. The class inherits from a Frame which means, you have a frame with all these colors gridded inside of it. This gives the colors in a table format. I would not recommend using pixels as it is not dynamic with different screen resolutions. You will have to create a list of colors for all the rows and columns, if not, colors will be repeated. Take a look:
from tkinter import *
class SquareGenerator(Frame): # Inherit from tkinter Frame
def __init__(self,parent:Tk,color_lst:list,rows:int,columns:int,side:int,padx:int=0,pady:int=0,*args,**kwargs):
Frame.__init__(self,parent,*args,**kwargs)
img = PhotoImage(height=1,width=1) # To use pixels with label
if len(color_lst) != rows*columns: # If not enough colors are given
print('WARNING: Did not recieve a valid color list, using custom list')
if len(color_lst) < rows*columns:
if (rows*columns)-len(color_lst) == 1:
color_lst.append(color_lst[0])
else:
color_lst = color_lst*((rows*columns)-len(color_lst)) # Repeat the list enough times
else:
color_lst = color_lst[:rows*columns]
# Table loop
for i in range(rows):
for j in range(columns):
each_color = color_lst[columns*i+j] # Index each item in list
l = Label(self,bg=each_color,image=img,width=side,height=side) # Create label
l.grid(row=i,column=j,padx=padx,pady=pady)
if __name__ == '__main__':
root = Tk()
colors = ['red','green','blue','orange']
gen = SquareGenerator(root,colors,rows=5,columns=1,side=100) # Replicate microsoft logo ;)
gen.pack()
root.mainloop()
This will create a frame with each colors in the given list kept in 2 rows and 2 columns. So total 4 colors are needed to be defined. You can play around and try passing in less than or more than 4 colors and see what happens with current code too.
However I used classes, just because you had asked for it, a non OOP approach would be:
from tkinter import *
root = Tk()
frame = Frame(root)
frame.pack()
ROWS = 5
COLUMNS = 1
PADX = 0
PADY = 0
SIDE_LENGTH = 100
colors = ['red','green','blue','orange']
if len(colors) != ROWS*COLUMNS: # If not enough colors are given
print('WARNING: Did not recieve a valid color list, using custom list')
if len(colors) > ROWS*COLUMNS:
colors = colors[:ROWS*COLUMNS]
else:
if (ROWS*COLUMNS)-len(colors) == 1:
colors.append(colors[0])
else:
colors = colors*((ROWS*COLUMNS)-len(colors)) # Repeat the list enough times
img = PhotoImage(height=1,width=1) # To use pixels with label
for i in range(ROWS):
for j in range(COLUMNS):
each_color = colors[COLUMNS*i+j] # Index each item in list
l = Label(frame,bg=each_color,image=img,width=SIDE_LENGTH,height=SIDE_LENGTH) # Create label
l.grid(row=i,column=j,padx=PADX,pady=PADY)
root.mainloop()

I've been trying to use the random.shuffle with tkinter. Can someone help me write a function to shuffle the colors on the buttons.

I am still a beginner and I have yet to understand how to write a function, that works.
import tkinter
root = tkinter.Tk()
root.title("Shuffle Colors")
# The list of colors
colors = ['red','yellow', 'pink', 'purple','green', 'blue', 'orange','Brown']
#looping each color
for c in colors:
#Buttons
button1 = tkinter.Button(root, text=c, bg=c)
button1.pack(fill = tkinter.X)
root.mainloop()
shuffle works in-place so:
import random
def my_shuffle(colors):
random.shuffle(colors)
return colors
new_colors = my_shuffle(colors)
but you dont really need a function - just do the shuffle as you need it - unless you need the original list to be unmodified

(Instantiating an array of Buttons, but only one works

I'm trying to create a GUI for a virtual board for the game Go. There should be an nxn grid of tiles where a player can place a stone, either black or white. Clicking on a tile will make it change from tan(the default) to black, click again to white, and click a third time to go back to tan. Player one can click once on a spot to place a stone there, and player two can click twice (you need to remove stones later, so three clicks resets it). I created a tile object and then used a nested for loop to instantiate 9 by 9 of them. Unfortunately, running the code only seems to produce 1 functional tile, not 81. This code should work on any python machine (I'm using Python 3.4), so you can try to run it and see for yourself. Can anyone point out the reason the loop is only running once?
from tkinter import *
window = Tk()
n = 9
"""
A tile is a point on a game board where black or white pieces can be placed. If there are no pieces, it remains tan.
The basic feature is the "core" field which is a tkinter button. when the color is changed, the button is configured to represent this.
"""
class tile(object):
core = Button(window, height = 2, width = 3, bg = "#F4C364")
def __init__(self):
pass
"""the cycle function makes the tile object actually change color, going between three options: black, white, or tan."""
def cycle(self):
color = self.core.cget("bg")
if(color == "#F4C364"): #tan, the inital value.
self.core.config(bg = "#111111")#white.
elif (color == "#111111"):
self.core.config(bg = "#DDDDDD")#black.
else:
self.core.config(bg = "#F4C364")#back to tan.
board = [] #create overall array
for x in range(n):
board.append([])#add subarrays inside it
for y in range(n):
board[x].append(tile())#add a tile n times in each of the n subarrays
T = board[x][y] #for clarity, T means tile
T.core.config(command = lambda: T.cycle()) #I do this now because cycle hadn't been defined yet when I created the "core" field
T.core.grid(row = x, column = y) #put them into tkinter.
window.mainloop()
As mhawke points out in his answer you need to make the core an instance variable, so that each Tile gets its own core.
And as I mention in my comment above, you also need to fix the Button's command callback function. The code you use in your question will call the .cycle() method of the current value of T, which happens to be the last tile created. So no matter where you click only the last tile changes color. One way to fix that is to pass the current tile as a default argument of the lambda function when you create it. But because you are using OOP to create your Tile there's a better way, which you can see below.
I've made a few modifications to your code.
Although many Tkinter examples use from tkinter import * it's not a good practice. When you do from some_module import * it brings all of the names from some_module into the current module (your script), which means you could accidentally override those names with your own names. Even worse, if you do import * with multiple modules each new module's names can clash with the previous module's names, and you have no way of knowing that's happened until you start getting mysterious bugs. Using import tkinter as tk means you need to do a little more typing, but it makes the resulting program less bug-prone and easier to read.
I've modified the __init__ method so that it is called with the window and the (x, y) location of the tile (it's customary to use x for the horizontal coordinate and y for the vertical coordinate). Each Tile object now keeps track of its current state, where 0=empty, 1=black, 2=white. This makes it easier to update the colors. And because we've passed in the window and (x, y) we can use that info to add the tile to the grid. The tile also remembers the location (in self.location), which may come in handy.
I've modified the cycle method so that it updates both the background color and the activebackground of the tile. So when the mouse hovers over the tile it changes to a color that's (roughly) halfway between its current color and the color it will turn if you click it. IMO, this is nicer than the tile always turning pale grey when the mouse hovers over it.
I've also optimized the code that creates all the tiles and stores them in the board list of lists.
import tkinter as tk
colors = (
#background, #activebackground
("#F4C364", "#826232"), #tan
("#111111", "#777777"), #black
("#DDDDDD", "#E8C8A8"), #white
)
class Tile(object):
""" A tile is a point on a game board where black or white pieces can be placed.
If there are no pieces, it remains tan.
The basic feature is the "core" field which is a tkinter button.
when the color is changed, the button is configured to represent this.
"""
def __init__(self, win, x, y):
#States: 0=empty, 1=black, 2=white
self.state = 0
bg, abg = colors[self.state]
self.core = tk.Button(win, height=2, width=3,
bg=bg, activebackground=abg,
command=self.cycle)
self.core.grid(row=y, column=x)
#self.location = x, y
def cycle(self):
""" the cycle function makes the tile object actually change color,
going between three options: black, white, or tan.
"""
#cycle to the next state. 0 -> 1 -> 2 -> 0
self.state = (self.state + 1) % 3
bg, abg = colors[self.state]
self.core.config(bg=bg, activebackground=abg)
#print(self.location)
window = tk.Tk()
n = 9
board = []
for y in range(n):
row = [Tile(window, x, y) for x in range(n)]
board.append(row)
window.mainloop()
The problem is that core is a class variable which is created once and shared by all instances of class tile. It should be an instance variable for each tile instance.
Move core = Button(window, height = 2, width = 3, bg = "#F4C364") into tile.__init__() like this:
class Tile(object):
def __init__(self):
self.core = Button(window, height = 2, width = 3, bg = "#F4C364")
The root of the problem is that core is shared by all instances of the class by virtue of how you've defined it. You need to move creation of the button into the initializer.
I also suggest moving the configuration of the command into the button itself. The caller shouldn't need (nor care) how the button works internally. Personally I'd have the tile inherit from Button, but if you favor composition over inheritance I'll stick with that.
Example:
class tile(object):
def __init__(self):
self.core = Button(window, height = 2, width = 3, bg = "#F4C364"
command=self.cycle)

Categories

Resources