我们知道,在移动设备程序中,很多按钮或标签块在程序运行时是可移动的。我们可能时常遇到一些时候,我们要用手指拖动一个按钮移到别的地方。那么这个功能如何在Kivy中实现呢?
其实大体上说,就是在拖动控件时,控件的位置要不断更新,根据手指拖动的位置,不断重新计算控件的位置。
一、程序大致思路
既然要在拖动控件时更新控件位置,那么,显然应该找一个当手指在屏幕上移动(或按着鼠标移动)时,触发的函数。
Kivy教程 Kivy - 输入 - 技术教程 里就有说到,对于一个Kivy控件,on_touch_move() 会在手指拖动时自动触发。所以若要让"重新计算控件位置"的程序在拖动时被触发,只需重载 on_touch_move() 函数即可。
那么,这个 on_touch_move() 的重载,应该是写在具体的可移动控件上,还是写在整个窗口上呢?经过研究和资料查询,或许需要写在整个窗口控件上。这是因为,根据 Kivy Label:屏幕上到处都会触发on_touch_down事件-腾讯云开发者社区-腾讯云 的说法,任何一个控件的 on_touch_down() 函数,都是在屏幕上的任何一个地方都会触发的。on_touch_move() 和 on_touch_up() 似乎也一样。因此,即使在某个具体的控件上重载,也无法通过该函数产生的事件判断是哪个按钮被触发了。因此,这个函数的重载写在整个窗口的控件(如BoxLayout)上更符合逻辑。
幸运的是,按钮的 on_click,on_release 事件是"按哪个按钮,产生的事件就是哪个按钮" (见Kivy的事件向方法传递的event是什么?-CSDN博客),所以可以分工:按钮的 on_click和on_release 事件用于让程序确认是哪个按钮被点击了,以及这个按钮原来的位置在哪里;而整个窗口的 on_touch_move() 用来计算移动量,且整个窗口也要通过 on_touch_move() 在检测到拖动时产生事件,通过事件输出拖动量,从而让程序知道按钮拖动到哪里。关于如何自定义事件让 on_touch_move() 来产生,请参阅 Kivy如何自定义事件-CSDN博客。
二、Kivy示例
(一)Kivy程序分析
首先,在程序里,要有一个容器(即变量对象)存放两条信息:一个是被点击的按钮,一个是该按钮被点击时,它的位置坐标。在均未被点击时,两个都是None。
python
class labelsApp(App):
click = [None, None]
这两条信息会在按钮被按下时被更新,松开时又恢复None。还是先绑定事件:
python
self.lb1.bind(on_press=self.onclick)
self.lb1.bind(on_release=self.onrelease)
然后定义函数
python
def onclick(self, event):
self.click[0] = event
if not event == None:
self.click[1] = tuple(event.pos)
else:
self.click[1] = None
#print(self.click)
def onrelease(self, event):
self.click[0] = None
self.click[1] = None
这里 event 就是点击的按钮,event.pos 就是被点击时按钮的位置。注意,这个位置,在按钮被拖动时会变化,但其存储的 self.click1 在鼠标移动时不会变化,因为 on_touch_move() 不会更新 self.click1,只有在放开后,self.click1 才会再更新为 None。
与此同时,对于整个窗口,我们要对 BoxLayout 进行扩展,重载 on_touch_move() 函数,令其产生事件。但对于触摸时发生移动导致运行的 def on_touch_move(self, touch) 里的 touch 参数,其值为移动后鼠标、手指的位置。
但是,我们计算移动后按钮的位置,应该是:移动前按钮的位置 + 鼠标移动了的位置。
所以,我们要想办法计算鼠标移动了的位置,即从开始点击算起,鼠标的位置变化了多少。
python
class MoveableBox(BoxLayout, EventDispatcher):
def __init__(self, *args, **kwargs):
self.currPos = (0, 0)
self.movedPos = (0, 0)
self.register_event_type('on_moving') # Event type must be registered at __init__
self.register_event_type('on_down')
self.register_event_type('on_up')
return super().__init__(*args, **kwargs)
def on_touch_down(self, touch):
self.currPos = touch.pos
self.dispatch('on_down')
return super().on_touch_down(touch) # Remember, super() function must be written
def on_touch_move(self, touch):
self.movedPos = (touch.pos[0] - self.currPos[0], touch.pos[1] - self.currPos[1])
self.dispatch('on_moving')
return super().on_touch_move(touch)
def on_touch_up(self, touch):
self.currPos = (0, 0)
self.movedPos = (0, 0)
self.dispatch('on_up')
return super().on_touch_up(touch)
def on_moving(self, *args):
pass
def on_down(self, *args):
pass
def on_up(self,*args):
pass
注意,在 on_touch_down() 函数中,self.currPos 被更新成鼠标当前位置,即开始点击的位置。
而在 on_touch_move() 函数中,touch.pos 是鼠标现在的位置,self.movedPos 被更新成鼠标现在位置和开始点击的位置 self.currPos 之间的差别,也就是本次移动迄今为止的坐标距离。
与此同时,移动时 on_moving 事件被触发了。所以看一下事件绑定的函数。
python
def onmove(self, event):
#print(self.click, event.movedPos)
if self.click[1] is not None:
self.click[0].pos = (event.movedPos[0] + self.click[1][0], event.movedPos[1] + self.click[1][1])
在这个函数里,event 就是这个MoveableBox,所以可以读取它的 movedPos。前面说到 self.click1 是按钮被移动之前的位置,所以新的按钮位置是被移动之前的位置 + 本次移动迄今为止的坐标距离。
现在附上完整程序:
python
# -*- coding: utf-8 -*-
"""
Created on Thu May 21 17:16:54 2026
@author: iven.dong
"""
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.event import EventDispatcher
class MoveableBox(BoxLayout, EventDispatcher):
def __init__(self, *args, **kwargs):
self.currPos = (0, 0)
self.movedPos = (0, 0)
self.register_event_type('on_moving') # Event type must be registered at __init__
self.register_event_type('on_down')
self.register_event_type('on_up')
return super().__init__(*args, **kwargs)
def on_touch_down(self, touch):
self.currPos = touch.pos
self.dispatch('on_down')
return super().on_touch_down(touch) # Remember, super() function must be written
def on_touch_move(self, touch):
self.movedPos = (touch.pos[0] - self.currPos[0], touch.pos[1] - self.currPos[1])
self.dispatch('on_moving')
return super().on_touch_move(touch)
def on_touch_up(self, touch):
self.currPos = (0, 0)
self.movedPos = (0, 0)
self.dispatch('on_up')
return super().on_touch_up(touch)
def on_moving(self, *args):
pass
def on_down(self, *args):
pass
def on_up(self,*args):
pass
class labelsApp(App):
click = [None, None]
def build(self):
self.box = MoveableBox(orientation='vertical')
self.lb1 = Button(background_color=(0,1,1,1))
self.lb1.bind(on_press=self.onclick)
self.lb1.bind(on_release=self.onrelease)
self.box.add_widget(self.lb1)
self.lb2 = Button(background_color=(1,0,1,1))
self.lb2.bind(on_press=self.onclick)
self.lb2.bind(on_release=self.onrelease)
self.box.add_widget(self.lb2)
self.box.bind(on_moving=self.onmove)
self.box.bind(on_down=self.ondown)
self.box.bind(on_up=self.onup)
return self.box
def onclick(self, event):
self.click[0] = event
if not event == None:
self.click[1] = tuple(event.pos)
else:
self.click[1] = None
#print(self.click)
def onrelease(self, event):
self.click[0] = None
self.click[1] = None
def ondown(self, event):
pass
def onup(self, event):
pass
def onmove(self, event):
#print(self.click, event.movedPos)
if self.click[1] is not None:
self.click[0].pos = (event.movedPos[0] + self.click[1][0], event.movedPos[1] + self.click[1][1])
app = labelsApp()
app.run()
(二)运行效果

(三)局限性
从运行效果中可看出,紫按钮总是在绿按钮前方。这是BoxLayout的一个弱点:按钮的先后顺序和按钮的位置,前后覆盖情况绑定。很难将后面的按钮提前。所以,要实现按钮拖动的功能,建议不要使用BoxLayout,而是使用FloatLayout,RelativeLayout等。参见Layouts --- Kivy 2.3.1 documentation。
三、总结
Kivy可以制造可移动的控件。方法是通过具体控件的事件确定按下的控件以及控件的旧位置,同时通过整个界面的事件确定拖动的距离,从而计算控件新位置进行更新。