I have added GridLayout to ScrollView, and I'm adding widgets in GridLayout dynamically from python program.Instead of using more space of window it's resizing height of older widgets.What am i doing wrong here?
I tried putting BoxLayout inside GridLayout but it's not working.
I also tried to add widgets directly to ScrollView but i found out ScrollView only supports one widget.
My kv code:
<Downloading>:
my_grid: mygrid
GridLayout:
cols: 1
size_hint_y : None
hight: self.minimum_height
id: mygrid
My python code:
class Downloading(ScrollView):
set_text = ObjectProperty()
my_grid = ObjectProperty()
def __init__(self, select, link, path, username, password):
self.select = select
self.link = link
self.path = path
self.username = username
self.password = password
self.p_bar = []
self.stat = []
self.parent_conn, self.child_conn = Pipe()
p = Process(target=main, args=(self.child_conn, self.select,
self.link, self.path,
self.username, self.password))
p.start()
super().__init__()
self.event = Clock.schedule_interval(self.download_GUI, 0.1)
def newFile(self, title):
# self.newId = "stat" + str(len(self.p_bar) + 1)
self.stat.append(Label(text=''))
self.p_bar.append(ProgressBar())
self.my_grid.add_widget(Label(text=title))
self.my_grid.add_widget(self.stat[-1])
self.my_grid.add_widget(self.p_bar[-1])
def download_GUI(self, a):
temp = self.parent_conn.recv()
print(temp)
if temp == "new":
self.downloading = True
return
if self.downloading:
self.newFile(temp)
self.downloading = False
return
if type(temp) == type({}):
self.complete = temp['complete']
if not self.complete:
status = "{0}//{1} # {2} ETA: {3}".format(temp['dl_size'],
temp['total_size'],temp['speed'],temp['eta'])
self.stat[-1].text = status
self.p_bar[-1].value = temp['progress']
return
if temp == "end":
self.event.cancel()
Clock.schedule_once(exit, 3)
I believe you just need to set the height for each widget that you add to your GridLayout. For example:
self.my_grid.add_widget(Label(text=title, size_hint=(1, None), height=50))
You might need to do the same for the other widgets you are adding. The GridLayout may give the initially added widgets more space than that, but will not squeeze them any any tighter than your specified height.
After applying following changes to kv file
<Downloading>:
size: self.size
my_grid: mygrid
GridLayout:
cols: 1
size_hint_y : None
row_default_height: '25dp'
row_force_default: True
id: mygrid
And adding this line in the end of constructor (__init__), it is working as expected.
self.my_grid.bind(minimum_height=self.my_grid.setter('height'))
Related
How do I increase the width of the dropdown list within a spinner? My button as you can see in the photo is small, and the values in my drop-down list do not appear completely. I did a little research on the internet and I could see that some people say that I need to create a class spinner and add these features. But I don't know how to do this. Could someone show me a code example of how I do this?
main.kv (simplified code)
...
Spinner:
id: spinnerrpi
size_hint: None, None
width: '30sp'
height: '30sp'
border: 0,0,0,0
background_normal: 'seta1.png'
background_down: 'seta2.png'
values: "Branco Neve","Banco Gelo","Amarelo","Rosa Claro","Bege"
on_text: app.spinner_rpiso(spinnerrpi.text)
...
main.py (simplified code)
...
class PrimeiraJanela(Screen):
pass
class GerenciadorDeJanelas(ScreenManager):
pass
class MainApp(App):
texture = ObjectProperty()
def build(self):
self.title = 'MyApp'
self.texture = Image(source = 'wave.png').texture
sm = ScreenManager()
sm.add_widget(PrimeiraJanela(name = 'primeira'))
sm.current = 'primeira'
return sm
def spinner_rpiso(self, value):
if (value=='Branco Neve'):
self.root.get_screen('primeira').ids.rpi.text = str('0.90')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
if (value=='Banco Gelo'):
self.root.get_screen('primeira').ids.rpi.text = str('0.70')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
if (value=='Amarelo'):
self.root.get_screen('primeira').ids.rpi.text = str('0.70')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
if (value=='Rosa Claro'):
self.root.get_screen('primeira').ids.rpi.text = str('0.60')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
if (value=='Bege'):
self.root.get_screen('primeira').ids.rpi.text = str('0.60')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
def exit(self):
App.get_running_app().stop()
aplicativo = MainApp()
aplicativo.run()
Here is an extension of Spinner that honors an option_width property:
class SpinnerWithOptionWidth(Spinner):
option_width = NumericProperty(0) # the new property
def __init__(self, **kwargs):
self.invisible_attacher = None
super(SpinnerWithOptionWidth, self).__init__(**kwargs)
def on_is_open(self, instance, value):
# This method is modified from Spinner
attacher = self
if value:
if self.option_width > 0:
if self.invisible_attacher is None:
# The DropDown is the same width as the widget it attaches to
# so make an invisible widget with the desired width
self.invisible_attacher = Widget(opacity=0, size_hint=(None, None))
self.add_widget(self.invisible_attacher)
self.invisible_attacher.pos = (self.center_x - self.option_width/2, self.y)
self.invisible_attacher.size = (self.option_width, self.height)
attacher = self.invisible_attacher
# open th DropDown
self._dropdown.open(attacher)
else:
if self._dropdown.attach_to:
if self.invisible_attacher:
self.remove_widget(self.invisible_attacher)
self.invisible_attacher = None
self._dropdown.dismiss()
I use Kivy's recycleview to show a list of data in a table like manner. I used the example from the docs as a base for my implementation.
In my program the RecycleDataView is based on a BoxLayout and its child widgets are generated dynamically.
This seems to work but the order in which the items are displayed is sometimes reversed and keeps changing if your resize the window. Even worse, if you scroll down, the layout gets completely crazy. This does not happen if I would use a simple label as item class, so I guess the problem is with my dynamicaly created widget logic but I don't understand why.
Here is some minimal code that shows the issue.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
Builder.load_string('''
<RV>:
viewclass: 'RVItem'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class Attribute:
def __init__(self, name, values):
self.name = name
self.values = values
class RVItem(RecycleDataViewBehavior, BoxLayout):
index = None
attribute = ObjectProperty()
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
self.create_widgets(data.pop('attribute', None))
return super(RVItem, self).refresh_view_attrs(
rv, index, data)
def create_widgets(self, value: Attribute):
"""Dynamically create the needed Widgets"""
if value is None:
return
self.add_widget(Label(text=value.name, height=self.height, size_hint=(1, None)))
if not isinstance(value.values, dict):
self.add_widget(Label(text=value.values, height=self.height, size_hint=(1, None)))
else:
for _, v in value.values.items():
self.add_widget(Label(text=v, height=self.height, size_hint=(1, None)))
image_button = Button(text='+')
#image_button.source = 'wm_ui/glyphs/plus.png'
image_button.size_hint = None, None
image_button.size = "30sp", "30sp"
image_button.bind(on_press=self.add_button_pressed)
self.add_widget(image_button)
def add_button_pressed(self, s):
print("Would add a new item to the recycleview if implemented.")
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'attribute': Attribute(str(x), "test")} for x in range(100)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
I changed some Widgets to their base classes for simplicity (like ImageButton and Label)
When you run the application you should see that the order if the items is reversed and starting with 10 instead of 100 for some reason.
After resizing the window with the mouse in one of the window's corners you should see the contents continuously reordering.
And if you scroll down, things get even more crazy.
Unfortunately I have no idea what causes the behavior. I developed some Kivy apps before but this is my first really deep dive that uses more than just labels and a few inputs.
Here is a modification of your code that seems to work correctly:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
Builder.load_string('''
<RV>:
viewclass: 'RVItem'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class Attribute:
def __init__(self, name, values):
self.name = name
self.values = values
class RVItem(RecycleDataViewBehavior, BoxLayout):
index = None
attribute = ObjectProperty()
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
self.create_widgets(data['attribute'])
return super(RVItem, self).refresh_view_attrs(
rv, index, data)
def create_widgets(self, value: Attribute):
rv = App.get_running_app().root
rv.cache_widgets(self.children)
self.clear_widgets()
label = rv.get_label()
label.text = value.name
self.add_widget(label)
if isinstance(value.values, dict):
for _,v in value.values.items():
label = rv.get_label()
label.text = v
self.add_widget(label)
else:
label = rv.get_label()
label.text = value.values
self.add_widget(label)
image_button = rv.get_button()
image_button.text = '+'
image_button.size_hint = None, None
image_button.size = "30sp", "30sp"
image_button.bind(on_press=self.add_button_pressed)
self.add_widget(image_button)
def add_button_pressed(self, s):
print("Would add a new item to the recycleview if implemented.")
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.label_cache = []
self.button_cache = []
self.data = [{'attribute': Attribute(str(x), "test")} for x in range(100)]
for i in range(100):
if i % 5 == 0:
self.data[i]['attribute'].values = {'1': 'test1', '2': 'test2', '3': 'test3'}
def get_button(self):
if len(self.button_cache) > 0:
return self.button_cache.pop()
else:
return Button()
def get_label(self):
if len(self.label_cache) > 0:
return self.label_cache.pop()
else:
return Label()
def cache_widgets(self, widgets):
for w in widgets:
if isinstance(w, Button):
self.button_cache.append(w)
else:
self.label_cache.append(w)
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
In this version, the refresh_view_attrs method always sets all the attributes of the RVItem. The line
self.create_widgets(data.pop('attribute', None))
is replaced with
self.create_widgets(data['attribute'])
because the pop() actually removes data, which I don't think you want to do.
The RV class now has a cache for Label widgets and another for Button widgets, and they get recycled (similar to what the RecycleView does. The create_widgets method removes all the children of the RVItem and adds them to the cache, then recycles or creates widgets, as needed, to fill out the RVItem.
I have added additional items to the values dict for some of the data, to help illustrate how this works.
I'm trying to make something like a "Bing images" layout.
That is:
Images are divided into several columns of the same width.
All images are the same width.
Images are added down in such a way that the images added first are
at the top of the layout.
Layout can be added to ScrollView to scroll with mousewheel
I did not find a way to do this using Stack Layout, so I decided to create my own layout.
I stopped here:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image
from kivy.properties import NumericProperty
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.uix.scrollview import ScrollView
KV = '''
#:import Window kivy.core.window.Window
ScrollView
size_hint: (1, None)
size: Window.size
MyLayout
id:my_l
Button
text:'1'
Button
size_hint_y: None
height: 900
text:'2'
Button
text:'3'
Button
text:'4'
Button
text:'5'
size_hint_y: None
height: 900
<MyLayout>:
#height: self.minimum_height
cols: 3
spacing: 10
size_hint_y:None
row_width: 300
'''
class MyLayout(FloatLayout):
def __init__(self, **kwargs):
super(MyLayout, self).__init__(**kwargs)
cols = NumericProperty(3)
row_width = NumericProperty(300)
spacing = NumericProperty(0)
def do_layout(self, *args):
self.i = 0
self.last_x = [self.height]*self.cols
for child in self.children[::-1]:
child.width = self.row_width
if isinstance(child, Image):
child.height = child.width / child.image_ratio
child.size_hint_y= None
child.size_hint_x= None
self.i+=1
if self.i == self.cols+1: self.i = 1
child.x = self.x+(self.i-1)*(self.row_width+self.spacing)
child.y = self.last_x[self.i-1]-child.height
self.last_x[self.i-1]-=child.height+self.spacing
def on_pos(self, *args):
self.do_layout()
def on_size(self, *args):
self.do_layout()
def add_widget(self, widget):
super(SuperGrid, self).add_widget(widget)
self.do_layout()
def remove_widget(self, widget):
super(SuperGrid, self).remove_widget(widget)
self.do_layout()
class MyApp(App):
def build(self):
self.root = Builder.load_string(KV)
Window.bind(on_dropfile=self.add)
def add(self, *args):
name= list(args)[1]
self.root.ids.my_l.add_widget(Image(source=name))
MyApp().run()
It is already partially working (you can run it and dragndrop some images from your folders to see what I'm about), but the problem is that I don't understand how to connect a ScrollView to it.
It looks like I need to add a line with something like height: self.minimum_height to KV string.
but it’s not clear where in layout class I need to calculate minimum_height.
How to make the code work with ScrollView?
You just need to calculate the height of your MyLayout instance. In your kv file add:
size_hint: (1, None)
in your MyLayout section
Then, in the do_layout method, calculate the height of your MyLayout. Do a self.height = just once at the end do_layout (to avoid infinite loop due to on_size method). For example here is a modified version of your do_layout:
def do_layout(self, *args):
self.i = 0
col_heights = [0] * self.cols # keeps track of the height of each column
self.last_x = [self.height]*self.cols
for child in self.children[::-1]:
child.width = self.row_width
if isinstance(child, Image):
child.height = child.width / child.image_ratio
child.size_hint_y= None
child.size_hint_x= None
self.i+=1
if self.i == self.cols+1:
self.i = 1
col_heights[self.i-1] += child.height + self.spacing
child.x = self.x+(self.i-1)*(self.row_width+self.spacing)
child.y = self.last_x[self.i-1]-child.height
self.last_x[self.i-1]-=child.height+self.spacing
if len(self.children) > 0:
self.height = max(col_heights)
When I click on the buttons in the left part, the label "Top 10 Plays of 2015" should be changed with the text of the button that I've clicked. I can see the variable changing text but the label is not changing text.
this is my work: when I click the buttons on left side the title in the middle should change:
python
class MainApp(Screen, EventDispatcher):
vid = StringProperty("Videos/Top 10 Plays of 2015.mp4")
title = StringProperty("Top 10 Plays of 2015")
def __init__(self,*args,**kwargs):
super(MainApp,self).__init__(*args, **kwargs)
pass
class OtherVideos(BoxLayout, EventDispatcher):
root = MainApp
def __init__(self, *args, **kwargs):
super(OtherVideos,self).__init__(*args, **kwargs)
self.loadVideos()
def loadVideos(self):
con = MongoClient()
db = con.nba
vids = db.videos.find()
vidnum = 1
for filename in vids:
myid = "vid" + str(vidnum)
getfilename = filename['filename']
button = Button(id=myid,
text=getfilename,
color=[0,0.7,1],
bold=1)
button.bind(on_release=lambda x:(self.change_Title(getfilename), self.change_Vid(getfilename)))
self.add_widget(button)
vidnum += 1
def change_Title(self, title):
self.root.title = title
def change_Vid(self, myfilename):
con = MongoClient()
db = con.nba
vids = db.videos.find()
for filename in vids:
file = os.path.basename(filename['video_path'])
if myfilename == filename['filename']:
self.root.vid = filename['video_path']
break
pass
kivy
BoxLayout:
orientation: 'vertical'
Label:
id: lblTitle
text: root.title
size_hint_y: None
height: 40
font_size: 25
bold: 1
canvas.before:
Color:
rgba: 1, 0, 0, 0.7
Rectangle:
pos: self.pos
size: self.size
VideoPlayer:
id: activeVid
source: root.vid
state: 'play'
when I'm printing root.title it's changing its text but the label is not changing which it should because it's getting its text from that variable.
But when I'm putting the change_title() method in __init__ of OtherVideos like this, the label is changing:
class OtherVideos(BoxLayout, EventDispatcher):
root = MainApp
def __init__(self, *args, **kwargs):
super(OtherVideos,self).__init__(*args, **kwargs)
self.change_Title("my title")
This is already taking me days of work, anyone can help?
There are several problems with your code:
Every widget inherits from EventDispatcher, so inheriting from it the second time is pointless.
This root = MainApp is not a reference to the MainApp (object), but to its class.
You don't need to specify a StringProperty to achieve your goal.
The easiest way to do this is to add a reference of the label you want to change in the function change_Title:
def change_Title(self, title):
# self.root.title = title
self.ids.my_label_to_change.text = title
The first screen of my app has a small menu (in a gridlayout) of three buttons. Two are supposed to open popups. One for Help and one for About.
The third one changes to another screen.
Only one popup works. The first one called (in the kivy file) works, the second doesn't open the popup. If I switch the order in cdd.kv, then the other one works.
Excerpt from cdd.kv:
CDDMainMenuLayout:
HelpButton:
size_hint: .5,.5
MetadataButton:
size_hint: .5,.5
on_release: app.root.current = 'metadata'
AboutButton:
size_hint: .5,.5
Excerpt from main.py:
class CDDMainMenuLayout(GridLayout):
"""
Provides the layout for the three buttons on the home screen.
"""
def __init__(self, *args, **kwargs):
super(CDDMainMenuLayout, self).__init__(*args, **kwargs)
self.rows = 1
self.cols = 3
self.size_hint = (.5,.5)
...
class CDDButton(Button):
def __init__(self, **kwargs):
super(CDDButton, self).__init__(**kwargs)
self.text = _('Button')
self.background_color = colors.grey2
class AboutButton(CDDButton):
def __init__(self, **kwargs):
super(AboutButton, self).__init__(**kwargs)
self.text = _("About the CDD")
self.background_color = colors.red1
a = Popup()
a.title = _("About Constraint Definition Designer, Version - " + __version__)
a.content = RstDocument(source='about.rst')
a.size_hint_x = .8
a.size_hint_y = .8
self.bind(on_release=a.open)
class HelpButton(CDDButton):
def __init__(self, **kwargs):
super(HelpButton, self).__init__(**kwargs)
self.text = _("Help")
self.background_color = colors.green1
h = Popup()
h.title = _("CDD Help")
h.content = RstDocument(source='help.rst')
h.size_hint_x = .8
h.size_hint_y = .8
self.bind(on_release=h.open)
Does anything change if you add extra lines self.popup = h and self.popup = a? One possibility is that your popups are simply being garbage collected since you don't store any references to them. I'm not sure if/how this would give your particular behaviour, but it's worth a try.