Python Tkinter Grid Checkbox - python

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

Related

'Event' object is not subscriptable

I am trying to write a tic-tac-toe in Python but I get this error: 'Event' object is not subscriptable
The general idea is that when a button is pressed, if it doesn't have a text, it will change it's text in an "X" or "O" depending on the turn.
But I am stuck due to this error.
I have not much experience with Python so I need a fix easy to understand and implement. Thanks!
Code:
from tkinter import *
window = Tk()
btn = [None] * 9
btn_id = [None] * 9
#position of button array
def BTNposition():
i = 0
c = 0
k = 0
while i < 9:
#declaration of button array
btn[i] = Button(window, text = "", height = 10, width = 20, borderwidth = 5, command = lambda i=i: GAME)
btn[i].place(x= c, y= k)
btn_id[i] = btn[i]
print(btn[i], btn_id[i])
i+= 1
if c == 300:
k += 150
c = 0
else:
c += 150
BTNposition()
def GAME(btn):
turn = 0
if btn["text"] == "" and turn % 2 == 0: #here the error
btn.config(text = "X")
turn += 1
elif btn["text"] == "" and turn % 2 == 1:
btn.config(text = "O")
turn += 1
i = 0
#while i < 9:
#btn[i].bind("<Button 1>", GAME)
#find button pressed
window.bind("<Button 1>", GAME)
#print(click_position.btnp_x, click_position.btnp_y)
window.title('PyTris')
window.geometry("700x450")
window.mainloop()
I will update the post whenever I do major changes to the code.
Several problems:
window.bind("<Button 1>", GAME) a useless button that calls to GAME without providing a parameter hence it getting provided the default ButtonPressEvent as parameter which GAME is indexing into wich leads to the reported error
no exact button placement although you calculate positions
confused usages of = vs == inside GAME function
use of global variables with danger of confusing global and local identically named ones
variable names reused (or not used at all in case of the lambda i)
Fixing your code with minimalistic changes to it:
from tkinter import *
window = Tk()
btn = [None] * 9
turn = 0
#position of button array
def BTNposition():
c, k = 0, 0
for i in range(9):
# put button in array, callback is provided with the buttons index
# in the global array holding all the buttons
btn[i] = Button(window, text="", height=10, width=20, borderwidth=5,
command= lambda idx=i: GAME(idx))
# fix button on coordinates
btn[i].place(x=c, y=k)
if c == 300:
k += 150
c = 0
else:
c += 150
# btn_nr is supposed to be an index into the global btn-array holding your buttons
def GAME(btn_nr):
global turn # turn count is held globally, we want to modify it
button = btn[btn_nr] # get the button we are at
# only allow modification on buttons that are not yet X or O
if button["text"] == "":
# place the correct marker and advance turns
button["text"] = "X" if turn % 2 == 0 else "O"
turn += 1
BTNposition()
window.title('PyTris')
window.geometry("700x450")
window.mainloop()

Tkinter function range issue

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.

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

How to left justify Python tkinter grid columns while filling entire cell

I'm trying to write a python class to display data in a tabular format. I'm sure there are classes out there already to do the same thing, however, I'm using this exercise as a way to teach myself Python and tkinter. For the most part, I have the class working the way I want it to, however I cannot get the header and data cells to fill their entire cell, while being aligned left. Here is what my class currently generates for a table:
I went ahead and changed the sticky on the cells to be (W,E) rather than just W, in order to show how I want the table to look, except each cell left justified. Below is what I'm shooting for:
Based on the research I've done, it would seem I need to be using the weight attribute of grid_columnconfigure and grid_rowconfigure, however every way I have tried using them I cannot, get it to work.
Here is the code for my class (I am using Python 3.4):
from tkinter import *
from tkinter import ttk
from tkinter import font
class TableData:
def __init__(self,parent,attributes,columns,data):
self.parent = parent
self.tableName = StringVar()
self.tableName.set(attributes['tableName'])
self.columns = columns
self.columnCount = 0
self.borderColor = attributes['borderColor']
self.titleBG = attributes['titleBG']
self.titleFG = attributes['titleFG']
self.titleFontSize = attributes['titleFontSize']
self.headerBG = attributes['headerBG']
self.headerFG = attributes['headerFG']
self.headerFontSize = attributes['headerFontSize']
self.dataRowColor1 = attributes['dataRowColor1']
self.dataRowColor2 = attributes['dataRowColor2']
self.dataRowFontSize = attributes['dataRowFontSize']
self.dataRowFG = attributes['dataRowFG']
self.data = data
self.tableDataFrame = ttk.Frame(self.parent)
self.tableDataFrame.grid(row=0,column=0)
self.initUI()
def countColumns(self):
cnt = 0
for i in self.columns:
cnt += 1
self.columnCount = cnt
def buildTableTitle(self):
tableTitleFont = font.Font(size=self.titleFontSize)
Label(self.tableDataFrame,textvariable=self.tableName,bg=self.titleBG,fg=self.titleFG,font=tableTitleFont, highlightbackground=self.borderColor,highlightthickness=2).grid(row=0,columnspan=self.columnCount,sticky=(W,E), ipady=3)
def buildHeaderRow(self):
colCount = 0
tableHeaderFont = font.Font(size=self.headerFontSize)
for col in self.columns:
Label(self.tableDataFrame,text=col,font=tableHeaderFont,bg=self.headerBG,fg=self.headerFG,highlightbackground=self.borderColor,highlightthickness=1).grid(row=1,column=colCount,sticky=W, ipady=2, ipadx=5)
colCount += 1
def buildDataRow(self):
tableDataFont = font.Font(size=self.dataRowFontSize)
rowCount = 2
for row in self.data:
if rowCount % 2 == 0:
rowColor = self.dataRowColor2
else:
rowColor = self.dataRowColor1
colCount = 0
for col in row:
Label(self.tableDataFrame,text=col,bg=rowColor,fg=self.dataRowFG,font=tableDataFont,highlightbackground=self.borderColor,highlightthickness=1).grid(row=rowCount,column=colCount,sticky=W,ipady=1, ipadx=5)
colCount += 1
rowCount += 1
def initUI(self):
self.countColumns()
self.buildTableTitle()
self.buildHeaderRow()
self.buildDataRow()
Here is a test file referencing the TableData class:
from tkinter import *
from tkinter import ttk
from tableData import TableData
import sqlite3
root = Tk()
root.geometry('1000x400')
mainframe = ttk.Frame(root).grid(row=0,column=0)
attributes = {}
attributes['tableName'] = 'Title'
attributes['borderColor'] = 'black'
attributes['titleBG'] = '#1975D1'
attributes['titleFG'] = 'white'
attributes['titleFontSize'] = 16
attributes['headerBG'] = 'white'
attributes['headerFG'] = 'black'
attributes['headerFontSize'] = 12
attributes['dataRowColor1'] = 'white'
attributes['dataRowColor2'] = 'grey'
attributes['dataRowFontSize'] = 10
attributes['dataRowFG'] = 'black'
columns = ['Col 1', 'Column 2', 'Column 3','Column 4']
results = [('1','Key','Desc','Attribute'),('2','Key Column','Description Column','AttributeColumn')]
table = TableData(mainframe,attributes,columns,results)
root.mainloop()
Thanks in advance for any insight. Please, let me know if there is any other info that would be helpful.
For any grid of geometry, add option sticky="W", for example,
self.tableDataFrame.grid(sticky="W", row=0, column=0)
If you want the text in a label to be left-aligned, use the anchor option. It takes a string representing a point on a compass (eg: "w" = "west", meaning the text is anchored to the left):
for col in row:
Label(..., anchor="w").grid(...)
You are not defining the width of your label so tkinter sets the width of Label equal to the width of the text. Thats why your labels do not occupy entire width of the cell. To make them occupy all the available width use stickey = E+W
Label(self.tableDataFrame,text=col,font=tableHeaderFont,bg=self.headerBG,fg=self.headerFG,highlightbackground=self.borderColor,highlightthickness=1).grid(row=1,column=colCount,sticky=W+E, ipady=2, ipadx=5)
Try to set the columnspan attribute when griding:
self.tableDataFrame.grid(row=0, column=0, columnspan=2)
2 may not be the correct size, try 'til you get it like you want it.

Categories

Resources