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()
Related
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()
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.
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.
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()
I have a number of text widgets floating on a scrollable canvas widget. I want to allow users to resize them by dragging their edges and/or corners, possibly moving them if they drag the upper left edges or corner. I'm open to making them into frames with text widgets inside them, since I'm likely to do that anyway.
I figure I can handle the events manually if I can just get it to show resize handles. Do I need to catch mouseovers and clicks on a border myself?
Adding a "<Configure>" event binding does nothing, as one would expect. ttk.Sizegrip allegedly only works on top-level windows. There are lots of resources for preventing resizes, but very few for facilitating them, and they all seem to be for top-level windows.
I ended up making a frame around the widget with a fat border, catching mouse events, and handling all the dirty resize logic myself.
I had to record both the initial location of the location of the click and the position of each successive mouse-move, and use them for top/left and bottom/right resizes, respectively.
Edit: Here's a handy (relatively) encapsulated implementation.
from Tkinter import *
class ResizableCanvasFrame(Frame):
'''
Class that handles creating resizable frames on a canvas.
Don't pack it.
Set save_callback to whatever you want to happen when the mouse
lets up on the border. You can catch <Configure> too, but at least
in my case I didn't want to save the new position on every mouse move.
'''
def __init__(self, master, x, y, w, h, *args, **kwargs):
# master should be a Canvas
self.frame_thickness = 5
Frame.__init__(
self,
master,
*args,
borderwidth = self.frame_thickness,
cursor = 'fleur',
**kwargs
)
self.canvas = master
self.resize_state = None
self.bind('<Button-1>', self.mousedown)
self.bind('<B1-Motion>', self.mousemove)
self.bind('<ButtonRelease-1>', self.mouseup)
self.bind('<Destroy>', self.delete_item)
# add self to canvas
self.itemid = self.canvas.create_window(
x,
y,
window=self,
anchor="nw",
width=w,
height=h
)
self.save_callback = None
def canvas_coords(self):
return map(int, self.canvas.coords(self.itemid))
def move(self, dx, dy):
# strictly, this is out of the range of RCF,
# but it helps with the law of demeter
self.canvas.move(self.itemid, dx, dy)
def mousedown(self, event):
window_width = self.winfo_width()
window_height = self.winfo_height()
self.resize_state = {
'start_coords': (event.x, event.y),
'last_coords': (event.x, event.y),
'left_edge': (0 <= event.x < self.frame_thickness),
'right_edge': (window_width - self.frame_thickness <= event.x < window_width),
'top_edge': (0 <= event.y < self.frame_thickness),
'bottom_edge': (window_height - self.frame_thickness <= event.y < window_height),
}
def mousemove(self, event):
if self.resize_state:
resize = self.resize_state # debug var
event_x = event.x
event_y = event.y
# distance of cursor from original position of window
delta = map(int, (event.x - self.resize_state['start_coords'][0],
event.y - self.resize_state['start_coords'][1]))
# load current pos, size
new_x, new_y = self.canvas_coords()
new_width = int(self.canvas.itemcget(self.itemid, 'width'))
new_height = int(self.canvas.itemcget(self.itemid, 'height'))
# handle x resize/move
if self.resize_state['left_edge']:
# must move pos and resize
new_x += delta[0]
new_width -= delta[0]
elif self.resize_state['right_edge']:
new_width += (event.x - self.resize_state['last_coords'][0])
# handle y resize/move
if self.resize_state['top_edge']:
new_y += delta[1]
new_height -= delta[1]
elif self.resize_state['bottom_edge']:
new_height += (event.y - self.resize_state['last_coords'][1])
# save new settings in item, not card yet
self.resize_state['last_coords'] = (event.x, event.y)
self.canvas.coords(self.itemid, new_x, new_y)
self.canvas.itemconfig(self.itemid, width=new_width, height=new_height)
def mouseup(self, event):
if self.resize_state:
self.resize_state = None
if self.save_callback:
self.save_callback()
def delete_item(self, event):
self.canvas.delete(self.itemid)
You could use the PanedWindow widget, or a combination of a few of them, inside your canvas. They are designed to do that. Getting the PanedWindow to stretch like the "sticky" command inside the canvas is an unknown though.
That is what I was looking for when stumbled across this post.