pythonandroidkivy

Why are position properties (pos and pos_hint) not affecting the widget placement in my Kivy app?


I'm working on a Kivy app where I need to adjust the position and height of certain widgets dynamically. However, no matter what I try, the pos and pos_hint properties do not seem to affect the placement of widgets within my layout. Specifically, I am using BoxLayout to organize widgets vertically, but changing the pos or pos_hint of the layout and individual widgets has no visible effect on their positioning.

Here’s what I have tried:

Using pos_hint on the TextInput and Button widgets to change their vertical placement. Adjusting the pos property on the BoxLayout to move the entire layout. Setting the height for widgets and using size_hint_y to control the size of widgets. Trying to move the layout up to create space at the bottom of the screen. None of these approaches seem to have any effect. The layout remains in its original position, and the widgets are not being placed or resized as expected.

My code:

from kivy.app import App
from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.core.window import Window
from kivy.uix.label import Label
from kivy.clock import Clock
import random

class LongStringApp(App):
    def build(self):
        # Set the window size (optional)
        Window.size = (800, 600)

        # Create a long string of text
        long_string = ("""Therefore, I urge you, brothers and sisters, in view of God’s mercy, to offer your bodies as a living sacrifice, holy and pleasing to God—this is your true and proper worship.""")
        self.original_text = long_string

        # Split the text into words
        words = long_string.split()

        
        num_blanks = len(words) // 6

        # Randomly select which words to replace with blanks
        indices_to_blank = random.sample(range(len(words)), num_blanks)

        # Replace the selected words with underscores of the same length
        for i in indices_to_blank:
            word_length = len(words[i])
            words[i] = "_" * word_length  # Replace with underscores

        # Reconstruct the string with blanks
        modified_string = " ".join(words)

        # Variables for easy adjustment
        self.HEIGHT_PERCENTAGE_TEXT = 0.8  # 80% of the screen height for the TextInput
        self.HEIGHT_PERCENTAGE_BUTTON = 0.1  # 10% of the screen height for the Button
        self.POS_PERCENTAGE_LAYOUT = 0.6  # Position of the entire layout, starting from the top

        # Create a BoxLayout to hold the TextInput and Submit Button
        self.layout = BoxLayout(orientation='vertical', padding=10, spacing=10)

        # Create a TextInput widget to allow the user to edit the string
        self.text_input = TextInput(text=modified_string, font_size=20, multiline=True, size_hint_y=None,
                                    background_color=(0, 0, 0, 1), foreground_color=(1, 1, 1, 1))  # Black background, white text
        self.text_input.height = Window.height * self.HEIGHT_PERCENTAGE_TEXT  # Set the height of the TextInput
        self.text_input.text_size = (Window.width - 20, None)  # Set the text size for wrapping
        self.text_input.bind(width=self.update_text_size)  # Bind width to text_size for wrapping

        # Add the TextInput to the layout
        self.layout.add_widget(self.text_input)

        # Create a submit button to check if the filled-in text matches the original
        submit_button = Button(text="Submit", size_hint_y=None, height=50, background_color=(0.2, 0.6, 0.2, 1))

        # Set height for the Button manually
        submit_button.height = Window.height * self.HEIGHT_PERCENTAGE_BUTTON  # Set the height of the Button

        submit_button.bind(on_press=self.check_answers)

        # Add the button to the layout
        self.layout.add_widget(submit_button)

        # Create a ScrollView and add the layout to it
        scroll_view = ScrollView()
        scroll_view.add_widget(self.layout)

        # Schedule the scroll to the top after the window has been fully created
        Clock.schedule_once(self.scroll_to_top, 0.1)

        # Move the entire layout up by 20% of the screen height (adjusting the top margin)
        self.layout.pos_hint = {'top': self.POS_PERCENTAGE_LAYOUT}  # Set the top position to be 80% from the top

        
        return scroll_view

    def update_text_size(self, *args):
        # Update the text_size with the current width of the TextInput minus padding
        self.text_input.text_size = (self.layout.width - 20, None)

    def scroll_to_top(self, dt):
        # This function will set the ScrollView to the top
        self.root.scroll_y = 1
        
        # Set focus to the TextInput and move the cursor to the start
        self.text_input.focus = True
        self.text_input.cursor = (0, 0)  # Moves the cursor to the start


    def check_answers(self, instance):
        # Get the filled-in text from the TextInput
        filled_text = self.text_input.text.strip()

        # Remove any extra spaces for comparison
        original_text = self.original_text
        original_text_clean = ' '.join(original_text.split())
        filled_text_clean = ' '.join(filled_text.split())

        # Check if the filled text matches the original text
        if filled_text_clean == original_text_clean:
            result_text = "Correct! You filled in all the blanks correctly."
        else:
            result_text = "There are some mistakes. Please try again."

        # Create a result label to show feedback
        result_label = Label(text=result_text, size_hint=(1, None), height=50, font_size=18, color=(1, 1, 1, 1))

        # Add the result label to the layout (within BoxLayout)
        self.layout.add_widget(result_label)

        # Optionally remove the result label after a few seconds
        Clock.schedule_once(lambda dt: self.remove_result_label(result_label), 2)

    def remove_result_label(self, label):
        self.layout.remove_widget(label)

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

What I expect: The pos_hint and pos properties should adjust the placement of the TextInput and Button widgets and the entire layout. I want to create space at the bottom (20% of the window should be empty, the bottom 20%) of the screen by shifting the layout and widgets up by 20%. What is happening: The widgets remain in their initial positions, and adjusting pos or pos_hint does not affect the layout. Additional Information: I have also tried using size_hint_y to adjust heights, but that only resizes the widgets, not their position. Could someone help me understand why the pos and pos_hint properties are not working for widget placement in my Kivy app?


Solution

  • The first problem is that you want the bottom 20% of the screen to be empty, but your TextInput and Button are sized to take up 90% of the screen. As a start, try changing:

    self.HEIGHT_PERCENTAGE_TEXT = 0.8
    

    to:

    self.HEIGHT_PERCENTAGE_TEXT = 0.7
    

    Also, for your ScrollView to work, its child layout must have size_hint_y set to None. See the documentation. So try changing:

    self.layout = BoxLayout(orientation='vertical', padding=10, spacing=10)
    

    to:

    self.layout = BoxLayout(orientation='vertical', padding=10, spacing=10, size_hint_y=None)
    

    Now you need to actually set the height of the BoxLayout, which can be done by binding the minimum_height as:

    self.layout.bind(minimum_height=self.layout.setter('height'))
    

    I think the above changes will give you what you have described.

    As far as using pos_hint inside a BoxLayout, see the documentation which mentions:

    Position hints are partially working, depending on the orientation:

    If the orientation is vertical: x, right and center_x will be used.
    
    If the orientation is horizontal: y, top and center_y will be used.
    

    Also, a BoxLayout with vertical orientation sets the pos of its children vertically within its box, so setting the pos of a child will have no effect.