Detect simultaneous left and right clicks using tkinter - python

I want to write a simple program to detect simultaneous left and right mouse clicks using tkinter. I referred to the question Detect simultaneous left and right clicks in tkinter gui and made some changes, but came across some problems. Here's the code:
from tkinter import *
def call_back_left(event, v, h):
global left, right
if right:
right = False
else:
left = True
root.after(1000, left_judge_right(v, h))
def left_judge_right(v, h):
global left, right
if left:
print(f'Button on row{v}, column{h}\t\tleft click')
left = False
else:
print(f'Button on row{v}, column{h}\t\tboth clicks')
def call_back_right(event, v, h):
global left, right
if left:
left = False
else:
right = True
root.after(1000, right_judge_left, v, h)
def right_judge_left(v, h):
global left, right
if right:
print(f'Button on row{v}, column{h}\t\tright click')
right = False
else:
pass
left = False
right = False
root = Tk()
vertical, horizontal = 4, 4
map_area = Frame(root, pady=10)
map_area.pack(side=TOP)
for v in range(vertical):
for h in range(horizontal):
button = Button(map_area, text=' ', width=5, height=2)
button.grid(row=v, column=h)
button.bind("<Button-1>", lambda event, i=v, j=h: call_back_left(event, i, j))
button.bind("<Button-3>", lambda event, i=v, j=h: call_back_right(event, i, j))
root.mainloop()
The detection for both clicks sometimes worked but sometimes didn't, so I made the first parameter to the root.after() function to be 1000 microseconds (as shown in the codes).
As I understand, if I run the code above and do left click firstly then right click within 1 second, the function running sequence should be:
call_back_left --> call_back_right --> wait about 1 sec --> left_judge_right --> right_judge_left
And the print outcome should be:
"Button on rowX, columnY both clicks"
However, the result is:
"Button on rowX, columnY left click"
"Button on rowX, columnY right click"
It seemed like the function running sequence is:
call_back_left --> left_judge_right --> wait about 1 sec --> call_back_right --> right_judge_left
Then I tried doing right click firstly and then left click within 1 sec, and it worked fine without any problems.
Please tell me why this problem occurs and how to fix it.

Related

Tkinter: Image change from blank tile to mark (X/O) is not permanent

I'm making a scalable tic-tac-toe program using Tkinter. When I press an empty square button, the image changes, as it should. When I press a different button, the image from the button first pressed disappears completely, when I'd expect it to stay, and the only button with the X/O image is the latest button pressed. Any help in fixing this? I'm a beginner.
If you have any tips on how to make a scalable win check as well (with scalable I mean the user can input board size, from 2x2 to as large as fits the screen), that would be extremely helpful.
Here are the images I used, if they're useful (link works for 24h, they're just basic 125x125 images for a box, X and O) https://picresize.com/b5deea04656747
from tkinter import *
class XOGame:
def main_game(self):
self.__game_window = Tk()
self.__grid_size = 3 # User inputted in a different part of the code
self.__game_window.title("Tic Tac Toe (" + str(self.__grid_size) + "x"
+ str(self.__grid_size) + ")")
self.build_board(self.__game_window)
self.__game_window.mainloop()
def build_board(self, window):
self.__size = self.__grid_size ** 2
self.__empty_square = PhotoImage(master=window,
file="rsz_empty.gif")
self.__squares = [None] * self.__size
# Building the buttons and gridding them
for i in range(self.__size):
self.__squares[i] = (Button(window, image=self.__empty_square))
row = 0
column = 0
number = 1
for j in self.__squares:
j.grid(row=row, column=column)
j.config(command=lambda index=self.__squares.index(j):
self.change_mark(index))
column += 1
if number % 3 == 0:
row += 1
column = 0
number += 1
# This is the part where the picture changing happens.
# I have yet to implement the turn system, thus self.__x being the only image being configured.
# This is the part with the issue.
def change_mark(self, i):
self.__x = PhotoImage(master=self.__game_window,
file="rsz_cross.gif")
self.__o = PhotoImage(master=self.__game_window,
file="rsz_nought.gif")
self.__squares[i].configure(image=self.__x, state=DISABLED)
def start(self):
# Function starts the first window of the game.
self.main_game()
def main():
ui = XOGame()
ui.start()
main()
Question: the image from the button first pressed disappears completely
You overwrite self.__x = PhotoImage(... at every click, which results in garbage collection of the previous image and tkinter looses the image reference.
Create the images only once and reuse it like self.empty_square.
Simplify to the following:
def build_board(self, window):
self.empty_square = PhotoImage(master=window, file="empty.gif")
self.x_square = PhotoImage(master=window, file="cross.gif")
self.squares = []
for row in range(3):
for column in range(3):
self.squares.append(Button(window, image=self.empty_square))
self.squares[-1].grid(row=row, column=column)
self.squares[-1].bind("<Button-1>", self.change_mark)
def change_mark(self, event):
w = event.widget
w.configure(image=self.x_square, state=DISABLED)

I'm trying to make a simple line drawing program with tkinter but it won't work

I'm trying to make this really simple program, all it does is store the current x/y pos of the mouse on the canvas and then use them to draw a line when you click for the second time. I've already bound it and I'm not getting any errors, it seems like it's not even being activated. Any help is greatly appreciated
from tkinter import *
main = Tk()
c = Canvas(main, width=600, height=600)
c.pack()
#For colored lines
presses = 0
def click(event):
if presses == 0:
initX = int(c.canvasx(event.x))
initY = int(c.canvasy(event.y))
presses == 1
elif presses == 1:
c.create_line(initX, initY,
int(c.canvasx(event.x)),
int(c.canvasy(event.y)))
presses == 0
c.bind("<Button-1>", click)
mainloop()
How does something like this work for you?
from tkinter import *
main = Tk()
c = Canvas(main, width=600, height=600)
c.pack()
line = []
def click(event):
global line
X = int(c.canvasx(event.x))
Y = int(c.canvasy(event.y))
line.append((X,Y))
if len(line) > 1:
startX,startY = line[-2]
c.create_line(startX, startY, X, Y)
c.bind("<Button-1>", click)
mainloop()
I've changed around your code a bit to store a list of the X,Y coordinates that have been clicked on. If more than 1 point on the screen has been clicked, it will draw a line between the current point clicked on and the last point clicked on.
Reason your code wasn't working was that initX and initY are forgotten in between calls on the the click function. Adding them to a list solves this.

Python tkinter - closing of new window

I made this program, I recommend run it first. The main idea is popping window, it should pop after clicking on text info. This works correctly, than, an user needs to close this window, so I made red closing button (rectangle with lines) in top right corner. After clicking on this button, new window should disappear, but first window with ovals should stay.
I need function for closing this window, I was trying to find solution a long time, but I am not able to. Any idea please?
import tkinter
class Desktop:
def __init__(self):
self.canvas = tkinter.Canvas()
self.canvas.pack()
x, y = 25,25
for i in range(1,61):
self.canvas.create_oval(x-10, y-10, x+10, y+10, fill='blue')
self.canvas.create_text(x, y, text=i)
x += 30
if i%10==0:
x = 25
y += 40
self.canvas.create_text(340, 150, text='info', font='arial 17', tags='info')
self.canvas.tag_bind('info', '<Button-1>', self.window)
def window(self, event):
self.canvas.create_rectangle(40,40,310,220,fill='white')
x, y = 75,100
for i in range(1,9):
self.canvas.create_rectangle(x-30,y-30,x+30,y+30)
self.canvas.create_text(x,y,text='number ' + str(i), font='arial 9')
x += 65
if i == 4:
x = 75
y += 70
self.rec = self.canvas.create_rectangle(310,40,290,60, fill="red", tags="cancel")
self.line = self.canvas.create_line(310,40,290,60,310,60,290,40, tags="cancel")
self.canvas.tag_bind('cancel', '<Button-1>', self.close)
def close(self,event):
#?
print('should close this part of screen')
d = Desktop()
What I use in my code is:
def quit(self):
root.destroy()
self.quit()
This might be a really long winded way to do it but it works for me every time.
EDIT: Just thinking about it now, depending on the way you open a new frame,root.destroy would be the way I would do it.

canvas.itemconfig results in infinite event-loop

I try to draw a little 3x3 board with a marker on each cell.
This marker should only show up, when the cell is touched with the mouse.
This works once, twice, sometimes 3 three times - but then the event-loop 'fires' infinitely (always the same event)...
import tkinter as tk
cellsize = 50
class Board(tk.Canvas):
def __init__(self):
tk.Canvas.__init__(self)
for row in range(3):
for column in range(3):
ulx, uly = column*cellsize, row*cellsize
lrx, lry = ulx+cellsize, uly+cellsize
_cell = self.create_rectangle(ulx, uly, lrx, lry,
fill='green')
_right = self.create_rectangle(ulx+39, uly+20, lrx-1, lry-20,
fill='red',
state='hidden')
self.tag_bind(_cell, '<Enter>',
lambda e, r=_right: self.show_pos('on', r))
self.tag_bind(_cell, '<Leave>',
lambda e, r=_right: self.show_pos('off', r))
def show_pos(self, onoff, right):
print('{} {}'.format(onoff, right))
if onoff == 'on':
self.itemconfig(right, state='normal')
elif onoff == 'off':
self.itemconfig(right, state='hidden')
root = tk.Tk()
Board().grid()
root.mainloop()
Perhaps this is sticked to the self.itemconfigure statement, because doing other things (e.g. updating a status line) work as expected.
Is there a solution for this?
Thx in advance
Marvin
Addition:
To be more precise: It seems to stick with the 'state=..."
Changing the itemconfig to 'fill=...' in 'show_pos' works as expected.
So the title should be
'canvas.itemconfig(state='...' results in infinite event-loop'
Using the mouse position approach you could use:
class BoardX(tk.Canvas):
__cells=None
__indicators=None
def __init__(self):
tk.Canvas.__init__(self)
self.__cells=[]
self.__indicators=[]
self.bind('&ltMotion&gt', self.show_pos)
for row in range(3):
for column in range(3):
ulx, uly = column*cellsize, row*cellsize
lrx, lry = ulx+cellsize, uly+cellsize
self.__cells.append(self.create_rectangle(ulx, uly, lrx, lry,
fill='green', tags="cell"))
self.__indicators.append(self.create_rectangle(ulx+39, uly+20, lrx-1, lry-20,
fill='red',
state='hidden', tags="indicator"))
def show_pos(self, event):
""" Get closest widget or widget that we are above,
tagged "cell" and indicate it
"""
# the loop is needed for not to run into value errors
halo=0
widget = self.find_closest(event.x, event.y, halo=halo)
# edit - avoid loop!
if not widget[0] in self.find_withtag("cell"):
return
index = self.__cells.index(widget[0])
for i in range(len(self.__indicators)):
state='hidden'
if i == index:
state='normal'
self.itemconfig(self.__indicators[i], state=state)
This does not fire leave events you bound to in your approach and should therefore solve your problem.
If you do not want to take this approach for whatever reason you could only bind to enter and hide every other indicator using the find_withtag("indicator")-Approach
Edit
Code sample is corrected to avoid the loop.

Open Tkinter Window so it sits on start menu bar

I would like to have a Tkinter window open at the bottom right of the screen or where ever the start bar is. Much like when you click on the battery icon on your laptp and the box pops up. My code currently hides it behind the start menu bar. I would essentially like it at the bottom right but sitting on top of the start menu bar. Also, not sure how to account for things if the start menu is not at the bottom.
My Code:
from Tkinter import *
def bottom_right(w=300, h=200):
# get screen width and height
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
# calculate position x, y
x = (screen_width - w)
y = (screen_height-h)
root.geometry('%dx%d+%d+%d' % (w, h, x, y))
root = Tk()
bottom_right(500, 300)
root.mainloop()
You can use the win32api's .GetMonitorInfo() to find the 'working area' of the monitor, which is the area of the monitor without the taskbar. Then, seeing where the taskbar is, you can place the window in a corner of the working area. See this example:
import win32api
import Tkinter as tk
for monitor in win32api.EnumDisplayMonitors():
monitor_info = win32api.GetMonitorInfo(monitor[0])
if monitor_info['Flags'] == 1:
break
work_area = monitor_info['Work']
total_area = monitor_info['Monitor']
width = 300
height = 200
side = [i for i in range(4) if work_area[i]!=total_area[i]]
# Left
if side ==[0]:
x = str(work_area[0])
y = str(work_area[3]-height)
# Top
elif side == [1]:
x = str(work_area[2]-width)
y = str(work_area[1])
# Right
elif side == [2]:
x = str(work_area[2]-width)
y = str(work_area[3]-height)
# Bottom
elif side == [3]:
x = str(work_area[2]-width)
y = str(work_area[3]-height)
else:
x = str(work_area[2]-width)
y = str(work_area[3]-height)
geom = '{}x{}+{}+{}'.format(width, height, x, y)
root = tk.Tk()
root.configure(background='red')
root.geometry(geom)
root.overrideredirect(True)
root.mainloop()
Note that I've used overrideredirect to get rid of the frame of the window, since this messes with the placing a bit.
What you are calling the 'start menu bar' is usually called the taskbar, at least on Windows. root.iconify() minimizes the root window to the taskbar, wherever it happens to be, just as when one clicks [_] in the upper right of the window. Clicking on the icon de_iconifies it, just as with any other app.
root = tk.Tk()
root.iconify()
<build gui>
root.deiconify()
root.mainloop()
is a common pattern in a polished app when the part takes long enough to cause potentially visible construction activity. I believe putting the gui in a frame and packing the frame as the last step has the same effect (of hiding construction).

Categories

Resources