python tkinter - using canvas.scan_dragto to reset after panning the canvas - python

I'm building a GUI that displays a map, with several objects drawn on a canvas to represent locations or items. I want the user to be able to pan and zoom on the canvas and have a button to reset the zoom and view back to origin.
I'm having trouble with the reset functionality. I can make the zoom and pan work fine, but if the user just clicks on the canvas then hits the reset button, it moves the canvas when it should be static.
I've created the example code below to illustrate the issue, where if you pan the canvas with a right click and drag then reset it works fine, but if you just right click then reset it moves the canvas, even though you haven't dragged it. I think the issue is because dragto compares the last mouse event to the dragto parameters (so a right click release generates an event with mouseclick x and y coords). So even if the event didn't result in a drag of the canvas, it thinks the canvas has moved. Is there a way to prevent a click release event from being compared into a dragto or an alternative way to reset the canvas to it's original position?
Thx
import tkinter as tk
from customtkinter import *
class App(CTk):
WIDTH = 500
HEIGHT = 700
def __init__(self):
super().__init__()
self.root = CTk._root
self.prior_offset_x = 0
self.prior_offset_y = 0
self.offset_x = 0
self.offset_y = 0
self.move_start_x = 0
self.move_start_y = 0
self.drag_x = 0
self.drag_y = 0
self.title("QSI model.py")
self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.canvas = tk.Canvas(master=self, width=App.WIDTH, background='yellow green', bd=2, height=App.HEIGHT)
self.canvas.place(x=0, y=0)
self.button_add = CTkButton(master=self.canvas, width=30, height=30, text="o", fg_color=("gray75", "gray30"), command=self.scale_reset, corner_radius=5)
self.button_add.place(x=15, y=15)
self.canvas.bind("<ButtonPress-2>", self.move_start)
self.canvas.bind("<B2-Motion>", self.move_move)
canvas_tile = self.canvas.create_rectangle(100, 100, 200, 200, outline='black', tags="oval")
def move_start(self, event):
self.canvas.scan_mark(event.x, event.y)
print('Mark', event.x, event.y)
self.prior_offset_x, self.prior_offset_y = self.offset_x, self.offset_y
self.move_start_x, self.move_start_y = event.x, event.y
def move_move(self, event):
self.canvas.scan_dragto(event.x, event.y, gain=1)
self.offset_x = self.prior_offset_x + (event.x - self.move_start_x)
self.offset_y = self.prior_offset_y + (event.y - self.move_start_y)
self.drag_x, self.drag_y = event.x, event.y
def scale_reset(self):
self.canvas.scan_dragto(self.drag_x - self.offset_x, self.drag_y - self.offset_y, gain=1)
self.drag_x = self.drag_x - self.offset_x
self.drag_y = self.drag_y - self.offset_y
self.offset_x = 0
self.offset_y = 0
self.prior_offset_x = 0
self.prior_offset_y = 0
def on_closing(self):
self.destroy()
if __name__ == '__main__':
app = App()
app.mainloop()

Related

Updating location of circle with mouse movement breaks on fast mouse movement, tkinter-python

I am making a draggable point(a circle). The code works, however, while dragging, if the mouse motion is quick the point stops moving. I have taken help from this code for making this program. I will be using this point later on for other purposes. Here is my full code,
from tkinter import *
import sys,os,string,time
class Point():
def __init__(self,canvas,x,y):
self.canvas = canvas
# It could be that we start dragging a widget
# And release it while its on another
# Hence when we land on a widget we set self.loc to 1
# And when we start dragging it we set self.dragged to 1
self.loc = self.dragged = 0
self.x = x
self.y = y
self.radius = 5
self.point = canvas.create_oval(self.x-self.radius,self.y-self.radius,self.x+self.radius,self.y+self.radius,fill="green",tag="Point")
canvas.tag_bind("Point","<ButtonPress-1>",self.down)
canvas.tag_bind("Point","<ButtonRelease-1>",self.chkup)
canvas.tag_bind("Point","<Enter>",self.enter)
canvas.tag_bind("Point","<Leave>",self.leave)
def down(self,event):
self.loc = 1
self.dragged = 0
event.widget.bind("<Motion>",self.motion)
canvas.itemconfigure(self.point,fill = "red")
def motion(self,event):
root.config(cursor = "exchange")
cnv = event.widget
cnv.itemconfigure(self.point,fill = "red")
self.x,self.y = cnv.canvasx(event.x), cnv.canvasy(event.y)
got = canvas.coords(self.point,self.x-self.radius,self.y-self.radius,self.x+self.radius,self.y+self.radius)
def enter(self,event):
canvas.itemconfigure(self.point,fill="blue")
self.loc = 1
if self.dragged == event.time:
self.up(event)
def up(self,event):
event.widget.unbind("<Motion>")
canvas.itemconfigure(self.point,fill="green")
self.canvas.update()
def chkup(self,event):
event.widget.unbind("<Motion>")
root.config(cursor = "")
canvas.itemconfigure(self.point,fill="green")
if self.loc: # is button released in the same widget as pressed
self.up(event)
else:
self.dragged = event.time
def leave(self,event):
self.up(event)
root = Tk()
root.title("Drag and Drop")
canvas = Canvas(root,width = 256, height = 256, borderwidth = 1)
point = Point(canvas,128,128)
canvas.pack()
root.mainloop()
Your problem is that your <Leave> binding can fire if you move the mouse outside of the tiny circle faster than you can process the move. That causes the binding for <Motion> to be disabled.
My recommendation is to a) don't bind on <Leave> to disable the binding, and b) bind on <B1-Motion> so that the binding is active only while the button is pressed.

Creating a crop tool for tkinter: The cropping tool crops in other places

I'm creating in tkinter a Crop Tool that is similar in Photoshop. This code has a function that is supposed to crop a moveable image within the cropping box (2 in x 2 in, passport size, and so on). The problem is, the code often crops portions of the image outside the box.
For example, if I have a portrait and aimed the face at the rectangle, the code would crop the hat instead, or anywhere but the face.
I tried to use bbox, event objects, etc. but the measurements end up wrong. Please help me. Thanks.
Here is a partial code. Sorry if it's a quite lengthy.
from tkinter import *
from tkinter import ttk
import tkinter as tk
from tkinter import messagebox
from tkinter.filedialog import askopenfilename, asksaveasfilename
from PIL import Image, ImageTk
class PictureEditor:
# Quits when called
#staticmethod
# Opens an image
def open_app(self, event=None):
self.canvas.delete(ALL)
# Opens a window to choose a file=
self.openfile = askopenfilename(initialdir = # "Filename here")
if self.openfile:
with open(self.openfile) as _file:
# if file is selected by user, I'm going to delete
# the contents inside the canvas widget
self.canvas.delete(1.0, END)
self.im = Image.open(self.openfile)
self.image = ImageTk.PhotoImage(self.im)
self.a1 = self.canvas.create_image(0, 0, anchor=NW,
image=self.image, tags="image")
self.image_dim = self.canvas.bbox(self.a1)
self.imx = self.image_dim[0]
self.imy = self.image_dim[1]
# updating text widget
window.update_idletasks()
def on_drag(self, event):
# record the item and its location
self._drag_data["item"] = self.canvas.find_closest(event.x, event.y)[0]
self._drag_data["x"] = event.x
self._drag_data["y"] = event.y
self.origx = event.x
self.origy = event.y
def on_release(self, event):
# when I release the mouse, this happens
# reset the drag information
self._drag_data["item"] = None
self._drag_data["x"] = 0
self._drag_data["y"] = 0
self.newx = event.x
self.newy = event.y
# Measures mouse movement from one point to another
self.movex = self.origx - self.newx
self.movey = self.origy - self.newy
def on_motion(self, event):
# handles the dragging of an object
# compute how much the mouse has moved
delta_x = event.x - self._drag_data["x"]
delta_y = event.y - self._drag_data["y"]
# move the object the appropriate amount
self.canvas.move(self._drag_data["item"], delta_x, delta_y)
# record the new position
self._drag_data["x"] = event.x
self._drag_data["y"] = event.y
def draw(self, event, x1=None, y1=None,x2=None,y2=None):
# deleting contents of border, if any.
try:
self.canvas.delete(self.border)
except:
pass
# if an item is selected
selection = self.combo.get()
if selection == 'No Crop':
x1, y1, x2, y2 = None, None, None, None
if selection == '2 in x 2 in':
x1, y1, x2, y2 = self.imx, self.imy, self.imx + 200, self.imy + 200
if selection == '1 in x 1 in':
x1, y1, x2, y2 = self.imx, self.imy, self.imx + 100, self.imy + 100
if selection == 'Passport Size':
x1, y1, x2, y2 = self.imx, self.imy, self.imx + 132.28, self.imy
+170.079
if x1 != None or y1 != None or x2 != None or y2 != None:
self.dimensions = {"x1":x1, "y1":y1, "x2":x2, "y2":y2}
width = 5
self.border = self.canvas.create_rectangle(x1+ width, y1 +
width, x2 + width, y2 + width, width=width, outline="#ffffff", fill ="",
tags = "rectangle")
else:
pass
def crop(self, event=None):
# cropping the image
try:
self.crop_image = self.im.crop((self.dimensions["x1"] +
self.movex,
self.dimensions["y1"] + self.movey,
self.dimensions["x2"] + self.movex,
self.dimensions["y2"] + self.movey))
except:
print("cropping failed")
return 1
self.newly_cropped = ImageTk.PhotoImage(self.crop_image)
try:
new_image = self.canvas.create_image(120, 120,
image=self.newly_cropped)
print("Image is cropped")
except:
print("Cropping failed")
def __init__(self,window):
frame1 = Frame(bg='red')
frame1.pack(side=TOP, fill=X)
frame2height = 600
frame2width = 600
frame2 = Frame(window, bd=2, relief=SUNKEN)
frame2.pack(side=LEFT, fill=X)
frame3 = Frame(bg='green')
frame3.pack(side=LEFT, fill=X)
# Button that open pictures
open = Button(frame1, text='Open Pic', padx=20, command =
self.open_app)
open.pack(pady=5, padx=5, side=LEFT)
# Creating a canvas widget
self.canvas = tk.Canvas(frame2, height=frame2height,
width=frame2width,
bg='gray')
self.xsb = Scrollbar(frame2, orient="horizontal",
command=self.canvas.xview)
self.ysb = Scrollbar(frame2, orient="vertical",
command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.ysb.set,
xscrollcommand=self.xsb.set)
self.canvas.configure(scrollregion=(0, 0, 1000, 1000))
# keeps track of data being dragged
self._drag_data = {"x": 0, "y": 0, "item": None}
# creating image and crop border
self.canvas.tag_bind("image","<ButtonPress-1>", self.on_drag)
self.canvas.tag_bind("image","<ButtonRelease-1>", self.on_release)
self.canvas.tag_bind("image","<B1-Motion>", self.on_motion)
# widget positions in frame2
self.xsb.pack(side=BOTTOM, fill=X)
self.canvas.pack(side=LEFT)
self.ysb.pack(side=LEFT, fill=Y)
self.combo = ttk.Combobox(frame1)
# Combobox selections
self.combo['values'] = ('No Crop', '2 in x 2 in', '1 in x 1 in',
'Passport Size')
self.combo.current(0)
self.combo.pack(pady=5, padx=5, side=LEFT)
self.combo.bind("<Button-1>", self.draw)
# Button that crops picture
self.crop = Button(frame1, text='Crop Pic', padx=20,
command=self.crop)
self.crop.pack(pady=5, padx=5, side=LEFT)
# this window has all the properties of tkinter.
# .Tk() declares this variable as the frame
window = tk.Tk()
# .title() will input whatever title you want for the app
window.title("ID Picture Generator")
# .geometry() sets the size in pixels of what the window will be
window.geometry("800x600")
app = PictureEditor(window)
# runs everything inside the window
window.mainloop()

Basic image editing on Python tkinter working but unwanted image scrolling course when dragging mouse

The following code produces a nice Canvas with an image and I can draw a square on top of it. However:
a) I can't get the Canvas to not scroll.
b) I only want the image to appear and nothing else and can't get the sizes right
As you will see, I have even tried to stop the scrolling but it does not work all the time. In addition the image is never fully aligned with the Canvas nor the window even though I set the sizes to be the same for the three (root, canvas and image).
Here is the code (partly taken already from another example with some portions commented out):
try:
from PIL import Image
except ImportError:
import Image
from PIL import ImageTk
try:
import Tkinter as tk # Python2
except ImportError:
import tkinter as tk # Python3s
import Tkinter
from Tkinter import *
import PIL as PILAll
class ExampleApp(Frame):
def __init__(self,master):
Frame.__init__(self,master=None)
self.x = 0
self.y = 0
self.canvas = Canvas(self, cursor="cross", width=640, height=480, confine=True, scrollregion=(10, 10, 10, 10), relief="groove", bg="blue")# and I have experimented with a few other options
#self.sbarv=Scrollbar(self,orient=VERTICAL)
#self.sbarh=Scrollbar(self,orient=HORIZONTAL)
#self.sbarv.config(command=self.canvas.yview)
#self.sbarh.config(command=self.canvas.xview)
self.canvas.config()#yscrollcommand=self.sbarv.set)
self.canvas.config()#xscrollcommand=self.sbarh.set)
self.canvas.config(scrollregion=self.canvas.bbox(ALL))
self.canvas.grid(row=0,column=0,sticky=N+S+E+W)
#self.sbarv.grid(row=0,column=1,stick=N+S)
#self.sbarh.grid(row=1,column=0,sticky=E+W)
self.canvas.bind("<ButtonPress-1>", self.on_button_press)
self.canvas.bind("<B1-Motion>", self.on_move_press)
self.canvas.bind("<ButtonRelease-1>", self.on_button_release)
self.canvas.bind("<Leave>", self.on_button_leave)
self.canvas.bind("<Enter>", self.on_button_enter)
self.canvas.bind("<Double-Button-1>", self.on_double_click)
self.canvas.create_line(0, 0, 200, 100)
self.canvas.create_line(0, 100, 200, 0, fill="red", dash=(4, 4))
self.canvas.create_rectangle(50, 25, 150, 75, fill="blue")
self.rect = None
self.text = None
self.start_x = None
self.start_y = None
self.im = PILAll.Image.open("../../" + "image6.JPG")
self.wazil,self.lard=self.im.size
self.canvas.config() #scrollregion=(0,0,self.wazil,self.lard))
self.tk_im = ImageTk.PhotoImage(self.im)
self.canvas.create_image(0,0,anchor="nw",image=self.tk_im)
out_of_scope = 1
def on_button_leave(self, event):
self.out_of_scope = 2
print "out_of_scope....", self.out_of_scope
def on_button_enter(self, event):
print("entering...")
self.out_of_scope = 1
def on_double_click(self, event):
print("double click")
def on_button_press(self, event):
# save mouse drag start position
self.start_x = self.canvas.canvasx(event.x)
self.start_y = self.canvas.canvasy(event.y)
# create rectangle if not yet exist
if not self.rect:
if self.out_of_scope == 1:
self.rect = self.canvas.create_rectangle(self.x, self.y, 1, 1, outline='blue', fill='yellow') #since it's only created once it always remains at the bottom
def get_out_of_scope(self, x, y):
return self.out_of_scope
def on_move_press(self, event):
curX = self.canvas.canvasx(event.x)
curY = self.canvas.canvasy(event.y)
var=self.get_out_of_scope(event.x, event.y)
print(var, event.x, event.y)
if var == 1:
w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
if event.x > 0.9*w:
self.canvas.xview_scroll(1, 'units')
elif event.x < 0.1*w:
self.canvas.xview_scroll(-1, 'units')
if event.y > 0.9*h:
self.canvas.yview_scroll(1, 'units')
elif event.y < 0.1*h:
self.canvas.yview_scroll(-1, 'units')
# expand rectangle as you drag the mouse
self.canvas.coords(self.rect, self.start_x, self.start_y, curX, curY)
def on_button_release(self, event):
print(event.x, event.y)
pass
root=Tk()
root.geometry("640x480")
app = ExampleApp(root)
app.grid()
root.mainloop()
I think your code would benefit from beingreviewed but I will try to limit myself to the question...
If the canvas needs to be the same size as the image why is it constructed with width=640, height=480? You figure out the width and height of the image further down:
self.im = PILAll.Image.open("../../" + "image6.JPG")
self.wazil,self.lard=self.im.size
(interesting variable name choice btw) so if self.wazil and self.lard represent the width and height of the image why don't you make that the width and height of the canvas?
self.im = PILAll.Image.open("../../" + "image6.JPG")
self.wazil,self.lard=self.im.size
self.canvas = Canvas(self, width=self.wazil, height=self.lard) #, ...)
then the canvas will be the correct size but the root window is still forcing itself to be 640x480 from:
root.geometry("640x480")
but since widgets will automatically scale themselves to the contents you can just comment that line out and it should be the correct size.
#root.geometry("640x480")
I should note that I was experiencing some very odd behaviour about the position of the image being 3 pixels too high and 3 pixels to the left, drawing the image with:
self.canvas.create_image(3,3,anchor="nw",image=self.tk_im)
fixed it for me but I have no idea why...
As for the scrolling you removed the parts about the scroll bars but you left in this in on_move_press:
w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
if event.x > 0.9*w:
self.canvas.xview_scroll(1, 'units')
elif event.x < 0.1*w:
self.canvas.xview_scroll(-1, 'units')
if event.y > 0.9*h:
self.canvas.yview_scroll(1, 'units')
elif event.y < 0.1*h:
self.canvas.yview_scroll(-1, 'units')
This is the section of code that is occasionally scrolling the canvas (happened when I tried to resize the window) so again you can comment that section out and it shouldn't scroll anymore.
Last note which is unrelated to question, you have:
def __init__(self,master):
Frame.__init__(self,master=None)
But I'm pretty sure you mean to have:
def __init__(self,master=None):
Frame.__init__(self,master)
since the first way you require a master argument but do not pass it to Frame.__init__. When Frame.__init__ receives a master of None it just uses the Tk instance which in your case is the same thing but if you used any other master it would cause very odd issues.

Tkinter: Draw rectangle using a mouse

Please, help me to resolve this problem.
I want to allow the user to draw a random rectangle around a specific region of interest in a picture using the mouse ( by clicking the right or left button of the mouse until he releases it).
I deal with large images (images larger than the resolution of my screen, such as this one), so the user needs to scroll the window in order to be able to see the picture fully.
Here is the code I tried just to display a large picture, but I have no idea on how to allow the user to draw using his mouse a rectangle over an object (say a person in a picture):
from Tkinter import *
import Image,ImageTk
root=Tk()
canv=Canvas(root,relief=SUNKEN)
sbarv=Scrollbar(root,orient=VERTICAL)
sbarh=Scrollbar(root,orien=HORIZONTAL)
sbarv.config(command=canv.yview)
sbarh.config(command=canv.xview)
canv.config(yscrollcommand=sbarv.set)
canv.config(xscrollcommand=sbarh.set)
canv.grid(row=0,column=0,sticky=N+S+E+W)
sbarv.grid(row=0,column=1,sticky=N+S)
sbarh.grid(row=1,column=0,sticky=E+W)
im=Image.open("image.jpg")
width,height=im.size
canv.config(scrollregion=(0,0,width,height))
im2=ImageTk.PhotoImage(im)
imgtag=canv.create_image(0,0,anchor="nw",image=im2)
root.mainloop()
EDIT 1:
The rectangle must not be filled. I mean I want to draw only its 4 lines (segments) but it must be empty inside, I want to draw only its contours in one pixel width.
I also want to draw as the cursor is moving (dragging) not after button release.
Also, note that the rectangle to draw may be a long one, I mean the vertical scroll-bar will need to move down to be able to delimitate the whole object of interest (let's say it is a person)
Any help will be highly appreciated.
Thank you very much in advance
EDIT 2:
Following the link given to me above, I coded this. My problem is that the scroll-bars do not appear. May be someone could tell me why ?
Note that in this code, I resolved the first and second problems highlighted in EDIT 1:
import PIL.Image
import Image
import ImageTk
from Tkinter import *
class ExampleApp(Frame):
def __init__(self,master):
Frame.__init__(self,master=None)
self.x = self.y = 0
self.canvas = Canvas(master, cursor="cross")
self.sbarv=Scrollbar(self,orient=VERTICAL)
self.sbarh=Scrollbar(self,orient=HORIZONTAL)
self.sbarv.config(command=self.canvas.yview)
self.sbarh.config(command=self.canvas.xview)
self.canvas.config(yscrollcommand=self.sbarv.set)
self.canvas.config(xscrollcommand=self.sbarh.set)
self.canvas.grid(row=0,column=0,sticky=N+S+E+W)
self.sbarv.grid(row=0,column=1,stick=N+S)
self.sbarh.grid(row=1,column=0,sticky=E+W)
self.canvas.bind("<ButtonPress-1>", self.on_button_press)
self.canvas.bind("<B1-Motion>", self.on_move_press)
self.canvas.bind("<ButtonRelease-1>", self.on_button_release)
self.rect = None
self.start_x = None
self.start_y = None
self.im = PIL.Image.open("logo.png")
self.wazil,self.lard=self.im.size
self.canvas.config(scrollregion=(0,0,self.wazil,self.lard))
self.tk_im = ImageTk.PhotoImage(self.im)
self.canvas.create_image(0,0,anchor="nw",image=self.tk_im)
def on_button_press(self, event):
# save mouse drag start position
self.start_x = event.x
self.start_y = event.y
# create rectangle if not yet exist
#if not self.rect:
self.rect = self.canvas.create_rectangle(self.x, self.y, 1, 1, fill="")
def on_move_press(self, event):
curX, curY = (event.x, event.y)
# expand rectangle as you drag the mouse
self.canvas.coords(self.rect, self.start_x, self.start_y, curX, curY)
def on_button_release(self, event):
pass
if __name__ == "__main__":
root=Tk()
app = ExampleApp(root)
root.mainloop()
The Scrollbars do not show because you grid them into a Frame (self.sbarv=Scrollbar(self, ...)) which you do not place into the parent window. You directly grid the Canvas into the parent window though (self.canvas = Canvas(master, ...)).
What you should do is also put the Canvas in self and then pack the Frame into the master window using
app = ExampleApp(root)
app.pack()
However, when scrolling, the event.x and event.y do not represent the correct position on tha canvas anymore, so you should use
self.start_x = self.canvas.canvasx(event.x)
self.start_y = self.canvas.canvasy(event.y)
and
curX = self.canvas.canvasx(event.x)
curY = self.canvas.canvasy(event.y)
Then, I understand you want to automatically scroll the canvas when the mouse is dragging to one of the borders of the canvas? To do that, you need to check if the mouse is at one of the edges of the canvas and scroll in that direction if it is. You can use something like:
w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
if event.x > 0.9*w:
self.canvas.xview_scroll(1, 'units')
elif event.x < 0.1*w:
self.canvas.xview_scroll(-1, 'units')
if event.y > 0.9*h:
self.canvas.yview_scroll(1, 'units')
elif event.y < 0.1*h:
self.canvas.yview_scroll(-1, 'units')
So, all that implemented in your code becomes:
import PIL.Image
import Image
import ImageTk
from Tkinter import *
class ExampleApp(Frame):
def __init__(self,master):
Frame.__init__(self,master=None)
self.x = self.y = 0
self.canvas = Canvas(self, cursor="cross")
self.sbarv=Scrollbar(self,orient=VERTICAL)
self.sbarh=Scrollbar(self,orient=HORIZONTAL)
self.sbarv.config(command=self.canvas.yview)
self.sbarh.config(command=self.canvas.xview)
self.canvas.config(yscrollcommand=self.sbarv.set)
self.canvas.config(xscrollcommand=self.sbarh.set)
self.canvas.grid(row=0,column=0,sticky=N+S+E+W)
self.sbarv.grid(row=0,column=1,stick=N+S)
self.sbarh.grid(row=1,column=0,sticky=E+W)
self.canvas.bind("<ButtonPress-1>", self.on_button_press)
self.canvas.bind("<B1-Motion>", self.on_move_press)
self.canvas.bind("<ButtonRelease-1>", self.on_button_release)
self.rect = None
self.start_x = None
self.start_y = None
self.im = PIL.Image.open("logo.png")
self.wazil,self.lard=self.im.size
self.canvas.config(scrollregion=(0,0,self.wazil,self.lard))
self.tk_im = ImageTk.PhotoImage(self.im)
self.canvas.create_image(0,0,anchor="nw",image=self.tk_im)
def on_button_press(self, event):
# save mouse drag start position
self.start_x = self.canvas.canvasx(event.x)
self.start_y = self.canvas.canvasy(event.y)
# create rectangle if not yet exist
if not self.rect:
self.rect = self.canvas.create_rectangle(self.x, self.y, 1, 1, outline='red')
def on_move_press(self, event):
curX = self.canvas.canvasx(event.x)
curY = self.canvas.canvasy(event.y)
w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
if event.x > 0.9*w:
self.canvas.xview_scroll(1, 'units')
elif event.x < 0.1*w:
self.canvas.xview_scroll(-1, 'units')
if event.y > 0.9*h:
self.canvas.yview_scroll(1, 'units')
elif event.y < 0.1*h:
self.canvas.yview_scroll(-1, 'units')
# expand rectangle as you drag the mouse
self.canvas.coords(self.rect, self.start_x, self.start_y, curX, curY)
def on_button_release(self, event):
pass
if __name__ == "__main__":
root=Tk()
app = ExampleApp(root)
app.pack()
root.mainloop()

Tkinter: Mouse drag a window without borders, eg. overridedirect(1)

Any suggestions on how one might create event bindings that would allow a user to mouse drag a window without borders, eg. a window created with overridedirect(1)?
Use case: We would like to create a floating toolbar/palette window (without borders) that our users can drag around on their desktop.
Here's where I'm at in my thinking (pseudo code):
window.bind( '<Button-1>', onMouseDown ) to capture the initial position of the mouse.
window.bind( '<Motion-1>', onMouseMove ) to track position of mouse once it starts to move.
Calculate how much mouse has moved and calculate newX, newY positions.
Use window.geometry( '+%d+%d' % ( newX, newY ) ) to move window.
Does Tkinter expose enough functionality to allow me to implement the task at hand? Or are there easier/higher-level ways to achieve what I want to do?
Yes, Tkinter exposes enough functionality to do this, and no, there are no easier/higher-level ways to achive what you want to do. You pretty much have the right idea.
Here's one example, though it's not the only way:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.floater = FloatingWindow(self)
class FloatingWindow(tk.Toplevel):
def __init__(self, *args, **kwargs):
tk.Toplevel.__init__(self, *args, **kwargs)
self.overrideredirect(True)
self.label = tk.Label(self, text="Click on the grip to move")
self.grip = tk.Label(self, bitmap="gray25")
self.grip.pack(side="left", fill="y")
self.label.pack(side="right", fill="both", expand=True)
self.grip.bind("<ButtonPress-1>", self.start_move)
self.grip.bind("<ButtonRelease-1>", self.stop_move)
self.grip.bind("<B1-Motion>", self.do_move)
def start_move(self, event):
self.x = event.x
self.y = event.y
def stop_move(self, event):
self.x = None
self.y = None
def do_move(self, event):
deltax = event.x - self.x
deltay = event.y - self.y
x = self.winfo_x() + deltax
y = self.winfo_y() + deltay
self.geometry(f"+{x}+{y}")
app=App()
app.mainloop()
Here is my solution:
from tkinter import *
from webbrowser import *
lastClickX = 0
lastClickY = 0
def SaveLastClickPos(event):
global lastClickX, lastClickY
lastClickX = event.x
lastClickY = event.y
def Dragging(event):
x, y = event.x - lastClickX + window.winfo_x(), event.y - lastClickY + window.winfo_y()
window.geometry("+%s+%s" % (x , y))
window = Tk()
window.overrideredirect(True)
window.attributes('-topmost', True)
window.geometry("400x400+500+300")
window.bind('<Button-1>', SaveLastClickPos)
window.bind('<B1-Motion>', Dragging)
window.mainloop()
The idea of Loïc Faure-Lacroix is useful, the following is my own simple code snippets on Python3.7.3, hope it will help:
from tkinter import *
def move_window(event):
root.geometry(f'+{event.x_root}+{event.y_root}')
root = Tk()
root.bind("<B1-Motion>", move_window)
root.mainloop()
But the position of the mouse is always in the upper left corner of the window. How can I keep it unchanged? Looking forward to a better answer!
Thanks to Bryan Oakley, because at the beginning I couldn't run your code on my computer, I didn't pay attention to it. Just now after the modification, it was very good to run, and the above situation would not happen (the mouse is always in the upper left corner), The updated code recently as follows:
def widget_drag_free_bind(widget):
"""Bind any widget or Tk master object with free drag"""
if isinstance(widget, Tk):
master = widget # root window
else:
master = widget.master
x, y = 0, 0
def mouse_motion(event):
global x, y
# Positive offset represent the mouse is moving to the lower right corner, negative moving to the upper left corner
offset_x, offset_y = event.x - x, event.y - y
new_x = master.winfo_x() + offset_x
new_y = master.winfo_y() + offset_y
new_geometry = f"+{new_x}+{new_y}"
master.geometry(new_geometry)
def mouse_press(event):
global x, y
count = time.time()
x, y = event.x, event.y
widget.bind("<B1-Motion>", mouse_motion) # Hold the left mouse button and drag events
widget.bind("<Button-1>", mouse_press) # The left mouse button press event, long calculate by only once
Try this, and it surely works;
Create an event function to move window:
def movewindow(event):
root.geometry('+{0}+{1}'.format(event.x_root, event.y_root))
Bind window:
root.bind('', movewindow)
Now you can touch the the window and drag
This code is the same as Bryan's solution but it does not use overridedirect.
It was tested with: python 3.7, Debian GNU/Linux 10 (buster), Gnome 3.30
import tkinter as tk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.floater = FloatingWindow(self)
class FloatingWindow(tk.Toplevel):
def __init__(self, *args, **kwargs):
tk.Toplevel.__init__(self, *args, **kwargs)
#self.overrideredirect(True)
self.resizable(0, 0) # Window not resizable
self.wm_attributes('-type', 'splash') # Hide title bar (Linux)
self.label = tk.Label(self, text="Click on the grip to move")
self.grip = tk.Label(self, bitmap="gray25")
self.grip.pack(side="left", fill="y")
self.label.pack(side="right", fill="both", expand=True)
self.grip.bind("<ButtonPress-1>", self.StartMove)
self.grip.bind("<ButtonRelease-1>", self.StopMove)
self.grip.bind("<B1-Motion>", self.OnMotion)
def StartMove(self, event):
self.x = event.x
self.y = event.y
def StopMove(self, event):
self.x = None
self.y = None
def OnMotion(self, event):
deltax = event.x - self.x
deltay = event.y - self.y
x = self.winfo_x() + deltax
y = self.winfo_y() + deltay
self.geometry("+%s+%s" % (x, y))
app = App()
app.mainloop()

Categories