Python 奇维菜单按钮

Python 奇维菜单按钮,python,kivy,Python,Kivy,我在创建一个按钮方面取得了一些重大进展,当按住该按钮0.3秒时,将生成一个下拉菜单: from kivy.app import App from kivy.clock import Clock from kivy.uix.gridlayout import GridLayout from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.relativelayout impor

我在创建一个按钮方面取得了一些重大进展,当按住该按钮0.3秒时,将生成一个下拉菜单:

from kivy.app import App
from kivy.clock import Clock
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.relativelayout import RelativeLayout
from functools import partial
from kivy.uix.behaviors.button import ButtonBehavior
from kivy.uix.label import Label
from kivy.properties import StringProperty, DictProperty


class MenuButton(ButtonBehavior, BoxLayout):

    # code inspired from:
        # https://github.com/kivy/kivy/wiki/Menu-on-long-touch

    # example use:
        # MenuButton(text = 'Button Text', action = 'function_as_string', options = { 'Option 1': 'function_as_string', 'Option 2': 'other_fun_as_str' } )
        # text will be applied to the buttons label, and action will only occur if the menu isn't triggered
        # in options, the keys are used as the menu options, and their values are the functions you wish to call


    text = StringProperty('')                                                           # stores the button text
    action = StringProperty('')                                                         # stores the button release action
    options = DictProperty({})                                                          # stores menu button text and actions

    def __init__(self, **kwargs):
        super(MenuButton,self).__init__(**kwargs)
        # by default, contains a button and relative layout
        # the menu() function adds the menu to the relative layout
        # this way, the menu can appear below the button
        self.orientation = 'vertical'
        self.size_hint_y = None
        button = Button(text=self.text, size_hint_y=None, height=32)
        self.add_widget(button)
        self.bind(on_touch_down=self.create_clock, on_touch_up=self.delete_clock)
        self.m = RelativeLayout()
        self.add_widget(self.m)

    def create_clock(self, widget, touch, *args):
        if self.children[1].collide_point(touch.x, touch.y):
            callback = partial(self.menu, touch)
            Clock.schedule_once(callback, .3)
            touch.ud['event'] = callback


    def delete_clock(self, widget, touch, *args):
        if self.children[1].collide_point(touch.x, touch.y):
            Clock.unschedule(touch.ud['event'])
            if self.m.children == []:
                eval(self.action)

    def menu(self, touch, *args):
        menu = BoxLayout(id='box',pos=[0,0],orientation='vertical',center=touch.pos)
        for text, fun_pass in self.options.iteritems():
            but = Button(id=fun_pass, text=text, height=32)
            but.bind(on_release=partial(self.close_menu, menu))
            but.bind(on_release=self.call_function)
            menu.add_widget(but)
        close = Button(text='close', height=32)
        close.bind(on_release=partial(self.close_menu, menu))
        menu.add_widget(close)
        self.m.add_widget(menu)


    def close_menu(self, widget, *args):                                                # clears all menu widgets from the relative layout
        self.m.clear_widgets()

    def call_function(self, widget, *args):                                             # calls function in the app by literally evaluating the string
        eval(widget.id) 


class Test(App):

    def build(self):
        f = GridLayout(cols=1)
        f.add_widget( MenuButton(text='Testing', action='Test().pp("going")', options={ '1': 'Test().pp("a")', '2': 'Test().pp("b")' } ))
        f.add_widget( MenuButton(text='Testing', action='Test().pp("going")', options={ '1': 'Test().pp("a")', '2': 'Test().pp("b")' } ))
        self.root = f

    def pp(self,text):
        print text

if __name__ == '__main__':
    Test().run()

现在的问题是,当我创建多个MenuButton()时,它们之间有很大的空间。我希望它们之间没有空格,当按下一个按钮时,下拉菜单将与它下面的任何小部件重叠。如何实现这一点?

我认为主要的问题是使用
GridLayout
,因为这显然是为了避免小部件重叠。如果您的应用程序使用
FloatLayout
,则没有限制,因此小部件重叠很容易。以下是我使用
FloatLayout
的代码版本:

from kivy.app import App
from kivy.clock import Clock
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.relativelayout import RelativeLayout
from functools import partial
from kivy.uix.behaviors.button import ButtonBehavior
from kivy.properties import StringProperty, DictProperty


class MenuButton(ButtonBehavior, BoxLayout):

    # code inspired from:
        # https://github.com/kivy/kivy/wiki/Menu-on-long-touch

    # example use:
        # MenuButton(text = 'Button Text', action = 'function_as_string', options = { 'Option 1': 'function_as_string', 'Option 2': 'other_fun_as_str' } )
        # text will be applied to the buttons label, and action will only occur if the menu isn't triggered
        # in options, the keys are used as the menu options, and their values are the functions you wish to call


    text = StringProperty('')                                                           # stores the button text
    action = StringProperty('')                                                         # stores the button release action
    options = DictProperty({})                                                          # stores menu button text and actions

    def __init__(self, **kwargs):
        super(MenuButton,self).__init__(**kwargs)
        # by default, contains a button and relative layout
        # the menu() function adds the menu to the relative layout
        # this way, the menu can appear below the button
        self.orientation = 'vertical'
        self.size_hint_y = None
        self.button = Button(text=self.text, size_hint_y=None, height=32)
        self.add_widget(self.button)
        self.height = self.button.height
        self.bind(on_touch_down=self.create_clock, on_touch_up=self.delete_clock)
        self.m = RelativeLayout()
        self.m.size_hint_y = None
        self.m.height = 0
        self.add_widget(self.m)

    def create_clock(self, widget, touch, *args):
        if self.children[1].collide_point(touch.x, touch.y):
            callback = partial(self.menu, touch)
            Clock.schedule_once(callback, .3)
            touch.ud['event'] = callback


    def delete_clock(self, widget, touch, *args):
        if self.children[1].collide_point(touch.x, touch.y):
            if 'event' in touch.ud:   # avoid non-existent key errors
                Clock.unschedule(touch.ud['event'])
            if self.m.children == []:
                eval(self.action)

    def menu(self, touch, *args):
        menu = BoxLayout(orientation='vertical')
        height_calc  = 0   # calculate the total height needed when the menu is opened
        for text, fun_pass in self.options.iteritems():
            but = Button(id=fun_pass, text=text, height=32, size_hint_y=None)
            height_calc += but.height
            but.bind(on_release=partial(self.close_menu, menu))
            but.bind(on_release=self.call_function)
            menu.add_widget(but)
        close = Button(text='close', height=32, size_hint_y=None)
        height_calc += close.height
        close.bind(on_release=partial(self.close_menu, menu))
        menu.add_widget(close)
        self.m.add_widget(menu)

        # adjust position and height of expanded MenuButton
        self.m.height = height_calc
        self.height = self.button.height + self.m.height
        self.pos = (0, self.orig_y - self.m.height)

        # make sure this MenuButton is drawn on top of any other MenuButtons
        app = App.get_running_app()
        app.root.remove_widget(self)
        app.root.add_widget(self)


    def close_menu(self, widget, *args):                                                # clears all menu widgets from the relative layout
        self.m.clear_widgets()

        # reset height and position of MenuButton when not expanded
        self.m.height = 0
        self.height = self.button.height
        self.pos = (0, self.orig_y)

    def call_function(self, widget, *args):                                             # calls function in the app by literally evaluating the string
        eval(widget.id)


class Test(App):

    def build(self):
        f = FloatLayout()
        self.mb1 = MenuButton(text='Testing1', action='App.get_running_app().pp("going")', options={ '1': 'App.get_running_app().pp("a")', '2': 'App.get_running_app().pp("b")' } )
        self.mb1.orig_y = f.height - self.mb1.button.height    # save the original y position, so it can be restored later
        self.mb1.pos = (0, self.mb1.orig_y)
        f.add_widget(self.mb1)
        self.mb2 = MenuButton(text='Testing2', action='App.get_running_app().pp("going")', options={ '1': 'App.get_running_app().pp("a")', '2': 'App.get_running_app().pp("b")' } )
        self.mb2.orig_y = f.height - self.mb1.button.height - self.mb2.button.height    # save the original y position, so it can be restored later
        self.mb2.pos = (0, self.mb2.orig_y)
        f.add_widget(self.mb2)
        self.root = f

        f.bind(size=self.sizeChanged)    # handle size adjustments when app is displayed

    def sizeChanged(self, *args):
        # make sure the MenuButtons are always at the top
        self.mb1.orig_y = self.root.height - self.mb1.button.height
        self.mb2.orig_y = self.root.height - self.mb1.button.height - self.mb2.button.height
        self.mb1.pos = (0, self.mb1.orig_y)
        self.mb2.pos = (0, self.mb2.orig_y)

    def pp(self,text):
        print text

if __name__ == '__main__':
    Test().run()

我还做了一些其他的改变。我在
delete\u-clock
方法中添加了防止关键错误的保护(我有时也会看到)。还添加了一个绑定,用于在应用程序大小更改时调整
菜单按钮的大小和位置。

谢谢!假设我正在动态地向
f
添加菜单按钮。如果是这种情况,添加并清除一组按钮后,将
self.sizeChanged
size
绑定到
f
将不起作用。我应该使用什么来代替大小来绑定函数,以便每次添加小部件时它都会触发函数?我不知道如何使用FloatLayouts来实现这一点。您可以绑定到
子项。类似于
f.bind(children=self.childrenChanged)
。当然,如果您使用不同数量的子项,您可能需要在
app.root.children
中处理每个
MenuButton
,而不仅仅是
self.mb1
self.mb2
。我必须将函数绑定到
children
size
,才能达到预期效果。谢谢你的帮忙!