I am trying to make a Python program in which you can move around widgets.
This is my code:
import tkinter as tk
main = tk.Tk()
notesFrame = tk.Frame(main, bd = 4, bg = "a6a6a6")
notesFrame.place(x=10,y=10)
notes = tk.Text(notesFrame)
notes.pack()
notesFrame.bind("<B1-Motion>", lambda event: notesFrame.place(x = event.x, y = event.y)
But, this gets super glitchy and the widget jumps back and forth.
The behavior you're observing is caused by the fact that the event's coordinates are relative to the dragged widget. Updating the widget's position (in absolute coordinates) with relative coordinates obviously results in chaos.
To fix this, I've used the .winfo_x() and .winfo_y() functions (which allow to turn the relative coordinates into absolute ones), and the Button-1 event to determine the cursor's location on the widget when the drag starts.
Here's a function that makes a widget draggable:
def make_draggable(widget):
widget.bind("<Button-1>", on_drag_start)
widget.bind("<B1-Motion>", on_drag_motion)
def on_drag_start(event):
widget = event.widget
widget._drag_start_x = event.x
widget._drag_start_y = event.y
def on_drag_motion(event):
widget = event.widget
x = widget.winfo_x() - widget._drag_start_x + event.x
y = widget.winfo_y() - widget._drag_start_y + event.y
widget.place(x=x, y=y)
It can be used like so:
main = tk.Tk()
frame = tk.Frame(main, bd=4, bg="grey")
frame.place(x=10, y=10)
make_draggable(frame)
notes = tk.Text(frame)
notes.pack()
If you want to take a more object-oriented approach, you can write a mixin that makes all instances of a class draggable:
class DragDropMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
make_draggable(self)
Usage:
# As always when it comes to mixins, make sure to
# inherit from DragDropMixin FIRST!
class DnDFrame(DragDropMixin, tk.Frame):
pass
# This wouldn't work:
# class DnDFrame(tk.Frame, DragDropMixin):
# pass
main = tk.Tk()
frame = DnDFrame(main, bd=4, bg="grey")
frame.place(x=10, y=10)
notes = tk.Text(frame)
notes.pack()
I came up with a different approach and it is useful when you want ALL of your widgets drag-able in the SAME window. I do also like the math used in this approach more than in the accepted answer.
The code is explained line by line as commented lines below:
import tkinter as tk
def drag_widget(event):
if (w:=root.dragged_widget): #walrus assignment
cx,cy = w.winfo_x(), w.winfo_y() #current x and y
#deltaX and deltaY to mouse position stored
dx = root.marked_pointx - root.winfo_pointerx()
dy = root.marked_pointy - root.winfo_pointery()
#adjust widget by deltaX and deltaY
w.place(x=cx-dx, y=cy-dy)
#update the marked for next iteration
root.marked_pointx = root.winfo_pointerx()
root.marked_pointy = root.winfo_pointery()
def drag_init(event):
if event.widget is not root:
#store the widget that is clicked
root.dragged_widget = event.widget
#ensure dragged widget is ontop
event.widget.lift()
#store the currently mouse position
root.marked_pointx = root.winfo_pointerx()
root.marked_pointy = root.winfo_pointery()
def finalize_dragging(event):
#default setup
root.dragged_widget = None
root = tk.Tk()
#name and register some events to some sequences
root.event_add('<<Drag>>', '<B1-Motion>')
root.event_add('<<DragInit>>', '<ButtonPress-1>')
root.event_add('<<DragFinal>>', '<ButtonRelease-1>')
#bind named events to the functions that shall be executed
root.bind('<<DragInit>>', drag_init)
root.bind('<<Drag>>', drag_widget)
root.bind('<<DragFinal>>', finalize_dragging)
#fire the finalizer of dragging for setup
root.event_generate('<<DragFinal>>')
#populate the window
for color in ['yellow','red','green','orange']:
tk.Label(root, text="test",bg=color).pack()
root.mainloop()
Tkinter has a module for this, documented in the module docstring. It was expected that it would be replaced by a tk dnd module, but this has not happened. I have never tried it. Searching SO for [tkinter] dnd returns this page. Below is the beginning of the docstring.
>>> from tkinter import dnd
>>> help(dnd)
Help on module tkinter.dnd in tkinter:
NAME
tkinter.dnd - Drag-and-drop support for Tkinter.
DESCRIPTION
This is very preliminary. I currently only support dnd *within* one
application, between different windows (or within the same window).
[snip]
Related
I am trying to swap the placement of two widget in a grid layout in tkinter using drag and drop. On mouse ButtonRelease-1 I want to get the widget over which the mouse was released (myTextLabel2), so that I can change the row and column of that widgets with the row and column of the clicked widget (myTextLabel1).
I tried search for the method that gets the widget at position x,y, put all I can find is the reverse, getting the position of widget not the other way arround.
Here is my code so far:
from tkinter import *
from functools import partial
def changeOrder(widget1,widget2):
widget1.grid(row=1,column=0)
widget2.grid(row=0,column=0)
def drag_start(event):
print(event.x)
def drag_motion(event):
x = event.x
y = event.y
def drag_release(event):
x = event.x
y = event.y
#I want to get the widget at position x,y, should be myTextLabel2
#Then swap the rows and columns for myTextLabel2 and myTextLabel1
root = Tk()
myTextLabel1 = Label(root,text="Label 1")
myTextLabel1.grid(row=0,column=0,padx=5,pady=5,sticky=E+W+S+N)
myTextLabel1.bind("<Button-1>",drag_start)
myTextLabel1.bind("<B1-Motion>",drag_motion)
myTextLabel1.bind("<ButtonRelease-1>",drag_release)
myTextLabel2 = Label(root,text="Label 2")
myTextLabel2.grid(row=1,column=0,padx=5,pady=5,sticky=E+W+S+N)
myButton = Button(root,text="Change order",command=partial(changeOrder,myTextLabel1,myTextLabel2))
myButton.grid(row=3,column=0,padx=5,pady=5,sticky=E+W+S+N)
root.mainloop()
You can try something like this
from tkinter import *
from functools import partial
def changeOrder(widget1,widget2,initial):
target=widget1.grid_info()
widget1.grid(row=initial['row'],column=initial['column'])
widget2.grid(row=target['row'],column=target['column'])
def on_click(event):
widget=event.widget
print(widget)
if isinstance(widget,Label):
start=(event.x,event.y)
grid_info=widget.grid_info()
widget.bind("<B1-Motion>",lambda event:drag_motion(event,widget,start))
widget.bind("<ButtonRelease-1>",lambda event:drag_release(event,widget,grid_info))
else:
root.unbind("<ButtonRelease-1>")
def drag_motion(event,widget,start):
x = widget.winfo_x()+event.x-start[0]
y = widget.winfo_y()+event.y-start[1]
widget.lift()
widget.place(x=x,y=y)
def drag_release(event,widget,grid_info):
widget.lower()
x,y=root.winfo_pointerxy()
target_widget=root.winfo_containing(x,y)
if isinstance(target_widget,Label):
changeOrder(target_widget,widget,grid_info)
else:
widget.grid(row=grid_info['row'],column=grid_info['column'])
root = Tk()
myTextLabel1 = Label(root,text="Label 1",bg='yellow')
myTextLabel1.grid(row=0,column=0,padx=5,pady=5,sticky=E+W+S+N)
myTextLabel2 = Label(root,text="Label 2",bg='lawngreen')
myTextLabel2.grid(row=1,column=0,padx=5,pady=5,sticky=E+W+S+N)
myButton = Button(root,text="Change order",command=partial(changeOrder,myTextLabel1,myTextLabel2))
myButton.grid(row=3,column=0,padx=5,pady=5,sticky=E+W+S+N)
root.bind("<Button-1>",on_click)
root.mainloop()
root.bind("<Button-1>",on_click) has been used to bind the entire window with the <Button-1> event, the function on_click adds the other 2 bindings if the widget clicked is an instance of Label. The grid_info() of the label is stored and passed accordingly.
drag_motion changes the position of the widget based on motion using place, and keeps the widget on top using the lift method.
drag_release lowers the widget using the lower method followed by obtaining the current position of the cursor using winfo_pointerxy method and passing the returned value to the winfo_containing method which returns the widget under the mouse pointer.
You can replace the isinstance logic by storing the widgets that are swappable in a list and then then checking if the widget lies in that.
PS: The button in your original post would not work due to the changes made to the code.
I am eventually trying to run a code that's a bit more sophisticated than this, but this is the hump I'm trying to get over. I just need to be able to run a code that takes the x,y coordinates of the pixel clicked and records them as either a variable or appends them to a list. For right now I have them set as L_Click_x and L_Click_y, I would eventually like to be able to add these coordinates to a list or be able to call upon them in the function. Nothing I have tried seems to work, I'm pretty new to this so there's probably something fundamental that I'm just not getting. (Also the red lines that appear are just for visual confirmation of the click)
from tkinter import *
def motion(event):
x, y = event.x, event.y
print("Current Position = ",(x,y))
def LeftClick(event):
L_Click_x, L_Click_y = event.x, event.y
redline = canvas.create_line(0,L_Click_y,400,L_Click_y,fill = "red")
redline = canvas.create_line(L_Click_x,0,L_Click_x,300,fill = "red")
print("Left Click = ",(L_Click_x,L_Click_y))
root = Tk()
root.bind('<Motion>',motion)
root.bind('<Button-1>',LeftClick)
canvas = Canvas(root, width = "400",height = "300")
canvas.pack()
root.mainloop()
You will need to keep track of the first clicked point, until the second is known, and then draw a line segment between the two.
This simple example uses a global variable, and the points are discarded when drawn; you will probably want to use a container to keep track of the line segments, and a program construction that allows you to access it without making it global.
Please note that I bound the events to the canvas instead of root; I changed the imports to avoid cluttering the namespace (as your app grows, you'll be glad you did), and changed the camelcase names to a more pythonic convention.
import tkinter as tk
def motion(event):
x, y = event.x, event.y
print("Current Position = ", (x, y))
def left_click(event):
global line_segment
line_segment.append(event.x)
line_segment.append(event.y)
canvas.create_oval(event.x+1, event.y+1, event.x-1, event.y-1)
if len(line_segment) == 4:
canvas.create_line(*line_segment, fill="red")
line_segment = []
if __name__ == '__main__':
line_segment = []
root = tk.Tk()
canvas = tk.Canvas(root, width="400", height="300")
canvas.pack()
canvas.bind('<Motion>', motion)
canvas.bind('<Button-1>', left_click)
root.mainloop()
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.
What I want to do is have the user click on somewhere on the Canvas, then click elsewhere and have a straight line drawn between the two points. I'm new to TKinter and after some googling and searching on here I'm having trouble finding a solid answer for this.
The way that I have been thinking about it, there should be an onclick event which passes the mouse coordinates on the canvas and then an onrelease event which passes those coordinates on the canvas, thus creating a line between them. This line would have to be an object that I could then remove at some point via another button but that's a separate issue.
Any help would be greatly appreciated, or even any links to articles/tutorials that may help also
The only thing you have to do is bind "<Button-1>" and "<ButtonRelease-1>" to the canvas:
from Tkinter import Tk, Canvas
start = None
def onclick_handler(event):
global start
start = (event.x, event.y)
def onrelease_handler(event):
global start
if start is not None:
x = start[0]
y = start[1]
event.widget.create_line(x, y, event.x, event.y)
start = None
master = Tk()
canvas = Canvas(master, width=200, height=200)
canvas.bind("<Button-1>", onclick_handler)
canvas.bind("<ButtonRelease-1>", onrelease_handler)
canvas.pack()
master.mainloop()
I don't like at all using global variables, it is much cleaner to wrap all the widgets and related functions in a class. However, as an example I think it is clear enough.
Looks something pretty starightforward for me.
Just check the documentation on Canvas here:
http://effbot.org/tkinterbook/canvas.htm
And on events here:
http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
And them, just type in some code like this -
the class is even more complicated than a sinple hello World -
two global variables would have done for simpler code:
from Tkinter import Canvas, Tk, mainloop
w = Tk()
c = Canvas(w)
c.pack()
class DrawLines(object):
def __init__(self, canvas):
self.canvas = canvas
self.start_coords = None
self.end_coords = None
def __call__(self, event):
coords = event.x, event.y
if not self.start_coords:
self.start_coords = coords
return
self.end_coords = coords
self.canvas.create_line(self.start_coords[0],
self.start_coords[1],
self.end_coords[0],
self.end_coords[1])
self.start_coords = self.end_coords
c.bind("<Button-1>", DrawLines(c))
mainloop()
So I have a bunch of text on a canvas in Tkinter and I want to make it so the text color changes when the mouse is hovering over the text. For the life of me I can't figure out how to do it, and there doesn't seem to be a lot of information about Tkinter anywhere.
for city in Cities:
CityText = Cities[i]
board.create_text(CityLocs[CityText][0], CityLocs[CityText][1], text=CityText, fill="white")
CityText = Cities[i]
i = i + 1
That's just my code to place the text on the canvas, although I'm not sure what else to post to get my point across. Is there no 'hover' function or something like that built into Tkinter?
You can bind arbitrary events (mouse, keyboard, window manager and possibly others) to any widget in Tkinter.
A nice documentation for that is at http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm -
For example, to bind color changes to widgets when mouse hover over them:
import Tkinter
from functools import partial
def color_config(widget, color, event):
widget.configure(foreground=color)
parent = Tkinter.Tk()
text = Tkinter.Label(parent, text="Hello Text")
text.bind("<Enter>", partial(color_config, text, "red"))
text.bind("<Leave>", partial(color_config, text, "blue"))
text.pack()
Tkinter.mainloop()
The use of functools.partial here allows you to re-use a variable for your text (Label) widget, since you are appending them to a list. If one would settle to simply using lambda you would have a disgusting surprise, as the variable referring to the widget in the body of the lambda function would always point to the last value it had inside the for loop. functools.partial "freeze" the variable content at the time it is called, and yields a new function.
However, since you are placing the items in a Canas, you can either set the "fill" and "fillactive" attributes for each item, as in #mgilson's answer, or you can create a more generic class to handle not only hovering, but other events you choose to implement later.
If your class has a __call__ method, you can pass an instance of it to the bind method of the canvas, so that the resulting object is called for each event on the canvas. In this case, mouse-motion event suffices:
from Tkinter import *
class Follower(object):
def __init__(self,on_color="#fff", off_color="#000"):
self.on_color = on_color
self.off_color = off_color
self.previous_item = None
def hover(self, canvas, item, x, y):
x1, y1, x2, y2 = canvas.bbox(item)
if x1 <= x <= x2 and y1 <= y <= y2:
return True
return False
def __call__(self, event):
canvas = event.widget
item = canvas.find_closest(event.x, event.y)
hovering = self.hover(canvas, item, event.x, event.y)
if (not hovering or item != self.previous_item) and self.previous_item is not None:
canvas.itemconfig(self.previous_item, fill=self.off_color)
if hovering:
canvas.itemconfig(item, fill=self.on_color)
self.previous_item = item
master=Tk()
canvas=Canvas(master)
canvas.pack()
canvas.create_text((40,20),text="Hello World!",fill="black")
canvas.create_text((60,80),text="FooBar",fill="black")
canvas.bind("<Motion>", Follower())
master.mainloop()
(ps. canvas and text placement example borrowed from #mgilson's answer)
Here's a (admittedly) pretty lame example that works on OS-X...
from Tkinter import *
master=Tk()
canvas=Canvas(master)
canvas.pack()
canvas.create_text((20,20),activefill="red",text="Hello World!",fill="black")
master.mainloop()
reference: http://effbot.org/tkinterbook/canvas.htm
This is already built into Tkinter. Use the 'activefill' option when you create your text to specify which color you want it to be.
See the following link for more information. Effbot.org/tkinterbook is my go to tkinter.
http://effbot.org/tkinterbook/canvas.htm#Tkinter.Canvas.create_text-method