pythontkinteropenslide

Manual Annotation with Tkinter


I am using Tkinter to import images with Openslide. I would like to integrate a manual annotation module into my program like this:

class ResizableCanvas(Canvas):

    def __init__(self, parent, **kwargs):
        Canvas.__init__(self, parent, **kwargs)
        self.bind("<Configure>", self.on_resize)
        self.height = self.winfo_reqheight()
        self.width = self.winfo_reqwidth()

    def on_resize(self, event):
        wscale = float(event.width) / self.width
        hscale = float(event.height) / self.height
        self.width = event.width
        self.height = event.height
        self.config(width=self.width, height=self.height)


    class ViewerTab:

    def __init__(self, master, model, dim=800):

        self.sideFrame = ttk.Frame(self.master, width=100)


        self.coords = {"x":0,"y":0,"x2":0,"y2":0}

        self.lines = []
    
    def click(self):

        self.coords["x"] = self.x
        self.coords["y"] = self.y
    
        self.lines.append(self.canvas.create_line(self.coords["x"],self.coords["y"],self.coords["x"],self.coords["y"]))
    
    def drag(self):
        # update the coordinates from the event
        self.coords["x2"] = self.x
        self.coords["y2"] = self.y
    

        self.canvas.coords(self.lines[-1], self.coords["x"],self.coords["y"],self.coords["x2"],self.coords["y2"])
    
        #self.canvas.bind("<Button-1>", self.dirbutton)
        #self.canvas.bind("<B1-Motion>", self.move)
        self.canvas.bind("<ButtonRelease-1>", self.nomove)
        self.canvas.bind("<Button-2>", self.get_position)
        self.canvas.bind("<ButtonPress-1>", self.click)
        self.canvas.bind("<B1-Motion>", self.drag) 

Solution

  • So if I got it right from the comments, the issue is to be able to both pan the slide and draw on it using binding to mouse clicks and motion. There are several way to do that, for instance:

    1. Use radiobuttons so that the user selects the "mode": either pan or annotate. Here is a small example based on https://stackoverflow.com/a/50129744/6415268 for the drawing part. The click() and drag() functions do different actions depending on the selected mode (stored a the StringVar).

       import tkinter as tk
      
       coords = {"x": 0, "y": 0, "x2": 0, "y2": 0}
       # keep a reference to all lines by keeping them in a list
       lines = []
      
       def click(event):
           if mode.get() == "pan":
               canvas.scan_mark(event.x, event.y)
           else:
               # define start point for line
               coords["x"] = canvas.canvasx(event.x)
               coords["y"] = canvas.canvasy(event.y)
      
               # create a line on this point and store it in the list
               lines.append(canvas.create_line(coords["x"], coords["y"], coords["x"], coords["y"]))
      
       def drag(event):
           if mode.get() == "pan":
               canvas.scan_dragto(event.x, event.y, gain=1)
           else:
               # update the coordinates from the event
               coords["x2"] = canvas.canvasx(event.x)
               coords["y2"] = canvas.canvasy(event.y)
      
               # Change the coordinates of the last created line to the new coordinates
               canvas.coords(lines[-1], coords["x"], coords["y"], coords["x2"], coords["y2"])
      
      
       root = tk.Tk()
       mode = tk.StringVar(root, "pan")
       toolbar = tk.Frame(root)
       toolbar.pack(fill='x')
      
       tk.Radiobutton(toolbar, text="Pan",
                      variable=mode, value="pan").pack(side='left')
       tk.Radiobutton(toolbar, text="Annotate",
                      variable=mode, value="annotate").pack(side='left')
      
       canvas = tk.Canvas(root, bg="white")
       canvas.create_rectangle(0, 0, 50, 50, fill='red')
       canvas.create_rectangle(400, 400, 450, 450, fill='blue')
       canvas.pack(fill='both')
      
       canvas.bind("<ButtonPress-1>", click)
       canvas.bind("<B1-Motion>", drag)
      
       root.mainloop()
      
    2. Another possibility is to use different bindings for the two kinds of actions using event modifiers (e.g. pressing the Ctrl, Shift or Alt key). For instance, the panning can be bound to Ctrl + mouse events while the drawing happens on simple mouse clicks and motion.

       import tkinter as tk
      
       coords = {"x": 0, "y": 0, "x2": 0, "y2": 0}
       # keep a reference to all lines by keeping them in a list
       lines = []
      
       def draw_click(event):
           # define start point for line
           coords["x"] = canvas.canvasx(event.x)
           coords["y"] = canvas.canvasy(event.y)
      
           # create a line on this point and store it in the list
           lines.append(canvas.create_line(coords["x"], coords["y"], coords["x"], coords["y"]))
      
       def draw_drag(event):
           # update the coordinates from the event
           coords["x2"] = canvas.canvasx(event.x)
           coords["y2"] = canvas.canvasy(event.y)
      
           # Change the coordinates of the last created line to the new coordinates
           canvas.coords(lines[-1], coords["x"], coords["y"], coords["x2"], coords["y2"])
      
      
       root = tk.Tk()
       toolbar = tk.Frame(root)
       toolbar.pack(fill='x')
      
       canvas = tk.Canvas(root, bg="white")
       canvas.create_rectangle(0, 0, 50, 50, fill='red')
       canvas.create_rectangle(400, 400, 450, 450, fill='blue')
       canvas.pack(fill='both')
      
       canvas.bind("<ButtonPress-1>", draw_click)
       canvas.bind("<B1-Motion>", draw_drag)
       canvas.bind('<Control-ButtonPress-1>', lambda event: canvas.scan_mark(event.x, event.y))
       canvas.bind("<Control-B1-Motion>", lambda event: canvas.scan_dragto(event.x, event.y, gain=1))
      
       root.mainloop()