在Kivy中制造可移动控件

我们知道,在移动设备程序中,很多按钮或标签块在程序运行时是可移动的。我们可能时常遇到一些时候,我们要用手指拖动一个按钮移到别的地方。那么这个功能如何在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可以制造可移动的控件。方法是通过具体控件的事件确定按下的控件以及控件的旧位置,同时通过整个界面的事件确定拖动的距离,从而计算控件新位置进行更新。

相关推荐
Zy_Yin1231 小时前
拆解如何用anthropic金融agent做投研
人工智能·python·深度学习·金融·github
清水白石0081 小时前
Python 变量的本质:从“盒子思维”到“引用思维”,彻底理解赋值到底发生了什么
java·python·ajax
yaoxin5211231 小时前
423. Java 日期时间 API - DayOfWeek 和 Month 枚举
开发语言·python
燐妤1 小时前
Python工具使用:Pycharm
python·pycharm
Wonderful U1 小时前
基于Python+Django的私有化云笔记系统:从痛点分析到完整实现
笔记·python·django
weixin_468466851 小时前
机器学习数据预处理新手实战指南
人工智能·python·算法·机器学习·编程·数据预处理
大数据魔法师1 小时前
Streamlit(二十)- API 参考文档(十三)- 缓存与状态管理组件
python·web
Cloud_Shy6182 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第一章 Item 7 - 9)
开发语言·数据库·python