tkinter - 如何拖放小部件?

10

我正试图制作一个Python程序,你可以在其中移动小部件。

这是我的代码:

import tkinter as tk 
main = tk.Tk()
notesFrame = tk.Frame(main, bd = 4, bg = "a6a6a6")
notesFrame.place(x=10,y=10)
notes = tk.Text(notesFrame)
notes.pack()
notesFrame.bind("<B1-Motion>", lambda event: notesFrame.place(x = event.x, y = event.y)
但是,这变得非常故障,并且小部件来回跳动。

4
你实际上尝试过这段代码吗?因为它有多个错误,在Python 3.5的Mac下似乎没有移动任何东西。 - Wayne Werner
你需要点击边框,而不是文本框。 - user6144948
3个回答

28
您所观察到的行为是由于事件的坐标是相对于拖动小部件而言的。使用相对坐标更新小部件在绝对坐标中的位置显然会导致混乱。
为了解决这个问题,我使用了.winfo_x().winfo_y()函数(它们允许将相对坐标转换为绝对坐标),以及Button-1事件来确定拖动开始时光标在小部件上的位置。
下面是一个使小部件可拖动的函数:
def make_draggable(widget):
    widget.bind("<Button-1>", on_drag_start)
    widget.bind("<B1-Motion>", on_drag_motion)

def on_drag_start(event):
    widget = event.widget
    widget._drag_start_x = event.x
    widget._drag_start_y = event.y

def on_drag_motion(event):
    widget = event.widget
    x = widget.winfo_x() - widget._drag_start_x + event.x
    y = widget.winfo_y() - widget._drag_start_y + event.y
    widget.place(x=x, y=y)

它可以这样使用:

main = tk.Tk()

frame = tk.Frame(main, bd=4, bg="grey")
frame.place(x=10, y=10)
make_draggable(frame)

notes = tk.Text(frame)
notes.pack()

如果您想采用更面向对象的方法,可以编写一个 mixin,使类的所有实例都可拖动:
class DragDropMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        make_draggable(self)

使用方法:

# As always when it comes to mixins, make sure to
# inherit from DragDropMixin FIRST!
class DnDFrame(DragDropMixin, tk.Frame):
    pass

# This wouldn't work:
# class DnDFrame(tk.Frame, DragDropMixin):
#     pass

main = tk.Tk()

frame = DnDFrame(main, bd=4, bg="grey")
frame.place(x=10, y=10)

notes = tk.Text(frame)
notes.pack()

动作1:什么也不做,动作2:改变位置。我认为这是一个小错误。 - Chris P
非常感谢 @Aran-Fey。这帮助我从一个标签子窗口中拖动我的主窗口。 - Andres Manuel Diaz

3

我想出了一种不同的方法,它在您希望所有小部件在同一个窗口中可拖动时非常有用。 我也更喜欢这种方法中使用的数学公式,而不是被接受的答案中的公式。

代码已经按行解释,如下所示:

import tkinter as tk

def drag_widget(event):
    if (w:=root.dragged_widget): #walrus assignment
        cx,cy = w.winfo_x(), w.winfo_y() #current x and y
        #deltaX and deltaY to mouse position stored
        dx = root.marked_pointx - root.winfo_pointerx()
        dy = root.marked_pointy - root.winfo_pointery()
        #adjust widget by deltaX and deltaY
        w.place(x=cx-dx, y=cy-dy)
        #update the marked for next iteration
        root.marked_pointx = root.winfo_pointerx()
        root.marked_pointy = root.winfo_pointery()

def drag_init(event):
    if event.widget is not root:
        #store the widget that is clicked
        root.dragged_widget = event.widget
        #ensure dragged widget is ontop
        event.widget.lift()
        #store the currently mouse position
        root.marked_pointx = root.winfo_pointerx()
        root.marked_pointy = root.winfo_pointery()

def finalize_dragging(event):
    #default setup
    root.dragged_widget = None
    
root = tk.Tk()
#name and register some events to some sequences
root.event_add('<<Drag>>', '<B1-Motion>')
root.event_add('<<DragInit>>', '<ButtonPress-1>')
root.event_add('<<DragFinal>>', '<ButtonRelease-1>')
#bind named events to the functions that shall be executed
root.bind('<<DragInit>>', drag_init)
root.bind('<<Drag>>', drag_widget)
root.bind('<<DragFinal>>', finalize_dragging)
#fire the finalizer of dragging for setup
root.event_generate('<<DragFinal>>')
#populate the window
for color in ['yellow','red','green','orange']:
    tk.Label(root, text="test",bg=color).pack()

root.mainloop()

2

Tkinter有一个模块可以实现拖放功能,该模块的文档记录在模块docstring中。曾经期望它会被tk dnd模块所取代,但却并未得到实现。我从未尝试过它。在SO上搜索[tkinter] dnd返回此页面。以下是docstring的开头。

>>> from tkinter import dnd
>>> help(dnd)
Help on module tkinter.dnd in tkinter:

NAME
    tkinter.dnd - Drag-and-drop support for Tkinter.

DESCRIPTION
    This is very preliminary.  I currently only support dnd *within* one
    application, between different windows (or within the same window).
[snip]

整个文档字符串可以在 源代码 中查看。 - martineau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接