I'm currently working on a Python GUI version of Reversi for a programming class. I've already programmed the game logic, and I'm currently trying to implement the GUI using Tkinter. I'm having some problems with resizing the game board (root window) and everything on it (canvases and shapes) proportionally. The game currently works, but everything that I've tried to get the board to resize correctly hasn't worked. The relevant code is as follows.
class OthelloApplication:
def __init__(self, game_state: othello.Othello):
self._game_state = game_state
self._game_state.new_game()
self._game_board = game_state.show_board()
self._root_window = tkinter.Tk()
for row in range(self._game_state.show_rows()):
for col in range(self._game_state.show_cols()):
canvas = tkinter.Canvas(self._root_window, width = 100, height = 100,
borderwidth = 0, highlightthickness = 1,
background = _BACKGROUND_COLOR, highlightbackground = 'black')
canvas.grid(row = row, column = col, padx = 0, pady = 0,
sticky = tkinter.N + tkinter.S + tkinter.E + tkinter.W)
self._root_window.rowconfigure(row, weight = 1)
self._root_window.columnconfigure(col, weight = 1)
self._turn_window = tkinter.Canvas(self._root_window, width = 200,
height = 100, highlightthickness = 0, background = 'white')
self._root_window.bind('<Button-1>', self._on_canvas_clicked)
self._root_window.bind('<Configure>', self.on_resize)
def draw_game_pieces(self) -> None:
for row in range(self._game_state.show_rows()):
for col in range(self._game_state.show_cols()):
if self._game_board[col][row] == ' ':
pass
else:
canvas = tkinter.Canvas(master = self._root_window, width = 100, height = 100,
borderwidth = 0, highlightthickness = 1,
background = _BACKGROUND_COLOR, highlightbackground = 'black')
canvas.grid(row = row, column = col, padx = 0, pady = 0,
sticky = tkinter.N + tkinter.S + tkinter.E + tkinter.W)
canvas.update()
canvas.create_oval(2, 2, canvas.winfo_width() - 2,
canvas.winfo_height() - 2, fill = self._which_color(col,
row), outline = self._which_color(col, row))
self._root_window.rowconfigure(row, weight = 1)
self._root_window.columnconfigure(col, weight = 1)
def display_turn(self) -> None:
if self._game_state.show_turn() == 'B':
turn = 'black'
else:
turn = 'white'
self._turn_window.grid(row = self._game_state.show_rows() // 2 - 1,
column = self._game_state.show_cols() + 1,
sticky = tkinter.N + tkinter.S + tkinter.E + tkinter.W)
self._turn_info = self._turn_window.create_text(10, 10,
font = 'Helvetica', anchor = 'nw')
self._turn_window.itemconfig(self._turn_info, text = 'Turn = ' + turn)
def on_resize(self, event: tkinter.Event) -> None:
self.draw_game_pieces()
def _which_color(self, col: int, row: int) -> str:
if self._game_board[col][row] == 'B':
return 'black'
elif self._game_board[col][row] == 'W':
return 'white'
def _on_canvas_clicked(self, event: tkinter.Event) -> (int):
print(event.widget.winfo_reqwidth(), event.widget.winfo_reqheight())
print(event.widget.winfo_width(), event.widget.winfo_height())
try:
grid_info = event.widget.grid_info()
move = (int(grid_info["row"]), int(grid_info["column"]))
self._game_state.player_move(move[1], move[0])
self.draw_game_pieces()
self.display_turn()
except AttributeError:
pass
except othello.InvalidMoveError:
print('Error: that wasn\'t a valid move.')
except othello.GameOverError:
print('The game is over.')
def start(self) -> None:
self.draw_game_pieces()
self.display_turn()
self._root_window.mainloop()
The draw_game_pieces() method draws the appropriately colored circle in the correct space on the board based on the size of the canvas on which it is being drawn.
My solution to my resize problem was binding on_resize() to the '<Configure>' event in the init method, but that causes the program to crash in a cycle of recursions. I'm new to tkinter and GUIs in general. Why is the on_resize() method being bound to '<Configure>' causing the program to crash?
Sorry about the messy code, I'm very much still working on it.
Thanks.
The crash due to recursion is probably because your on_resize method creates new widgets. Here's what's happening:
the widget gets a event which ...
calls on_resize which ...
creates a nested loop which...
creates a single canvas, which ...
causes a configuration change in the widget
you call update inside the nested loop which ...
causes the event to be processed, which ...
calls on_resize, which...
creates a nested loop which ...
creates a canvas which ...
causes a configuration change in the widget
you call update inside the nested loop which ...
causes the event to be processed, which ...
calls on_resize, which ...
...
As a rule of thumb you should never call update unless you are absolutely certain you need to, and even then you need to stop and think hard about it. A rule that should almost never, ever be broken is to call update in a function that is called in response to an event. Every time you call update it's almost as if you're calling mainloop again -- it creates a nested event loop which tries to process all pending events. If the processing of pending events causes new events to be generated, you'll end up with the recursive situation you now find yourself in.
The first fix is to remove the call to update. The second fix is probably to not create new widgets in the handling of the configure event. You really only need to create all of those widgets once. The redraw event only needs to redraw the ovals, because the canvas objects will resize themselves automatically.
My advice is to step back and take it slower. Rewrite your program to just draw the board without worrying about the ovals or game logic. Get the resize behavior of the board working the way you want, and then worry about redrawing all of the ovals.
Related
I intend to make a Py code which creates a tkinter dot that turns on a key press and deletes on a key press of couple keys.
The dot already is functional but i need it switch on and off on certain keypresses/mouse clicks which means i need an outside tkinter.mainloop() Update function.
The Update function with a while in it to constantly check if conditions to turn it off/on are present. But the Tkinter widget Somehow gets applied to the screen Only when the function nds. Like widget could be created but it will only take effect when function ends. And i need to turn it off/on dynamically.
I have tried to use a tkinter.after() with additional one at the end of called function only to find out an error of Recursion depth. What i expected to happen was that the function would be called over and over again, instead it runs that function like a while loop. I also have tried Asyncio.run() but it would result not making it visible till the function ends at least once. And I need to change it dynamically.
from tkinter import *
from tkinter import Canvas
from winsound import Beep
from time import sleep
import asyncio
import keyboard
import mouse
root = Tk()
width = root.winfo_screenwidth()
height = root.winfo_screenheight()
class tk_Dot():
def __init__(self,x=-1,y=-1,radius=4,color="red"):
self.x = x
if x == -1:
self.x = width/2-radius//2
print(self.x)
self.y = y
if y == -1:
self.y = height/2+radius//2
print(self.y)
self.radius=radius
self.color = color
self.lines = []
self.count = 1
def line(self,i):
return canvas.create_line(self.x, self.y-i, self.x+self.radius, self.y-i, fill=self.color)
def create(self):
self.lines = []
for i in range(0,self.radius):
self.lines.append(self.line(i))
def delete(self):
for i in range(0,self.radius):
canvas.delete(self.lines[i])
canvas.dtag(self.lines[i])
opacity_of_tk_window = 1 # From 0 to 1 0 meaning completely transparent 1 meaning everything created in canvas will give the color it was given
root.attributes('-alpha',opacity_of_tk_window)
# Invisible Tkinter window label
root.overrideredirect(True)
# Makes Transparent background
transparent_color = '#f0f0f0'
root.wm_attributes('-transparent', transparent_color)
canvas = Canvas()
# Rectangle filled with color that is specified above as the transparent color so practically making transparent background
canvas.create_rectangle(0, 0, width, height, fill=transparent_color)
canvas.pack(fill=BOTH, expand=1)
radius = 2
radius = 1+radius\*2
# Create a dot class
game_dot = tk_Dot(width/2-radius//2+1,height/2+1+radius//2,radius,"Red")
# Create a Dot at the middle of the calorant crosshair
# game_dot.create()
# Delete the dot
# game_dot.delete()
def Update():
game_dot.create()
print("Dot should be visible by now")
print("Is it?")
sleep(5) #sec
print("Oh yeah after the function ends.") # the problem
def Delete():
game_dot.delete()
root.geometry('%dx%d+%d+%d' % (width, height, -2,-2))
# Tkinter window always on top
root.attributes('-topmost',True)
root.after(1000,Update())
root.mainloop()
root = tk.Tk()
text = tk.Text(root, width = 40, height = 1, wrap = 'word')
text.pack(pady = 50)
after_id = None
# Now i want to increase the height after wraping
def update():
line_length = text.cget('width')
lines = int(len(text.get(1.0, 'end'))/line_length) + 1 # This is to get the current lines.
text.config(height = lines) # this will update the height of the text widget.
after_id = text.after(600, update)
update()
root.mainloop()
Hi I'm making a text widget and I want to update it when some input the passed else keep it idle, Right now I'm using this code. but I don't know how to keep it idle when no input is passed or no button is pressed.
I know there is a better way to do this operation but didn't found yet. Please Help!!!
Hi after reading some documentation and article i got a solution of my problem. In this we can use KeyPress event and we can bind over update method with this event.
Here is the code....
import tkinter as tk
root = tk.Tk()
text = tk.Text(root, width = 40, height = 1, wrap = 'word')
text.pack(pady = 50)
# first of all we need to get the width of the Text box, width are equl to number of char.
line_length = text.cget('width')
Init_line = 1 # to compare the lines.
# Now we want to increase the height after wraping of text in the text box
# For that we will use event handlers, we will use KeyPress event
# whenever the key is pressed then the update will be called
def Update_TextHeight(event):
# Now in this we need to get the current number of char in the text box
# for the we will use .get() method.
text_length = len(text.get(1.0, tk.END))
# Now after this we need to get the total number of lines int the textbox
bline = int(text_length/line_length) + 1
# bline will be current lines in the text box
# text_length is the total number of char in the box
# Since we have line_length number of char in one line so by doing
# text_length//line_length we will get the totol line of numbers.
# 1 is added since initially it has one line in text box
# Now we need to update the length
if event.char != 'Return':
global Init_line
if Init_line +1 == bline:
text.config(height = bline)
text.update()
Init_line += 1
# Nowe we will bind the KeyPress event with our update method.
text.bind("<KeyPress>",Update_TextHeight)
root.mainloop()
Thanks in advance for your time. I am writing a chess program using the tkinter module. I have put the board interface on the left side of the screen, while on the right side I would like to have the algebraic notation (e.g. 1. e4 e5 2. Nf3 Nc6) printed and updating as the players make their moves.
Since the text is updating, I tried to create a Label widget and a StringVar() object. But it is having some trouble : Basically, there is no right side to the screen, and the with each move the window flickers violently before showing the move in algebraic notation in the center of the right side of the window. Then, for each move, the same spasm occurs, the window expands a tiny bit, and eventually the string runs off the screen.
Ideally, every two moves, there would be a new line, but for now I would be content to just have the text 'wrap' once it hits the end of the frame.
I tried to create a Canvas widget and use the create_text() method to do it, but it was a bit unwieldy (though that seems to have been the best so far).
import tkinter as tk
master = tk.Tk()
class Chessboard(tk.Frame):
def __init__(self):
tk.Frame.__init__(self)
self.pack()
self.master.title('Chess')
self.mainframe = tk.Frame(master, width = 1600, height = 800)
self.mainframe.pack(side = 'top', anchor = 'n')
self.move_data = {'x': 0, 'y': 0, 'piece': None, 'to': None, 'from': None, 'last': None, 'color':'w', 'enem_color':'b'}
self.draw_board(master)
self.counter=2
self.en_passant = []
self.pgn_frame = tk.Canvas(self.mainframe, width = 500, height = 640, bd = 5, relief='ridge', cursor = 'trek')
self.pgn_frame.pack(side='right')
self.pgn_txt = ''
self.pgn_str = tk.StringVar()
self.pgn_label = tk.Label(self.pgn_frame, textvariable = self.pgn_str)
self.pgn_label.pack(fill='both', expand = True)
def draw_board(self, master):
self.board = tk.Canvas(self.mainframe, width = 680, height = 680, borderwidth = 20)
self.board.pack(fill='both', expand=True, side='left')
for r in range(7, -1, -1):
for c in range(8):
if c&1 ^ r&1:
fill = 'sienna2'
else:
fill = 'wheat1'
x0, y0 = c*80+24, r*80+24
x1, y1 = c*80+104, r*80+104
coords = (x0, y0, x1, y1)
center = ((x0+x1)/2, (y0+y1)/2)
sq = files[c] + ranks[-r-1]
self.board.create_rectangle(coords, fill=fill, width = 0, tags = ('square', sq))
Chessboard().mainloop()
I will note that the draw_board() function creates a canvas within the self.mainframe Frame and places it to the 'left'. Again, thank you for your time !
After searching the annals of SO, I stumbled across an ancient piece of wisdom from this question's first commenter :
"tkinter widgets are designed to shrink (or expand) to exactly fit their children." -Label within a Frame (Tkinter)
This little tidbit has put me on the right track to solving my problem : previously, I was trying to pack a Label widget into a Frame or Canvas widget, with the logic that the child should adapt to fit its parent. But, thanks to Mr. Oakley's insight, I have simplified the code to evict the superfluous widget and simply design my Label widget to meet the needs of the program. So, reformatted, it looks like
import tkinter as tk
master = tk.Tk()
class Chessboard(tk.Frame):
def __init__(self):
tk.Frame.__init__(self)
self.pack()
self.master.title('Chess')
self.mainframe = tk.Frame(master, width = 1600, height = 800)
self.mainframe.pack(side = 'top', anchor = 'n')
self.move_data = {'x': 0, 'y': 0, 'piece': None, 'to': None, 'from': None, 'last': None, 'color':'w', 'enem_color':'b'}
self.draw_board(master)
self.counter=2
self.en_passant = []
self.pgn_txt = ''
self.pgn_str = tk.StringVar()
self.pgn_frame = tk.Label(self.mainframe, width = 500, height = 640, bd = 5, relief='ridge', cursor = 'trek', textvariable = self.pgn_str)
self.pgn_frame.pack(side='right')
def draw_board(self, master):
self.board = tk.Canvas(self.mainframe, width = 680, height = 680, borderwidth = 20)
self.board.pack(fill='both', expand=True, side='left')
for r in range(7, -1, -1):
for c in range(8):
if c&1 ^ r&1:
fill = 'sienna2'
else:
fill = 'wheat1'
x0, y0 = c*80+24, r*80+24
x1, y1 = c*80+104, r*80+104
coords = (x0, y0, x1, y1)
center = ((x0+x1)/2, (y0+y1)/2)
self.board.create_rectangle(coords, fill=fill, width = 0, tags = ('square'))
Chessboard().mainloop()
It ain't pretty at the moment, but soon, it will be. In the actual program, the text updates without the aforementioned spasms, though will need to work a little on the formatting.
I'm trying to figure out how the tkinter control flow works.
I want to display a rectangle and to make it blink three times. I wrote this code, but it doesn't work. I guess it's because blink is executed before mainloop, and it doesn't actually draw anything. If so, how can I swap the control flow between blink and mainloop to make it work?
My code:
from tkinter import *
from time import *
def blink(rectangle, canvas):
for i in range(3):
canvas.itemconfigure(rectangle, fill = "red")
sleep(1)
canvas.itemconfigure(rectangle, fill = "white")
sleep(1)
root = Tk()
fr = Frame(root)
fr.pack()
canv = Canvas(fr, height = 100, width = 100)
canv.pack()
rect = canv.create_rectangle(25, 25, 75, 75, fill = "white")
blink(rect, canv)
root.mainloop()
Event-driven programming requires a different mindset from procedural code. Your application is running in an infinite loop, pulling events off of a queue and processing them. To do animation, all you need to do is place items on that queue at an appropriate time.
Tkinter widgets have a method named after which lets you schedule functions to run after a certain period of time. The first step is to write a function that does one "frame" of your animation. In your case, you're defining animation as switching between two colors. A function that checks the current color, then switches to the other color is all you need:
def blink(rect, canvas):
current_color = canvas.itemcget(rect, "fill")
new_color = "red" if current_color == "white" else "white"
canvas.itemconfigure(rect, fill=new_color)
Now, we just need to have that function run three times at one second intervals:
root.after(1000, blink, rect, canv)
root.after(2000, blink, rect, canv)
root.after(3000, blink, rect, canv)
When you start your main loop, after one second the color will change, after another second it will change again, and after a third second it will change again.
That works for your very specific need, but that's not a very good general solution. A more general solution is to call blink once, and then have blink call itself again after some time period. blink then must be responsible to know when to stop blinking. You can set a flag or counter of some sort to keep track of how many times you've blinked. For example:
def blink(rect, canvas):
...
# call this function again in a second to
# blink forever. If you don't want to blink
# forever, use some sort of flag or computation
# to decide whether to call blink again
canvas.after(1000, blink, rect, canvas)
As a final bit of advice, I recommend that you define your program as a class, then create an instance of that class. This makes it so that you don't need global functions, and you don't need to pass around so many arguments. It doesn't really matter for a 20 line program, but it starts to matter when you want to write something substantial.
For example:
from tkinter import *
class MyApp(Tk):
def __init__(self):
Tk.__init__(self)
fr = Frame(self)
fr.pack()
self.canvas = Canvas(fr, height = 100, width = 100)
self.canvas.pack()
self.rect = self.canvas.create_rectangle(25, 25, 75, 75, fill = "white")
self.do_blink = False
start_button = Button(self, text="start blinking",
command=self.start_blinking)
stop_button = Button(self, text="stop blinking",
command=self.stop_blinking)
start_button.pack()
stop_button.pack()
def start_blinking(self):
self.do_blink = True
self.blink()
def stop_blinking(self):
self.do_blink = False
def blink(self):
if self.do_blink:
current_color = self.canvas.itemcget(self.rect, "fill")
new_color = "red" if current_color == "white" else "white"
self.canvas.itemconfigure(self.rect, fill=new_color)
self.after(1000, self.blink)
if __name__ == "__main__":
root = MyApp()
root.mainloop()
Each widget has an 'after' function - that is to say it can call a another function after a specified time period - So, what you would want to do is call:
root.after( 1000, blink )
If you want it to be a repeating call, just call 'after' again inside your blink function. The only problem you will have is passing arguments to blink - maybe look at using lamda inside of 'after' for that.
I'm trying to figure out how the tkinter control flow works.
I want to display a rectangle and to make it blink three times. I wrote this code, but it doesn't work. I guess it's because blink is executed before mainloop, and it doesn't actually draw anything. If so, how can I swap the control flow between blink and mainloop to make it work?
My code:
from tkinter import *
from time import *
def blink(rectangle, canvas):
for i in range(3):
canvas.itemconfigure(rectangle, fill = "red")
sleep(1)
canvas.itemconfigure(rectangle, fill = "white")
sleep(1)
root = Tk()
fr = Frame(root)
fr.pack()
canv = Canvas(fr, height = 100, width = 100)
canv.pack()
rect = canv.create_rectangle(25, 25, 75, 75, fill = "white")
blink(rect, canv)
root.mainloop()
Event-driven programming requires a different mindset from procedural code. Your application is running in an infinite loop, pulling events off of a queue and processing them. To do animation, all you need to do is place items on that queue at an appropriate time.
Tkinter widgets have a method named after which lets you schedule functions to run after a certain period of time. The first step is to write a function that does one "frame" of your animation. In your case, you're defining animation as switching between two colors. A function that checks the current color, then switches to the other color is all you need:
def blink(rect, canvas):
current_color = canvas.itemcget(rect, "fill")
new_color = "red" if current_color == "white" else "white"
canvas.itemconfigure(rect, fill=new_color)
Now, we just need to have that function run three times at one second intervals:
root.after(1000, blink, rect, canv)
root.after(2000, blink, rect, canv)
root.after(3000, blink, rect, canv)
When you start your main loop, after one second the color will change, after another second it will change again, and after a third second it will change again.
That works for your very specific need, but that's not a very good general solution. A more general solution is to call blink once, and then have blink call itself again after some time period. blink then must be responsible to know when to stop blinking. You can set a flag or counter of some sort to keep track of how many times you've blinked. For example:
def blink(rect, canvas):
...
# call this function again in a second to
# blink forever. If you don't want to blink
# forever, use some sort of flag or computation
# to decide whether to call blink again
canvas.after(1000, blink, rect, canvas)
As a final bit of advice, I recommend that you define your program as a class, then create an instance of that class. This makes it so that you don't need global functions, and you don't need to pass around so many arguments. It doesn't really matter for a 20 line program, but it starts to matter when you want to write something substantial.
For example:
from tkinter import *
class MyApp(Tk):
def __init__(self):
Tk.__init__(self)
fr = Frame(self)
fr.pack()
self.canvas = Canvas(fr, height = 100, width = 100)
self.canvas.pack()
self.rect = self.canvas.create_rectangle(25, 25, 75, 75, fill = "white")
self.do_blink = False
start_button = Button(self, text="start blinking",
command=self.start_blinking)
stop_button = Button(self, text="stop blinking",
command=self.stop_blinking)
start_button.pack()
stop_button.pack()
def start_blinking(self):
self.do_blink = True
self.blink()
def stop_blinking(self):
self.do_blink = False
def blink(self):
if self.do_blink:
current_color = self.canvas.itemcget(self.rect, "fill")
new_color = "red" if current_color == "white" else "white"
self.canvas.itemconfigure(self.rect, fill=new_color)
self.after(1000, self.blink)
if __name__ == "__main__":
root = MyApp()
root.mainloop()
Each widget has an 'after' function - that is to say it can call a another function after a specified time period - So, what you would want to do is call:
root.after( 1000, blink )
If you want it to be a repeating call, just call 'after' again inside your blink function. The only problem you will have is passing arguments to blink - maybe look at using lamda inside of 'after' for that.