I'm trying to make it possible for my program's user to drag an image from a widget and drop it in another without removing the image from its first position.
I thought on using a TopLevel for this. Upon a click+drag event, a TopLevel, containing the same image, would pop up right above the clicked image on the event.x and event.y position. Then it would change its position along with the mouse and only upon ButtonRelease-1 the TopLevel would be destroyed.
If the Button Release event was triggered on the master x and y coordinates corresponding to where is my other widget (in the case, a canvas), then it would trigger a canvas.create_image() using the event.x and event.y.
The problems I'm facing are:
Only 1 TopLevel should appear at once, but I had no success limiting it. Several windows overlap as I click and drag the mouse.
I can't make Toplevel.geometry use the bound event's x and y positions.
How to make the Toplevel display the same image as the user clicked, without magic numbers and variables?
Here's my code so far:
class animalImgList():
def __init__(self, a):
#Constructor
initX = 75
initY = 40
animalList = ['pig2.jpg', 'pig3.jpg', 'pig4.jpg']
for a in animalList:
vars(self)[a+'img'] = PIL.Image.open(a)
vars(self)[a+'tkimg'] = PIL.ImageTk.PhotoImage(vars(self)[a+'img'])
vars(self)[a+'labelName'] = Label(anmlCanvas, image=vars(self)[a+'tkimg'])
vars(self)[a+'canvasImg'] = anmlCanvas.create_image(initX, initY,image=(vars(self)[a+'tkimg']))
initY = initY + 70
anmlImgList = []
anmlImgList.append(vars(self)[a+'canvasImg'])
imgTags = anmlCanvas.addtag_all("img")
anmlCanvas.tag_bind("img", "<Button-1>", self.createImg)
def createImg(self, event):
newImg = Toplevel(root)
newImg.geometry("50x40"+"+"+ x+"+"+y)
newImgMsg = Message(newImg, text="This is supposed to be an image")
newImgMsg.pack()
newImg.update_idletasks()
newImg.overrideredirect(True)
createImgOpen = True
if createImgOpen == True:
pass
To drag an image between two canvases without removing the image from the first canvas.
The idea is the following:
When the user clicks on the canvas can1 (click1 function):
get the item the user clicked on with can1.find_closest
get its image with can1.itemcget
create a toplevel containing the image
bind mouse motion to drag along the toplevel: for that you need to use the event.x_root and event.y_root to change the toplevel's geometry.
When the user releases the left mouse button (release function):
unbind mouse motion
if the toplevel is inside the canvas can2, create the image in can2 at the mouse position
destroy the toplevel
This way, there can be only one toplevel because each time the button is released, the toplevel is destroyed.
Here is the code:
import tkinter as tk
class DragToplevel(tk.Toplevel):
def __init__(self, master, image, x, y):
tk.Toplevel.__init__(self, master)
self.overrideredirect(True)
self.geometry('+%i+%i' % (x, y))
self.image = image
self.label = tk.Label(self, image=image, bg='red')
self.label.pack()
def move(self, x, y):
self.geometry('+%i+%i' % (x, y))
root = tk.Tk()
can1 = tk.Canvas(root, width=300, height=300, bg='white')
can2 = tk.Canvas(root, width=300, height=300, bg='white')
can1.pack(side='left')
can2.pack(side='right')
root.geometry('800x800')
im = tk.PhotoImage('tux', master=root, file='/home/juliette/Images/tux_mini.png')
drag_id = ''
dragged = None
can1.create_image(100, 200, image=im)
def click1(event):
global drag_id, dragged
items = can1.find_closest(event.x, event.y)
if items:
image = can1.itemcget(items[0], 'image')
dragged = DragToplevel(root, image, event.x_root, event.y_root)
drag_id = root.bind('<Motion>', lambda e: dragged.move(e.x_root, e.y_root))
def release(event):
global drag_id, dragged
root.unbind('<Motion>', drag_id)
drag_id = ""
xr, yr = event.x_root, event.y_root
x2, y2 = can2.winfo_rootx(), can2.winfo_rooty()
w2, h2 = can2.winfo_width(), can2.winfo_height()
if dragged and xr >= x2 and xr < x2 + w2 and yr >= y2 and yr < y2 + h2:
can2.create_image(xr - x2, yr - y2, image=dragged.image, anchor='nw')
if dragged:
dragged.destroy()
dragged = None
can1.bind('<ButtonPress-1>', click1)
root.bind('<ButtonRelease-1>', release)
root.mainloop()
Related
Currently my program will open an image, and with that image you can click anywhere on it and it'll return the pixel coordinates of where you clicked. And from those coordinates you are able to type some text and it'll display the edited image back to you. Yet, I was wondering if there was a way that would be visually helpful to the user showing them where they clicked. Here's my current code for opening the image and clicking on it to return the coordinates. I am using tkinter and I know I should've down from tkinter import *, but it's too late now :/.
### Function to open files ###
img_window = tkinter.PanedWindow(top, orient='vertical')
def file_opener():
global actual_img
global original_img
top.filename = filedialog.askopenfilename(initialdir="*/Images/", title ="Select an Image", filetypes=((".png", "*.png"),(".jpg", "*.jpg")))
original_img = Image.open(top.filename)
#################### CLICK LOCATION OF WHERE YOU WANT TO ADD TEXT ####################
event2canvas = lambda e, c: (c.canvasx(e.x), c.canvasy(e.y))
imgApp = tkinter.Toplevel()
# creating an img var of my original image
img = ImageTk.PhotoImage(original_img)
#setting up a tkinter canvas with scrollbars
frame = tkinter.Frame(imgApp, bd=2, relief=tkinter.SUNKEN)
frame.grid_rowconfigure(0, weight=1)
frame.grid_columnconfigure(0, weight=1)
xscroll = tkinter.Scrollbar(frame, orient=tkinter.HORIZONTAL)
xscroll.grid(row=1, column=0, sticky=tkinter.E+tkinter.W)
yscroll = tkinter.Scrollbar(frame)
yscroll.grid(row=0, column=1, sticky=tkinter.N+tkinter.S)
canvas = tkinter.Canvas(frame, bd=0, xscrollcommand=xscroll.set, yscrollcommand=yscroll.set, width = img.width(), height = img.height())
canvas.grid(row=0, column=0, sticky=tkinter.N+tkinter.S+tkinter.E+tkinter.W)
xscroll.config(command=canvas.xview)
yscroll.config(command=canvas.yview)
frame.pack(fill=tkinter.BOTH, expand = 1)
# adding the image to canvas
canvas.create_image(0,0,image=img,anchor="nw")
canvas.config(scrollregion=canvas.bbox(tkinter.ALL))
#function to be called when mouse is clicked
def returncoords(event):
global cx, cy
#outputting x and y coords to console
cx, cy = event2canvas(event, canvas)
return cx, cy
#mouseclick event
canvas.bind("<ButtonPress-1>", returncoords)
# canvas.bind("<ButtonRelease-1>", returncoords)
imgApp.title("Please select an area on Image")
imgApp.config(bg="red")
imgApp.mainloop()
This was my solution: When the image gets opened up, it'll display the tcross cursor and when the user clicks it would change cursor's until the user releases.
canvas.create_image(0,0,image=img,anchor="nw")
canvas.config(cursor = "tcross")
canvas.config(scrollregion=canvas.bbox(tkinter.ALL))
# function to be called when mouse is clicked
def returnCoords(event):
global cx, cy
# change the cursor when button is held / click so user knows they selected a coordinate
canvas.config(cursor = "dotbox")
cx, cy = event2canvas(event, canvas)
return cx, cy
def returnCursor(event):
canvas.config(cursor = "arrow")
time.sleep(.7)
imgApp.destroy()
# mouseclick event / release
canvas.bind("<ButtonPress-1>", returnCoords)
canvas.bind("<ButtonRelease-1>", returnCursor)
Using this script i am trying to take a screenshot of my desktop of a particular area.(using Tkinter gui)
But with this code i can only take screenshot of the fix area (frame) of desktop. So what i want to do is try to set the value of (bbox of imagegrab) dynamic.
And by dynamic i mean it should only capture the screen area which is selected(highlighted) by my mouse cursor any where on screen and can be of any size.
import tkinter as tk
from tkinter import *
from PIL import Image, ImageGrab
root = tk.Tk()
def area_sel():
# using the grab method
img = ImageGrab.grab(bbox = (400,500,400,500)) #i want these values to be dynamic
img.show()
sel_btn = tk.Button(root, text='select area', width=20, command=area_sel)
sel_btn.pack()
root.mainloop()
example image
here is what i am trying to do is to take coordinates from your code and then recording that particular area of screen.
def recording_screen():
global recording
recording = True
while recording:
sel=area_sel()
img = ImageGrab.grab(bbox=(Click_x, Click_y, Release_x, Release_y))
frame = np.array(img)
out.write(frame)
You can use Pillow to do what you want:
import tkinter as tk
from PIL import Image, ImageTk, ImageGrab, ImageEnhance
root = tk.Tk()
root.resizable(0, 0)
def show_image(image):
win = tk.Toplevel()
win.image = ImageTk.PhotoImage(image)
tk.Label(win, image=win.image).pack()
win.grab_set()
win.wait_window(win)
def area_sel():
x1 = y1 = x2 = y2 = 0
roi_image = None
def on_mouse_down(event):
nonlocal x1, y1
x1, y1 = event.x, event.y
canvas.create_rectangle(x1, y1, x1, y1, outline='red', tag='roi')
def on_mouse_move(event):
nonlocal roi_image, x2, y2
x2, y2 = event.x, event.y
canvas.delete('roi-image') # remove old overlay image
roi_image = image.crop((x1, y1, x2, y2)) # get the image of selected region
canvas.image = ImageTk.PhotoImage(roi_image)
canvas.create_image(x1, y1, image=canvas.image, tag=('roi-image'), anchor='nw')
canvas.coords('roi', x1, y1, x2, y2)
# make sure the select rectangle is on top of the overlay image
canvas.lift('roi')
root.withdraw() # hide the root window
image = ImageGrab.grab() # grab the fullscreen as select region background
bgimage = ImageEnhance.Brightness(image).enhance(0.3) # darken the capture image
# create a fullscreen window to perform the select region action
win = tk.Toplevel()
win.attributes('-fullscreen', 1)
win.attributes('-topmost', 1)
canvas = tk.Canvas(win, highlightthickness=0)
canvas.pack(fill='both', expand=1)
tkimage = ImageTk.PhotoImage(bgimage)
canvas.create_image(0, 0, image=tkimage, anchor='nw', tag='images')
# bind the mouse events for selecting region
win.bind('<ButtonPress-1>', on_mouse_down)
win.bind('<B1-Motion>', on_mouse_move)
win.bind('<ButtonRelease-1>', lambda e: win.destroy())
# use Esc key to abort the capture
win.bind('<Escape>', lambda e: win.destroy())
# make the capture window modal
win.focus_force()
win.grab_set()
win.wait_window(win)
root.deiconify() # restore root window
# show the capture image
if roi_image:
show_image(roi_image)
tk.Button(root, text='select area', width=30, command=area_sel).pack()
root.mainloop()
During selecting region:
Show the capture image after selecting region:
Use pynput is a way to do this(Maybe only use tkinter can do this,but I don't know),You only need to know the position of mouse button pressed and mouse button released:
Read more about pynput module
import tkinter as tk
# from tkinter import *
from PIL import Image, ImageGrab,ImageTk
from pynput import mouse
from pynput.keyboard import Key, Listener
def getPostion():
def on_click(x, y, button, pressed):
global Click_x, Click_y, Release_x, Release_y, STOP
if pressed:
Click_x = x
Click_y = y
else:
Keyboardlistener.stop()
Release_x = x
Release_y = y
STOP = False
return False
def on_release(key):
global STOP
if key == Key.esc:
Mouselistener.stop()
STOP = True
return False
with mouse.Listener(on_click=on_click) as Mouselistener, Listener(on_release=on_release) as Keyboardlistener:
Mouselistener.join()
Keyboardlistener.join()
root = tk.Tk()
def area_sel():
global Click_x, Click_y, Release_x, Release_y, STOP
Click_x, Click_y, Release_x, Release_y= 0,0,0,0
# using the grab method
top = tk.Toplevel() # create a toplevel
top.wm_attributes('-alpha',0.3)
top.state('zoomed') # make window fullscreen
top.overrideredirect(1)
# background = ImageTk.PhotoImage(image=ImageGrab.grab()) # get a screenshot
fullCanvas = tk.Canvas(top) # make a fullscreen canvas
# fullCanvas.create_image(xx,xx) # create a screenshot image in this canvas.
fullCanvas.pack()
top.update()
getPostion()
if Click_x and Click_y and Release_x and Release_y:
if STOP:
return False
top.withdraw()
img = ImageGrab.grab(bbox = (Click_x, Click_y, Release_x, Release_y))
img.show()
STOP = False
sel_btn = tk.Button(root, text='select area', width=20, command=area_sel)
sel_btn.pack()
root.mainloop()
edit now it is a complete tool can take a scroonshot,it is needn't to use pynput
full code :
import tkinter as tk
# from tkinter import *
from PIL import Image, ImageGrab, ImageTk
import ctypes, sys
if sys.getwindowsversion().major == 10:
ctypes.windll.shcore.SetProcessDpiAwareness(2) # Set DPI awareness
root = tk.Tk()
def area_sel():
def getPress(event): # get press position
global press_x,press_y
press_x,press_y = event.x,event.y
def mouseMove(event): # movement
global press_x, press_y, rectangleId
fullCanvas.delete(rectangleId)
rectangleId = fullCanvas.create_rectangle(press_x,press_y,event.x,event.y,width=5)
def getRelease(event): # get release position
global press_x, press_y, rectangleId
top.withdraw()
img = ImageGrab.grab((press_x, press_y,event.x,event.y))
img.show()
top = tk.Toplevel()
top.state('zoomed')
top.overrideredirect(1)
fullCanvas = tk.Canvas(top)
background = ImageTk.PhotoImage(ImageGrab.grab().convert("L"))
fullCanvas.create_image(0,0,anchor="nw",image=background)
# bind event for canvas
fullCanvas.bind('<Button-1>',getPress)
fullCanvas.bind('<B1-Motion>',mouseMove)
fullCanvas.bind('<ButtonRelease-1>',getRelease)
fullCanvas.pack(expand="YES",fill="both")
top.mainloop()
rectangleId = None
sel_btn = tk.Button(root, text='select area', width=20, command=area_sel)
sel_btn.pack()
root.mainloop()
I'm using a canvas and I want to know the cursor's position, I use canvas.bind("<Button-1>", callback) and in the callback event.x and event.y
But my issue comes when I use a Scrollbar to move the content inside the canvas: I want the cursor's position to be adjusted by the Scrollbar offset.
def callback(event):
x = event.x
y = event.y
#need these x and y to by adjusted with the Scrollbar offset
root = tk.Tk()
yscrollbar = tk.Scrollbar(root)
canvas = tk.Canvas(root, yscrollcommand = yscrollbar
canvas.pack()
canvas.bind("<Button-1>", callback)
yscrollbar.config(command = canvas.yview)
yscrollbar.pack()
As indicated you can use the canvas.canvasx / canvas.canvasy methods:
class MyApp:
def __init__(self, app):
# Initialize canvas with scrollable region and mouse down binding
self.canvas = ...
# Exemplaric mouse down method
def onMouseDown(self, event):
# Get the position on the scrollable canvas
xOnCanvas = self.canvas.canvasx(event.x)
yOnCanvas = self.canvas.canvasy(event.y)
Reference answer: How to convert Tkinter canvas coordinate to window?
It's all right there in the documentation. canvasx and canvasy methods. – Bryan Oakley
My requirement is i need to drag an image to desired location.
Based on the link board-drawing code to move an oval
the following is the code snapshot i tried. I am not getting any errors its blank. Please let me know the way to take it forward.
Sample image segment attached
import Tkinter as tk
from Tkinter import *
from PIL import ImageTk, Image
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
# create a canvas
self.canvas = tk.Canvas(width=400, height=400)
self.canvas.pack(fill="both", expand=True)
# this data is used to keep track of an
# item being dragged
self._drag_data1 = {"x": 0, "y": 0, "item1": None}
startframe = tk.Frame(root)
canvas = tk.Canvas(startframe,width=1280,height=720)
startframe.pack()
canvas.pack()
one = tk.PhotoImage(file=r'images/test1.gif')
root.one = one # to prevent the image garbage collected.
canvas.create_image((0,0), image=one, anchor='nw',tags="img1")
self.canvas.tag_bind("img1", "<1>", self.on_token_press1)
self.canvas.tag_bind("img1", "<1>", self.on_token_release1)
self.canvas.tag_bind("token", "<B1-Motion>", self.on_token_motion1)
def on_token_press1(self, event):
print("sss")
# record the item and its location
self._drag_data1["item1"] = self.canvas.find_closest(event.x, event.y)[0]
self._drag_data1["x"] = event.x
self._drag_data1["y"] = event.y
def on_token_release1(self, event):
# reset the drag information
self._drag_data1["item1"] = None
self._drag_data1["x"] = 0
self._drag_data1["y"] = 0
def on_token_motion1(self, event):
'''Handle dragging of an object'''
# compute how much the mouse has moved
delta_x = event.x - self._drag_data1["x"]
delta_y = event.y - self._drag_data1["y"]
# move the object the appropriate amount
self.canvas.move(self._drag_data1["item1"], delta_x, delta_y)
# record the new position
self._drag_data1["x"] = event.x
self._drag_data1["y"] = event.y
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
As said in the comments:
Indentation is important!
Within the Example-class its initiation method this entire code-block is misaligned:
startframe = tk.Frame(root)
canvas = tk.Canvas(startframe,width=1280,height=720)
startframe.pack()
canvas.pack()
one = tk.PhotoImage(file=r'images/test1.gif')
root.one = one # to prevent the image garbage collected.
canvas.create_image((0,0), image=one, anchor='nw',tags="img1")
self.canvas.tag_bind("img1", "<1>", self.on_token_press1)
self.canvas.tag_bind("img1", "<1>", self.on_token_release1)
self.canvas.tag_bind("token", "<B1-Motion>", self.on_token_motion1)
If I simply add a Tab to the indentation of the entire block I was able to run the OP's code without any problem.
Since the script requires a GIF file to be placed in images/test1.gif I downloaded and used this GIF:
tkinter doesn't seem to actually play the gif (which isn't asked by the OP), but it does indeed show it.
I know how to remove a border from a Tkinter window using overrideredirect, but whenever I do that the window becomes unresponsive. I can't move it using alt and dragging, or any other method.
I want to make an application that looks like one of those "riced" applications that are just a bare window, and obviously I can't get very far if it just sits unresponsive in the upper-left corner. So, how do I do this?
To make the window draggable, put bindings for <Button-1> (mouse clicks) and <B1-Motion> (mouse movements) on the window.
All you need to do is store the x and y values of a mouse down event and then during mouse motion events, you position the window based on the current pointer x and y, delta the original event x and y.
The handler for the mouse click binding stores the original event x and y.
The handler for the mouse movement binding calls the TopLevel method geometry() to reposition the window, based on current mouse position and the offset you have stored from the most recent mouse click. You supply a geometry string to the geometry method.
Here is a very minimal example which does not take into account the edges of the screen:
import tkinter
class Win(tkinter.Tk):
def __init__(self,master=None):
tkinter.Tk.__init__(self,master)
self.overrideredirect(True)
self._offsetx = 0
self._offsety = 0
self.bind('<Button-1>',self.clickwin)
self.bind('<B1-Motion>',self.dragwin)
def dragwin(self,event):
x = self.winfo_pointerx() - self._offsetx
y = self.winfo_pointery() - self._offsety
self.geometry('+{x}+{y}'.format(x=x,y=y))
def clickwin(self,event):
self._offsetx = event.x
self._offsety = event.y
win = Win()
win.mainloop()
EDIT by TheLizzard:
The code above works but doesn't behave correctly when there is more than one widget so this is the fixed code:
import tkinter as tk
class Win(tk.Tk):
def __init__(self):
super().__init__()
super().overrideredirect(True)
self._offsetx = 0
self._offsety = 0
super().bind("<Button-1>" ,self.clickwin)
super().bind("<B1-Motion>", self.dragwin)
def dragwin(self,event):
x = super().winfo_pointerx() - self._offsetx
y = super().winfo_pointery() - self._offsety
super().geometry(f"+{x}+{y}")
def clickwin(self,event):
self._offsetx = super().winfo_pointerx() - super().winfo_rootx()
self._offsety = super().winfo_pointery() - super().winfo_rooty()
root = Win()
label_1 = tk.Label(root, text="Label 1")
label_1.pack(side="left")
label_2 = tk.Label(root, text="Label 2")
label_2.pack(side="left")
root.mainloop()
Thanks to #dusty's answer, it had a jumping problem, and I solved it by saving the window location.
import tkinter
class Win(tkinter.Tk):
def __init__(self,master=None):
tkinter.Tk.__init__(self,master)
self.overrideredirect(True)
self._offsetx = 0
self._offsety = 0
self._window_x = 500
self._window_y = 100
self._window_w = 500
self._window_h = 500
self.geometry('{w}x{h}+{x}+{y}'.format(w=self._window_w,h=self._window_h,x=self._window_x,y=self._window_y))
self.bind('<Button-1>',self.clickwin)
self.bind('<B1-Motion>',self.dragwin)
def dragwin(self,event):
delta_x = self.winfo_pointerx() - self._offsetx
delta_y = self.winfo_pointery() - self._offsety
x = self._window_x + delta_x
y = self._window_y + delta_y
self.geometry("+{x}+{y}".format(x=x, y=y))
self._offsetx = self.winfo_pointerx()
self._offsety = self.winfo_pointery()
self._window_x = x
self._window_y = y
def clickwin(self,event):
self._offsetx = self.winfo_pointerx()
self._offsety = self.winfo_pointery()
win = Win()
win.mainloop()
self._window_x and self._window_y are the primary position of the window.
self._window_h and self._window_w are the height and width of the window.
This solution is works for me:
from tkinter import *
import mouse
global x, y
def standard_bind():
root.bind('<B1-Motion>', lambda e: event(e, Mode=True))
def event(widget, Mode=False):
global x, y
if Mode:
x = widget.x
y = widget.y
root.bind('<B1-Motion>', lambda e: event(e))
root.geometry('+%d+%d' % (mouse.get_position()[0]-x, mouse.get_position()[1]-y))
root = Tk()
root.overrideredirect(True)
root.bind('<B1-Motion>', lambda e: event(e, Mode=True))
root.bind('<ButtonRelease-1>', lambda e: standard_bind())
root.geometry('%dx%d+%d+%d' % (600, 60, 50, 50))
mainloop()
Here a bit more sophisticated method which assumes that you don't want to just click any where on the tkinter app to move it, but rather clicking on the title bar to move the app around while retaining the familiar "X" to close the app.
Works for python3.0 and later
Since tkinter does not (by default) allow you to directly achieve this, we must:
Remove the tkinter frame's title bar
Create our own title bar and recreate the "x" for closing the app
bind the event for clicking, such that the app moves when dragged
from tkinter import *
root = Tk()
root.title('The Name of Your Application')
root.geometry("500x300")
# remove title bar
root.overrideredirect(True)
def move_app(e):
root.geometry(f'+{e.x_root}+{e.y_root}')
def quitter(e):
root.quit()
#root.destroy()
# Create Fake Title Bar
title_bar = Frame(root, bg="darkgreen", relief="raised", bd=0)
title_bar.pack(expand=1, fill=X)
# Bind the titlebar
title_bar.bind("<B1-Motion>", move_app)
# Create title text
title_label = Label(title_bar, text=" My Awesome App!!", bg="darkgreen", fg="white")
title_label.pack(side=LEFT, pady=4)
# Create close button on titlebar
close_label = Label(title_bar, text=" X ", bg="darkgreen", fg="white", relief="sunken", bd=0)
close_label.pack(side=RIGHT, pady=4)
close_label.bind("<Button-1>", quitter)
my_button = Button(root, text="CLOSE!", font=("Helvetica, 32"), command=root.quit)
my_button.pack(pady=100)
root.mainloop()