【问题标题】:Moving Around A Python Canvas Without Using Scrollbars在不使用滚动条的情况下移动 Python 画布
【发布时间】:2013-09-23 22:05:30
【问题描述】:

背景:
我有一个使用 Tkinter 作为 GUI 基础的程序。该程序有一个画布,其中填充了大量对象。目前,为了移动屏幕上的所有对象,我只是将一个移动函数绑定到标签“all”,这当然会移动屏幕上的所有对象。但是,跟踪 所有 画布对象的位置对我来说至关重要 - 即在每次移动后我都会记录新位置,这似乎不必要地复杂。

问题:
仅使用鼠标(不使用滚动条)有效滚动/拖动整个画布(屏幕大小的几倍)的最佳方法是什么?

我的尝试:
我已经实现了滚动条,并找到了几个设置滚动条的指南,但没有一个能满足这一特殊要求。

不使用的滚动条方法示例:

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

我的特殊问题

我的程序生成所有画布对象坐标,然后绘制它们。这些对象以各种模式排列,但至关重要的是,它们必须“知道”彼此的位置。当使用@abarnert 提供的方法在画布上移动时,以及我编写的移动所有画布对象的类似方法时,出现的问题是每个对象都“认为”它位于绘制对象之前生成的画布坐标处。例如,如果我将画布向左拖动 50 像素并单击程序中的一个对象,它会向右跳 50 像素回到它的原始位置。我对此的解决方案是编写一些代码,在释放鼠标按钮时,记录最后一个位置并更新每个对象的坐标数据。但是,我正在寻找一种方法来删除这最后一步——我希望有一种方法可以移动画布,使对象位置是绝对的,并假设类似于“滚动”功能的功能可以做到这一点。我意识到我在这里闲逛了,但是我在上面的示例中添加了几行代码,这些代码突出了我的问题——通过移动画布,您可以看到坐标发生了变化。再次感谢。

【问题讨论】:

  • 我不太清楚你想要什么。在不使用滚动条的情况下如何在画布上滚动?您希望能够单击背景上的任意位置并拖动,就像在某些照片编辑器中一样?还是有什么不同?
  • 无论如何,如果您有任何知道如何滚动其内容的小部件,则不需要滚动条。您编写的用于将滚动条连接到小部件的代码,只需在您想要滚动时直接运行该代码。例如,如果您正在执行vscrollbar['command'] = widget.yview,只需使用适当的参数调用widget.yview。 (如果您可以向我们展示您实际的基于滚动条的代码,最好是 tiny, stripped-down example program,我可以向您展示如何编写显式滚动,而不是含糊其辞。)
  • 您好,感谢您的回复!我在问题中添加了一个剥离示例。
  • @BryanOakley:我认为他的观点是,因为他不知道如何滚动画布,所以他通过向相反方向移动项目来伪装它。
  • @MarkyD43:你的困惑可能是我的错——我的上一个版本有一个错误,我忘记了canvasx/canvasy-映射drag_item中的坐标。我现在已经解决了。

标签: python python-2.7 tkinter tkinter-canvas


【解决方案1】:

我会先给你最简单版本的代码,然后解释它,以便你根据需要扩展它。

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)

首先,最简单的部分:手动滚动画布。正如documentation 解释的那样,您使用xviewyview 方法,就像您的滚动条commands 一样。或者您可以直接调用xview_movetoyview_moveto(或foo_scroll 方法,但它们似乎不是您想要的)。你可以看到我实际上并没有使用这些;我会在下面解释。

接下来,要在画布上捕获点击和拖动事件,您只需绑定&lt;B1-Motion&gt;,就像普通拖放一样。

这里的棘手之处在于,拖动事件为您提供屏幕像素坐标,而 xview_movetoyview_moveto 方法从顶部/左侧的 0.0 到底部/右侧的 1.0 取一小部分。因此,您需要捕获原始点击的坐标(通过绑定&lt;Button-1&gt;;使用它,拖动事件的坐标和画布的bbox,您可以计算moveto 分数。如果您使用scale 方法并且想要在放大/缩小时适当拖动,您也需要考虑到这一点。

但除非您想做一些不寻常的事情,否则 scan 辅助方法会为您完成该计算,因此调用它们会更简单。


请注意,这还将捕获画布上项目的单击和拖动事件,而不仅仅是背景。这可能是您想要的,除非您打算使项目在画布内可拖动。在后一种情况下,在所有其他项目下方添加一个背景矩形项目(透明的,或具有您打算用于画布本身的任何背景),并 tag_bind 而不是 bind 画布本身。 (IIRC,对于旧版本的 Tk,您必须为背景项目和 tag_bind 创建一个标签……但如果是这样,您可能已经必须这样做才能绑定所有其他项目,所以这里是一样的. 不管怎样,即使没有必要,我也会这样做,因为标签是创建可以全部绑定在一起的项目组的便捷方式。)

所以:

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

现在您可以通过背景拖动整个画布,但拖动其他项目(标记为“可拖动”的项目)将执行您想要的任何其他操作。


如果我正确理解了您的 cmets,那么您剩下的问题是您在需要画布坐标时尝试使用窗口坐标。文档中的坐标系部分解释了这种区别。

因此,假设您有一个项目放置在 500, 500,原点位于 0, 0。现在,您将画布滚动到 500, 0。项目的窗口坐标现在为 0 , 500,但它的画布坐标仍然是 500, 500。正如文档所说:

要将窗口坐标转换为画布坐标,请使用canvasxcanvasy 方法

【讨论】:

  • 感谢您的回复 :) 我意识到我对这个问题的措辞有点不合标准,因此感谢您为我尝试解决方案。这种方法当然有效(我已经在我的代码中按原样实现了它)并按照说明解决了我的答案。但是,我将更新我的代码以稍微澄清我的问题,如果更新充分改变了问题的性质,我很乐意将其作为一个新问题分拆出来。再次感谢你! :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-04
  • 1970-01-01
相关资源
最近更新 更多