Kivy: can you make UI responsive while adding lots of widgets? - python

I have an app that needs to add lots of widgets dynamically. Here's a working app to simulate this:
from threading import Thread
from kivy.app import App
from kivy.uix.stacklayout import StackLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.clock import Clock
class LotsOfWidgets(App):
def build(self):
self.widgets_amt = 5000
root = GridLayout(cols=1)
self.label_container = StackLayout()
generate_button = Button(text='Generate lots of labels',
size_hint_y=None, height=44)
generate_button.bind(on_press=self.generate_lots_of_labels)
hooray_button = Button(text='Print hooray',
size_hint_y=None, height=44)
hooray_button.bind(on_press=self.print_hooray)
for widget in (generate_button, hooray_button,
self.label_container):
root.add_widget(widget)
return root
def generate_lots_of_labels(self, *args):
for _ in xrange(self.widgets_amt):
label = Label(text='a', size_hint=(None, None), size=(10,10))
self.label_container.add_widget(label)
def scheduled_generate_lots_of_labels(self, *args):
Clock.schedule_once(self.generate_lots_of_labels)
def threaded_generate_lots_of_labels(self, *args):
thread = Thread(target=self.generate_lots_of_labels)
thread.start()
def print_hooray(self, *args):
print 'hooray'
LotsOfWidgets().run()
We have a grid layout that has 2 buttons and a stack layout. By clicking the first button, 5000 labels will be generated inside the stack layout. The second button only prints "hooray" to the console.
Adding 5000 widgets to the stack layout and drawing them on the screen takes quite a bit of time which is fine. When you press the button to generate them and immediately press the "print hooray" button, on my computer hooray gets printed about 3 seconds later after the labels appear on the screen. So the problem is, that the UI becomes unresponsive while generating the labels.
I tried to solve this with threading by binding generate_button.on_press to scheduled_generate_lots_of_labels and threaded_generate_lots_of_labels (of course not at the same time) methods instead the one shown in the code, but they don't seem to help.
Is there anything you can do to keep the UI responsive even if it's generating all those widgets?

You could add the labels in batches scheduled by Kivy's Clock module.

I looked into the idea that brousch gave in his answer. What I tried first was to split those 5000 items into smaller chunks and iterating each chunk in its own Clock.schedule_once.
The result turned out to be very similar to just scheduling once the whole shebang of 5000 items. If you schedule it to be executed in 1 second, you have 1 second to click the hooray button. After that UI becomes unresponsive until all the widgets have been generated.
So in most cases, the only option is to use Clock.schedule_interval and here's an experiment of that. The build method remains the same.
def chunk(self, l, n):
for i in xrange(0, len(l), n):
yield l[i:i+n]
def generate_lots_of_labels(self, *args):
n = 500
interval = 0.01
self.chunks = list(self.chunk(range(self.widgets_amt), n))
self.i = 0
Clock.schedule_interval(self.iterate, interval)
def iterate(self, *args):
for _ in self.chunks[self.i]:
label = Label(text='a', size_hint=(None, None), size=(10,10))
self.label_container.add_widget(label)
self.i += 1
if self.i >= len(self.chunks):
Clock.unschedule(self.iterate)
This is a compromise between the widget generation speed and responsiveness of the UI. Depending on the application and executing environment, different values of scheduling interval and n give the best result.

Related

Kivy Buttons on_press method doesn't work

So I have written this basic kivy code. I want to read sentences from a file, and dynamically create buttons for each one of them. Then, I want these buttons to disappear when they are clicked. In a for loop, I create my buttons, and put my buttons with the index i in a list. Then with the on_press method, it should delete itself.
Button_List[i].bind(on_press= lambda x: self.remove_widget(Button_List[i]))
So there is a Button in Button_List[i] , and when it is clicked, it should run:
self.remove_widget(Button_List[i])
so it should delete itself
I have 5 buttons for example, the problem is, that it whichever button I click, it deletes the button with the highest index. And the other buttons dont get deleted. I feel like kivy is only executing the last index, but I am not sure.
Here is my code:
new.py:
import kivy.uix.button as kb
from kivy.app import App
from kivy.uix.widget import Widget
from Nils_Programm_verkürzt import lektionstextlesen
from kivy.uix.gridlayout import GridLayout
sentences = ['example_sentence1','example_sentence2','example_sentence3','example_sentence4','example_sentence5',]
class Button_Widget(Widget):
def __init__(self, **kwargs):
super(Button_Widget, self).__init__(**kwargs)
global Button_List
Button_List = []
for i in range(len(sentences)):
print(i)
Button_List.append(kb.Button(text=sentences[i],pos=(self.width * i, self.height * i)))
Button_List[i].size = 50, 50
print('binding'+ str(i))
Button_List[i].bind(on_press= lambda x: self.remove_widget(Button_List[i]))
print(Button_List[i])
self.add_widget(Button_List[i])
class ButtonApp(App):
def build(self):
return Button_Widget()
if __name__ == "__main__":
ButtonApp().run()
Your help is very much appreciated :).
That's a common problem when using lambda in a loop. They all get the last value of the loop. A fix is to create a new variable that holds the current loop variable value. So, try replacing:
Button_List[i].bind(on_press= lambda x: self.remove_widget(Button_List[i]))
with:
Button_List[i].bind(on_press= lambda x, j=i: self.remove_widget(Button_List[j]))

Kivy Python Scrollview event misfiring when in a class instantiated multiple times

In a Kivy/Python program, I have a class which contains a scrollview. I instantiate this class multiple times. The scrollview is bound to the on_scroll_stop event. My desired outcome is that the on_scroll_stop event will only fire within the instantiated class to which it belongs, but it seems that the event fires across all of the instantiated classes. Am I doing something wrong or is this expected behavior? Working sample below. In this example the "Left" section shows the issue most often, however the error is seen in the right section once you scroll up at the top or scroll down once reaching the bottom. To recreate scroll only one side, preferably the "Left" side. In my actual code which is far more complex the issue is much more prevalent.
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
class DisplaySection(Widget):
def CreateSection(self):
self.display_panel_main_layout = BoxLayout(orientation='vertical')
self.elements_layout = FloatLayout(size_hint_y=None)
self.sv = ScrollView()
self.sv.bind(on_scroll_stop=self.on_scrolling_stop)
self.sv.add_widget(self.elements_layout)
self.display_panel_main_layout.add_widget(self.sv)
return self.display_panel_main_layout
def LoadElements(self,section_name):
self.section_name = section_name
number_of_elemements = 100
element_hieght = 40
layout_height = number_of_elemements * element_hieght
self.elements_layout.height = layout_height
xcord = 0
for x in range(number_of_elemements):
ycord = self.elements_layout.height - element_hieght*x
name = Label(text='Name' + str(x),size_hint_y=None, height=40,pos=(xcord,ycord))
self.elements_layout.add_widget(name)
def on_scrolling_stop(self, sv, value):
#print(sv,value)
print('I am',self.section_name)
class testscrollapp(App):
def build(self):
main_layout = BoxLayout()
section_names = ['Left','Right']
for x, section_name in enumerate(section_names):
section_layout = BoxLayout(orientation='vertical')
btn = Button(text=section_name,size_hint=(1,None))
section_layout.add_widget(btn)
# instantiate the class containing the scroll view
scroll_layout = DisplaySection()
scr = scroll_layout.CreateSection()
section_layout.add_widget(scr)
scroll_layout.LoadElements(section_name)
main_layout.add_widget(section_layout)
return main_layout
testscrollapp().run()
The on_scroll_stop event is dispatched just like touch events, so any ScrollView instance that subscribes to on_scroll_stop will get all the on_scroll_stop events. And just like a touch event, the subscribing ScrollView instance must determine which of the events it is interested in, and ignore the rest. Since the value argument to your on_scrolling_stop is the MouseMotionEvent, you can do a collide_point() test to determine if the scroll event is within the ScrollView:
def on_scrolling_stop(self, sv, value):
if sv.collide_point(*value.pos):
print('\tI am',self.section_name)

How do you create a scrollview in kivy python with for loop and have each button their own function parameter

So I am trying to create a program where you click on a button and it gives you a random task from a list of tasks you provide and another button to list all those tasks. So there is no errors in the code, the only problem is that when I run it, I want each button to call the same function but give different parameters depending on the i variable in the loop. Also I took out the button that gets the task since that is not in any way related to the problem.
GUI code:
#imports
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.uix.textinput import TextInput
import Functions
import sys
import time
class Home(FloatLayout):
#function that setups the first page
def setup(self, obj):
self.clear_widgets()
List = Button(
text="Your List",
font_size=20,
size_hint=(1/6, 1/12),
pos_hint={"x":2.5/6, "y":1/6},
on_press = self.LIISST
)
self.add_widget(List)
#function that lists tasks
def LIISST(self, obj):
self.clear_widgets()
FUNC_ = Functions.Choose_("GE", 5)
FUNC_.sort()
LT = GridLayout(
cols=1,
spacing=10,
size_hint_y=None,
)
LT.bind(minimum_height=LT.setter('height'))
SCR = ScrollView(
size_hint=(1/3.5, 2/3),
pos=(self.width*137/384, self.height/3.25)
)
for i in range(len(FUNC_)):
but_ = Button(text=str(FUNC_[i]),
size_hint=(18/20, None),
height=40,
font_size=self.width/75,
on_press=lambda s:Functions.Choose_("DE", but_.text)
)
LT.add_widget(but_)
SCR.add_widget(LT)
ACC_ = Button(
text="Back",
font_size=20,
size_hint=(1/8, 1/14),
pos_hint={"x":3.5/8, "y":1/6},
on_press=self.setup
)
self.add_widget(SCR)
self.add_widget(ACC_)
def __init__(self, **kwargs):
super(Home, self).__init__(**kwargs)
Window.clearcolor = (255/255, 255/255, 255/255, 255/255)
self.setup(self)
class App_(App):
def build(root):
return Home()
if __name__ == '__main__':
App_().run()
Function to get the task: (separate file)
import random
import sys
def Choose_(vall_, VAAL_):
try:
#variables
cfile = open(r"Choice.txt", "r+")
cfile.seek(0)
dfile = open(r"Done.txt", "r+")
dfile.seek(0)
items = []
DON = []
#appenders for items in file to line
[items.append(line) for line in cfile]
[DON.append(line) for line in dfile]
stripp1 = [s.strip() for s in items]
stripp2 = [s.strip() for s in DON]
stripp1.sort()
stripp2.sort()
if vall_ == "DE":
print(VAAL_)
if vall_ == "GE":
return stripp1
sys.exit()
for s in stripp2:
if s in stripp1:
stripp1.remove(s)
if not stripp1:
dfile.seek(0)
dfile.truncate(0)
return False
sys.exit()
luck = random.randint(0, (len(stripp1)-1))
dfile.write(stripp1[luck])
dfile.write("\n")
return(stripp1[luck])
finally:
cfile.close()
dfile.close()
Task file (same directory as above codes):
ClIP STUDIO PAINT
CYBRARY (HACKING)
CYBRARY (LINUX)
VIRTUAL DJ
RASPBERRY PI
PACKET TRACER
VIRTUALBOX
PHOTOSHOP
BLENDER
SOLIDWORKS
KHAN ACADEMY (ANATOMY)
SOLOLEARN
UNITY
KHAN ACADEMY (ELECTRICAL)
PROGRAMMING
KHAN ACADEMY (PHYSICS)
ADOBE PREMIERE
Tasks already done(again, same directory as above files):
ClIP STUDIO PAINT
CYBRARY (HACKING)
CYBRARY (LINUX)
VIRTUAL DJ
RASPBERRY PI
PACKET TRACER
I expected the output to print the button's text for each different button, but it only texts the very last item in the task file for each button, which is virtualbox. Also the code sorts the tasks in abc order, which is why the last item is virtualbox.
Problem
I expected the output to print the button's text for each different
button, but it only texts the very last item in the task file for each
button, which is virtualbox.
I want each button to call the same function but give different
parameters depending on the i variable in the loop.
Root Cause
The text printed is always "VIRTUALBOX" because it is the last button added and it is referenced in the call by but_.text.
Solution
The following enhancements are required to solve the problem.
main.py
Implement a new method, callback()
Bind button's on_press event to the new method, callback()
Snippets - main.py
for i in range(len(FUNC_)):
but_ = Button(text=str(FUNC_[i]),
size_hint=(18 / 20, None),
height=40,
font_size=self.width / 75,
on_press=self.callback
)
LT.add_widget(but_)
SCR.add_widget(LT)
...
def callback(self, instance):
print(f"\ncallback: instance={instance}, text={instance.text}")
Functions.Choose_("DE", instance.text)
Output

PyQt progress bar not updating or appearing until 100%

EDIT: There are a number of similar posts on PyQt4 progress bars not updating. They all focus on the issue of threads & where the program actually updates the window. Although helpful, my code was so structured that the replies were not practical. The accepted answer given here is simple, to the point & works.
I am using Python 2.7 and PyQT 4 on a Win 7 x64 machine.
I am trying to clear my window of one widget, an 'Accept' button, see code, and replace it with a progress bar.
Even though I close the 'Accept' button & add the progress bar before the processing loop is entered into. The window is only updated after the loop has finished & the progress bar jumps straight to 100%.
My code,
from PyQt4 import QtCore, QtGui
import sys
import time
class CentralWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(CentralWidget, self).__init__(parent)
# set layouts
self.layout = QtGui.QVBoxLayout(self)
# Poly names
self.pNames = QtGui.QLabel("Import file name", self)
self.polyNameInput = QtGui.QLineEdit(self)
# Polytype selection
self.polyTypeName = QtGui.QLabel("Particle type", self)
polyType = QtGui.QComboBox(self)
polyType.addItem("")
polyType.addItem("Random polyhedra")
polyType.addItem("Spheres")
polyType.addItem("Waterman polyhedra")
polyType.activated[str].connect(self.onActivated)
# Place widgets in layout
self.layout.addWidget(self.pNames)
self.layout.addWidget(self.polyNameInput)
self.layout.addWidget(self.polyTypeName)
self.layout.addWidget(polyType)
self.layout.addStretch()
# Combobox choice
def onActivated(self, text):
if text=="Random polyhedra":
self.randomPolyhedra(text)
if text=="Spheres": # not implementaed yet
self.polyTypeName.setText("Not implemented yet.")
self.polyTypeName.adjustSize()
if text=="Waterman polyhedra": # not implementaed yet
self.polyTypeName.setText("Not implemented yet.")
self.polyTypeName.adjustSize()
# New options for random polyhedra choice
def randomPolyhedra(self, text):
self.polyNumberLbl = QtGui.QLabel("How many: ", self)
self.polyNumber = QtGui.QLineEdit(self)
self.acceptSeed = QtGui.QPushButton('Accept') # Accept button created
self.acceptSeed.clicked.connect(lambda: self.ranPolyGen())
self.layout.addWidget(self.polyNumberLbl)
self.layout.addWidget(self.polyNumber)
self.layout.addWidget(self.acceptSeed) # Accept button in layout
self.randFlag = True
self.polyTypeName.setText(text)
self.polyTypeName.adjustSize()
# Act on option choices for random polyhedra
def ranPolyGen(self):
polyCount = int(self.polyNumber.text())
self.progressBar = QtGui.QProgressBar() # Progress bar created
self.progressBar.setMinimum(1)
self.progressBar.setMaximum(polyCount)
self.acceptSeed.close() # Accept button closed
self.layout.addWidget(self.progressBar) # Add progressbar to layout
for poly in range(1, polyCount+1):
time.sleep(1) # Calls to main polyhedral generating code go here
print poly
self.progressBar.setValue(poly)
self.doneLbl = QtGui.QLabel("Done", self)
self.layout.addWidget(self.doneLbl)
# Creates GUI
class Polyhedra(QtGui.QMainWindow):
def __init__(self):
super(Polyhedra, self).__init__()
# Place central widget in layout
self.central_widget = CentralWidget(self)
self.setCentralWidget(self.central_widget)
# Set up window
self.setGeometry(500, 500, 300, 300)
self.setWindowTitle('Pyticle')
self.show()
# Combo box
def onActivated(self, text):
self.central_widget.onActivated(text)
def main():
app = QtGui.QApplication(sys.argv)
poly = Polyhedra()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Below is a picture of during loop execution & after completion.
I dont think I have got my head around the addWidget() method. I was under the impression that this would add another widget to the present layout (a vbox layout here) & that the .close() method removed a widget when directed to do so.
What am I missing?
You can add:
from PyQt4.QtGui import QApplication
Then in your for loop:
QApplication.processEvents()
Your app is actually becoming unresponsive, you need to call processEvents() to process the events and redraw the gui. I am not overly familiar with pyqt but I imagine another alternative is using a thread.

Animate canvas Line on Kivy

I have an array with x and y positions. I want to show these points linking a line successively for each point, then, creating an animation. It is just like a path tracking whose trail is the line. I'm using python-kivy to try to display it.
I couldn't find any help on google.
There's a button that triggers this animation.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics.vertex_instructions import Line
from kivy.graphics.context_instructions import Color
from time import sleep
x_moves = [250, 250.4305, 249.8804, 246.0923, 239.7496, 233.8188, 225.7797, 215.8385, 205.8413, 196.6497, 189.7026, 181.2445, 174.9816, 171.9882, 166.1171, 161.6505, 159.9929, 161.1338, 164.853, 168.2874, 170.768, 178.6918, 184.5233, 190.0262, 195.607, 202.0255, 210.5954, 216.1031, 219.6285, 224.9134, 230.2314, 237.7017, 243.7408, 250.5839, 256.2949]
y_moves = [250, 240.0093, 230.0244, 220.7697, 213.0386, 204.9872, 199.0396, 197.9567, 197.7209, 201.6598, 208.8527, 214.1875, 221.9834, 231.5249, 239.62, 248.567, 258.4287, 268.3634, 277.6461, 287.0379, 296.7253, 302.8256, 310.9492, 319.299, 327.5969, 335.2652, 340.4185, 348.7651, 358.1231, 366.6125, 375.0812, 381.7291, 389.6996, 396.9915, 405.2003]
class my_App(App):
def build(self):
self.widget = Widget()
self.widget.on_touch_down = self.touch
with self.widget.canvas:
Color(0, 1, 0, 1) #just initial config
Line(points = [0,0,500,0,500,500,0,500], close = True) #just initial config
return self.widget
def touch(self, touch): #executes the animation
pts = []
for i in range(len(x_moves)):
pts.append(x_moves[i])
pts.append(y_moves[i])
self.widget.canvas.add(Line(points = pts))
sleep(0.1)
if __name__ == '__main__':
obj = my_App()
obj.run()
This is my code that doesn't work. But that's the idea.
You're blocking the UI from updating because your function doesn't return and you use sleep(). For obvious reasons, Kivy can't do anything while your code is running. If you want to wait before running some more code, you can use Kivy's Clock for this. Then Kivy will be able to update the screen.
But you should probably just look at Kivy's Animation, which is built to do this much better.

Categories

Resources