質問

Background:
I have a program using Tkinter as the basis of the GUI. The program has a canvas which is populated with a large number of objects. Currently, in order to move all objects on the screen, I am simply binding a movement function to the tag 'all' which of course moves all objects on the screen. However, it is vital for me to keep track of all canvas object positions- i.e. after every move I log the new position, which seems unnecessarily complicated.

Question:
What is the best way to effectively scroll/drag around the whole canvas (several times the size of the screen) using only the mouse (not using scrollbars)?

My Attempts:
I have implemented scrollbars and found several guides to setting up scrollbars, but none that deal with this particular requirement.

Example of disused scrollbar method:

from Tkinter import *

class Canvas_On:    
    def __init__(self, master):

        self.master=master
        self.master.title( "Example")
        self.c=Canvas(self.master, width=find_res_width-20, height=find_res_height, bg='black', scrollregion=(0,0,5000,5000))
        self.c.grid(row=0, rowspan=25, column=0)
        self.c.tag_bind('bg', '<Control-Button-1>', self.click)
        self.c.tag_bind('bg', '<Control-B1-Motion>', self.drag)

        self.c.tag_bind('dot', '<Button-1>', self.click_item)
        self.c.tag_bind('dot', '<B1-Motion>', self.drag_item)

        draw=Drawing_Utility(self.c)
        draw.drawer(self.c)

    def click(self, event):
        self.c.scan_mark(event.x, event.y)

    def drag(self, event):
        self.c.scan_dragto(event.x, event.y)

    def click_item(self, event):
        self.c.itemconfigure('dot 1 text', text=(event.x, event.y))
        self.drag_item = self.c.find_closest(event.x, event.y)
        self.drag_x, self.drag_y = event.x, event.y
        self.c.tag_raise('dot')
        self.c.tag_raise('dot 1 text')

    def drag_item(self, event):
        self.c.move(self.drag_item, event.x-self.drag_x, event.y-self.drag_y)
        self.drag_x, self.drag_y = event.x, event.y

class Drawing_Utility:
    def __init__(self, canvas):
        self.canvas=canvas
        self.canvas.focus_set()


    def drawer(self, canvas):
        self.canvas.create_rectangle(0, 0, 5000, 5000, 
                            fill='black', tags='bg')
        self.canvas.create_text(450,450, text='', fill='black', activefill='red', tags=('draggable', 'dot', 'dot 1 text'))
        self.canvas.create_oval(400,400,500,500, fill='orange', activefill='red', tags=('draggable', 'dot', 'dot 2'))
        self.canvas.tag_raise(("dot"))

root=Tk()
find_res_width=root.winfo_screenwidth()
find_res_height=root.winfo_screenheight()
run_it=Canvas_On(root)  
root.mainloop() 

My Particular Issue

My program generates all canvas object coordinates and then draws them. The objects are arranged in various patterns, but critically they must 'know' where each other is. When moving around the canvas using the method @abarnert kindly supplied, and a similar method I wrote that moved all canvas objects, the issue arises that each object 'thinks' it is at the canvas coordinates generated before the objects were drawn. For example if I drag the canvas 50 pixels to the left and clicked on an object in my program, it jumps 50 pixels back to the right to it's original position. My solution to this was to write some code that, upon release of the mouse button, logged the last position and updated the coordinate data of each object. However, I'm looking for a way to remove this last step- I was hoping there was a way to move the canvas such that the object positions were absolute, and assumed a function similar to a 'scroll' function would do this. I realise I've rambled here, but I've added a couple of lines to the example above which highlights my issue- by moving the canvas you can see that the coordinates change. Thank you again.

役に立ちましたか?

解決

I'll give you the code for the simplest version first, then explain it so you can expand it as needed.

class Canvas_On:    
    def __init__(self, master):
        # ... your original code here ...
        self.c.bind('<Button-1>', self.click)
        self.c.bind('<B1-Motion>', self.drag)

    def click(self, event):
        self.c.scan_mark(event.x, event.y)

    def drag(self, event):
        self.c.scan_dragto(event.x, event.y)

First, the easy part: scrolling the canvas manually. As the documentation explains, you use the xview and yview methods, exactly as your scrollbar commands do. Or you can just directly call xview_moveto and yview_moveto (or the foo_scroll methods, but they don't seem to be what you want here). You can see that I didn't actually use these; I'll explain below.

Next, to capture click-and-drag events on the canvas, you just bind <B1-Motion>, as you would for a normal drag-and-drop.

The tricky bit here is that the drag event gives you screen pixel coordinates, while the xview_moveto and yview_moveto methods take a fraction from 0.0 for the top/left to 1.0 for the bottom/right. So, you'll need to capture the coordinates of the original click (by binding <Button-1>; with that, the coordinates of the drag event, and the canvas's bbox, you can calculate the moveto fractions. If you're using the scale method and want to drag appropriately while zoomed in/out, you'll need to account for that as well.

But unless you want to do something unusual, the scan helper methods do exactly that calculation for you, so it's simpler to just call them.


Note that this will also capture click-and-drag events on the items on the canvas, not just the background. That's probably what you want, unless you were planning to make the items draggable within the canvas. In the latter case, add a background rectangle item (either transparent, or with whatever background you intended for the canvas itself) below all of your other items, and tag_bind that instead of binding the canvas itself. (IIRC, with older versions of Tk, you'll have to create a tag for the background item and tag_bind that… but if so, you presumably already had to do that to bind all your other items, so it's the same here. Anyway, I'll do that even though it shouldn't be necessary, because tags are a handy way to create groups of items that can all be bound together.)

So:

class Canvas_On:    
    def __init__(self, master):
        # ... your original code here ...
        self.c.tag_bind('bg', '<Button-1>', self.click)
        self.c.tag_bind('bg', '<B1-Motion>', self.drag)
        self.c.tag_bind('draggable', '<Button-1>', self.click_item)
        self.c.tag_bind('draggable', '<B1-Motion>', self.drag_item)
    # ... etc. ...
    def click_item(self, event):
        x, y = self.c.canvasx(event.x), self.c.canvasy(event.y)
        self.drag_item = self.c.find_closest(x, y)
        self.drag_x, self.drag_y = x, y
        self.tag_raise(item)
    def drag_item(self, event):
        x, y = self.c.canvasx(event.x), self.c.canvasy(event.y)
        self.c.move(self.drag_item, x-self.drag_x, y-self.drag_y)
        self.drag_x, self.drag_y = x, y

class Drawing_Utility:
    # ...
    def drawer(self, canvas):
        self.c.create_rectangle(0, 0, 5000, 5000, 
                                fill='black', tags='bg')
        self.c.create_oval(50,50,150,150, fill='orange', tags='draggable')
        self.c.create_oval(1000,1000,1100,1100, fill='orange', tags='draggable')

Now you can drag the whole canvas around by its background, but dragging other items (the ones marked as 'draggable') will do whatever else you want instead.


If I understand your comments correctly, your remaining problem is that you're trying to use window coordinates when you want canvas coordinates. The section Coordinate Systems in the docs explains the distinction.

So, let's say you've got an item that you placed at 500, 500, and the origin is at 0, 0. Now, you scroll the canvas to 500, 0. The window coordinates of the item are now 0, 500, but its canvas coordinates are still 500, 500. As the docs say:

To convert from window coordinates to canvas coordinates, use the canvasx and canvasy methods

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top