Bind a tkinter event generated in a class outside of class - python

I am working on a program that would move a ball around depending on joystick inputs. The GUI I am using is Tkinter.
Recently, I found a demo of a method that used Tkinter and was compatible with Pygame (Prior to this, I thought that Pygame was incompatible with Tkinter. I was psyched when I could found this.)
I defined the virtual event "LT" in the class Find_Joystick. ("LT" refers to the "LT" button on a gamepad.) When pressed, it's supposed to move the ball to the left. However, when I try to bind the event to the actual movement function, the function doesn't appear to receive the input.
Here is my code (somewhat simplified):
from tkinter import *
import pygame
class Find_Joystick:
def __init__(self, root):
self.root = root
## initialize pygame and joystick
pygame.init()
if(pygame.joystick.get_count() < 1):
# no joysticks found
print("Please connect a joystick.\n")
self.quit()
else:
# create a new joystick object from
# ---the first joystick in the list of joysticks
joystick = pygame.joystick.Joystick(0)
# tell pygame to record joystick events
joystick.init()
## start looking for events
self.root.after(0, self.find_events)
def find_events(self):
## check everything in the queue of pygame events
events = pygame.event.get()
joystick = pygame.joystick.Joystick(0)
LT = joystick.get_button(6)
for event in events:
# event type for pressing any of the joystick buttons down
if event.type == pygame.JOYBUTTONDOWN:
self.root.event_generate('<<JoyFoo>>')
if LT == 1:
self.root.event_generate('<<LT>>')
if event.type == pygame.JOYAXISMOTION:
self.root.event_generate('<<JoyMove>>')
#Move left
if axisX < 0:
self.root.event_generate('<<Left>>')
#Move right
if axisX > 0:
self.root.event_generate('<<Right>>')
#Move upwards
if axisY < -0.008:
self.root.event_generate('<<Up>>')
#Move downwards
if axisY > -0.008:
self.root.event_generate('<<Down>>')
## return to check for more events in a moment
self.root.after(20, self.find_events)
def main():
## Tkinter initialization
root = Tk()
app = Find_Joystick(root)
frame = Canvas(root, width=500, height = 250)
frame.pack()
ball = frame.create_oval([245, 120], [255, 130], outline = 'red', fill = 'red')
def callback(event):
frame.focus_set()
def moveLeftFunc(event):
frame.move(ball, -3, 0)
print('Move left')
frame.bind('<Button-1>', callback)
frame.bind('<<LT>>', moveLeftFunc)
root.mainloop()
if __name__ == "__main__":
main()
Here is the demo I was referring to earlier (the class is the main part I am using).

The solution might be in the event binding.
You bind the events to the frame Canvas, and then events are generated by the root widget (self.root.event_generate('<<LT>>'))
It could work in the other way, the event are propagated from the widget to the parent, but not from the root to the children widgets.
Can you try with :
root.bind('<Button-1>', callback)
root.bind('<<LT>>', moveLeftFunc)
(you should also remove the line self.root.event_generate('<<JoyFoo>>'))
EDIT :
Here is a reproducible case without the joystick stuff:
(the child button propagates the event to close the window, but the event triggered by the root timer doesn't hit the frame binding to close)
from tkinter import *
import sys
def quit(*arg):
sys.exit()
class ChildFrame(Frame):
def __init__(self, root):
Frame.__init__(self, root)
self.grid()
button = Button(self, text="Propagate Quit", command=self.child_event)
button.pack()
def child_event(self):
self.event_generate('<<Propagated>>') # triggers the root binding
root = Tk()
frame = ChildFrame(root)
def root_event(*arg):
global root
root.event_generate('<<NotPropagated>>') # doesn't trigger child binding
#root.event_generate('<<Propagated>>') # triggers the root binding
root.bind('<<Propagated>>', quit)
frame.bind('<<NotPropagated>>', quit)
root.after(2000, root_event) # timer to create event from root to child
root.mainloop()

Related

Tkinter scale's command being triggered when grid is called on button

I am attempting to create a layout using Tkinter for Python3 that involves several buttons and scales. The buttons work fine, but the command that I give to the scale widget is called when I call grid on the scale. Why is this happening and what can I do to stop it?
Here is a simplified version of my code:
import tkinter
import time
WINDOW_HEIGHT = 150
WINDOW_WIDTH = 340
class Player(object):
def __init__(self):
self.window = tkinter.Tk()
self.window.geometry(str(WINDOW_WIDTH) + 'x' + str(WINDOW_HEIGHT))
self.current_time = tkinter.DoubleVar()
self.progress_bar = tkinter.Scale(self.window,
variable = self.current_time,
command = self.set_current_time,
orient = tkinter.HORIZONTAL,
showvalue = 0, resolution=.001)
self.progress_bar.grid(row=1, column=10)
def set_current_time(self, time):
print('setting current time')
print(time)
def update(self):
self.window.update_idletasks()
self.window.update()
def main():
media_player = Player()
while True:
media_player.update()
time.sleep(.1)
if __name__ == "__main__":
main()
The set_current_time function should only be called when the slider is actually clicked and moved, however as soon as grid is executed, set_current_time is called with a time value of 0. How can I place the slider without executing the command? After placement the slider works as expected, but I would like to avoid the initial calling of the set_current_time function.

Move objects automatically

The objective of my exercise is to create a game in which a falling ball needs to be caught by a bar at the bottom of the screen. Below code does not make the ball to fall automatically. I referred to below posts, but could not find a solution:
Tkinter bind to arc, Automatically Moving Shape? Python 3.5 Tkinter
import tkinter as tk
class Game(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.can = tk.Canvas(self, width=400, height=400)
self.can.pack(fill="both", expand=True)
self.ball = self.can.create_oval(40, 40, 60, 60, fill="red", tag="ball")
self.player = self.can.create_rectangle(300,345,350,360, fill="red")
self.bind("<Key>", self.move_player)
self.can.tag_bind("ball",self.move_b)
self.mainloop()
def move_b(self,event=None):
self.can.move(self.ball, 1, 0)
print(self.ball)
# move again after 25ms (0.025s)
self.can.after(25, self.move_b)
def move_player(self, event):
key = event.keysym
if key == "Left":
self.can.move(self.player, -20, 0)
elif key == "Right":
self.can.move(self.player, 20, 0)
if __name__ == '__main__':
Game()
2nd positional argument to tag_bind is an event, whereas in your code it's passed as the actual callback, self.move_b. First, add an event:
self.can.tag_bind("ball", "<ButtonRelease-1>", self.move_b)
If you don't want it to have event, simply pass None:
self.can.tag_bind("ball", None, self.move_b)
or don't use tag_bind at all, and simply call:
self.move_b()
Whenever you want the animation to start.

Open executable file from graphics button without closing parent window

I've created a game "dice poker" using Zelle's graphing package, and have a button on the main screen which opens a text file. The text file opens when the button is clicked, but the main window closes. How can I keep the parent window open?
The button class is below:
from graphics import *
from tkinter import Button as tkButton
class Button():
"""A button is a labeled rectangle in a window.
It is activated or deactivated with the activate()
and deactivate() methods. The clicked(p) method
returns true if the button is active and p is inside it."""
def __init__(self, win, center, width, height, label):
""" Creates a rectangular button, eg:
qb = Button(myWin, centerPoint, width, height, 'Quit') """
w,h = width/2.0, height/2.0
x,y = center.getX(), center.getY()
self.xmax, self.xmin = x+w, x-w
self.ymax, self.ymin = y+h, y-h
p1 = Point(self.xmin, self.ymin)
p2 = Point(self.xmax, self.ymax)
self.rect = Rectangle(p1,p2)
self.rect.setFill('lightgray')
self.rect.draw(win)
self.label = Text(center, label)
self.label.draw(win)
self.deactivate()
def clicked(self, p):
"Returns true if button active and p is inside"
return (self.active and
self.xmin <= p.getX() <= self.xmax and
self.ymin <= p.getY() <= self.ymax)
def getLabel(self):
"Returns the label string of this button."
return self.label.getText()
def activate(self):
"Sets this button to 'active'."
self.label.setFill('black')
self.rect.setWidth(2)
self.active = True
def deactivate(self):
"Sets this button to 'inactive'."
self.label.setFill('darkgrey')
self.rect.setWidth(1)
self.active = False
How can I include a command argument that can open an executable in a fashion similar to this tkinter implementation:
import Tkinter as tk
def create_window():
window = tk.Toplevel(root)
root = tk.Tk()
b = tk.Button(root, text="Create new window", command=create_window)
b.pack()
root.mainloop()
Where the command can be subprocess.run(['open', '-t', 'poker_help.txt']) and still keep the original window open?
I have to make some assumptions since you didn't include top level code (e.g. you're on a Mac):
Zelle graphics, unlike tkinter and turtle, which is also built on tkinter, doesn't have an explicit win.mainloop() call to turn control over to the Tk event handler to idle awaiting events to happen. Instead, you have to patch one together yourself, otherwise once you get the mouse click that fires off your button, the program falls through the end of the file and the main window closes:
import subprocess
from graphics import *
from button import Button
win = GraphWin()
help_button = Button(win, Point(150, 150), 50, 50, "Help")
help_button.activate()
quit_button = Button(win, Point(50, 50), 50, 50, "Quit")
quit_button.activate()
while True:
point = win.getMouse()
if help_button.clicked(point):
subprocess.call(['open', '-t', 'poker_help.txt'])
elif quit_button.clicked(point):
win.close()
Where from button import Button brings in your button code above. Another thing to check is your window is actually closing, and not simply being obscured by the new window opened atop it.

I want to toggle a real pushbutton and display it on tkinter GUI

I want to toggle a pushbutton and show its changes on a label using tkinter.
If I press the button it shows "on" on the label and when I press again it shows "off" on the label
So I try these codes and If I'm trying the wrong code please help me write the correct using tkinter.
I have a problem in combining this code
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BOARD)
GPIO.setup(22,GPIO.IN,up_down=GPIO.PUD_UP)
while(1):
if GPIO.input(22)==1:
if bs == False :
x.set("on")
bs=True
sleep(0.5)
else:
x.set("off")
bs=False
sleep(0.5)
This works okay but I want to connect it to a GUI label to print on it on or off.
Here is the tkinter code
import tkinter.*
root = tk()
x = StringVar()
s=Label(root,textvariable=x)
s.grid(column=0,row=0)
root.mainloop()
When I try to combine it I make it like this
from Tkinter import *
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BOARD)
GPIO.setup(7,GPIO.IN)
b=False
def check_button1():
if GPIO.input(7)== 1:
if b == False :
labelText1.set("on")
print"on"
b=True
time.sleep(0.5)
else:
labelText1.set("off")
print"off"
b=False
time.sleep(0.5)
mamdouh.after(10,check_button1)
mamdouh = Tk()
labelText1 = StringVar()
x1 = Label(mamdouh,textvariable=labelText1)
x1.config(font=('Helvetica',25,'bold'))
x1.grid(row=0,column=0)
mamdouh.title("mamdouh")
mamdouh.geometry('1200x700')
mamdouh.after(10,check_button1)
mamdouh.mainloop()
but it didn't works it keeps blank every time I press the push button actually If it works well I will put 17 push button
I think that the problem is in placing this if statment on the right place and placing the b variable in it's right place and I think also there is a problem between this if statment and tkinter because I tried this code wich works perfect but it is not toggling the push button so I want to change this lets add this code here also :
from Tkinter import *
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
GPIO.setup(7,GPIO.IN)
def check_button1():
if(GPIO.input(7) == GPIO.LOW):
labelText1.set("on")
else:
labelText1.set("off")
mamdouh.after(10,check_button1)
mamdouh = Tk()
labelText1 = StringVar()
x1 = Label(mamdouh,textvariable=labelText1)
x1.config(font=('Helvetica',25,'bold'))
x1.grid(row=0,column=0)
mamdouh.title("mamdouh")
mamdouh.geometry('1200x700')
mamdouh.after(10,check_button1)
mamdouh.mainloop()
So how I can make this toggle push button on an Label?
Your problem is recognizing button down and button up events. Your OS mouse driver does this for your mouse buttons. If your GPIO module does not do this for you, you will have to detect these events by comparing the current state to the previous state. (I am ignoring here any possible need to 'de-bounce' the button.) You are sort of trying to do this with the time.sleep(.5) calls, but do not use time.sleep in gui code.
Your driver should be self-contained and independent of any tk widgets other than the root needed for .after. For multiple buttons, you will need your own GPIOButton class. Your code that works is a starting point. Tkinter allows you to tie a command to button-up events. Your class init should similarly take up and or down event commands (callbacks).
Here is something untested that might get you started.
class GPIOButton:
def __init__(self, master, buttons, command_down=None, command_up=None):
self.master = master
self.buttons = buttons
self.command_down = command_down
self.command_up = command_up
GPIO.setmode(GPIO.BOARD)
for button in buttons:
GPIO.setup(button, GPIO.IN)
self.state = [GPIO.HIGH] * len(buttons) # best initial value?
self.check_buttons() # or call this elsewhere
def check_buttons(self):
for i, button in enumerate(self.buttons):
oldstate = self.state[i]
newstate = GPIO.input(button)
if oldstate != newstate:
self.state[i] = newstate
command = (self.command_down if newstate==GPIO.LOW
else self.command_up)
command(button)
self.master.after(10, self.check_button)
Let me preface my answer with a disclaimer—I don't have a Raspberry Pi, so couldn't verify this works with the real thing. For testing I used a proxy class that simulates random button pressing. You may have to adjust the DELAY value depending on how fast the GPIO interface works.
However, I have put commented-out code in near the top showing what I think you would need to use based on of doing so in your code.
try:
import Tkinter as tk
import tkFont
except ImportError: # Python 3
import tkinter as tk
import tkinter.font as tkFont
#import RPi.GPIO as GPIO
#
#GPIO.setmode(GPIO.BOARD)
#
#class GPIOButton(object):
# """ Encapsulates GPIO button interface. """
# def __init__(self, pin):
# self.pin = pin
# self.status = 0
# GPIO.setup(pin, GPIO.IN)
#
# def update_status(self):
# self.status = GPIO.input(pin) == GPIO.LOW
### Proxy class since I don't have a Rasperry Pi. ###
import random
class GPIOButton(object):
def __init__(self, pin):
self.pin = pin
self.status = 0
def update_status(self):
if not random.randint(0, 99) % 20: # occassionally toggle status
self.status = not self.status
class App(tk.Frame):
STRIDE = 8
DELAY = 100 # delay in millsecs between button status updates
def __init__(self, gpio_buttons, master=None):
tk.Frame.__init__(self, master)
self.grid()
self.gpio_buttons = gpio_buttons
self.create_widgets()
self.after(self.DELAY, self.update_buttons, self.DELAY) # start updates
def create_widgets(self):
self.btn_font = tkFont.Font(family="Helvetica", size=12, weight='bold')
self.gui_buttons = []
for i, button in enumerate(self.gpio_buttons):
is_pressed = tk.BooleanVar()
is_pressed.set(False)
radiobutton = tk.Radiobutton(self,
text=format(i+1, '02d'),
font=self.btn_font,
value=True,
variable=is_pressed,
relief=tk.RIDGE)
row, col = divmod(i, self.STRIDE)
radiobutton.grid(column=col, row=row)
self.gui_buttons.append(is_pressed)
def update_buttons(self, delay):
for i, gpio_button in enumerate(self.gpio_buttons):
previous_status = gpio_button.status
gpio_button.update_status()
if gpio_button.status != previous_status:
self.gui_buttons[i].set(gpio_button.status)
self.after(delay, self.update_buttons, delay) # rinse and repeat
gpio_buttons = [GPIOButton(pin) for pin in range(16)]
app = App(gpio_buttons)
app.master.title('Rasberry Pi Buttons')
app.mainloop()
Here's what the simulation looks like running on my Windows computer:

Creating two button methods Python

I am creating a program in Python that creates a shape based on user input. I need to create two functions to create buttons using Zeller's graphic.py file. One button needs to say Quit and the second needs to say Process. Here is what i have so far but as you can see, they are not in defined functions:
#create Quit button
quitButton = Text(Point(70,73), "Quit")
quitButton.draw(w)
Rectangle(Point(45, 50), Point(95,97)).draw(w)
#create Process button
enterButton = Text(Point(145,73), "Process")
enterButton.draw(w)
Rectangle(Point(120, 48), Point(170,98)).draw(w)
Here is a description of the necessary methods
createButton(text, pt1button, pt2button, win) creates a rectangle with corner points pt1button and pt2button with centered text in window win
clickedButton(button, clickPt) returns true/false if clickPt is in button.
I tried to create the function and received the following error.
Here is my function:
def createButton(text, pt1button, pt2button, win):
button = Text(Point(pt1button, pt2button), text)
button.draw(win)
Here is where I called the function:
createButton("Process",145,73,win)
createButton("Quit",70,73,win)
Here is the error that was thrown:
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/project4FINAL.p‌​y", line 77, in <module> main()
File "/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/project4FINAL.p‌​y", line 27, in main buttonCreate("Process", 145,73, win)
NameError: global name 'win' is not defined
Any suggestions?
So from looking at the code it looks like you want to create a callback function for each button and then assign each to the canvas via the setMouseHandler of method of GraphWin.
So from the example given in the API:
from graphics import *
def example_callback():
print "I love unicorns!"
def main():
# win is a Canvas with a setMouseHandler method
win = GraphWin("My Circle", 100, 100)
c = Circle(Point(50,50), 10)
c.draw(win)
#Add a callback to the canvas
c.cavas.setMouseHandler(example_callback)
# or win.setMouseHandler(example_callback)
win.getMouse() # Pause to view result
win.close() # Close window when done
main()
Unless you have bounds checking in you callbacks (to see which shape on the canvas the mouse is inside), you should only have one canvas per drawn shape.
An example following the use of a createButton function:
def createButton(text, pt1button, pt2button, win):
button = Text(Point(pt1button, pt2button), text)
button.draw(win)
return button
def _callback(pt):
print "I love unicorns!"
print
print "Mouse clicked at x=%d, y=%d"%(pt.x,pt.y)
print
def test():
win = GraphWin()
win.setCoords(0,0,100,100)
quitButton = createButton("Quit",70,73,win)
Rectangle(Point(45, 50), Point(95,97)).draw(win)
win.setMouseHandler(_callback)
while True:
win.getMouse()
win.close()
Below is a complete example using a new Button object:
from graphics import *
class Button(object):
def __init__(self, text, text_pos, rect_pos, win, callback):
self.win = win
self.text = Text(text_pos, text)
self.text.draw(self.win)
# the limits of the button will be defined by the rectangle
self.coords = [rect_pos[0].x,rect_pos[0].y,rect_pos[1].x,rect_pos[1].y]
self.rect = Rectangle(*rect_pos)
self.rect.draw(self.win)
# set the buttons callback
self.callback = callback
def _is_inside(self,click):
limits = self.coords
return (limits[0] < click.x < limits[2]) and (limits[1] < click.y < limits[3])
class MyWindow(object):
def __init__(self,coords=(0,0,100,100)):
self.win = GraphWin()
self.win.setCoords(*coords)
# a list of all possible buttons
self.buttons = []
# register the master callback function
self.win.setMouseHandler(self._callback)
self._quit = False
#create a quit and confess button with a custom create method
self.create_button("Quit",(Point(10,10),Point(40,40)),Point(20,20),self.quit)
self.create_button("Confess",(Point(50,50),Point(80,80)),Point(60,60),self.confess)
def create_button(self,text,coords,text_coords,callback):
button = Button(text,text_coords,coords,self.win,callback)
# create a button and add it to our list of buttons
self.buttons.append(button)
def confess(self,point):
print
print "I love unicorns!"
print
def quit(self,point):
self._quit = True
self.win.close()
# This function is called for any click on the canvas
def _callback(self,point):
# Need to do a coordinate transform here to get the click in the coordinates of the button
x,y = self.win.trans.world(point.x,point.y)
point = Point(x,y)
print "Clicked at x=%d, y=%d"%(point.x,point.y)
# cycle through buttons and execute all for which the clicked point lies inside their rectangle
for button in self.buttons:
if button._is_inside(point):
button.callback(point)
# a loop to keep getting mouse input
def run(self):
while not self._quit:
try:
self.win.getMouse()
except GraphicsError:
break
if __name__ == "__main__":
x = MyWindow()
x.run()

Categories