I'm playing with wxPython event bindings in order to make a dragging algorithm. However I've encountered a problem, when the mouse is not directly over my frame the event doesn't trigger.
This becomes a problem while dragging seeing as if the mouse escapes the frame (like if the user moved it quickly), the frame neglects to update it's position.
Is there anyway to change the bindings so that they trigger even if the mouse isn't over the frame in question?
Snippet:
self.Bind(wx.EVT_LEFT_DOWN, self.relative_mouse_position)
self.Bind(wx.EVT_LEFT_UP, self.wid_unbind)
Snippet:
def relative_mouse_position (self, event):
cx, cy = wx.GetMousePosition()
x, y = self.GetPosition()
RelX = cx - x
RelY = cy - y
self.Bind(wx.EVT_MOTION, lambda event: self.wid_drag(event, RelX, RelY))
def wid_drag (self, event, RelX, RelY):
cx, cy = wx.GetMousePosition()
x = cx - RelX
y = cy - RelY
if x < 0:
x = 0
if y < 0:
y = 0
self.SetPosition((x, y))
def wid_unbind (self, event):
self.Unbind(wx.EVT_MOTION)
When you start a drag, call CaptureMouse to keep the mouse locked to the window that you're dragging.
Not tested but probably bind, EVT_LEAVE_WINDOW to trigger when mouse is outside window.
Related
In my user interface I want to allow a user to drag a crosshair over a canvas which will be displaying a photo. I observed that it is unreasonably difficult for user to click on the lines of a crosshair to get a "direct hit". I want to be able to drag if the users click is anywhere within the RADIUS of the crosshair, not just on one of the lines. The code here demonstrates this not working, even though my click function is carefully and successfully detecting a hit within that radius.
I would also like for the user to be able to click in a new location, have the crosshair appear there and then be able to drag it from that point.
So far, failing on both counts. You have to hit it directly on the circle or one of the lines in order to drag it. Can anyone suggest a clean fix?
import tkinter as tk
import math
CROSS_HAIR_SIZE = 30
class View(tk.Canvas):
def __init__(self, parent, width=1000, height=750):
super().__init__(parent, width=width, height=height)
self.pack(fill="both", expand=True)
self.crosspoint = [-100, -100]
self.bind("<Button-1>", self.click)
def crosshair(self, x, y, size, color, tag="cross"):
a = self.create_line(x - size, y, x + size, y, fill=color, width=1, tag=tag)
b = self.create_line(x, y - size, x, y + size, fill=color, width=1, tag=tag)
self.create_oval(x - size, y - size, x + size, y + size, outline=color, tag=tag)
self.crosspoint = [x, y]
def click(self, event):
click1(self, event, (event.x, event.y))
def startDragging(self, event, tag):
# note: dragPoint is carried in physical coordinates, not image coordinates
self.tag_bind(tag, "<ButtonRelease-1>", self.dragStop)
self.tag_bind(tag, "<B1-Motion>", self.drag)
self.dragPoint = (event.x, event.y)
self.dragTag = tag
def dragStop(self, event):
if not self.dragPoint: return
self.crosspoint = (event.x, event.y)
self.dragPoint = None
def drag(self, event):
if not self.dragPoint: return
# compute how much the mouse has moved
xy = (event.x, event.y)
delta = sub(xy, self.dragPoint)
self.move(self.dragTag, delta[0], delta[1])
self.dragPoint = xy
def sub(v1, v2):
return (v1[0] - v2[0], v1[1] - v2[1])
def click1(view, event, xy):
if math.dist(view.crosspoint, xy) <= CROSS_HAIR_SIZE:
view.startDragging(event, "cross")
print("drag inside radius")
else:
view.crosspoint = xy
# it's simplest to just start over with the crosshair
x, y = xy
view.delete("cross")
view.crosshair(x, y, CROSS_HAIR_SIZE, "red", tag="cross")
view.startDragging(event, "cross")
print("drag outside radius")
root = tk.Tk()
root.geometry("600x600")
view = View(root)
cross = view.crosshair(150, 150, CROSS_HAIR_SIZE, "red")
root.mainloop()
I am writing an app where the user can "paint" a canvas and now I want to add support for shapes.
I have a canvas called paint_canvas and I want to draw a shape between the 2 points the user clicks
def draw_square(event):
x1 = event.x
y1 = event.y
# now I want to get the next two points the user clicks
# how???????
paint_canvas = Canvas(root)
paint_canvas.bind('<Button-3>', draw_square)
You can make two events,
One for the press event ans one for release.
In the first event you store x and y mouse position in another scope (startx , starty)
And in the second event you store mouse position store x and y mouse position in another scope (endx, endy) and then draw your shape with this coordinates
See : https://docstore.mik.ua/orelly/other/python/0596001886_pythonian-chp-16-sect-9.html
And : https://www.codegrepper.com/code-examples/delphi/how+to+draw+rectangle+in+tkinter
If you want to show your rect animation you can use Motion events
Mouse Position Python Tkinter
You don't have to work with double-events. You can just as well store the previously clicked coordinates and draw a rectangle (and reset the previously stored coordinates) as soon as you have two clicks:
from tkinter import *
def draw_square(event):
x1 = event.x
y1 = event.y
w = event.widget ## Get the canvas object you clicked on
if w.coords == None:
w.coords = (x1,y1)
else:
w.create_rectangle(w.coords[0],w.coords[1],x1,y1,fill="#ff0000")
w.coords = None
root = Tk()
paint_canvas = Canvas(root,width=400,height=400,bg="#ffffff")
paint_canvas.pack()
paint_canvas.bind('<Button-3>', draw_square)
paint_canvas.coords=None
root.mainloop()
You could even create a temporary point to mark the first click, which may then be removed as soon as you hit the second one. This point (w.temp in the example below) can also be an attribute of the canvas, so you can access it easily via the click:
def draw_square(event):
x1 = event.x
y1 = event.y
w = event.widget ## Get the canvas object you clicked on
if w.coords == None:
w.coords = (x1,y1)
w.temp = w.create_oval(x1-1,y1-1,x1+1,y1+1,fill="#00ff00")
else:
w.create_rectangle(w.coords[0],w.coords[1],x1,y1,fill="#ff0000")
w.delete(w.temp)
w.coords = None
I programmed a method like this on a canvas:
When I press button1, the variable "state" changes to 0 and every click on the canvas results in a circle
When I press button2, the variable "state" changes to 1. When the item that I clicked is a circle, my variable "selected" changes from None to 1 and I am also saving the coordinates of my mouseclick
My question: how do I code, that python should wait for the second click, and look if its another circle too? And if so, how can I draw a line between them?
def newKnotornewEdge(event):
if self.state == 0:
self.canvas.create_oval(event.x-25,event.y-25,event.x+25, event.y+25, fill="blue")
self.canvas.create_text(event.x, event.y, text="A", fill="white")
elif self.state == 1:
if self.canvas.itemcget(self.canvas.find_overlapping(event.x, event.y, event.x, event.y), "fill") == "blue":
self.selected = 1
start_x = event.x
start_y = event.y
else:
self.selected = None
if self.selected == 1 and #second mouseclick is a circle too:
#draw line that connects those circels
self.canvas.bind("<Button -1>", newKnotornewEdge)
The following canvas screenshot shows the app with 8 circles drawn; 2 and 4 of them are joined with lines; the red circle has been selected (this is what the color red indicates), and is ready to be joined to another circle, on the next click; one circle is not joined and not selected.
make_circle:
a click on the button selects the action 'draw a circle', and purges the circles that may have already been selected.
when this action is active, a click on the canvas draws a blue circle.
join_circles:
When clicked, it activates the action 'join circle' and purges the circles that may have already been selected.
The first click on a circle on the canvas selects this circle whose color changes to red; the second click on a circle on canvas selects the second circle, joins the two with a line, resets the colors to blue, and purges the selection.
A click on an empty part of the canvas does nothing.
You will need to keep track of which action to perform: draw a new circle, or select two circles to join. You also need to keep track of how many circles have been selected.
Then, after successfully drawing a line between circles, you will need to purge the selection.
I chose to use the "functions as a first class object" capability of python to avoid messy state accounting: you select the action to be performed by clicking the relevant button, then, clicks on the canvas will be related to this action.
The following code does that on the canvas, and prints which action is selected, and which is performed in the console:
import tkinter as tk
class CommandButtons(tk.Frame):
def __init__(self, master):
self.master = master
super().__init__(self.master)
self.make_circle_btn = tk.Button(root, text='make_circle', command=make_circle)
self.make_circle_btn.pack()
self.join_circles_btn = tk.Button(root, text='join_circles', command=select_circles)
self.join_circles_btn.pack()
self.pack()
class CircleApp(tk.Frame):
def __init__(self, master):
self.master = master
super().__init__(self.master)
self.canvas = tk.Canvas(root, width=600, height=600, bg='cyan')
self.canvas.pack(expand=True, fill=tk.BOTH)
self.pack()
def make_circle():
_purge_selection()
print('cmd make_circle selected')
c_app.canvas.bind('<Button-1>', _make_circle)
def _make_circle(event):
c_app.canvas.create_oval(event.x - 25, event.y - 25, event.x + 25, event.y + 25, fill="blue")
def select_circles():
_purge_selection()
print('cmd join_circles selected')
c_app.canvas.bind('<Button-1>', _select_circles)
def _select_circles(event):
print(f'select circle {event}')
x, y = event.x, event.y
selection = c_app.canvas.find_overlapping(x, y, x, y)
if selection is not None and len(selected_circles) < 2:
selected_circles.append(selection)
c_app.canvas.itemconfigure(selection, fill='red')
if len(selected_circles) == 2:
if all(selected_circles):
_join_circles()
_purge_selection()
def _join_circles():
coordinates = []
for item in selected_circles:
x, y = find_center(item)
print(x, y)
coordinates.append(x)
coordinates.append(y)
c_app.canvas.create_line(coordinates)
def _purge_selection():
global selected_circles
for item in selected_circles:
c_app.canvas.itemconfigure(item, fill='blue')
selected_circles = []
def find_center(item):
x0, y0, x1, y1 = c_app.canvas.bbox(item)
return (x0 + x1) / 2, (y0 + y1) / 2
if __name__ == '__main__':
selected_circles = []
root = tk.Tk()
command_frame = CommandButtons(root)
c_app = CircleApp(root)
root.mainloop()
A better version would use a class to encapsulate the selected items; here I used a collection as global variable. It would also properly handle overlapping circle selection.
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.
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()