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()
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.
im using tkinter with class abd im having trouble with adding a product
class Add_Page():
def __init__(self, child):
self.child = child
child.title = "Ajouter"
self.l1=Label(child,text="Ajouter produit :",bg="blue").grid(row=0,columnspan=2)
self.l2=Label(child,text="Matricule").grid(row=1,column=0)
self.vlrm = StringVar()
self.en2 = Entry(child, textvariable=self.vlrm, width=30).grid(row=1,column=1)
self.l3=Label(child,text="Nom").grid(row=2,column=0)
self.vlrn = StringVar()
self.en3 = Entry(child, textvariable=self.vlrn, width=30).grid(row=2,column=1)
self.l4=Label(child,text="Prix").grid(row=3,column=0)
self.vlrp = IntVar()
self.en4 = Entry(child, textvariable=self.vlrp, width=30).grid(row=3,column=1)
self.b2=Button(child,text="Valider",command=self.add_p).grid(row=4,columnspan=2)
#Add product function
def add_p(self):
print(self.vlrm.get())
print(self.vlrp.get())
the results are anempty chaine and 0
i dont seem to find the problem especially that i used the get method in users class and its working just fine
Heelp
You don't need create a variable to entry, only make this for radiobutton or checkbutton. And you can change your create of tkinter object, like that
change this
self.l1=Label(child,text="Ajouter produit :",bg="blue").grid(row=0,columnspan=2)
for this
self.l1 = Label(child, text = "Ajouter produit :", bg = "blue")
self.l1.grid(row = 0, columnspan = 2) # remove all variables StringVar() and IntVar()
if you need make some future change using .config or .get() you don't can make that in the first example. You can continue using variable, but i don't recommend that, if you make this change .get() will work now.
I maked a easy way to draw in tkinter, you can use or make change, is for python 2
from Tkinter import *
class Draw_tk():
Row, Column, List = 0, 0, []
def __init__(self, child):
self.child = child
child.title = "Ajouter"
def labelAndEntry(self, text): # def to create a entry and a label
self.l = Label(self.child, text = text) # create label
self.l.grid(row = Draw_tk.Row, column = Draw_tk.Column) # place label
Draw_tk.Column += 1 # add 1 in Column to place the entry
self.e = Entry(self.child, width = 30) # create entry
self.e.grid(row = Draw_tk.Row, column = Draw_tk.Column) # place entry
Draw_tk.List.append(self.e) # add the entry in a list
Draw_tk.Row, Draw_tk.Column = Draw_tk.Row + 1, 0
def label(self, text):
self.l = Label(self.child, text = text, bg = "blue") # def to create a simple label
self.l.grid(row = Draw_tk.Row, columnspan=2) # place the label
Draw_tk.Row += 1
def button(self, text, var): # create a simple button
self.b = Button(self.child, text = text, command = var) # create button
self.b.grid(row = Draw_tk.Row, column = Draw_tk.Column) # place the button
def valid():
for item in Draw_tk.List: # run a variable in your values list
print item.get() # get the value and print
root = Tk()
controller = Draw_tk(root) # create object Draw_tk
controller.label('Ajouter produit')
controller.labelAndEntry('Matricule')
controller.labelAndEntry('Nom')
controller.labelAndEntry('Prix')
controller.button('Valider', valid)
root.mainloop()
I'm trying to have a program which would have a grid of 8x8 buttons which would on-click change their color
My code looks like this:
def Function(self):
for i in range(8):
for j in range(8):
a=Button(self,width=2,height=1,command=lambda widget="button"+str(i)+str(j):Click1(self,widget))
a.grid(row=i,column=j)
def Click1(self):
a["bg"]="blue"
The problem I have with this is that I keep getting an error saying:
NameError: name 'Click1' is not defined
any solutions for this?
You are not saving references to the Buttons, so you can't change them later on. What you should do is save references to the Buttons, for example in a list of lists (like a grid), so you can access them with the row and column number.
Here's a working example:
from Tkinter import *
class App():
def __init__(self, root):
self.root = root
def Function(self):
self.grid = []
for i in range(8):
row = []
for j in range(8):
row.append(Button(self.root,width=2,height=1,command=lambda i=i, j=j: self.Click1(i, j)))
row[-1].grid(row=i,column=j)
self.grid.append(row)
def Click1(self, i, j):
self.grid[i][j]["bg"]="blue"
root = Tk()
app = App(root)
app.Function()
root.mainloop()
Is there any way to use ttk Treeview with editable rows?
I mean it should work more like a table. For example on double click on the item make the #0 column 'editable'.
If this isn't possible, any way to allow mouse selecting on the item would be just fine. I haven't found any mention of this in tkdocs or other documents.
After long research I haven't found such feature so I guess there's any. Tk is very simple interface, which allows programmer to build 'high-level' features from the basics. So my desired behaviour this way.
def onDoubleClick(self, event):
''' Executed, when a row is double-clicked. Opens
read-only EntryPopup above the item's column, so it is possible
to select text '''
# close previous popups
# self.destroyPopups()
# what row and column was clicked on
rowid = self._tree.identify_row(event.y)
column = self._tree.identify_column(event.x)
# get column position info
x,y,width,height = self._tree.bbox(rowid, column)
# y-axis offset
# pady = height // 2
pady = 0
# place Entry popup properly
text = self._tree.item(rowid, 'text')
self.entryPopup = EntryPopup(self._tree, rowid, text)
self.entryPopup.place( x=0, y=y+pady, anchor=W, relwidth=1)
This is method within a class which composes ttk.Treeview as self._tree
And EntryPopup is then very simple sub-class of Entry:
class EntryPopup(Entry):
def __init__(self, parent, iid, text, **kw):
''' If relwidth is set, then width is ignored '''
super().__init__(parent, **kw)
self.tv = parent
self.iid = iid
self.insert(0, text)
# self['state'] = 'readonly'
# self['readonlybackground'] = 'white'
# self['selectbackground'] = '#1BA1E2'
self['exportselection'] = False
self.focus_force()
self.bind("<Return>", self.on_return)
self.bind("<Control-a>", self.select_all)
self.bind("<Escape>", lambda *ignore: self.destroy())
def on_return(self, event):
self.tv.item(self.iid, text=self.get())
self.destroy()
def select_all(self, *ignore):
''' Set selection on the whole text '''
self.selection_range(0, 'end')
# returns 'break' to interrupt default key-bindings
return 'break'
You could also pop up a tool window with the editable fields listed with Entries to update the values. This example has a treeview with three columns, and does not use subclasses.
Bind your double click to this:
def OnDoubleClick(self, treeView):
# First check if a blank space was selected
entryIndex = treeView.focus()
if '' == entryIndex: return
# Set up window
win = Toplevel()
win.title("Edit Entry")
win.attributes("-toolwindow", True)
####
# Set up the window's other attributes and geometry
####
# Grab the entry's values
for child in treeView.get_children():
if child == entryIndex:
values = treeView.item(child)["values"]
break
col1Lbl = Label(win, text = "Value 1: ")
col1Ent = Entry(win)
col1Ent.insert(0, values[0]) # Default is column 1's current value
col1Lbl.grid(row = 0, column = 0)
col1Ent.grid(row = 0, column = 1)
col2Lbl = Label(win, text = "Value 2: ")
col2Ent = Entry(win)
col2Ent.insert(0, values[1]) # Default is column 2's current value
col2Lbl.grid(row = 0, column = 2)
col2Ent.grid(row = 0, column = 3)
col3Lbl = Label(win, text = "Value 3: ")
col3Ent = Entry(win)
col3Ent.insert(0, values[2]) # Default is column 3's current value
col3Lbl.grid(row = 0, column = 4)
col3Ent.grid(row = 0, column = 5)
def UpdateThenDestroy():
if ConfirmEntry(treeView, col1Ent.get(), col2Ent.get(), col3Ent.get()):
win.destroy()
okButt = Button(win, text = "Ok")
okButt.bind("<Button-1>", lambda e: UpdateThenDestroy())
okButt.grid(row = 1, column = 4)
canButt = Button(win, text = "Cancel")
canButt.bind("<Button-1>", lambda c: win.destroy())
canButt.grid(row = 1, column = 5)
Then confirm the changes:
def ConfirmEntry(self, treeView, entry1, entry2, entry3):
####
# Whatever validation you need
####
# Grab the current index in the tree
currInd = treeView.index(treeView.focus())
# Remove it from the tree
DeleteCurrentEntry(treeView)
# Put it back in with the upated values
treeView.insert('', currInd, values = (entry1, entry2, entry3))
return True
Here's how to delete an entry:
def DeleteCurrentEntry(self, treeView):
curr = treeView.focus()
if '' == curr: return
treeView.delete(curr)
I have tried #dakov solution but it did not work for me since my treeView has multiple columns and for few more reasons. I made some changes that enhanced it so here is my version
class Tableview(ttk.Treeview):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tv.bind("<Double-1>", lambda event: self.onDoubleClick(event))
def onDoubleClick(self, event):
''' Executed, when a row is double-clicked. Opens
read-only EntryPopup above the item's column, so it is possible
to select text '''
# close previous popups
try: # in case there was no previous popup
self.entryPopup.destroy()
except AttributeError:
pass
# what row and column was clicked on
rowid = self.identify_row(event.y)
column = self.identify_column(event.x)
# handle exception when header is double click
if not rowid:
return
# get column position info
x,y,width,height = self.bbox(rowid, column)
# y-axis offset
pady = height // 2
# place Entry popup properly
text = self.item(rowid, 'values')[int(column[1:])-1]
self.entryPopup = EntryPopup(self, rowid, int(column[1:])-1, text)
self.entryPopup.place(x=x, y=y+pady, width=width, height=height, anchor='w')
The EntryPopup class
class EntryPopup(ttk.Entry):
def __init__(self, parent, iid, column, text, **kw):
ttk.Style().configure('pad.TEntry', padding='1 1 1 1')
super().__init__(parent, style='pad.TEntry', **kw)
self.tv = parent
self.iid = iid
self.column = column
self.insert(0, text)
# self['state'] = 'readonly'
# self['readonlybackground'] = 'white'
# self['selectbackground'] = '#1BA1E2'
self['exportselection'] = False
self.focus_force()
self.select_all()
self.bind("<Return>", self.on_return)
self.bind("<Control-a>", self.select_all)
self.bind("<Escape>", lambda *ignore: self.destroy())
def on_return(self, event):
rowid = self.tv.focus()
vals = self.tv.item(rowid, 'values')
vals = list(vals)
vals[self.column] = self.get()
self.tv.item(rowid, values=vals)
self.destroy()
def select_all(self, *ignore):
''' Set selection on the whole text '''
self.selection_range(0, 'end')
# returns 'break' to interrupt default key-bindings
return 'break'
from tkinter import ttk
from tkinter import *
root = Tk()
columns = ("Items", "Values")
Treeview = ttk.Treeview(root, height=18, show="headings", columns=columns) #
Treeview.column("Items", width=200, anchor='center')
Treeview.column("Values", width=200, anchor='center')
Treeview.heading("Items", text="Items")
Treeview.heading("Values", text="Values")
Treeview.pack(side=LEFT, fill=BOTH)
name = ['Item1', 'Item2', 'Item3']
ipcode = ['10', '25', '163']
for i in range(min(len(name), len(ipcode))):
Treeview.insert('', i, values=(name[i], ipcode[i]))
def treeview_sort_column(tv, col, reverse):
l = [(tv.set(k, col), k) for k in tv.get_children('')]
l.sort(reverse=reverse)
for index, (val, k) in enumerate(l):
tv.move(k, '', index)
tv.heading(col, command=lambda: treeview_sort_column(tv, col, not reverse))
def set_cell_value(event):
for item in Treeview.selection():
item_text = Treeview.item(item, "values")
column = Treeview.identify_column(event.x)
row = Treeview.identify_row(event.y)
cn = int(str(column).replace('#', ''))
rn = int(str(row).replace('I', ''))
entryedit = Text(root, width=10 + (cn - 1) * 16, height=1)
entryedit.place(x=16 + (cn - 1) * 130, y=6 + rn * 20)
def saveedit():
Treeview.set(item, column=column, value=entryedit.get(0.0, "end"))
entryedit.destroy()
okb.destroy()
okb = ttk.Button(root, text='OK', width=4, command=saveedit)
okb.place(x=90 + (cn - 1) * 242, y=2 + rn * 20)
def newrow():
name.append('to be named')
ipcode.append('value')
Treeview.insert('', len(name) - 1, values=(name[len(name) - 1], ipcode[len(name) - 1]))
Treeview.update()
newb.place(x=120, y=(len(name) - 1) * 20 + 45)
newb.update()
Treeview.bind('<Double-1>', set_cell_value)
newb = ttk.Button(root, text='new item', width=20, command=newrow)
newb.place(x=120, y=(len(name) - 1) * 20 + 45)
for col in columns:
Treeview.heading(col, text=col, command=lambda _col=col: treeview_sort_column(Treeview, _col, False))
root.mainloop()
After so much research while doing my project got this code, it helped me a lot.
Double click on the element you want to edit, make the required change and click 'OK' button
I think this is what exactly you wanted
#python #tkinter #treeview #editablerow
New row
Editable row
This is just for creating a tree for the specified path that is set in the constructor. you can bind your event to your item on that tree. The event function is left in a way that the item could be used in many ways. In this case, it will show the name of the item when double clicked on it. Hope this helps somebody.
import ttk
from Tkinter import*
import os*
class Tree(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
path = "/home/...."
self.initUI(path)
def initUI(self, path):
self.parent.title("Tree")
self.tree = ttk.Treeview(self.parent)
self.tree.bind("<Double-1>", self.itemEvent)
yScr = ttk.Scrollbar(self.tree, orient = "vertical", command = self.tree.yview)
xScr = ttk.Scrollbar(self.tree, orient = "horizontal", command = self.tree.xview)
self.tree.configure(yscroll = yScr.set, xScroll = xScr.set)
self.tree.heading("#0", text = "My Tree", anchor = 'w')
yScr.pack(side = RIGHT, fill = Y)
pathy = os.path.abspath(path)
rootNode = self.tree.insert('', 'end', text = pathy, open = True)
self.createTree(rootNode, pathy)
self.tree.pack(side = LEFT, fill = BOTH, expand = 1, padx = 2, pady = 2)
self.pack(fill= BOTH, expand = 1)
def createTree(self, parent, path)
for p in os.listdir(path)
pathy = os.path.join(path, p)
isdir = os.path.isdir(pathy)
oid = self.tree.insert(parent, 'end' text = p, open = False)
if isdir:
self.createTree(oid, pathy)
def itemEvent(self, event):
item = self.tree.selection()[0] # now you got the item on that tree
print "you clicked on", self.tree.item(item,"text")
def main():
root = Tk.Tk()
app = Tree(root)
root.mainloop()
if __name__ == '__main__'
main()
You should not do this manually
there are ready to use pack that have this Feature and many more such as
tkintertable
it have some insane features
there is also pygubu-editable-treeview
if you are intrested in pygubu,
as for the the reason you shouldnt code your own ,
in order to do a good treeview you will need to build more Feature that make your gui easier to use
however such Feature takes hundred lines of code to create.(takes a long time to get right)
unless you are making a custom TREE-View-widget,it doesnot worth the effort.
I don't know about making the row editable, but to capture clicking on a row, you use the <<TreeviewSelect>> virtual event. This gets bound to a routine with the bind() method, then you use the selection() method to get the ids of the items selected.
These are snippets from an existing program, but show the basic sequence of calls:
# in Treeview setup routine
self.tview.tree.bind("<<TreeviewSelect>>", self.TableItemClick)
# in TableItemClick()
selitems = self.tview.tree.selection()
if selitems:
selitem = selitems[0]
text = self.tview.tree.item(selitem, "text") # get value in col #0