I want to obtain a button out of a Canvas. I've tried to pack the canvas in the button widget, but that didn't work. Googling a bit, I've found (here: How do you create a Button on a tkinter Canvas?) that the Canvas method create_window might help. But there should be something wrong in the way I'm using it.
import Tkinter
DIM = 100
root = Tkinter.Tk()
frame = Tkinter.Frame(root)
button = Tkinter.Button(None, width=DIM, height=DIM, command=root.quit)
circle = Tkinter.Canvas(frame, width=DIM, height=DIM)
circle.create_oval(5, 5, DIM-5, DIM-5, fill="red")
circle.create_window(0, 0, window=button)
frame.grid()
circle.grid(row=1, column=1)
root.mainloop()
If I erase the create_window line, I can se my painting but I can't (obviously) click on it. But in this way, the button widget cover my circle and shows a sad empty button.
Basically, I want to create a button with a red circle painted inside.
Tkinter doesn't allow you to directly draw on widgets other than the canvas, and canvas drawings will always be below embedded widgets.
The simple solution is to create the effect of a button using just the canvas. There's really nothing special about doing this: just create a canvas, then add bindings for ButtonPress and ButtonRelease to simulate a button being pressed.
Here's a rough idea:
class CustomButton(tk.Canvas):
def __init__(self, parent, width, height, color, command=None):
tk.Canvas.__init__(self, parent, borderwidth=1,
relief="raised", highlightthickness=0)
self.command = command
padding = 4
id = self.create_oval((padding,padding,
width+padding, height+padding), outline=color, fill=color)
(x0,y0,x1,y1) = self.bbox("all")
width = (x1-x0) + padding
height = (y1-y0) + padding
self.configure(width=width, height=height)
self.bind("<ButtonPress-1>", self._on_press)
self.bind("<ButtonRelease-1>", self._on_release)
def _on_press(self, event):
self.configure(relief="sunken")
def _on_release(self, event):
self.configure(relief="raised")
if self.command is not None:
self.command()
To complete the illusion you'll want to set a binding on <Enter> and <Leave> (to simulate the active state), and also make sure that the cursor is over the button on a button release -- notice how real buttons don't do anything if you move the mouse away before releasing.
What you can do is bind the canvas to the mouse:
import Tkinter
DIM = 100
root = Tkinter.Tk()
frame = Tkinter.Frame(root)
circle = Tkinter.Canvas(frame)
circle.create_oval(5, 5, DIM-5, DIM-5, fill="red")
frame.grid()
circle.grid(row=1, column=1)
##################################
def click(event):
root.quit()
circle.bind("<Button-1>", click)
##################################
root.mainloop()
Now, if a user clicks inside the canvas, function click will be called (essentially, the canvas has now been made a button).
Notice though that function click will be called if a user clicks anywhere in the canvas. If you want to make it so that click is only called when a user clicks in the circle, you can use event.x and event.y to get a hold of the x and y coordinates of the click. Once you have those, you can run a calculation to determine whether those coordinates are within the circle. Here is a reference on that.
Related
I am trying to understand how to apply a button to a transparent background while keeping its shape. When I generate the code below, there is a gray background around the border that appears, and it also looks like it loses its shape.
Colors Used
Sidebar: #2E3A4B at 53%
Button: #2C2F33 at 100%
from tkinter import *
def btn_clicked():
""" Prints to console a message every time the button is clicked """
print("Button Clicked")
root = Tk()
# Configures the frame, and sets up the canvas
root.geometry("1440x1024")
root.configure(bg="#ffffff")
canvas = Canvas(root, bg="#ffffff", height=1024, width=1440, bd=0, highlightthickness=0, relief="ridge")
canvas.place(x=0, y=0)
background_img = PhotoImage(file=f"background.png")
background = canvas.create_image(719.5, 512.5, image=background_img)
img0 = PhotoImage(file=f"img0.png")
alarm_button = Button(image=img0, borderwidth=0, highlightthickness=0, command=btn_clicked, relief="flat")
alarm_button.place(x=9, y=119, width=90, height=90)
root.resizable(False, False)
root.mainloop()
Required Images
How it looks:
How it should look:
Good news! I was able to get that answer to a related question you found to work. To make it easier to reuse I've converted it into a formal class and added a couple of methods. In addition I made it flash the image off and back on when it's clicked to give the user some visual feedback like "real" tkinter Buttons do.
Note that it responds to mouse button <ButtonRelease-1> events. That's a better choice in most cases than the <Button-1> event because if the user accidentally presses the button, they can move the mouse off the widget image to avoid setting off the event.
Turns out that using the PIL module was unnecessary. Here's the code:
import tkinter as tk # PEP 8 recommends avoiding `import *`.
class CanvasButton:
""" Create leftmost mouse button clickable canvas image object.
The x, y coordinates are relative to the top-left corner of the canvas.
"""
flash_delay = 100 # Milliseconds.
def __init__(self, canvas, x, y, image_path, command, state=tk.NORMAL):
self.canvas = canvas
self.btn_image = tk.PhotoImage(file=image_path)
self.canvas_btn_img_obj = canvas.create_image(x, y, anchor='nw', state=state,
image=self.btn_image)
canvas.tag_bind(self.canvas_btn_img_obj, "<ButtonRelease-1>",
lambda event: (self.flash(), command()))
def flash(self):
self.set_state(tk.HIDDEN)
self.canvas.after(self.flash_delay, self.set_state, tk.NORMAL)
def set_state(self, state):
""" Change canvas button image's state.
Normally, image objects are created in state tk.NORMAL. Use value
tk.DISABLED to make it unresponsive to the mouse, or use tk.HIDDEN to
make it invisible.
"""
self.canvas.itemconfigure(self.canvas_btn_img_obj, state=state)
BGR_IMG_PATH = "sunset_background.png"
BUTTON_IMG_PATH = "alarm_button.png"
def btn_clicked():
""" Prints to console a message every time the button is clicked """
print("Button Clicked")
root = tk.Tk()
background_img = tk.PhotoImage(file=BGR_IMG_PATH)
bgr_width, bgr_height = background_img.width(), background_img.height()
root.geometry(f'{bgr_width}x{bgr_height}')
root.title("TKinter button over transparent background")
root.configure(bg="white")
canvas = tk.Canvas(root, bg="white", height=bgr_height, width=bgr_width, bd=0,
highlightthickness=0, relief="ridge")
canvas.place(x=0, y=0)
background = canvas.create_image(0, 0, anchor='nw', image=background_img)
canvas_btn1 = CanvasButton(canvas, 0, 128, BUTTON_IMG_PATH, btn_clicked)
canvas_btn2 = CanvasButton(canvas, 0, 256, BUTTON_IMG_PATH, btn_clicked)
root.resizable(False, False)
root.mainloop()
Screenshot of the result:
Close up:
Alright, so to start I'm using Python 3.7 with tkinter.
I have a canvas and I can drag a label around with that using the mouse events. My next step is to be able to drag around something on which I can put more widgets. So imagine a box which has a text box and an image on it, perhaps a combobox too. That box can then be dragged around.
I figured perhaps what I needed was a frame widget on my canvas which I could then set up in the same way as I had done with the label. But this is where it seems to fall apart - clearly I'm doing something wrong.
Here's the code I've been playing around with, to no avail:
root = tk.Tk()
root.geometry("800x600")
def ClickedCallback(event):
print(f'Clicked: coords: {event.x}, {event.y}')
def ReleaseCallback(event):
print(f'Released: coords: {event.x}, {event.y}')
def MotionCallback(event):
print(f'Motion: coords: {event.x}, {event.y}')
canvas.coords(frame_id, event.x, event.y)
canvas = tk.Canvas(root, width=1000, height=600, bg='blue')
canvas.bind("<B1-Motion>", MotionCallback)
canvas.pack()
frame = tk.Frame(canvas, bg='green')
frame.pack()
l2 = tk.Label(frame, bg='red')
l2.bind("<Button-1>", ClickedCallback)
l2.pack()
l2['text'] = "Test"
frame_id = canvas.create_window((300,300), window=frame)
label_id = canvas.create_window((100, 100), window=l2)
root.mainloop()
My thinking here is that I attach the frame to the canvas and then the label to the frame so that, f I move the frame, the label within it will be moved too.
The above won't work though, and it tells me the following:
File "C:\Users\JohnSmith\AppData\Local\Programs\Python\Python37-32\lib\tkinter__init__.py", line 2480, in _create
*(args + self._options(cnf, kw))))
_tkinter.TclError: can't use .!canvas.!frame.!label in a window item of this canvas
I may be going about this in entirely the wrong way. I had looked around for something more, but if there's something I'm doing entirely wrong or an established way I should be doing this, I would appreciate it if you could point me in the right direction.
I want to ask u guys if there is a way of getting name or some id of widget which is mouse pointer currently on. Is there a way of doing this? Thanks for any response.
Normally you get this information from a binding. However, if you want to poll the system at any point to find out which widget is under the mouse, you can use winfo_pointerxy to get the coordinates of the mouse, and then pass those to winfo_containing to get the widget under those coordinates.
Here's an example program that continuously prints out the widget under the mouse:
import tkinter as tk
def print_widget_under_mouse(root):
x,y = root.winfo_pointerxy()
widget = root.winfo_containing(x,y)
print("widget:", widget)
root.after(1000, print_widget_under_mouse, root)
root = tk.Tk()
label_foo = tk.Label(root, text="Foo", name="label_foo")
label_bar = tk.Label(root, text="Bar", name="label_bar")
button = tk.Button(root, text="Button", name="button")
button.pack(side="bottom")
label_foo.pack(fill="both", expand=True)
label_bar.pack(fill="both", expand=True)
print_widget_under_mouse(root)
root.mainloop()
I'm making a card game (called monster master) to develop my python skills, specifically OOP.
I have a GUI that has a few static objects: Player 1's side of the table, Player 2's side, a wee line in the middle and I'm now trying to implement an 'end turn' button.
I have tried a lot of different things to try to get this button to display, but I just can't get it to appear even if there are no errors. Just saying that there are a few commented out lines that I've temporarily taken away for the sake of trying to understand the problem with this button.
Here's the code that I'm currently using to try:
def RunGame():
class App():
"""docstring for App"""
def draw():
# Setting up canvas dimensions
canvas_width = 640
canvas_height = 480
master = Toplevel()
master.title("Monster Master by Charles Cameron - Game")
master.resizable(width=False, height=False)
master.geometry("640x480")
w = Canvas(master,
width=canvas_width,
height=canvas_height)
w.pack()
# Drawing static objects
CentrePoints = [(0, canvas_height/2), (canvas_width/2, canvas_height/2),
(canvas_width, canvas_height/2)]
#Left, centre and right centres (save me from retyping them)
Player1Area = w.create_rectangle(CentrePoints[0], canvas_width,
canvas_height, fill="#303AFE") #Player1 Area
Player2Area = w.create_rectangle(0, 0, CentrePoints[2],
fill="#C31B1B") #Player2 Area
Barrier = w.create_line(CentrePoints[0], CentrePoints[2],
fill="#0090E3", width=20) # Centre barrier
# class GameBtn():
# class EndTurnBtn():
# def __init__(self, BtnName, master):
BtnName = Button(master, bg="white", command=lambda:print("Clicked!"))
image = ImageTk.PhotoImage(file="imgs\EndTurn.png")
BtnName.config(image=image, width="70", height="70")
BtnName.pack(side=RIGHT)
# ChangeTurn = GameBtn.EndTurnBtn('ChangeTurn', master)
master.mainloop()
Window = App()
App.draw()
The button code for the actual button worked fine when I tried it in its own script but stopped working when I put it inside this program.
Hope it's not too dumb a question to ask, quite an amateur at python still and honestly can't find the answer anywhere online.
Many thanks
Your button exists, but it's past the edge of the window. This is because you made your window 640x480, and then completely filled it with a 640x480 canvas. Remove the master.geometry("640x480") line and the window will stretch to contain both your canvas and your button.
You might be thinking "but I don't want the button to appear to the side of the canvas. I want the button to be on the canvas. The canvas only really exists because I wanted to color sections of the background". Embedding widgets on a canvas is possible using create_window (see How to place a widget in a Canvas widget in Tkinter?), but it may be more practical to create colored backgrounds by stacking Frame objects together and giving them individual colors. Example:
from tkinter import Tk, Frame, Button
root = Tk()
root.geometry("640x480")
top_player_frame = Frame(root, height=230, bg="red")
barrier = Frame(root, height=20, bg="green")
bottom_player_frame = Frame(root, height = 230, bg="blue")
#configure column 0 so frames can stretch to fit width of window
root.columnconfigure(0, weight=1)
top_player_frame.grid(row=0, sticky="we")
barrier.grid(row=1, sticky="we")
bottom_player_frame.grid(row=2, sticky="we")
bottom_player_end_turn_button = Button(bottom_player_frame, text="End Turn")
#use `place` here because `pack` or `grid` will collapse the frame to be only as tall as all the widgets it contains, i.e. just this button
bottom_player_end_turn_button.place(x=10,y=10)
root.mainloop()
Result:
When I hover over the canvas I want some labels on top of the canvas to display x,y coordinates which stay the same if I keep my cursor still but change when I move it. How would I do this?
You can use a callback method and bind it to a Motion event.
import tkinter
root = tkinter.Tk()
canvas = tkinter.Canvas(root)
canvas.pack()
def moved(event):
canvas.itemconfigure(tag, text="(%r, %r)" % (event.x, event.y))
canvas.bind("<Motion>", moved)
tag = canvas.create_text(10, 10, text="", anchor="nw")
root.mainloop()
Also use <Enter> event. So when you switch between windows (<Alt>+<Tab> hotkey), your cursor will show the correct coordinates.
For example, you put your cursor on the canvas and <Motion> event will track it, but when you press <Alt>+<Tab> and switch to another window, then move your cursor a bit and <Alt>+<Tab> on your canvas again -- coordinates of the your cursor will be wrong, because <Motion> event doesn't track switches between windows. To fix it use <Enter> event.
import tkinter
root = tkinter.Tk()
canvas = tkinter.Canvas(root)
canvas.pack()
def get_coordinates(event):
canvas.itemconfigure(tag, text='({x}, {y})'.format(x=event.x, y=event.y))
canvas.bind('<Motion>', get_coordinates)
canvas.bind('<Enter>', get_coordinates) # handle <Alt>+<Tab> switches between windows
tag = canvas.create_text(10, 10, text='', anchor='nw')
root.mainloop()