I have a music app written with kivymd that scans your default download directory and extracts information about your audio files. (Basically retrieving the IDV3 tags and other things)
I only have 20 audio files at max on my desktop which shouldn't be a problem but I decided to test my program with many audio (like 120) files and the results were horrible.
The interface was so slow that it barely worked. How am I supposed to display large set of widgets in kivy without causing such catastrophic performance degradation?
Also, how can I make the loading of the widgets asynchronous so that they don't take up a lot of time on startup as well?
A minimal reproducible example of my code:
import os
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivy.properties import ObjectProperty
from kivymd.uix.behaviors import RectangularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.app import MDApp
class Song:
"""Class for extracting information about an audio file."""
def __init__(self, path: str):
self._path = path
self._title = "My Song"
self._artist = "Unknown artist"
# the album cover is going to a texture retrieved from the file itself
# but in this case, I will refer to a file.
self._album_cover = "Default-Album-Cover.jpg"
#property
def path(self):
return self._path
#property
def title(self):
return self._title
#property
def artist(self):
return self._artist
#property
def album_cover(self):
return self._album_cover
class SongCard(ButtonBehavior, RectangularRippleBehavior, MDBoxLayout):
"""Custom widget for creating cards."""
song_obj = ObjectProperty(Song("dummy song"), rebind=True)
class MainApp(MDApp):
def __init__(self, **kwargs):
super(MainApp, self).__init__(**kwargs)
self.theme_cls.theme_style = "Dark"
self.kv = Builder.load_string('''
#:kivy 2.0.0
<SongCard>:
orientation: "vertical"
size_hint_y: None
height: "300dp"
radius: ("10dp",)
canvas.before:
Color:
rgba: app.theme_cls.bg_dark
RoundedRectangle:
pos: self.pos
size: self.size
radius: root.radius
FitImage:
source: root.song_obj.album_cover
MDLabel:
text: root.song_obj.title
adaptive_height: True
MDLabel:
text: root.song_obj.artist
adaptive_height: True
ScrollView:
do_scroll_x: False
MDGridLayout:
id: song_grid
cols: 2
adaptive_height: True
spacing: "10dp"
padding: "10dp"
''')
def build(self):
return self.kv
def on_start(self):
for _ in range(25):
for audio_file in os.listdir(os.path.join(os.path.expanduser('~'), "Downloads")):
if audio_file.endswith(".mp3"):
song_obj = Song(audio_file)
self.kv.ids.song_grid.add_widget(
SongCard(
song_obj=song_obj
)
)
if __name__ == '__main__':
MainApp().run()
You can use the RecycleView class to manage the amount of widgets and help your program to be able to accept a big amount of widgets, the ScrollView is great but recycleview manages data as dictionary and then you can use two BoxLayout class under a BoxLayout widget to have the GridLayout class functionality, I tested this with 213 elements and it works with a lot of widgets and good performance:
import os
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivy.properties import ObjectProperty, StringProperty
from kivymd.uix.behaviors import RectangularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.app import MDApp
from kivy.uix.recyclegridlayout import RecycleGridLayout
class Song:
"""Class for extracting information about an audio file."""
def __init__(self, path: str):
self._path = path
self._title = "My Song"
self._artist = "Unknown artist"
# the album cover is going to a texture retrieved from the file itself
# but in this case, I will refer to a file.
self._album_cover = "Default-Album-Cover.jpg"
## #property
## def path(self):
## return self._path
##
## #property
## def title(self):
## return self._title
##
## #property
## def artist(self):
## return self._artist
##
## #property
## def album_cover(self):
## return self._album_cover
class SongCard(MDBoxLayout):
"""Custom widget for creating cards."""
#song_obj = ObjectProperty(Song("dummy song"), rebind=True)
song_obj = StringProperty()
class B2(ButtonBehavior, RectangularRippleBehavior, MDBoxLayout):
"""Custom widget for creating cards."""
pass
class MainApp(MDApp):
def __init__(self, **kwargs):
super(MainApp, self).__init__(**kwargs)
self.theme_cls.theme_style = "Dark"
self.kv = Builder.load_string('''
#:kivy 2.0.0
<SongCard>:
orientation: "vertical"
size_hint_y: None
height: "300dp"
#radius: ("10dp",)
BoxLayout:
padding: dp(10)
spacing: dp(5)
B2:
padding: dp(10)
canvas.before:
Color:
rgba: [0,1,0,.2]
RoundedRectangle:
pos: self.pos
size: self.size
radius: [30,]
orientation: "vertical"
spacing: dp(5)
FitImage:
size_hint: 1,1
source: "Help5/cc.png" #root.song_obj.album_cover
MDLabel:
text: root.song_obj[:40] #.title
adaptive_height: True
MDLabel:
text: " - jbsidis" #.artist
adaptive_height: True
B2:
padding: dp(10)
canvas.before:
Color:
rgba: [1,0,0,.2]
RoundedRectangle:
pos: self.pos
size: self.size
radius: [30,]
orientation: "vertical"
spacing: dp(5)
FitImage:
source: "Help5/cc.png" #root.song_obj.album_cover
MDLabel:
text: root.song_obj[:40] #.title
adaptive_height: True
MDLabel:
text: " - jbsidis" #.artist
adaptive_height: True
Screen:
BoxLayout:
orientation: "vertical"
#do_scroll_x: False
RecycleSupportGridLayoutjbsidis:
id: song_grid
key_viewclass: 'viewclass'
key_size: 'height'
RecycleBoxLayout:
padding: dp(10)
default_size: None, dp(248)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
<RecycleSupportGridLayoutjbsidis#RecycleView>:
''')
def build(self):
return self.kv
def on_start(self):
#for _ in range(25):
for audio_file in os.listdir("/media/jbsidis/Android/Q/D/Anthony Robbins/"): #os.path.join(os.path.expanduser('~'), "Downloads")
#if audio_file.endswith(".mp3"):
#song_obj = Song(audio_file)
self.kv.ids.song_grid.data.append(
{
"viewclass": "SongCard",
"song_obj": audio_file
}
)
## {
## "viewclass": "CustomOneLineIconListItem",
## "icon": name_icon,
## "text": name_icon,
## "on_release": lambda:Clipboard.copy(name_icon),
## }
## self.kv.ids.song_grid.add_widget(
## SongCard(
## song_obj=song_obj
## )
## )
if __name__ == '__main__':
MainApp().run()
Pictures:
Related
I am pretty new to the kivy language and faced this problem when try to run the following code. it just runs without any errors. But it just only generate a blank application. without any widgets.
from kivymd.app import MDApp
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager
from kivy.uix.gridlayout import GridLayout
from kivymd.uix.boxlayout import MDBoxLayout
kv = """
<SCManager>:
Screen:
c_screen:
<c_screen>:
cols: 2
canvas.before:
Color:
rgba: 1,0,0,1
Rectangle:
pos: self.pos
size: self.size
MDBoxLayout:
md_bg_color: (1, 1, 1 ,1)
padding: 10
adaptive_height: True
MDLabel:
text: "helow 2"
color: (0.0003, 0.34, 0.60,1)
font_style: "H6"
halign: "left"
adaptive_height: True
<navigator>:
canvas.before:
Color:
rgba: 1,1,0,1
Rectangle:
pos: self.pos
size: self.size
orientation: "vertical"
MDLabel:
text: "hellow"
adaptive_height: True
MDList:
OneLineAvatarListItem:
text : "Dashboard"
IconLeftWidgetWithoutTouch:
icon: "view-dashboard"
OneLineAvatarListItem:
text : "Manage Users"
IconLeftWidgetWithoutTouch:
icon: "account"
MDRoundFlatIconButton:
text: "bye"
font_size: 11
"""
class SCManager(ScreenManager):
pass
class navigator(MDBoxLayout):
pass
class c_screen(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.n = navigator()
self.add_widget(self.n)
class test(MDApp):
def build(self):
Builder.load_string(kv)
return SCManager()
if __name__ == '__main__':
test().run()
But if i add another widget(a label) to Screen from kv string, it works fine or if i use add_widget(c_screen) in SCManager it works fine.But i need to add c_screen class from the kv string. Is there any way to add widgets without adding any widgets to Screen??
here is the working code
from kivymd.app import MDApp
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager
from kivy.uix.gridlayout import GridLayout
from kivymd.uix.boxlayout import MDBoxLayout
kv = """
<SCManager>:
Screen:
MDLabel:
text: "helow 2"
adaptive_height: True
c_screen:
<c_screen>:
cols: 2
canvas.before:
Color:
rgba: 1,0,0,1
Rectangle:
pos: self.pos
size: self.size
MDBoxLayout:
md_bg_color: (1, 1, 1 ,1)
adaptive_height: True
MDLabel:
text: "helow 2"
adaptive_height: True
<navigator>:
canvas.before:
Color:
rgba: 1,1,0,1
Rectangle:
pos: self.pos
size: self.size
orientation: "vertical"
MDLabel:
text: "hellow"
adaptive_height: True
MDList:
OneLineAvatarListItem:
text : "Dashboard"
IconLeftWidgetWithoutTouch:
icon: "view-dashboard"
OneLineAvatarListItem:
text : "Manage Users"
IconLeftWidgetWithoutTouch:
icon: "account"
MDRoundFlatIconButton:
text: "bye"
font_size: 11
"""
class SCManager(ScreenManager):
pass
class navigator(MDBoxLayout):
pass
class c_screen(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.n = navigator()
self.add_widget(self.n)
class test(MDApp):
def build(self):
Builder.load_string(kv)
return SCManager()
if __name__ == '__main__':
test().run()
Each kivy carousel slide consists of an image at the top (with a checkbox) and text below. The direction of the carousel is 'right' (one after the other horizontally). Image and text data are fed to the carousel with a recycleview.
The minimum code results with a) Each slide stacked vertically; b) the checkbox not displaying.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty, ListProperty, NumericProperty
Builder.load_string("""
<CarouselSlide>:
BoxLayout:
orientation: 'vertical'
size_hint_y: 1
AsyncImage:
source: root.tile
size_hint_y: 0.5
CheckBox:
id: cb
root_ref: root.index
on_press: app.on_checkbox_press(self, self.active, self.state, self.root_ref)
TextInput:
text: root.text
size_hint_y: 0.5
multiline: True
<CarouselScreen>:
name: 'carousel_screen'
Carousel:
direction: 'right'
ignore_perpendicular_swipes: True
size: root.width, root.height
RV:
id: rv
viewclass: 'CarouselSlide'
RecycleBoxLayout:
orientation: 'vertical'
size_hint_y: None
default_size_hint: 1, None
height: self.minimum_height
default_size: 1, root.height
""")
class CarouselScreen(Screen):
pass
class CarouselSlide(Screen):
tile = StringProperty('')
text = StringProperty('')
index = NumericProperty(-1)
cb_state = StringProperty('normal')
def __init__(self, **kwargs):
super(CarouselSlide, self).__init__(**kwargs)
self.bind(cb_state=self.set_cb_state)
def set_cb_state(self, carouselslide, cb_state_value):
self.ids.cb.state = cb_state_value
class RV(RecycleView):
data = ListProperty('[]')
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.get_slide_data()
def get_slide_data(self):
text="Ooh, a storm is threatening. My very life today. If I don't get some shelter. Ooh yeah I'm gonna fade away. War, children. It's just a shot away. It's just a shot away. War, children. It's just a shot away. It's just a shot away."
self.data = [{'tile': 'The Rolling Stones', 'text': text, "index": i, "cb_state": 'normal'} for i in range(41)]
class ThisApp(App):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def build(self):
self.sm = ScreenManager()
self.sm.add_widget(CarouselScreen(name="carousel_screen"))
return self.sm
def on_checkbox_press(self, checkbox, active, state, root_ref):
if active:
print(active, state, root_ref)
else:
print(active, state, root_ref)
new_state = checkbox.state
checkbox.state = 'normal'
rv = self.root.get_screen('carousel_screen').ids.rv
rv.data[root_ref]['cb_state'] = new_state
rv.refresh_from_data()
if __name__ == "__main__":
ThisApp().run()
I believe the problem is that you are defining the CheckBox as a child of AsyncImage. The AsyncImage is not intended to be a container. Try un-indenting the CheckBox in your kv, and adding size/position attributes:
<CarouselSlide>:
BoxLayout:
orientation: 'vertical'
size_hint_y: 1
AsyncImage:
source: root.tile
size_hint_y: 0.5
CheckBox:
id: cb
size_hint: None, None
size: 48, 48
pos_hint: {'center_x':0.5}
root_ref: root.index
on_press: app.on_checkbox_press(self, self.active, self.state, self.root_ref)
TextInput:
text: root.text
size_hint_y: 0.5
multiline: True
How can I pass data from RequestRecycleView to refresh_view_data? I tried with global variables and instantiating data in RequestRecycleView but still can't trigger refresh_view_data by appending Observable data. It is working when I return RequestRecycleView as root but I want ScreenManager to be my root.
from kivy.config import Config
Config.set('graphics', 'multisamples', '0')
from random import sample
from string import ascii_lowercase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import BooleanProperty
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
kv = """
#:import FadeTransition kivy.uix.screenmanager.FadeTransition
ScreenManager:
transition: FadeTransition()
RequestsScreen:
<RequestRow#BoxLayout>
size_hint_y: None
orientation: 'vertical'
row_index: id_row_index.text
row_index:''
pos: self.pos
size: self.size
Label:
id: id_row_index
text: root.row_index
<RequestRecycleView#RecycleView>:
#id: rv
viewclass: 'RequestRow'
SelectableRecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
<RequestsScreen>
name: 'RequestsScreen'
BoxLayout:
orientation: 'vertical'
Label:
text: 'recycle'
RequestRecycleView:
"""
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class RequestRow(RecycleDataViewBehavior):
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
self.row_index = str(index)
self.row_content = data['text']
return super(RequestRow, self).refresh_view_attrs(
rv, index, data)
class ScreenManagement(ScreenManager):
pass
class RequestRecycleView(RecycleView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.data = []
for r in range(30):
row = {'text': ''.join(sample(ascii_lowercase, 6))}
self.data.append(row)
class RequestsScreen(Screen):
pass
Builder.load_string(kv)
sm = ScreenManagement()
sm.add_widget(RequestsScreen(name = 'requests'))
class TestApp(App):
def build(self):
return sm
if __name__ == '__main__':
TestApp().run()
By modifying
<RequestRow#BoxLayout>
size_hint_y: None
orientation: 'vertical'
row_index: id_row_index.text
row_index:''
pos: self.pos
size: self.size
Label:
id: id_row_index
text: root.row_index
to
<RequestRow#BoxLayout>
text: 'abba'
size_hint_y: None
orientation: 'vertical'
row_index: id_row_index.text
row_index:''
pos: self.pos
size: self.size
Label:
id: id_row_index
text: self.parent.text
results in working code. Note that the dictionary keys in the data are expected to be attributes of the viewclass. The added text attribute in RequestRow, provides that attribute, and the text: self.parent.text in the Label places that text (from the viewclass) into the Label. Also, you can replace the lines:
Builder.load_string(kv)
sm = ScreenManagement()
sm.add_widget(RequestsScreen(name = 'requests'))
with just:
sm = Builder.load_string(kv)
since your kv file specifies the ScreenManager as the root object.
How to make press method on this code working? When I press a button the list populates, but when I call it from Clock then not. I can see populate print on the console but the list does not appear in the view. I mean simply: how to stimulate pressing the button in the code?
from kivy.config import Config
Config.set('graphics', 'multisamples', '0')
from random import sample
from string import ascii_lowercase
import pyrebase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
kv = """
<Row#BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
text: root.value
<Test>:
canvas:
Color:
rgba: 0.3, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
rv: rv
orientation: 'vertical'
GridLayout:
cols: 3
rows: 2
size_hint_y: None
height: dp(108)
padding: dp(8)
spacing: dp(16)
Button:
id: populate_btn
text: 'Populate list'
on_press: root.populate()
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
"""
Builder.load_string(kv)
class Test(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def populate(self):
print("populate")
self.rv.data = [{'value': ''.join(sample(ascii_lowercase, 6))}
for x in range(50)]
def press(self):
self.ids.populate_btn.dispatch('on_press')
def interval(dt):
x = Test()
x.press()
Clock.schedule_interval(interval, 3)
class TestApp(App):
def build(self):
return Test()
if __name__ == '__main__':
TestApp().run()
The error is caused because the Test object created in interval function is different from the Test object that returns build method, besides that the Test object created in interval is eliminated since it is a local variable. So the solution is to use the same reference by passing it to the interval function for it I will use functools.partial() function.
# ...
from functools import partial
# ...
def interval(x, dt):
x.press()
class TestApp(App):
def build(self):
t = Test()
Clock.schedule_interval(partial(interval, t), 3)
return t
# ...
I am trying to implement a custom closable tab header in kivy.
What I did was combine a class:TabbedPanelHeader object with a custom class:CloseButton object. Both of these widgets are inside a class:BoxLayout, side-by-side.
However, once I add this into a class:TabbedPanel object, nothing shows up..
I am not sure how to move forward and would greatly appreciate all the help!
Below is the relevant part of the code.
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.image import Image
from kivy.graphics import *
from kivy.uix.tabbedpanel import TabbedPanelHeader
class CloseButton(ButtonBehavior, Image):
def __init__(self, **kwargs):
super(CloseButton, self).__init__(**kwargs)
self.source = 'atlas://data/images/defaulttheme/close'
self.size_hint_x = .2
def on_press(self):
self.source = 'atlas://data/images/defaulttheme/checkbox_radio_off'
def on_release(self):
self.source = 'atlas://data/images/defaulttheme/checkbox_radio_off'
## do the actual closing of the tab
class ClosableTabHeader(BoxLayout):
def __init__(self, **kwargs):
super(ClosableTabHeader, self).__init__(**kwargs)
self.size = (100, 30)
self.size_hint = (None, None)
self.canvas.before.add(Color(.25, .25, .25))
self.canvas.before.add(Rectangle(size=(105, 30)))
self.add_widget(TabbedPanelHeader(background_color=(.65, .65, .65, 0), text='testing'))
self.add_widget(CloseButton())
if __name__ == '__main__':
from kivy.app import App
class TestApp(App):
def build(self):
return ClosableTabHeader()
TestApp().run()
Here is some code which comes close to achieve what you are trying to achieve
from kivy.app import App
from kivy.animation import Animation
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelHeader
from kivy.factory import Factory
from kivy.lang import Builder
class CloseableHeader(TabbedPanelHeader):
pass
class TestTabApp(App):
def build(self):
return Builder.load_string('''
TabbedPanel:
do_default_tab: False
FloatLayout:
BoxLayout:
id: tab_1_content
Label:
text: 'Palim 1'
BoxLayout:
id: tab_2_content
Label:
text: 'Palim 2'
BoxLayout:
id: tab_3_content
Label:
text: 'Palim 3'
CloseableHeader:
text: 'tab1'
panel: root
content: tab_1_content.__self__
CloseableHeader:
text: 'tab2'
panel: root
content: tab_2_content.__self__
CloseableHeader:
text: 'tab3'
panel: root
content: tab_3_content.__self__
<CloseableHeader>
color: 0,0,0,0
disabled_color: self.color
# variable tab_width
text: 'tabx'
size_hint_x: None
width: self.texture_size[0] + 40
BoxLayout:
pos: root.pos
size_hint: None, None
size: root.size
padding: 3
Label:
id: lbl
text: root.text
BoxLayout:
size_hint: None, 1
orientation: 'vertical'
width: 22
Image:
source: 'tools/theming/defaulttheme/close.png'
on_touch_down:
if self.collide_point(*args[1].pos) :\
root.panel.remove_widget(root); \
''')
if __name__ == '__main__':
TestTabApp().run()
It is based on https://github.com/kivy/kivy/blob/master/examples/widgets/tabbed_panel_showcase.py