Zu Jiry
Zu Jiry

Reputation: 295

How to put images into a dynamic text in Kivy

I'm working myself into kivy and want to embed a picture into a displayed text. The text is loaded a simple string, which is then displayed which is easy to do, but I somehow can not find a clue how to display an image in said string.

An answer to this question was to build a new layout for these which in this case is the TextWrapper.

Example code:

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Label, Button
from kivy.lang import Builder
from kivy.properties import BooleanProperty, StringProperty
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.image import Image
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.scrollview import ScrollView

Builder.load_string("""
<ScreenSpecies>:
    BoxLayout:
        orientation: 'vertical'

        Label:
            pos_hint: {"x": .45, "top": 1}
            size_hint: .1, .1
            text: "The Species"

        GridLayout:
            id: species_layout
            rows: 1
            cols: 2
            padding: dp(10)
            spacing: dp(10)
            orientation: 'horizontal'

            SpeciesView:
                id: species_list_view

            SpeciesLabel:
                id: species_text
                text_selected: "Text" if not species_list_view.text_selected else species_list_view.text_selected
                name_selected: "" if not species_list_view.name_selected else species_list_view.name_selected


<SpeciesView>:
    viewclass: 'SelectableLabel'
    text_selected: ""
    name_selected: ""

    SelectableRecycleBoxLayout:
        orientation: 'vertical'
        default_size: None, dp(32)
        default_size_hint: .6, None
        size_hint: 1, .9
        multiselect: False
        touch_multiselect: False


<SpeciesLabel>:
    size_hint_y: .85
    Label:
        halign: 'left'
        valign: 'middle'
        size_hint_y: None
        height: self.texture_size[1]
        text_size: self.width, None
        text: root.text_selected



<SelectableLabel>:
    canvas.before:
        Color:
            rgba: (.05, 0.5, .9, .8) if self.selected else (.5, .5, .5, 1)
        Rectangle:
            pos: self.pos
            size: self.size
""")


class TextWrapper(BoxLayout):
    def __init__(self, text="", **kwargs):
        super(TextWrapper, self).__init__(**kwargs)
        self.content = self.wrap_text(text)

    def wrap_text(self, source):
        text = source.split("|")

        for i in range(0, len(text)):
            if "img=" in text[i]:
                self.add_widget(Image(source=text[i][4:]))
            else:
                self.add_widget(Label(text=text[i]))
        return text


class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
                                 RecycleBoxLayout):
    pass


class SelectableLabel(RecycleDataViewBehavior, Label):
    ''' Add selection support to the Label '''
    index = None
    selected = BooleanProperty(False)
    selectable = BooleanProperty(True)

    def refresh_view_attrs(self, rv, index, data):
        ''' Catch and handle the view changes '''
        self.index = index
        return super(SelectableLabel, self).refresh_view_attrs(
            rv, index, data)

    def on_touch_down(self, touch):
        ''' Add selection on touch down '''
        if super(SelectableLabel, self).on_touch_down(touch):
            return True
        if self.collide_point(*touch.pos) and self.selectable:
            return self.parent.select_with_touch(self.index, touch)

    def apply_selection(self, rv, index, is_selected):
        ''' Respond to the selection of items in the view. '''
        self.selected = is_selected
        if is_selected:
            print("selection changed to {0}".format(rv.data[index]))
            rv.name_selected = rv.data[index]['text']
            rv.text_selected = rv.data[index]['description']
        else:
            print("selection removed for {0}".format(rv.data[index]))


class ScreenSpecies(Screen):
    pass


class SpeciesView(RecycleView):
    def __init__(self, **kwargs):
        super(SpeciesView, self).__init__(**kwargs)

        self.species_data = [
            {"text": "Test1", "description": "Test1.textbf |img=serveimage.png| Test1.textaf"},
            {"text": "Test2", "description": "Test2.text"},
            {"text": "Test3", "description": "Test3.text"}
        ]

        for species in self.species_data:
            species["description"] = TextWrapper(species["description"])
        # clean keywords out

        self.data = self.species_data


class SpeciesLabel(ScrollView):
    text_selected = StringProperty("")
    name_selected = StringProperty("")


screen_manager = ScreenManager()
screen_manager.add_widget(ScreenSpecies(name="screen_species"))


class TestApp(App):
    def build(self):
        return screen_manager


test_app = TestApp()
test_app.run()

This code does not work! As you can not click on a label without breaking the program, which is the problem. The text_selected only accepts str

 ValueError: SpeciesView.text_selected accept only str

I do not know how to display the TextWrapper that is in the description of each element of the view.

The purpose of it all is, that I can change the text depending on e.g. a selectable list of options and still have Images in each text.
The result should look similar to this:

The Buttons are already done!

Again, the question is how to dynamically change the text on the right with changing images in them depending on the button that was pressed last.

Thanks for your help!

Upvotes: 5

Views: 2763

Answers (2)

Kacperito
Kacperito

Reputation: 1347

Wraping

I haven't found any specific wrapping tool in kivy to solve your problem, so I've written a temporary function for that. Ideally I believe there should be a TextMarkup option to allow embedding images within the text, or even a separate widget, but for now this will be better than nothing.

def wrap_text(target, source):
    text = source.split("|")

    for i in range(0, len(text)):
        if "img=" in text[i]:
            target.add_widget(Image(source=text[i][4:]))
        else:
            target.add_widget(Label(text=text[i]))

Provided code samples works under the assumption that you put your image references in your string in following format - |img=path|. Example:

class Main(App):

    def build(self):
        base = Builder.load_file("Main.kv")

        text_field = "This is text, below it should be the first image|img=example_1.png|" \
                     "Whereas this text should appear in between the images|img=example_2.png|" \
                     "And this text under the second image"

        wrap_text(base, text_field)

        return base

Main.kv contents

BoxLayout:
    orientation: 'vertical'

Naturally, you can introduce number of improvements if you'd like, such as instead of adding general Label, you can make your own custom one. Or you could make the images to be always added horizontal, unless the \n character was in the string right before the image reference. Or you could try expanding existing kivy tools to include your desired functionality (as people do in kivy's Garden).

Retrieving text from your widget

rv.text_selected = rv.data[index]['description'] does not work because rv.data[index]['description'] is a TextWrapper widget, not a string. Note that you specify it to be the wrapper in the following line: species["description"] = TextWrapper(species["description"])

If you want to retrieve text fields from within your widget (which in fact is text within labels within the widget), use following code:

# Initialise a new, empty string, to add to it later
text = ""
        
# Iterate over widget's children (might as well dynamically assign them an id to find them faster)
for widget in rv.data[index]['description'].walk():
            
    # Add text if a label is found
    if isinstance(widget, Label):
        text += widget.text
        
# Assign the text to the field
rv.text_selected = text

Output:

Output after clicking one of the text fields

Similarly, you can retrieve the image in the same way. However, note that if you want to display both the text and the image, you don't have to retrieve the text or the images manually, just display the TextWrapper instead. You can add it by calling add_widget() for example to a BoxLayout.

Upvotes: 1

Ronald Saunfe
Ronald Saunfe

Reputation: 651

To archieve this you will have to create a custom widget, First create a Button and then add a FloatLayout to the Button .In the FloatLayout add an Image and a Label
here is an example .py

from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.properties import ObjectProperty
from kivy.uix.image import Image

#the TextImage control
class TextImage(Button):
   lbl = ObjectProperty()
   img = ObjectProperty()
   box = ObjectProperty()

   def __init__(self, **kwargs):
        super(TextImage, self).__init__(**kwargs)

   #function to change specific widget position
   def orient1(self):
       children = self.ids.box.children[:]
       self.ids.box.clear_widgets()
       for child in sorted(children):
       wid.add_widget(child) 
    .... # you can create your own funtions for arrangement

class Form(FloatLayout):
    def __init__(self, **kwargs):
        super(Form, self).__init__(**kwargs)

        Textimg = TextImage()
        Textimg.orient1() #for index arrangement
        #an image
        self.Img = Image(source='pic.png', size_hint=(None,None),size=(100,100))   
        # change the widget organization
        Textimg.ids.box.orientation='vertical' 
        Textimg.ids.box.add_widget(self.Img) 
        #change your picture
        Textimg.ids.img.source='myexample.png'
        #Change the text
        Textimg.ids.lbl.text='Picture1'
        self.add_widget(Textimg)

class MainApp(App):
    def build(self):
        return Form()

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

.kv file

<TextImage>:
    lbl: lbl
    img: img
    box: box

    FloatLayout:
        BoxLayout: # add more Image widget and change there orientation
            id: box
            Image:
                id: img
                source: 'myexample.png'
                pos_hint: {'center_x':.5,'center_y':.5}
                size_hint: 1,.9

            Label:
               id: lbl
               size_hint: 1,.1
               pos_hint: {'center_x':.5,'center_y':.5}
               halign:'center'

Upvotes: 1

Related Questions