stop canvas shape from following cursor after a mouse click - python

I am making a GUI to draw weighted graphs using Tkinter, so I made a button that when clicked creates a circle(graph vertex) using canvas. then the circle should follow the cursor to any position on the canvas and stop when the mouse clicks.
I managed to make the the circle follow the cursor, but I have no idea how to make it stop following.
this is the function I made
def buttonClick():
def Mouse_move(event):
x,y = event.x , event.y
canvas.moveto(vertex,x,y )
vertex= canvas.create_oval(650, 100, 750, 200)
canvas.bind("<Motion>", Mouse_move)

You can bind the mouse click event <Button-1> and unbind the <Motion> event in the callback:
def buttonClick():
def mouse_move(event):
x, y = event.x, event.y
canvas.moveto(vertex, x, y)
def mouse_click(event):
canvas.unbind("<Motion>")
vertex = canvas.create_oval(650, 100, 750, 200)
canvas.bind("<Motion>", mouse_move)
canvas.bind("<Button-1>", mouse_click)

Related

tkinter drag-and-drop when clicking near linear objects

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()

How to draw a square on canvas when he user clicks 2 locations with tkinter

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

how do I get mouse position while pressing a button in tkinter?

I'm trying to get the mouse position while pressing a button, but instead of getting the position of the mouse on Tkinter root, it gives me the position of the mouse on the button. For example, if the button is placed on 200, 200, and I press the top left of the button, it prints 0, 0 instead of 200, 200.
import Tkinter as tk
def leftclick(event):
print("left")
x, y = event.x, event.y
print('{}, {}'.format(x, y))
root = tk.Tk()
add_user = tk.Button(root, height=63, width=195 ,text="sign up a user")
add_user.place(x= 20, y = 30)
root.bind("<Button-1>", leftclick)
root.mainloop()
You can call the winfo_pointerx and winfo_pointery methods of a widget to get each individual coordinate, or you can call winfo_pointerxy to get them both:
def leftclick(event):
x, y = event.widget.winfo_pointerxy()
print('{}, {}'.format(x, y))

python tkinter - track and save mousecklicks to build canvas items

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.

Make widgets/frames inside tkinter canvas resizable

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.

Categories