A textfield and a button in a horizontal box layout the textfield taking most of the screen space button getting little


The problem is the MDTextField taking most of the width and the MDButton getting very little space at the right side. Tried size_hint_x = .5 in both l, not working, .7 .3 not working. 1:1 not working. Tried fixed wide for the buttons, not working. And also vertical spacing between the each row is too much high, not compact.

Using pydroid 3, kivymd2, android

The problem reproducible code is here:

from kivymd.app import MDApp from kivymd.uix.screen import MDScreen from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.appbar import MDTopAppBar, MDTopAppBarTitle from kivymd.uix.label import MDLabel from kivymd.toast import toast from kivymd.uix.button import MDButton, MDButtonIcon, MDButtonText from kivymd.uix.widget import MDWidget from kivymd.icon_definitions import md_icons from kivymd.uix.textfield import ( MDTextField, MDTextFieldHintText, MDTextFieldMaxLengthText ) from kivymd.uix.segmentedbutton import ( MDSegmentedButton, MDSegmentedButtonItem, MDSegmentButtonLabel ) from kivymd.uix.dialog import ( MDDialog, MDDialogHeadlineText, MDDialogSupportingText, MDDialogButtonContainer ) from kivy.uix.widget import Widget from kivy.core.window import Window from kivy.core.text import Label as CoreLabel from kivy.metrics import dp class TxtField(MDTextField): def __init__(self, ilen = 7, htxt="", shintx=0.7, **kwargs): super().__init__(**kwargs) self.mode = "outlined" #"filled" self.size_hint_x = .5 self.pos_hint={"center_x": .5} self.input_filter = lambda s, u: s if ( len(self.text) < ilen and s.isdigit() ) else "" if not htxt: htxt = "input:" self.add_widget( MDTextFieldHintText( text = htxt ) ) class mkbtn(MDButton): def __init__( self, text, style="elevated", # radius=[20,20,20,20], icon=None, func=lambda x: dummy_func(x), **kwargs): super().__init__(**kwargs) self.style = style self.radius = radius self.size_hint_x = None self.pos_hint={"center_x": .5} #padding=[5, 5, 5, 5] #self.adaptive_width=True #self.size_hint_min_x = 0.3 self.wide = dp(120) if icon: self.add_widget(MDButtonIcon(icon)) self.add_widget(MDButtonText(text=text)) self.bind(on_release=func) def dummy_func(x="", y=""): pass class MainScreen(MDScreen): def __init__(self, **kwargs): super().__init__(**kwargs) self.md_bg_color = [1, 1, 1, 1] #md_bg_color: self.theme_cls.secondaryContainerColor self.main_layout = MDBoxLayout(orientation="vertical") self.app_bar = MDTopAppBar(type="small") self.abtitle = MDTopAppBarTitle( text="WOS Squad Ratio (Bear/CJ)", halign="center" ) self.app_bar.add_widget(self.abtitle) self.content_layout = MDBoxLayout(orientation="vertical", padding=[5, 5, 5, 5], spacing=dp(10),) self.hlay1 = MDBoxLayout(orientation="horizontal", padding=[5, 5, 5, 5], spacing=dp(10)) self.bdctf = TxtField(htxt="Bear DC", ilen=6) self.prof_btn = mkbtn("Profiles", icon="triangle-small-down") self.hlay1.add_widget(self.bdctf) self.hlay1.add_widget(self.prof_btn) #self.hlay1.add_widget(MDWidget()) ### self.hlay2 = MDBoxLayout(orientation="horizontal", padding=[5, 5, 5, 5], spacing=dp(10),) self.cdctf = TxtField(htxt="CJ DC", ilen=6) self.aprof_btn = mkbtn("Add Profiles") self.hlay2.add_widget(self.cdctf) self.hlay2.add_widget(self.aprof_btn) ### self.hlay3 = MDBoxLayout(orientation="horizontal", padding=[5, 5, 5, 5], spacing=dp(10),) self.titf = TxtField(htxt="Total Infantry", ilen=7) self.cb_btn = mkbtn("Calc Bear") self.hlay3.add_widget(self.titf) self.hlay3.add_widget(self.cb_btn) ### self.hlay4 = MDBoxLayout(orientation="horizontal", padding=[5, 5, 5, 5], spacing=dp(10),) self.tltf = TxtField(htxt="Total Lancer", ilen=7) self.cc_btn = mkbtn("Calc CJ") self.hlay4.add_widget(self.tltf) self.hlay4.add_widget(self.cc_btn) ### self.hlay5 = MDBoxLayout(orientation="horizontal", padding=[5, 5, 5, 5], spacing=dp(10),) self.tmtf = TxtField(htxt="Total Lancer", ilen=7) self.sd_btn = mkbtn("Save Data") self.hlay5.add_widget(self.tmtf) self.hlay5.add_widget(self.sd_btn) ### self.tlbl = MDLabel(halign="left",role="large",padding=[5, 0, 0, 500],text = "") def cng_lbl(txt): self.tlbl.text = txt ### self.content_layout.add_widget(self.hlay1) self.content_layout.add_widget(self.hlay2) self.content_layout.add_widget(self.hlay3) self.content_layout.add_widget(self.hlay4) self.content_layout.add_widget(self.hlay5) self.content_layout.add_widget(MDWidget()) ### self.main_layout.add_widget(self.app_bar) self.main_layout.add_widget(self.content_layout) ### self.add_widget(self.main_layout) class MyApp(MDApp): def build(self): root = MainScreen() return root if __name__ == "__main__": MyApp().run()
3
Apr 20 at 12:06 PM
User AvatarUnBorn
#python#android#kivy#kivymd

Accepted Answer

I don't know if I resolve all problems but I describe what I found.

It seems MDButton works different than other widgets.

Here is original MDButton.__init__

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        Clock.schedule_once(self.adjust_width, 0.2)
        Clock.schedule_once(self.adjust_pos, 0.2)

It uses Clock to run self.adjust_width after 0.2 seconds to create button with minimal width - so it doesn't allow to use self.width at once in own __init__ because it will be replaced by width calculated in self.adjust_width

(by the way: as I remember __init__ can't calculate it directly because Kivy creates real objects later, sets values from files .kv, and after that code can change sizes - and this is why it has to use Clock to run code later)

I see two methods for this:

  1. use also Clock to run own function which will change width little later
from kivy.clock import Clock

class mkbtn(MDButton):
    def __init__(...):

        # ... code ...

        Clock.schedule_once(self.update_later, 0.2)

    def update_later(self, args):
        self.width = dp(120)
  1. replace adjust_width with own function - empty code - and it will not calculate own width
class mkbtn(MDButton):
    def __init__(...):

        # ... code ...

        self.width = dp(120)

    def adjust_width(self, args):
        pass

Result on desktop Linux

enter image description here

I also centered text on button (but text+icon may need different method)

#from kivy.clock import Clock

# if you put this before class mkbtn then you can use `func=dummy_func`
def dummy_func(x="", y=""):
    pass


class mkbtn(MDButton):
    def __init__(
        self,
        text,
        style="elevated",  #
        radius=[20, 20, 20, 20],
        icon=None,
        # func=lambda x: dummy_func(x),
        func=dummy_func,
        **kwargs
    ):

        super().__init__(**kwargs)
        self.style = style
        self.radius = radius
        self.size_hint_x = None

        # self.adaptive_width=True
        # self.size_hint_min_x = 0.3
        # self.wide = dp(120)
        self.width = dp(120)

        if icon:
            self.add_widget(MDButtonIcon(icon))
        self.add_widget(MDButtonText(text=text))
        self.bind(on_release=func)

        # center text on button - has to be after `add_widget(MDButtonText(..>))`
        self._button_text.pos_hint = {"center_x": 0.5, "center_y": 0.5}

        # center button 
        # self.pos_hint = {"center_x": 0.5, "center_y": 0.5}  # center Y

        # Clock.schedule_once(self.update_later, 0.2)

    # def update_later(self, args):
    #    self.width = dp(120)

    def adjust_width(self, args):
        pass

If in Button and TextField you use pos_hint = {"cente_y": 0.5} then both widgets will looks like in one line.

enter image description here

#from kivy.clock import Clock


class TxtField(MDTextField):
    def __init__(self, ilen=7, htxt="", shintx=0.7, **kwargs):
        super().__init__(**kwargs)
        self.mode = "outlined"  # "filled"
        self.size_hint_x = 0.5

        # center textfield in layout (Box)
        self.pos_hint = {"center_x": 0.5, "center_y": 0.5}  # center Y

        self.input_filter = lambda s, u: (
            s if (len(self.text) < ilen and s.isdigit()) else ""
        )

        if not htxt:
            htxt = "input:"

        self.add_widget(MDTextFieldHintText(text=htxt))


# if you put this before class mkbtn then you can use `func=dummy_func`
def dummy_func(x="", y=""):
    pass


class mkbtn(MDButton):
    def __init__(
        self,
        text,
        style="elevated",  #
        radius=[20, 20, 20, 20],
        icon=None,
        # func=lambda x: dummy_func(x),
        func=dummy_func,
        **kwargs
    ):

        super().__init__(**kwargs)
        self.style = style
        self.radius = radius
        self.size_hint_x = None

        # self.adaptive_width=True
        # self.size_hint_min_x = 0.3
        # self.wide = dp(120)
        self.width = dp(120)

        if icon:
            self.add_widget(MDButtonIcon(icon))
        self.add_widget(MDButtonText(text=text))
        self.bind(on_release=func)

        # center text on button - has to be after `add_widget(MDButtonText(..>))`
        self._button_text.pos_hint = {"center_x": 0.5, "center_y": 0.5}

        # center button in layout (Box)
        self.pos_hint = {"center_x": 0.5, "center_y": 0.5}  # center Y

        # Clock.schedule_once(self.update_later, 0.2)

    # def update_later(self, args):
    #    self.width = dp(120)

    def adjust_width(self, args):
        pass

If you add background color in MDBoxLayout then you see how it is centered

        self.hlay5 = MDBoxLayout(
            orientation="horizontal",
            padding=[5, 5, 5, 5],
            spacing=dp(10),
            md_bg_color=[1, 0, 0, 1],
        )

Before centering:

enter image description here

After centering:

enter image description here


To reduce height in rows I have to use size_hint_y=None in every hlay* and bind function which will calculates minimal height for every hlay*

self.hlay4.bind(minimum_height=self.hlay4.setter("height"))

enter image description here

class MainScreen(MDScreen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.md_bg_color = [1, 1, 1, 1]
        # md_bg_color: self.theme_cls.secondaryContainerColor

        self.main_layout = MDBoxLayout(orientation="vertical")

        self.app_bar = MDTopAppBar(type="small")
        self.abtitle = MDTopAppBarTitle(
            text="WOS Squad Ratio (Bear/CJ)", halign="center"
        )
        self.app_bar.add_widget(self.abtitle)

        self.content_layout = MDBoxLayout(
            orientation="vertical",
            padding=[5, 5, 5, 5],
            spacing=dp(10),
        )

        self.hlay1 = MDBoxLayout(
            orientation="horizontal",
            padding=[5, 5, 5, 5],
            spacing=dp(10),
            size_hint_y=None,
        )
        self.bdctf = TxtField(htxt="Bear DC", ilen=6)
        self.prof_btn = mkbtn("Profiles", icon="triangle-small-down")
        self.hlay1.add_widget(self.bdctf)
        self.hlay1.add_widget(self.prof_btn)
        self.hlay1.bind(minimum_height=self.hlay1.setter("height"))

        ###
        self.hlay2 = MDBoxLayout(
            orientation="horizontal",
            padding=[5, 5, 5, 5],
            spacing=dp(10),
            size_hint_y=None,
        )
        self.cdctf = TxtField(htxt="CJ DC", ilen=6)
        self.aprof_btn = mkbtn("Add Profiles")
        self.hlay2.add_widget(self.cdctf)
        self.hlay2.add_widget(self.aprof_btn)
        self.hlay2.bind(minimum_height=self.hlay2.setter("height"))

        ###
        self.hlay3 = MDBoxLayout(
            orientation="horizontal",
            padding=[5, 5, 5, 5],
            spacing=dp(10),
            size_hint_y=None,
        )
        self.titf = TxtField(htxt="Total Infantry", ilen=7)
        self.cb_btn = mkbtn("Calc Bear")
        self.hlay3.add_widget(self.titf)
        self.hlay3.add_widget(self.cb_btn)
        self.hlay3.bind(minimum_height=self.hlay3.setter("height"))

        ###
        self.hlay4 = MDBoxLayout(
            orientation="horizontal",
            padding=[5, 5, 5, 5],
            spacing=dp(10),
            size_hint_y=None,
        )
        self.tltf = TxtField(htxt="Total Lancer", ilen=7)
        self.cc_btn = mkbtn("Calc CJ")
        self.hlay4.add_widget(self.tltf)
        self.hlay4.add_widget(self.cc_btn)
        self.hlay4.bind(minimum_height=self.hlay4.setter("height"))

        ###
        self.hlay5 = MDBoxLayout(
            orientation="horizontal",
            padding=[5, 5, 5, 5],
            spacing=dp(10),
            md_bg_color=[1, 0, 0, 1],
            size_hint_y=None,
        )
        self.tmtf = TxtField(htxt="Total Lancer", ilen=7)
        self.sd_btn = mkbtn("Save Data")
        self.hlay5.add_widget(self.tmtf)
        self.hlay5.add_widget(self.sd_btn)
        self.hlay5.bind(minimum_height=self.hlay5.setter("height"))

        ###
        self.tlbl = MDLabel(
            halign="left",
            role="large",
            padding=[5, 0, 0, 500],
            text="",
        )

        def cng_lbl(txt):
            self.tlbl.text = txt

        ###
        self.content_layout.add_widget(self.hlay1)
        self.content_layout.add_widget(self.hlay2)
        self.content_layout.add_widget(self.hlay3)
        self.content_layout.add_widget(self.hlay4)
        self.content_layout.add_widget(self.hlay5)
        self.content_layout.add_widget(
            MDWidget(
                # size_hint_y=1,  # doesn't need this
                md_bg_color=[0, 1, 0, 1],
            )
        )

        ###
        self.main_layout.add_widget(self.app_bar)
        self.main_layout.add_widget(self.content_layout)

        ###
        self.add_widget(self.main_layout)

It keeps sizes after resizing window.

enter image description here

but problem is when window is too small to keep all elements

enter image description here

User Avatarfuras
Apr 20 at 12:18 PM
1