Tkinter function range issue - python

I am creating a TicTacToe game in tkinter, consisting of a 3x3 grid made out of buttons.
In the code below, once a player has drawn on a tile (by clicking on the button), the program should remove this tile from the list 'self.flattenedButtons'. This is to prevent the computer (player 2) from drawing on the same tile.
The method this check is made in is self.add_move(). This works on all buttons apart from the bottom right, I assume this is as I took away 1 from the ending range. If I do not do this I am given an 'out of range' error.
How would I change my method so it works on all buttons?
CODE:
from tkinter import *
from functools import partial
from itertools import *
import random
class Window(Frame):
def __init__(self, master = None): # init Window class
Frame.__init__(self, master) # init Frame class
self.master = master # allows us to refer to root as master
self.rows = 3
self.columns = 3
self.guiGrid = [[None for x in range(self.rows)] for y in range(self.columns)] # use this for the computer's moves
self.buttonText = StringVar(value = '')
self.buttonText2 = StringVar(value = 'X')
self.buttonText3 = StringVar(value = 'O')
self.button_ij = None
self.flattenedButtons = []
self.create_window()
self.add_buttons()
def create_window(self):
self.master.title('Tic Tac Toe')
self.pack(fill = BOTH, expand = 1)
for i in range(0,3):
self.grid_columnconfigure(i, weight = 1)
self.grid_rowconfigure(i, weight = 1)
def add_buttons(self):
rows = 3
columns = 3
for i in range (rows):
for j in range(columns):
self.button_ij = Button(self, textvariable = self.buttonText, command = lambda i=i, j=j: self.add_move(i,j))
self.guiGrid[i][j] = self.button_ij # place button into 2d array to access later on
self.flattenedButtons.append(self.button_ij)
self.button_ij.grid(row = i,column = j, sticky =E+W+S+N)
def add_move(self, i,j):
pressedButton = self.guiGrid[i][j]
self.guiGrid[i][j].config(textvariable =self.buttonText2)
for i in range(0, len(self.flattenedButtons)-1):
if (self.flattenedButtons[i] == pressedButton):
self.flattenedButtons.remove(self.flattenedButtons[i])
print('removed')
else:
pass
root = Tk() # creating Tk instance
rootWidth = '500'
rootHeight = '500'
root.geometry(rootWidth+'x'+rootHeight)
ticTacToe = Window(root) # creating Window object with root as master
root.mainloop() # keeps program running

It is not recommended to operate the list when you iterate it.
If your code is:
for i in range(0, len(self.flattenedButtons)-1):
if (self.flattenedButtons[i] == pressedButton):
self.flattenedButtons.remove(self.flattenedButtons[i])
print('removed')
else:
pass
print(self.flattenedButtons)
You will see that your button 9 will never be removed.
Change your for loop to a easy list-comprehension:
self.flattenedButtons = [i for i in self.flattenedButtons if i != pressedButton]
print(self.flattenedButtons)
You will see the change.

Related

Tkinter - pass button grid values between classes upon clicking button

I am trying to create a basic Battleship game in Python using Tkinter.
Below is a very simplified version of my code. Essentially I am creating a 10*10 grid of buttons and positioning them using .grid. What I'd like to do is click one of those buttons and pass that buttons grid values (x, y) from the GameBoard class to the Battleship class to position the ship.
I have tried using self.row = row and self.column = column, however when I do this I immediately receive an attribute error, 'GameBoard' object has no attribute 'row'.
import tkinter as tk
class GameBoard:
def __init__(self):
self.mw = tk.Tk()
self.size = 10
def build_grid(self):
for x in range(self.size):
for y in range(self.size):
self.button = tk.Button(self.mw, text = '', width = 2, height = 1,\
command = lambda row = x, column = y: self.clicked(row, column))
self.button.grid(row = x, column = y)
def clicked(self, row, column):
print(row, column)
self.row = row
self.column = column
class Battleship:
def __init__(self, board):
self.gboard = board
def position_ship(self):
x = self.gboard.row
y = self.gboard.column
for i in range (3):
self.submarine = tk.Button(self.gboard.mw, background = "black", text = '',\
width = 2, height = 1)
self.submarine.grid(row = x, column = y)
def main():
gboard = GameBoard()
gboard.build_grid()
bt = Battleship(gboard)
bt.position_ship()
main()
As #acw1668 pointed out in a comment, the problem is the gboard attributes row and column haven't been created yet when you call bt.position_ship() in the main() function.
I don't know your overall game design, but a very simple way to fix that would be to assign a random board position to them in the GameBoard.__init__() method.
I've also modified the code to show how to call bt.position_ship() when a button is clicked. This is done by passing the BattleShip instance bt to the build_grid() function so it can be included in calls to the clicked() method, which can now call it when its called.
from random import randrange
import tkinter as tk
class GameBoard:
def __init__(self):
self.mw = tk.Tk()
self.size = 10
self.row = randrange(self.size)
self.column = randrange(self.size)
def build_grid(self, bt):
for x in range(self.size):
for y in range(self.size):
self.button = tk.Button(self.mw, text='', width=2, height=1,
command=lambda row=x, column=y:
self.clicked(bt, row, column))
self.button.grid(row=x, column=y)
def clicked(self, bt, row, column):
print(row, column)
self.row = row
self.column = column
bt.position_ship()
class Battleship:
def __init__(self, board):
self.gboard = board
def position_ship(self):
x = self.gboard.row
y = self.gboard.column
for i in range (3):
self.submarine = tk.Button(self.gboard.mw, background="black", text='',
width=2, height=1)
self.submarine.grid(row=x, column=y)
def main():
gboard = GameBoard()
bt = Battleship(gboard)
gboard.build_grid(bt)
bt.position_ship()
tk.mainloop()
main()

tkinter not binding all the specified labels?

So I was trying to create a Tkinter application in which when you click and drag the mouse, it will print the initial and final position of the mouse pointer.
There is a nested list containing labels whom I want to bind. I first bind the left click on each label, then when the label is left-clicked I bind its release. And when the left click is released, it prints the initial and the final position of the mouse pointer relative to the label.
So here is the code
from tkinter import *
class game(Frame):
def __init__(self,parent):
Frame.__init__(self,parent)
self.llist = [[0]*4]*4
for i in range(4):
for j in range(4):
self.llist[i][j] = Label(self,bd = 1, relief = 'ridge',height = 4, width = 8)
self.llist[i][j].grid(row = i, column = j)
self.llist[i][j].bind('<Button-1>',lambda event, r = i, c = j: self.click(event,r,c))
def click(self,event,r,c):
a,b = event.x,event.y
self.llist[r][c].bind('<ButtonRelease-1>',lambda event: self.release(event,a,b))
def release(self,event,a,b):
print(a,b,event.x,event.y)
r = Tk()
r.geometry('300x300')
f = game(r)
f.pack()
r.mainloop()
But the problem is that only the last 4 labels work!
The problem is with the second binding. When I test only the first binding, all the labels work!
Please Help!!!
It is caused by the line:
self.llist = [[0]*4]*4
It created list of 4 lists that reference to the same list, i.e. self.llist[r][c] reference to the same self.llist[0][c].
Change it to self.llist = [[0]*4 for _ in range(4)].

TKinter dealing with multiple monitors and image display

I have functioning code but there are a few things which I would like to change about it but don't know how to so thought i'd ask here. My code is as follows:
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
#Define the target, source and output arrays. Source has to be completely white otherwise it kills everything
def initialize(x,y):
xarr = np.zeros(x)
yarr = np.zeros(y)
target = np.meshgrid(xarr,yarr)
target = target[0]
source = np.meshgrid(xarr,yarr)
source = source[0]
output = np.meshgrid(xarr,yarr)
output = output[0]
for i in range(x):
for n in range(y):
source[n][i] = 1
return target, source, output
# creates trap between XTrapMin-XTrapMax and YTrapMin-YTrapMax on Array
def trap(xtmi,xtma,xs,ytmi,ytma,ys,array):
for i in range(xs):
if xtmi < i < xtma:
for n in range(ys):
if ytmi < n < ytma:
array[n][i] = 255
return
#Returns the amplitude of a complex number
def Amplitude(x):
if isinstance(x, complex):
return np.sqrt(x.real**2+x.imag**2)
else:
return np.abs(x)
#Returns the phase of a complex number
def Phase(z):
return np.angle(z)
#Main GS algorithm implementation using numpy FFT package
#performs the GS algorithm to obtain a phase distribution for the plane, Source
#such that its Fourier transform would have the amplitude distribution of the plane, Target.
def GS(target,source):
A = np.fft.ifft2(target)
for i in range(5):
B = Amplitude(source) * np.exp(1j * Phase(A))
C = np.fft.fft2(B)
D = Amplitude(target) * np.exp(1j * Phase(C))
A = np.fft.ifft2(D)
output = Phase(A)
return output
#Make array into PIL Image
def mkPIL(array):
im = Image.fromarray(np.uint8(array))
return im
def up():
global ytmi
global ytma
ytmi -= 10
ytma -= 10
return
def down():
global ytmi
global ytma
ytmi += 10
ytma += 10
return
def right():
global xtmi
global xtma
xtmi += 10
xtma += 10
return
def left():
global xtmi
global xtma
xtmi -= 10
xtma -= 10
return
xtmi = 125
xtma = 130
xs = 1024
ytmi = 0
ytma = 5
ys = 768
root = tk.Tk()
root.attributes('-fullscreen', True)
def main():
app = Lower(root)
root.mainloop()
class Lower:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(self.master).pack()
self.displayimg = tk.Button(self.frame, text = 'Display', width = 25, command = self.plot)
self.displayimg.pack()
self.makewidg()
def makewidg(self):
self.fig = plt.figure(figsize=(100,100), frameon=False) #changing figsize doesnt cange the size of the plot display
self.fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
self.fig.tight_layout()
self.ax = self.fig.add_subplot(111)
self.ax.set_yticklabels([])
self.ax.set_xticklabels([])
self.canvas = FigureCanvasTkAgg(self.fig, master=self.master)
self.canvas.get_tk_widget().pack(expand=True)
self.canvas.figure.tight_layout()
self.canvas.draw()
self.new_window()
def new_window(self):
self.newWindow = tk.Toplevel()
self.app = Display(self.newWindow)
def plot(self):
global xtmi, xtma, xs, ytmi, ytma, ys, i
target,source,output=initialize(xs,ys)
trap(xtmi,xtma,xs,ytmi,ytma,ys,target)
output = GS(target,source)
self.ax.imshow(output, cmap='gray')
self.ax.set_yticklabels([])
self.ax.set_xticklabels([])
self.canvas.draw()
self.ax.clear()
def kill(self):
root.destroy()
class Display:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(self.master)
self.frame.pack()
self.up = tk.Button(self.frame, text = 'Up', width = 25, command = up)
self.up.pack()
self.down = tk.Button(self.frame, text = 'Down', width = 25, command = down)
self.down.pack()
self.right = tk.Button(self.frame, text = 'Right', width = 25, command = right)
self.right.pack()
self.left = tk.Button(self.frame, text = 'Left', width = 25, command = left)
self.left.pack()
self.kill = tk.Button(self.frame, text = 'Kill', width = 25, command = self.kill)
self.kill.pack()
def kill(self):
root.destroy()
main()
Currently the button displayimg from the class Lower is displayed above the image, is there a way in which I can have the display button on the Display class and still have it manipulate the image on the Lower screen? Also, I intend to display the window opened by Lower on a separate monitor, but can't drag it seeing as it is fullscreen, is there a way around that I can get it on my second monitor?
I try that as such:
self.displayimg = tk.Button(self.top, text = 'Display', width = 25, command = Lower.plot(Lower))
self.displayimg.pack()
But this causes a misreference I think as I get an error code
AttributeError: type object 'Lower' has no attribute 'ax'
Call to Lower.plot
You are using Lower.plot as your button command. It needs one argument, self which must be an instance of Lower - so Lower.plot(Lower) is passing a class where an instance is expected. Instead you need to use the app instance you've made, and call app.plot(). The arguement self is automatically the instance itself, this is fundamental to OOP in python. Calling the method on an instance passes self as the first arg, so it's missing from the call. Calling Lower.plot(...) is calling the method on the class Lower, so there is no instance, and you have to supply your own. I'd avoid calling methods without an instance like this in this situation, and use your app instance.
Command for the display button
Your button creation becomes something like:
self.displayimg = tk.Button(self.top, text = 'Display', width = 25, command = app.plot)
If you need to pass additional args to plot, you need to delay the function call so it happens on click, not on creation of the button. You can use lambda : app.plot("red", 10, whatever) to make a nameless function, taking no arguments, that when called will go on to call app.plot with the given args.
Positioning the window
You can control the position of the app window using wm_geometry:
app.wm_geometry("200x200+100+500")
Will cause the app window to be 200px by 200px, positioned 100px left and 500px down from the origin, and on a windows machine, this is the top left corner of your primary monitor. You can keep the width and height the same and just move the window with eg
app.wm_geometry("+100+500")
You can use more scripting to build the string +{xpos}+{ypos} with whichever values you like to match your desktop layout.

tkinter Multiple Buttons Colour Change

I'm using tkinter to create a 8x8 button matrix, which when the individual buttons are pressed add to a final list (eg finalList = ((0,0),(5,7),(6,6), ...), allowing me to quickly create 8x8 (x,y) co-ordinate images. I have created the window with the buttons but now have issues trying to reference these buttons in a function to add to a list or even change the colour of the button
I have read that once the button is created and you create another it moves to that button reference. I suspect I need to use a dict or 2D array to store all these reference of buttons but am struggling to figure out a solution.
from tkinter import *
class App:
def updateChange(self):
'''
-Have the button change colour when pressed
-add coordinate to final list
'''
x , y = self.xY
self.buttons[x][y].configure(bg="#000000")
def __init__(self, master):
frame = Frame(master)
frame.pack()
self.buttons = [] # Do I need to create a dict of button's so I can reference the particular button I wish to update?
for matrixColumn in range(8):
for matrixRow in range(8):
self.xY = (matrixColumn,matrixRow)
stringXY = str(self.xY)
self.button = Button(frame,text=stringXY, fg="#000000", bg="#ffffff", command = self.updateChange).grid(row=matrixRow,column=matrixColumn)
self.buttons[matrixColumn][matrixRow].append(self.button)
root = Tk()
app = App(root)
root.mainloop()
Example of the 8x8 Matrix
Below are 2 examples, the first is if you just want to change the colour and nothing else then you can do it without using a list. The second involves using a list and demonstrates what Delioth has pointed out
class App(object):
def __init__(self, master):
self._master = master
for col in range(8):
for row in range(8):
btn = tk.Button(master, text = '(%d, %d)' % (col, row), bg = 'white')
btn['command'] = lambda b = btn: b.config(bg = 'black')
btn.grid(row = row, column = col)
class App(object):
def __init__(self, master):
self._master = master
self._btn_matrix = []
for col in range(8):
row_matrix = []
for row in range(8):
btn = tk.Button(master, text = '(%d, %d)' % (col, row), bg = 'white',
command = lambda x = row, y = col: self.update(x, y))
btn.grid(row = row, column = col)
row_matrix.append(btn)
self._btn_matrix.append(row_matrix)
def update(self, row, col):
self._btn_matrix[col][row].config( bg = 'black' )
if __name__ == '__main__':
root = tk.Tk()
app = App(root)
root.mainloop()
self.xY is set to 7,7 in your double for loop and never changed. If you want it to be different for each button, you may want to change updateChange to take two parameters (x,y), and pass them in as the command for the button using something like; lambda x=matrixColumn y=matrixRow: self.updateChange(x,y)
Example updateChange
def updateChange(self, x, y):
'''...'''
self.buttons[x][y].configure(bg="black")

Python Tkinter Grid Checkbox

I was wondering if there is an easy way to create a grid of checkboxes using Tkinter. I am trying to make a grid of 10 rows and columns (so 100 checkboxes) so that only two checkboxes can be selected per row.
Edit: I'm using python 2.7 with spyder
What I have so far:
from Tkinter import*
master = Tk()
master.title("Select Groups")
rows=10
columns=10
for x in range(rows):
for y in range(columns):
Label(master, text= "Group %s"%(y+1)).grid(row=0,column=y+1)
Label(master, text= "Test %s"%(x+1)).grid(row=x+1,column=0)
Checkbutton(master).grid(row=x+1, column=y+1)
mainloop()
I'm trying to use state='Disabled' to grey out a row once two checkboxes have been selected.
Here's a version that puts everything into a class so we don't need to use global variables. It also avoids the import * construction which is generally considered bad style in Python. True, lots of example code uses import * but it's not a good practice because it clutters up the global namespace with all the names from the imported module. So those names can clash with the names of your own variables, and they can also clash with the names of other modules you import using import *.
The program prints lists of the selected Groups for each Test row when the window closes.
#!/usr/bin/env python
''' Create a grid of Tkinter Checkbuttons
Each row permits a maximum of two selected buttons
From http://stackoverflow.com/q/31410640/4014959
Written by PM 2Ring 2015.07.15
'''
import Tkinter as tk
class CheckGrid(object):
''' A grid of Checkbuttons '''
def __init__(self, rows=10, columns=10):
master = tk.Tk()
master.title("Select Groups")
rowrange = range(rows)
colrange = range(columns)
#Create the grid labels
for x in colrange:
w = tk.Label(master, text="Group %s" % (x + 1))
w.grid(row=0, column=x+1)
for y in rowrange:
w = tk.Label(master, text="Test %s" % (y + 1))
w.grid(row=y+1, column=0)
#Create the Checkbuttons & save them for future reference
self.grid = []
for y in rowrange:
row = []
for x in colrange:
b = tk.Checkbutton(master)
#Store the button's position and value as attributes
b.pos = (y, x)
b.var = tk.IntVar()
#Create a callback bound to this button
func = lambda w=b: self.check_cb(w)
b.config(variable=b.var, command=func)
b.grid(row=y+1, column=x+1)
row.append(b)
self.grid.append(row)
#Track the number of on buttons in each row
self.rowstate = rows * [0]
master.mainloop()
def check_cb(self, button):
''' Checkbutton callback '''
state = button.var.get()
y, x = button.pos
#Get the row containing this button
row = self.grid[y]
if state == 1:
self.rowstate[y] += 1
if self.rowstate[y] == 2:
#Disable all currently off buttons in this row
for b in row:
if b.var.get() == 0:
b.config(state=tk.DISABLED)
else:
self.rowstate[y] -= 1
if self.rowstate[y] == 1:
#Enable all currently off buttons in this row
for b in row:
if b.var.get() == 0:
b.config(state=tk.NORMAL)
#print y, x, state, self.rowstate[y]
def get_checked(self):
''' Make a list of the selected Groups in each row'''
data = []
for row in self.grid:
data.append([x + 1 for x, b in enumerate(row) if b.var.get()])
return data
def main():
g = CheckGrid(rows=10, columns=10)
#Print selected Groups in each Test row when the window closes
data = g.get_checked()
for y, row in enumerate(data):
print "Test %2d: %s" % (y + 1, row)
if __name__ == '__main__':
main()
Here's an example using your provided 10x10 grid. It should give you the basic idea of how to implement this.
Just make sure you keep a reference to every Checkbutton (boxes in the example) as well as every IntVar (boxVars in the example).
Here's why:
-Checkbuttons are needed to call config(state = DISABLED/NORMAL).
-IntVars are needed to determine the value of each Checkbutton.
Aside from those crucial elements its basically just some 2D array processing.
Here's my example code (now based off of your provided code).
from Tkinter import *
master = Tk()
master.title("Select Groups")
rows=10
columns=10
boxes = []
boxVars = []
# Create all IntVars, set to 0
for i in range(rows):
boxVars.append([])
for j in range(columns):
boxVars[i].append(IntVar())
boxVars[i][j].set(0)
def checkRow(i):
global boxVars, boxes
row = boxVars[i]
deselected = []
# Loop through row that was changed, check which items were not selected
# (so that we know which indeces to disable in the event that 2 have been selected)
for j in range(len(row)):
if row[j].get() == 0:
deselected.append(j)
# Check if enough buttons have been selected. If so, disable the deselected indeces,
# Otherwise set all of them to active (in case we have previously disabled them).
if len(deselected) == (len(row) - 2):
for j in deselected:
boxes[i][j].config(state = DISABLED)
else:
for item in boxes[i]:
item.config(state = NORMAL)
def getSelected():
selected = {}
for i in range(len(boxVars)):
temp = []
for j in range(len(boxVars[i])):
if boxVars[i][j].get() == 1:
temp.append(j + 1)
if len(temp) > 1:
selected[i + 1] = temp
print selected
for x in range(rows):
boxes.append([])
for y in range(columns):
Label(master, text= "Group %s"%(y+1)).grid(row=0,column=y+1)
Label(master, text= "Test %s"%(x+1)).grid(row=x+1,column=0)
boxes[x].append(Checkbutton(master, variable = boxVars[x][y], command = lambda x = x: checkRow(x)))
boxes[x][y].grid(row=x+1, column=y+1)
b = Button(master, text = "Get", command = getSelected, width = 10)
b.grid(row = 12, column = 11)
mainloop()

Categories

Resources