Python tkinter 设计用鼠标拖动控件、缩放控件算法及程序

6 篇文章 2 订阅
6 篇文章 2 订阅

在使用tkinter开发的软件中, 常常需要拖动控件, 或者用鼠标改变控件的大小。为此, 用tkinter设计了一个拖曳、缩放控件的程序。

1.功能

拖动控件即可实现改变控件位置; 拖动控件边角的8个滑块可改变控件大小。
功能

2.算法

  1. 获取前、后鼠标位置。
  2. 计算鼠标前后x, y位置之差dx和dy。
  3. 将控件的x,y坐标分别加上dx和dy。

缩放控件的算法与拖动相同, 也是计算dx和dy。
算法

3.初次实现

程序使用控件的startx, starty属性记录控件位置, 使用drag()实现控件的拖动。

import tkinter as tk
def mousedown(event):
    widget=event.widget
    widget.startx=event.x # 开始拖动时, 记录控件位置
    widget.starty=event.y
def drag(event):
    widget=event.widget
    dx=event.x-widget.startx
    dy=event.y-widget.starty
    # winfo_x(),winfo_y() 方法获取控件的坐标
    if isinstance(widget,tk.Wm):
        widget.geometry("+%d+%d"%(widget.winfo_x()+dx,
                                  widget.winfo_y()+dy)
    else:
        widget.place(x=widget.winfo_x()+dx,
                     y=widget.winfo_y()+dy)
def draggable(tkwidget):
    # tkwidget为一个控件(Widget)或一个窗口(Wm)
    tkwidget.bind("<Button-1>",mousedown,add='+')
    tkwidget.bind("<B1-Motion>",drag,add='+')

root=tk.Tk()
root.title("Test")
button=tk.Button(root,text="Drag!")
button.place(width=80,height=30)
draggable(button)
root.mainloop()

4.再次实现

程序将接收鼠标事件的控件被拖动的控件分离, 这里用了字典bound两种控件的对应关系
字典的键是鼠标能拖的那个控件dragger, 值是包含1个或多个绑定事件的列表, 列表的每一项是一个元组, 包含被拖动控件tkwidget, 和一些其他的绑定参数。
由于拖动时控件的坐标改变了,event.xevent.y不能真实地反映鼠标移动的距离差值,因此这里调用了控件的winfo_pointerx()winfo_pointery()winfo_pointerxy()这三个方法,获取鼠标的绝对坐标。
此外,可用控件的winfo_x(),winfo_y(),winfo_width(),winfo_height()方法,获取控件大小和位置。
注意: 更改控件大小和位置后, 需调用控件的update()方法, 确保获取到的是最新的大小和位置。
最终的程序 (源代码见作者的 gitcode):

import tkinter as tk
import tkinter.ttk as ttk

__version__="1.1.4"

# tkinter控件的对象能够作为字典键。
# bound的键是dragger, 值是一个列表, 包含了若干个绑定事件, 用于存储绑定数据
# 列表的每个项是一个元组, 包含了tkwidget和其他绑定参数
bound = {}
def __add(wid,data):# 添加绑定数据
    bound[wid]=bound.get(wid,[])+[data]
def __remove(wid,key): # 用于从bound中移除绑定
    for i in range(len(bound[wid])):
        try:
            if bound[wid][i][0]==key:
                del bound[wid][i]
        except IndexError:pass
def __get(wid,key=''): # 用于从bound中获取绑定数据
    if not key:return bound[wid][0]
    if key=='resize':
        for i in range(len(bound[wid])):
            for s in 'nwse':
                if s in bound[wid][i][0].lower():
                    return bound[wid][i]
    for i in range(len(bound[wid])):
        if bound[wid][i][0]==key:
            return bound[wid][i]
def move(widget,x=None,y=None,width=None,height=None):
    "移动控件或窗口widget至某坐标, 参数都为可选参数。"
    x=x if x!=None else widget.winfo_x()
    y=y if y!=None else widget.winfo_y()
    width=width if width!=None else widget.winfo_width()
    height=height if height!=None else widget.winfo_height()
    if isinstance(widget,tk.Wm):
        widget.geometry("%dx%d+%d+%d"%(width,height,x,y))
    else:
        widget.place(x=x,y=y,width=width,height=height)
    return x,y,width,height

def _mousedown(event):
    if event.widget not in bound:return
    lst=bound[event.widget]
    for data in lst: # 开始拖动时, 在每一个控件记录位置和控件尺寸
        widget=data[1]
        widget.mousex,widget.mousey = widget.winfo_pointerxy() # 获取初始鼠标位置
        widget.startx,widget.starty = widget.winfo_x(),widget.winfo_y() # 获取相对坐标
        widget.start_w=widget.winfo_width()
        widget.start_h=widget.winfo_height()
def _drag(event):
    if event.widget not in bound:return
    lst=bound[event.widget]
    for data in lst: # 多个绑定
        if data[0]!='drag':return
        widget=data[1]
        dx = widget.winfo_pointerx()-widget.mousex # 计算鼠标当前位置和开始拖动时位置的差距
        # 注: 鼠标位置不能用event.x和event.y
        # event.x,event.y与控件的位置、大小有关,不能真实地反映鼠标移动的距离差值
        dy = widget.winfo_pointery()+widget.winfo_vrooty()-widget.mousey 
        move(widget,widget.startx + dx if data[2] else None,
                    widget.starty + dy if data[3] else None)
def _resize(event):
    data=__get(event.widget,'resize')
    if data is None:return
    widget=data[1]
    dx = widget.winfo_pointerx()-widget.mousex # 计算位置差
    dy = widget.winfo_pointery()-widget.mousey

    type = data[0].lower()
    minw,minh = data[2:4]
    if 's' in type:
        move(widget,height=max(widget.start_h+dy,minh))
    elif 'n' in type:
        move(widget,y=min(widget.starty+dy,widget.starty+widget.start_h-minh),
                    height=max(widget.start_h-dy,minh))

    __remove(event.widget,data[0])# 取消绑定, 为防止widget.update()中产生新的事件, 避免_resize()被tkinter反复调用
    widget.update() # 刷新控件, 使以下左右缩放时, winfo_height()返回的是新的控件坐标, 而不是旧的
    __add(event.widget,data) # 重新绑定
    
    if 'e' in type:
        move(widget,width=max(widget.start_w+dx,minw))
    elif 'w' in type:
        move(widget,x=min(widget.startx+dx,widget.startx+widget.start_w-minw),
                    width=max(widget.start_w-dx,minw))

def draggable(tkwidget,x=True,y=True):
    """调用draggable(tkwidget) 使tkwidget可拖动。
tkwidget: 一个控件(Widget)或一个窗口(Wm)。
x 和 y: 只允许改变x坐标或y坐标。"""
    bind_drag(tkwidget,tkwidget,x,y)

def bind_drag(tkwidget,dragger,x=True,y=True):
    """绑定拖曳事件。
tkwidget: 被拖动的控件或窗口,
dragger: 接收鼠标事件的控件,
调用bind_drag后,当鼠标拖动dragger时, tkwidget会被带着拖动, 但dragger
作为接收鼠标事件的控件, 位置不会改变。
x 和 y: 同draggable()函数。"""
    dragger.bind("<Button-1>",_mousedown,add='+')
    dragger.bind("<B1-Motion>",_drag,add='+')
    __add(dragger,('drag',tkwidget,x,y)) # 在bound字典中记录数据

def bind_resize(tkwidget,dragger,anchor,min_w=0,min_h=0,move_dragger=True):
    """绑定缩放事件。
anchor: 缩放"手柄"的方位, 取值为N,S,W,E,NW,NE,SW,SE,分别表示东、西、南、北。
min_w,min_h: 该方向tkwidget缩放的最小宽度(或高度)。
move_dragger: 缩放时是否移动dragger。
其他说明同bind_drag函数。"""
    dragger.bind("<Button-1>",_mousedown,add='+')
    dragger.bind("<B1-Motion>",_resize,add='+')
    data=(anchor,tkwidget,min_w,min_h,move_dragger)
    __add(dragger,data)

方法2:调用Windows API函数,获取鼠标位置
这里调用GetCursorPos这个API函数,获取鼠标的位置,只需将前面的代码稍作修改即可:

def getpos():
    # 调用API函数获取当前鼠标位置。返回值以(x,y)形式表示。
    po = _PointAPI()
    windll.user32.GetCursorPos(byref(po))
    return int(po.x), int(po.y)
def xpos():return getpos()[0]
def ypos():return getpos()[1]
def _mousedown(event):
    if event.widget not in bound:return
    lst=bound[event.widget]
    for data in lst: # 开始拖动时, 在每一个控件记录位置和控件尺寸
        widget=data[1]
        widget.mousex,widget.mousey = getpos()
        widget.startx,widget.starty = widget.winfo_x(),widget.winfo_y()
        widget.start_w=widget.winfo_width()
        widget.start_h=widget.winfo_height()
def _drag(event):
    if event.widget not in bound:return
    lst=bound[event.widget]
    for data in lst: # 多个绑定
        if data[0]!='drag':return
        widget=data[1]
        dx = xpos()-widget.mousex # 计算鼠标当前位置和开始拖动时位置的差距
        # 注: 鼠标位置不能用event.x和event.y
        # event.x,event.y与控件的位置、大小有关,不能真实地反映鼠标移动的距离差值
        dy = ypos()-widget.mousey 
        move(widget,widget.startx + dx if data[2] else None,
                    widget.starty + dy if data[3] else None)
def _resize(event):
    data=__get(event.widget,'resize')
    if data is None:return
    widget=data[1]
    dx = xpos()-widget.mousex # 计算位置差
    dy = ypos()-widget.mousey

    # --snip-- (部分代码省略)

主程序,用于测试:

def test():
    btns=[] # 用btns列表存储创建的按钮
    def add_button(func,anchor):
        # func的作用是计算按钮新坐标
        b=ttk.Button(root)
        b._func=func
        bind_resize(btn,b,anchor)
        x,y=func()
        b.place(x=x,y=y,width=size,height=size)
        b.bind('<B1-Motion>',adjust_button,add='+')
        b.bind('<B1-ButtonRelease>',adjust_button,add='+')
        btns.append(b)
    def adjust_button(event=None):
        # 改变大小或拖动后,调整手柄位置
        for b in btns:
            x,y=b._func()
            b.place(x=x,y=y)
    root=tk.Tk()
    root.title("Test")
    root.geometry('500x350')
    btn=ttk.Button(root,text="Button")
    draggable(root)
    draggable(btn)
    btn.bind('<B1-Motion>',adjust_button,add='+')
    btn.bind('<B1-ButtonRelease>',adjust_button,add='+')
    x1=20;y1=20;x2=220;y2=170;size=10
    btn.place(x=x1,y=y1,width=x2-x1,height=y2-y1)
    root.update()
    # 创建各个手柄, 这里是控件缩放的算法
    add_button(lambda:(btn.winfo_x()-size, btn.winfo_y()-size),
               'nw')
    add_button(lambda:(btn.winfo_x()+btn.winfo_width()//2,
                       btn.winfo_y()-size), 'n')
    add_button(lambda:(btn.winfo_x()+btn.winfo_width(), btn.winfo_y()-size),
               'ne')
    add_button(lambda:(btn.winfo_x()+btn.winfo_width(),
                       btn.winfo_y()+btn.winfo_height()//2),'e')
    add_button(lambda:(btn.winfo_x()+btn.winfo_width(),
                       btn.winfo_y()+btn.winfo_height()), 'se')
    add_button(lambda:(btn.winfo_x()+btn.winfo_width()//2,
                       btn.winfo_y()+btn.winfo_height()),'s')
    add_button(lambda:(btn.winfo_x()-size, btn.winfo_y()+btn.winfo_height()),
               'sw')
    add_button(lambda:(btn.winfo_x()-size,
                    btn.winfo_y()+btn.winfo_height()//2), 'w')
    root.mainloop()

if __name__=="__main__":test()

运行效果:
效果图

  • 12
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qfcy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值