Understanding tkinter tag_bind - python

I cannot figure out how to use the tag_bind method associated with a canvas. I created an oval and expected to be able to generate an event by clicking on it. When I do this, nothing happens. What is the correct use of tag_bind?
import tkinter as tk
class Window():
def __init__(self, master):
self.master = master
self.create_widgets()
def create_widgets(self):
self.c = tk.Canvas(self.master, bg='white', height=200, width=300)
self.c.pack()
b = tk.Button(self.master, text='Draw', command=self.draw)
b.pack()
def draw(self):
self.c.delete('all')
self.oval = self.c.create_oval([30,50], [130,80])
self.rect = self.c.create_rectangle([180,10], [280,80])
self.c.tag_bind(self.oval, '<Button-1>', self.oval_func)
def oval_func(self, event):
self.c.delete(self.rect)
self.c.create_text(150, 150, text='Hello, world!', anchor='w')
if __name__ == '__main__':
root = tk.Tk()
app = Window(root)
root.mainloop()

The code is working. However, when you bind to a canvas object, you have to click on part of the drawn object. Since you didn't fill the oval, that means you must click on its outline.
If you fill it with the same color as the background (or any other color) you can click anywhere in the oval.
self.oval = self.c.create_oval([30,50], [130,80], fill="white")

Related

How to animate objects of a class from within another class?

I'm using Python and tkinter to create a GUI. I've created two classes, App and drawBall. The App class inherits from Tk.Frame. I'm having trouble creating a drawBall object from within App.
I'd appreciate any other feedback about my code as well, I'm fairly new to OOP.
After creating the class App, which inherits from Tk.Frame. I'd like to create another class to draw a ball on the screen (using a canvas). I've created a base GUI, but when trying to call the class drawBall, I receive the following error: 'drawBall' object has no attribute 'canvas'.
class App(tk.Frame):
def __init__(self,master):
super().__init__(master)
#create title and size for the window
self.master.geometry("640x360")
self.canvas = tk.Canvas(self.master,relief = 'raised',borderwidth = 1)
self.canvas.grid(row = 0,column = 0,sticky = 'NW')
#create a startSimulation button, place it in the bottom right corner
self.startButton = tk.Button(self.master,text = 'Start',command = self.startCallback)
self.startButton.grid(row = 2,column = 3)
#create a quit button, place it in the bottom right corner
self.quitButton = tk.Button(self.master,text = "Quit",command = self.master.destroy)
self.quitButton.grid(row = 3, column =3)
#callback for start button click
def startCallback(self):
#### this is where the error occurs #####
self.ball1 = drawBall(self.master,self.canvas)
class drawBall():
def __init__(self,master,canvas):
self.canvas.create_oval(25,75,35,85,fill = 'blue')
def moveBall(self):
deltaX = 1
self.canvas.move(self.seed,deltaX,0)
self.canvas.after(50,self.moveBall)
if __name__ == '__main__':
window = tk.Tk()
simulate = App(window)
window.mainloop()
I'd hope that the call "self.ball1 = drawBall(self.master,self.canvas)" would result in the circle being drawn on the screen.
You need a class Ball, that will take a canvas, and has the ability to move itself.
Then, in the App, you create a collection of balls, and order them to move.
something like this:
import tkinter as tk
class App(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.master.geometry("640x360")
self.canvas = tk.Canvas(self.master, relief='raised', borderwidth=1)
self.canvas.grid(row=0, column=0, sticky='NW')
self.startButton = tk.Button(self.master, text='animate', command=self.launch_animation)
self.startButton.grid(row=2, column=3)
self.stopButton = tk.Button(self.master, text='stop', command=self.stop_animation)
self.stopButton.grid(row=3, column=3)
self.quitButton = tk.Button(self.master, text="Quit",command=self.master.destroy)
self.quitButton.grid(row=4, column=3)
self.balls = [Ball(self.canvas)]
self.anim_is_on = False
def stop_animation(self):
self.anim_is_on = False
def launch_animation(self):
if self.anim_is_on: # prevent launching several overlapping animation cycles
return
self.anim_is_on = True
self.animate()
def animate(self):
if not self.anim_is_on:
return
for ball in self.balls:
ball.moveball()
self.after(100, self.animate)
class Ball():
def __init__(self, canvas):
self.canvas = canvas
self.id = self.canvas.create_oval(25, 75, 35, 85, fill='blue')
def moveball(self):
delta_x = 1
self.canvas.move(self.id, delta_x, 0)
window = tk.Tk()
simulate = App(window)
window.mainloop()

Tkinter bind callback to activating a window

I'm writing a Tkinter application in Python 3 and I've created a custom Title Bar (the bar at the top of every Windows application with the application name, close button etc.). The way I achieved this was to create an invisible window (we'll call this window test) that controls the main application window's behavior (which we'll call Main App). Below is a piece of test code that illustrates this:
from tkinter import Tk, Toplevel
from tkinter.ttk import Button, Label, Frame, Style
class NewRoot(Tk):
def __init__(self):
Tk.__init__(self)
self.attributes('-alpha', 1.0) # This is normally set to 0.0
class MyMain(Toplevel):
def __init__(self, master):
Toplevel.__init__(self, master)
self.overrideredirect(1)
self.geometry('750x650+400+600')
self.style = Style()
self.style.configure('TTitleBar.Label',
width=8,
relief='flat',
foreground='red',
background='red',
anchor='center',
borderwidth=-1)
self.style.configure('TMainWindow.Label',
width=8,
relief='flat',
background='blue',
anchor='center',
borderwidth=-1)
self.tk_setPalette(background='green')
self.x_win = None
self.y_win = None
self.start_x = None
self.start_y = None
# make a frame for the title bar
title_bar = Frame(self, style='TTitleBar.Label')
title_bar.grid(row=0, column=0, sticky='wn')
label = Label(title_bar, text='Main App', style='TMainWindow.Label')
label.grid(row=0, column=0, padx=(4, 2), pady=(4, 0), sticky='nw')
minimize_button = Button(title_bar, text='MIN', command=self.minimize_window,
style='TMainWindow.Label', takefocus=False)
minimize_button.grid(row=0, column=2, padx=(563.5, 0), sticky='nw')
maximise_button = Button(title_bar, text='MAX', command=self.maximize_window,
style='TMainWindow.Label', takefocus=False)
maximise_button.grid(row=0, column=3, pady=(1.4, 0), sticky='nw')
close_button = Button(title_bar, text='CLOSE', command=self.close_window,
style='TMainWindow.Label', takefocus=False)
close_button.grid(row=0, column=4, sticky='nw')
window = Frame(self)
window.grid(row=1, column=0, sticky='ne')
# bind title bar motion to the move window function
title_bar.bind('<B1-Motion>', self.move_window)
title_bar.bind('<Button-1>', self.get_pos)
self.master.bind("<Map>", self.on_root_deiconify)
self.master.bind("<Unmap>", self.on_root_iconify)
self.mainloop()
def minimize_window(self):
self.master.iconify()
def maximize_window(self):
pass
def close_window(self):
self.master.destroy()
def on_root_iconify(self, event):
# print('unmap')
self.withdraw()
def on_root_deiconify(self, event):
# print('map')
self.deiconify()
def get_pos(self, event):
self.x_win = self.winfo_x()
self.y_win = self.winfo_y()
self.start_x = event.x_root
self.start_y = event.y_root
self.y_win = self.y_win - self.start_y
self.x_win = self.x_win - self.start_x
def move_window(self, event):
# print('+{0}+{1}'.format(event.x_root, event.y_root))
self.geometry('+{0}+{1}'.format(event.x_root + self.x_win, event.y_root + self.y_win))
self.start_x = event.x_root
self.start_y = event.y_root
if __name__ == '__main__':
root = NewRoot()
root.title('test')
app = MyMain(root)
In the code above, whenever the test window is minimized, the Main App window is also minimized, which works as intended.
The problem is that whenever the test window is made active, the Main App window doesn't become active also. For example, if another app covers Main App but test is not minimized, I need to click on the test icon in the Windows Task Bar three times for it to appear.
I was wondering if there is a way to fix this using something like:
self.master.bind(<some_command>, self.some_other_command)
However, I can't find a comprehensive list of bind commands anywhere.
Is this a good way of going about this, or is there something else I should be doing?
Also, I noticed that using self.overrideredirect(1) causes the shadows made by the windows to disappear, which causes overlapping windows in my application to 'merge together', since the background colors are the same. Is there a way to add the shadows back?
Thank you in advance.
I found a solution to this for anyone else with a similar problem. You can create a 'dummy' button in the invisible window that will become active when the window is in focus. You then have that call a function that places the main application window in focus.
class NewRoot(Tk):
def __init__(self):
Tk.__init__(self)
self.attributes('-alpha', 0.0)
entry = Button()
entry.pack()
entry.focus_set()
entry.pack_forget()
Then add this to __init__ in the MyMain class:
self.master.bind('<FocusIn>', self.on_root_deiconify)

Python GUI to scroll through photos in a window

I am trying to develop a Python Tkinter widget that opens a window displaying a photo and allows the user to scroll through the photos using a scroll bar. For context, the idea is to be able to scroll through a timer-series of photographs.
So far I've been able to make a Tkinter canvas displaying an image, and a Tkinter "Scale" slider, but I am having trouble combining them to meet my goal. The result of the below code is an empty canvas window and a separate slider. The slider works and prints its position when moved, but no photo ever loads. I was hoping that when the bar was moved to position 3, photo 3 would load.
Any idea what I'm missing?
import Tkinter as tk
from PIL import ImageTk, Image
from Tkinter import *
class ImageCanvas(Frame):
def __init__(self, master):
Frame.__init__(self, master)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canvas = Canvas(self, width=720, height=480, bd=0)
self.canvas.grid(row=0, column=0, sticky='nsew', padx=4, pady=4)
self.root = tk.Tk()
self._job = NONE
self.slider = tk.Scale(self.root, from_=0, to=3, orient = "horizontal", command=self.updateValue)
self.slider.pack()
self.root.mainloop()
def updateValue(self, event):
if self._job:
self.root.after_cancel(self._job)
self._job = self.root.after(500, self.result)
def result(self):
self._job=None
print self.slider.get()
returnedValue = self.slider.get()
class ImgTk(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.main = ImageCanvas(self)
self.main.grid(row=0, column=0, sticky='nsew')
self.c = self.main.canvas
self.currentImage = {}
self.load_imgfile(images[ImageCanvas.returnedValue()])
def load_imgfile(self, filename):
self.img = Image.open(filename)
self.currentImage['data'] = self.img
self.photo = ImageTk.PhotoImage(self.img)
self.c.xview_moveto(0)
self.c.yview_moveto(0)
self.c.create_image(0, 0, image=self.photo, anchor='nw', tags='img')
self.c.config(scrollregion=self.c.bbox('all'))
self.currentImage['photo'] = self.photo
images = ['/Users/Evan/Documents/Temp/4744.png', '/Users/Evan/Documents/Temp/4750.png', '/Users/Evan/Documents/Temp/4757.png']
app = ImgTk()
A couple suggestions may get you on the right track. Move the self.root.mainloop() out of the ImageCanvas class and put it at the end, after app = ImgTk(), as simply mainloop(), so it only get's called ones per instance of your app.
Also, ImageCanvas class doesn't have a method called returnedValue, and the result method doesn't return any value. So, add return returnedValue to the result method. Then ImageCanvas.returnedValue() would need to be ImageCanvas.result(), or you could just say self.main.result().
After making these changes, the first image displays, more work will need to be done to get the other images to load.

Lift and raise a Canvas over a Canvas in tkinter

I'm creating a game, and I am using tkinter to build the GUI.
In this game, I want the user to start with a window, filled by a Canvas, with an image as background, and some buttons in some sort of item windows. And this Canvas is the home Canvas of the game.
The problem is that I want the user to click on a Button to land on other Canvas, who will host other things like the map or others buttons for others actions. Actually, I would like to superimpose several canvas (as in the Z-index method), and put a canvas on the top of the list, when I want it (if I click on a button for example).
I already searched and I had to change my mind several times, and now I really don't know how to do it.
I find the following code here on Stack Overflow, but it is coded for Python 2 (I think), and I'm starting coding in Python 3, so I am not able to translate it to Python 3 and solve my problem.
import Tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.frame = tk.Frame(self)
self.frame.pack(side="top", fill="both", expand=True)
self.label = tk.Label(self, text="Hello, world")
button1 = tk.Button(self, text="Click to hide label",
command=self.hide_label)
button2 = tk.Button(self, text="Click to show label",
command=self.show_label)
self.label.pack(in_=self.frame)
button1.pack(in_=self.frame)
button2.pack(in_=self.frame)
def show_label(self, event=None):
self.label.lift(self.frame)
def hide_label(self, event=None):
self.label.lower(self.frame)
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
My code using grid :
from tkinter import *
fenetre = Tk()
fenetre.title(my game)
# acceuil
# variable acceuil
largeur = 700
hauteur = 430
BG_acceuil = PhotoImage(file="BG_acceuil.gif")
acceuil = Canvas(fenetre, width=largeur, height=hauteur)
acceuil.create_image(0, 0, anchor=NW, image=BG_acceuil)
acceuil.grid(row=0)
acceuil.pack()
# fond
fond = PhotoImage(file="BG_acceuil.gif")
acceuil2 = Canvas(fenetre, width=largeur, height=hauteur)
acceuil2.create_image(0, 0, anchor=NW, image=fond)
acceuil2.pack()
# variable bt_jouer
x0 = 80
y0 = 230
class hide_me():
def hide_me(event, widget, pos):
widget.grid_forget()
def show_me(event, widget, pos):
widget.grid(row=pos)
# Boutton jouer
BT_jouer = Button(acceuil, text="Jouer", command=hide_me())
BT_jouer.configure(width=10, activebackground="#33B5E5", relief=GROOVE)
BT_jouer_window = acceuil.create_window(x0, y0, window=BT_jouer,)
BT_jouer.bind('<Button-1>', lambda event: hide_me(event, BT_jouer, 1))
BT_jouer.grid(row=1)
# Bouton règle
BT_regle = Button(acceuil2, text="Règles", command=fenetre.destroy)
BT_regle.configure(width=10, activebackground="#33B5E5", relief=FLAT, bd=0)
BT_regle_window = acceuil2.create_window(x0, y0 + 50, window=BT_regle)
# Boutton quitter
BT_quit = Button(acceuil, text="Quitter", command=fenetre.destroy)
BT_quit.configure(width=10, activebackground="#33B5E5", relief=FLAT)
BT_quit_window = acceuil.create_window(x0, y0 + 100, window=BT_quit)
fenetre.mainloop()
The answer is very easy: To convert to Python3, change Tkinter to tkinter, and it works!
import tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.frame = tk.Frame(self)
self.frame.pack(side="top", fill="both", expand=True)
self.label = tk.Label(self, text="Hello, world")
button1 = tk.Button(self, text="Click to hide label",
command=self.hide_label)
button2 = tk.Button(self, text="Click to show label",
command=self.show_label)
self.label.pack(in_=self.frame)
button1.pack(in_=self.frame)
button2.pack(in_=self.frame)
def show_label(self, event=None):
self.label.lift(self.frame)
def hide_label(self, event=None):
self.label.lower(self.frame)
def main():
app = SampleApp()
app.mainloop()
return 0
if __name__ == '__main__':
main()
Note: You are not really hiding the label - it still occupies space on the canvas. The following code, from this entry, really removes the item. It can then be recalled with a pack() call:
from Tkinter import *
def hide_me(event):
event.widget.pack_forget()
root = Tk()
btn=Button(root, text="Click")
btn.bind('<Button-1>', hide_me)
btn.pack()
btn2=Button(root, text="Click too")
btn2.bind('<Button-1>', hide_me)
btn2.pack()
root.mainloop()
I did some testing, and made an equivalent program to yours... The only problem is that the unhidden widget is always packed at the end:
from tkinter import *
def hide_me(event, widget):
widget.pack_forget()
def show_me(event, widget):
widget.pack()
root = Tk()
lbl = Label(root, text="Victim")
btn = Button(root, text="Hide the victim")
btn.bind('<Button-1>', lambda event: hide_me(event, lbl))
btn.pack()
btn2 = Button(root, text="Show the victim")
btn2.bind('<Button-1>', lambda event: show_me(event, lbl))
btn2.pack()
lbl.pack()
root.mainloop()
A better version uses the grid() packer. Here you can actually restore the 'forgotten' widget to its original position. Only slightly more complicated :)
from tkinter import *
def hide_me(event, widget, pos):
widget.grid_forget()
def show_me(event, widget, pos):
widget.grid(row = pos)
root = Tk()
lbl = Label(root, text="Victim")
lbl.grid(row = 0)
btn = Button(root, text="Hide the victim")
btn.bind('<Button-1>', lambda event: hide_me(event, lbl, 0))
btn.grid(row = 1)
btn2 = Button(root, text="Show the victim")
btn2.bind('<Button-1>', lambda event: show_me(event, lbl, 0))
btn2.grid(row = 2)
root.mainloop()
EDIT: Another observation from the comments: Bryan Oakley commented that if you use .grid_remove() instead of .grid_forget(), then the coordinates will not be lost, and a simple .grid() will restore the widget at its location.

Need help placing buttons on a PhotoImage .gif

Here is my program as of yet:
from tkinter import *
from collections import deque
class App():
def __init__(self, *images):
self.root = Tk()
self.root.title("Skin")
self.image_dict = {image: PhotoImage(file=image) for image in images}
self.image_queue = deque(images)
b = Button(self.root, text="Click here to see the diagram!", command=self.change_image)
b.pack(fill=X)
self.label = Label(self.root, image=self.image_dict["1.gif"])
self.label.image = self.image_dict["1.gif"]
self.label.pack()
def change_image(self):
self.image_queue.rotate(-1)
next_image = self.image_queue[0]
self.label.configure(image=self.image_dict[next_image])
self.label.image = self.image_dict[next_image]
if __name__ == "__main__":
app = App('1.gif', '2.gif')
app.root.mainloop()
What this does is when you run the scipt, a window comes up diplaying "1.gif", and a button. When you click the button, "1.gif" changes to "2.gif". "1.gif" is a blank diagram, "2.gif" is a diagram with labels showing what each part of the diagram is.
Now for the next stage of my program, I need some way to add multiple invisible buttons, or something like it, over each word on the diagram on "2.gif", and when you click on it, I need a seperate window to come up with text on it. Is there any way to implement that into my current program? I have no idea where to start. Thank you!
I think you will be better off using a Canvas to hold your image(s) rather
than a Label. You can then place 'hotspots' over the diagram and bind
events to them. eg. something like:
from tkinter import *
class App():
def __init__(self):
self.root = Tk()
self.messages = {}
self.canvas = Canvas(self.root, bd=0, highlightthickness=0)
self.image = PhotoImage(file='2.gif')
self.canvas.create_image(0,0, image=self.image, anchor='nw')
w,h = self.image.width(), self.image.height()
self.canvas.config(width=w, height=h)
self.canvas.pack()
self.balloon = b = Toplevel(self.root)
b.withdraw()
b.wm_overrideredirect(1)
self.msg = Label(b, bg='black', fg='yellow', bd=2, text='')
self.msg.pack()
self.set_up_hotspots()
def set_up_hotspots(self):
box1 = self.canvas.create_polygon(50,100,100,100,100,150,50,150,
outline='blue', fill='')
#note: outline can be '' also ;)
self.messages[box1] = "The machine head consists of a "\
"Flynn mechanism,\na Demmel crank, "\
"and a heavy-duty frobulator. "
box2 = self.canvas.create_polygon(150,100,200,100,200,150,150,150,
outline='red', fill='')
self.messages[box2] = "And now for something completely different"
for item in [box1, box2]:
for event, handler in [('<Enter>', self.on_hotspot_enter),
('<Leave>', self.on_hotspot_leave)]:
self.canvas.tag_bind(item, event, handler)
def on_hotspot_enter(self, event):
if not self.balloon.winfo_ismapped():
txt = self.messages[event.widget.find_withtag('current')[0]]
self.msg.config(text=txt)
x,y = event.x_root, event.y_root
self.balloon.geometry('+%d+%d' %(x+16,y))
self.balloon.deiconify()
def on_hotspot_leave(self, event):
if self.balloon.winfo_ismapped():
self.balloon.withdraw()
if __name__ == "__main__":
app = App()
app.root.mainloop()

Categories