I need to load a big file into a TextInput in a python GUI app built with kivy. What is the most efficient way to do this without blocking the UI?
For the sake of this example, I'm using a file named big_file.txt. War And Peace (the book) is big: 3.3 MB in plaintext, available from Project Gutenberg here:
https://www.gutenberg.org/ebooks/2600.txt.utf-8
Save that file to some directory:
wget -O big_file.txt https://www.gutenberg.org/ebooks/2600.txt.utf-8
Consider the following simplified application named main.py and located in the same directory as big_file.txt
import time
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout
class MyApp(App):
def build(self):
self.count = 0
layout = BoxLayout()
# add the label
self.label1 = Label(id='label1', text='0')
layout.add_widget(self.label1)
# add the button
layout.add_widget( Button(text='Increment', on_release=self.increment) )
# add the textinput
self.textinput1 = TextInput(text='Click button to load file.')
layout.add_widget(self.textinput1)
# add the button
layout.add_widget( Button(text='Load File', on_release=self.load_file) )
return layout
def load_file(self, *args):
start = time.time()
print( "loading file" )
with open( 'big_file.txt' ) as big_file:
self.textinput1.text = big_file.read()
end = time.time()
print( "finished loading file in " + str(end-start) + " seconds" )
def increment(self, *args):
self.count+=1
self.label1.text = str(self.count)
if __name__ == "__main__":
MyApp().run()
This kivy application has:
An "Increment" Label
An "Increment" Button
A TextInput
A "Load File" Button
The problem is that it takes ~15 seconds to read() the 3.3 MB big_file.txt. And during that time, the UI is blocked.
For example, after clicking the Load File button, the user can't click the Increment button for ~15 seconds -- until the file is finished being read by the load_file() function.
How can I efficiently load the contents of the kivy TextInput with big_file.txt without blocking the UI?
Try using a RecycleView, which is designed for handling large amounts of data. Here is a modified version of your code that uses a RecycleView:
import threading
import time
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.recycleview import RecycleView
from kivy.uix.boxlayout import BoxLayout
class RV(RecycleView):
pass
Builder.load_string('''
<MyLabel#Label>:
halign: 'center'
size_hint: 1, None
height: dp(25)
text_size: self.size
<RV>:
viewclass: 'MyLabel'
RecycleBoxLayout:
default_size: None, dp(25)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class MyApp(App):
def build(self):
self.count = 0
layout = BoxLayout()
# add the label
self.label1 = Label(text='0', size_hint_x=0.1)
layout.add_widget(self.label1)
# add the button
layout.add_widget( Button(text='Increment', on_release=self.increment, size_hint_x=0.1))
# add the RecycleView
self.rv = RV(size_hint_x=0.7)
layout.add_widget(self.rv)
# add the button
layout.add_widget( Button(text='Load File', on_release=self.load_file, size_hint_x=0.1))
return layout
def load_file(self, *args):
threading.Thread(target=self.actual_load, daemon=True).start()
def actual_load(self):
start = time.time()
print( "loading file" )
with open( 'big_file.txt' ) as big_file:
text = big_file.read()
end = time.time()
print( "finished loading file in " + str(end-start) + " seconds" )
lines = []
start = time.time()
for line in text.splitlines(keepends=False):
lines.append({'text': line})
end = time.time()
print("finished loading lines in " + str(end-start) + " seconds")
self.rv.data = lines
def increment(self, *args):
self.count+=1
self.label1.text = str(self.count)
if __name__ == "__main__":
MyApp().run()
This uses a Label for each line of text. The RecycleView creates a fixed number of Labels, and just reuses them as you scroll.
The read() isn't actually what's slow. What's slow is the call to update the TextInput.text with a lot of data.
Threaded Solution
You can prevent from locking the UI by creating a background thread that runs asynchronously
import time, threading
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout
class MyApp(App):
def build(self):
self.count = 0
self.update_textinput1_bg = None
layout = BoxLayout()
# add the label
self.label1 = Label(text='0')
layout.add_widget(self.label1)
# add the button
layout.add_widget(
Button(text='Increment', on_release=self.increment)
)
# add the textinput
self.textinput1 = TextInput(
text='Click button to load file.',
readonly=True
)
layout.add_widget(self.textinput1)
# add the button
layout.add_widget(
Button(text='Load File', on_release=self.load_file)
)
return layout
# function that's called when the "Load File" button is pressed
def load_file(self, *args):
# is there already a thread running attempting to update the textinput?
if self.update_textinput1_bg != None:
# a thread is already running; tell the user to be patient
self.textinput1.text = "Still Loading File. Please be patient."
else:
# no background thread is running yet; start one now
start = time.time()
print( "loading file" )
self.textinput1.text = "Loading File. Please wait.."
with open( 'big_file.txt' ) as big_file:
file_contents = big_file.read()
end = time.time()
print( "finished loading file in " + str(end-start) + " seconds" )
# we use treading.Thread() instead of multiprocessing.Process
# because it can update the widget's contents directly without
# us having to pass data in-memory between the child process.
# Con: We can't kill threads, so it should only be used for
# short-running background tasks that won't get stuck
self.update_textinput1_bg = threading.Thread(
target = self.update_textinput1,
args = (file_contents,)
)
self.start = time.time()
self.update_textinput1_bg.start()
# Register the update_textinput1_tick() function as a callback to be
# executed every second, and we'll use that to update the UI with a
# status message from the update_textinput1() thread and check to see
# if it finished running
Clock.schedule_interval(self.update_textinput1_tick, 1)
# very simple function that updates the contents of the TextInput
# this is intended to be called in a background Thread so the UI doesn't
# get locked when "contents" is very large
def update_textinput1( self, contents, *args ):
self.textinput1.text = contents
# callback function that's executed every ~1 second after being secheduled
# by load_file()
def update_textinput1_tick( self, dt ):
print( "called update_textinput1_tick()" )
# is the background thread still running?
if not self.update_textinput1_bg.is_alive():
# the background thread finished running; calculate runtime and
# unschedule this callback function from being called every second
end = time.time()
print( "finished udpdating widget text in " + str(end-self.start) + " seconds" )
self.update_textinput1_bg = None
Clock.unschedule( self.update_textinput1_tick )
# increase the integer displayed in the text of the label widget by one
def increment(self, *args):
self.count+=1
self.label1.text = str(self.count)
if __name__ == "__main__":
MyApp().run()
Here's an example execution. Note that it takes:
less that 0.2 seconds to read from the file, and
over 18 seconds to update the TextInput
user#buskill:~/tmp/kivy_file_textinput$ /tmp/kivy_appdir/opt/python3.7/bin/python3.7 main.py
...
[INFO ] [Kivy ] v1.11.1
...
[INFO ] [Python ] v3.7.8 (default, Jul 4 2020, 10:00:57)
[GCC 9.3.1 20200408 (Red Hat 9.3.1-2)]
...
[INFO ] [Base ] Start application main loop
loading file
finished loading file in 0.01690673828125 seconds
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
finished udpdating widget text in 18.336608171463013 seconds
Kivy v2.1.0 TextInput Improvements
Note that last year (in 2021), Kivy made some improvements to the slow loading of large data into a TextInput. These were released earlier this year (2022-03-06) in Kivy v2.1.0.
Kivy v2.1.0 Release Notes
Issue #7642: TextInput loading time optimisation for large texts
That said, these optimizations appear to be pretty limited. I tested the original code example in both Kivy v1.11.1 (~15 seconds to load the data into the TextInput) and v2.1.0 (~12 seconds)
Why is it so slow?
I mentioned this to issue to the Kivy developers in the link above, and Gabriel Pettier (tshirtman) provided some insight.
Using py-spy top -- python main.py, you can profile the code and get something like this
%Own %Total OwnTime TotalTime Function (filename)
0.00% 0.00% 3.53s 16.71s _create_line_label (kivy/uix/textinput.py)
0.00% 0.00% 3.10s 22.70s _refresh_text (kivy/uix/textinput.py)
0.00% 0.00% 2.75s 10.12s refresh (kivy/core/text/__init__.py)
0.00% 0.00% 2.23s 6.13s render (kivy/core/text/__init__.py)
0.00% 0.00% 1.47s 2.32s _get_font_id (kivy/core/text/text_sdl2.py)
0.00% 0.00% 1.39s 3.71s get_extents (kivy/core/text/text_sdl2.py)
0.00% 0.00% 1.22s 1.64s __init__ (kivy/core/text/__init__.py)
0.00% 0.00% 1.11s 1.11s __init__ (kivy/weakmethod.py)
Or produce a flame graph with py-spy top -- python main.py
This shows that the majority of the time is actually spent trying to figure out how wide each line needs to be, which is especially evident at how much the UI locks-up when you resize the window.
so almost all the time is spent in _refresh_text unsurprisingly, and _create_line_label in it.
My guess, looking at the code from _create_line_label is that most of the time is spent looking for the ideal length of the line, while we start with the logical line length if it's too long, we cut it in half, until it fits and then grows again (half the length) until it does't fit anymore, etc, following a bisection until the stop is less than 2. Any improvement to the initial guess of the line's length could make a significant difference, for example, just by enlarging the window of the example, so most logical lines fit in one display line, the rendering time was cut by more than 50%.
Another idea could be to be able to compute how many words of the line can fit, without rendering the full line, that could be achieved by rendering + caching each unique word encountered, with line options as well (font, etc), so the words texture could be looked up from cache, adding their width until we don't fit the current line anymore, allowing us to either render the line from these words at this point, or reworking the rendering system so a line can just use these textures directly through multiple rectangle instructions, in this technique it would be important to properly keep track of the space between words as well, which depends on the specific character(s) used.
I'm sure there is research about how to do these things efficiently as well, that could be worth looking into (famously, Knuth spent a lot of time on efficient and correct ways to layout text).
Related
I am trying to update the image source on a changeImageSource function it changes the source instantly but when I use time.sleep() method in that function, function executes but doesn't update the source of the image. updates after time.sleep() call completed.
from kivy.app import App
from kivy.uix.image import AsyncImage
from kivy.uix.button import Button
from kivy.uix.widget import Widget
import time
# creating the App class
class MyApp(App):
def build(self):
parent = Widget()
#creating and adding image to widget
self.img = AsyncImage(
source='http://kivy.org/logos/kivy-logo-black-64.png')
self.img.pos = (400,400)
#creating btn and adding press handler
self.change_img_btn = Button(text="Change Image ")
self.change_img_btn.bind(on_press = self.changeImageSource)
#adding widget to Widget instance
parent.add_widget(self.img)
parent.add_widget(self.change_img_btn)
return parent;
def changeImageSource(self,*args):
self.img.source = "https://cdn.sstatic.net/Sites/stackoverflow/Img/apple-touch-icon.png?v=c78bd457575a"
time.sleep(4)
# run the App
MyApp().run()
One: The "return parent;" should be "return parent"
Two: Why do you need time.sleep()?
You can also try flipping line 29 and 30.
I would like to run a Method when the user tries to exit the app , kind of like a "are you sure you want to exit" or "Do you want to save the file" type of message whenever the user tries to exit by clicking the Exit button on top of the window
Some thing like
on_quit: app.root.saveSession()
If you want your application to simply run things after the GUI has closed, the easiest and smallest approach would be to place any exit code after TestApp().run(). run() creates a endless loop which also clears any event-data from within kivy so it doesn't hang. That endless loop breaks as soon as the window/gui instance dies. So there for, any code after will execute only after the GUI dies too.
If you want to create a graceful shutdown of the GUI with for instance socket-closing events or a popup asking the user if that's what they really want to do, then creating a hook for the on_request_close event is the way to go:
from kivy.config import Config
Config.set('kivy', 'exit_on_escape', '0')
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.popup import Popup
from kivy.core.window import Window
class ChildApp(App):
def build(self):
Window.bind(on_request_close=self.on_request_close)
return Label(text='Child')
def on_request_close(self, *args):
self.textpopup(title='Exit', text='Are you sure?')
return True
def textpopup(self, title='', text=''):
"""Open the pop-up with the name.
:param title: title of the pop-up to open
:type title: str
:param text: main text of the pop-up to open
:type text: str
:rtype: None
"""
box = BoxLayout(orientation='vertical')
box.add_widget(Label(text=text))
mybutton = Button(text='OK', size_hint=(1, 0.25))
box.add_widget(mybutton)
popup = Popup(title=title, content=box, size_hint=(None, None), size=(600, 300))
mybutton.bind(on_release=self.stop)
popup.open()
if __name__ == '__main__':
ChildApp().run()
Courtesy of pythonic64 who created a gist on the topic in a issue way back when.
I am populating a treeview in Kivy that takes some time depending on how large it is.
In the case the tree is large and takes awhile, I would like to display a popup while it is populating so the user is aware the program has not frozen, and close this popup when the logic for populating the tree finishes.
Here is what I have come up with through some research on the topic, but the popup still seems to only come once the tree is finished populating:
def show(self, *args):
self.error_popup.open()
def populate_tree(self, model):
#Clock.schedule_once(self.error_popup.open())
popup_thread = threading.Thread(target=self.show())
popup_thread.start()
# order the dictionary for better user experience
ordered_data = collections.OrderedDict(sorted(model.items()))
# logic to populate tree
for county, value in ordered_data.items():
if county != "model_name":
# set initial county dropdowns in tree
county_label = self.treeview.add_node(TreeViewButton(text=str(county), on_press=self.edit_node))
i = 0 # keep count of rules
# add rules children to county
for rule_obj, rule_list in value.items():
for rule in rule_list:
i += 1
# set rule number in tree
rule_label = self.treeview.add_node(TreeViewButton(text='Rule ' + str(i), on_press=self.edit_node), county_label)
# add conditions children to rule
for condition in rule:
self.treeview.add_node(TreeViewButton(text=condition, on_press=self.edit_node), rule_label)
#Clock.schedule_once(self.error_popup.dismiss())
#somehow close popup_thread
I included a kivy Clock attempt in case that is more on the right track of what I am looking for, however currently it will just open the popup and never populate the tree. I am new to GUI programming and event callbacks, so any help is greatly appreciated.
I tried keeping the code short, if more is needed please let me know.
I built an app which does something similar to what you're doing (different computation, but as you said the point was it was time-consuming and you want to thread a popup that shows the app isn't crashed - it's just crankin' the numbers). What ended up working for me was to set up a button to execute a dummy function which toggles both the popup and the calculation. Run the popup first and then thread the calculation through the 'from threading import Thread' module to execute the computation on a separate thread.
Here's a working example. It's just sleeping for 5 seconds but you can stick your computation into that function and it should work just fine. What it does is opens the popup before the computation and closes the popup when the calculation is done. Also, you can stick a 'Loading.gif' file into the folder and it'll import that as your loading gif if you want to use something other than what kivy pulls up (which is essentially a loading gif for loading your Loading.gif which isn't loading because it's not there... haha). Also added an 'ABORT' button if your user gets tired of waiting.
Finally just as a side note, I've had difficulties getting the .kv file to build into the pyinstaller application bundeler, so just as a heads up, using the builder.load_string(KV) function is a good alternative for that.
from threading import Thread
from sys import exit
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.popup import Popup
from kivy.lang import Builder
KV = '''
<Pop>:
id:pop
title: ''
auto_dismiss: False
padding: 10
spacing: 10
BoxLayout:
BoxLayout:
padding: 10
spacing: 10
orientation: 'vertical'
Label:
font_size: 22
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
text: "Process is currently running."
Label:
id: error_msg
size_hint_x: 0.3
text: ''
BoxLayout:
orientation: 'vertical'
Button:
background_color: (1,0,0,1)
text: "ABORT"
on_press: root.sysex()
AsyncImage:
source: 'Loading.gif'
<MetaLevel>:
rows: 1
cols: 1
Button:
text: 'RUN'
on_release: root.dummy()
'''
Builder.load_string(KV)
class MetaLevel(GridLayout):
def dummy(self, *args):
App.get_running_app().pop.open()
Thread(target=self.calculate, args=(args,), daemon=True).start()
def calculate(self, *args):
import time
time.sleep(5)
App.get_running_app().pop.dismiss()
class Pop(Popup):
def sysex(self):
exit()
class Cruncher(App):
def build(self):
self.pop = Pop()
return MetaLevel()
if __name__ == "__main__":
Cruncher().run()
Were you able to get this sorted?
I think it works if you use the thread for populating the tree rather than using it for showing the popup. After populating the tree, in the same thread you can close the pop up using Popup.dismiss()
main.py file
from kivy.app import App
from kivy.uix.popup import Popup
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
import time, threading
class popupTestApp(App):
def waitSec(self):
time.sleep(5)
self.p.dismiss()
def popUpFunc(self):
self.p = Popup(title='Test Popup', content=Label(text='This is a test'), size_hint=(None,None), size=(400,400))
self.p.open()
popUpThread = threading.Thread(target=self.waitSec)
popUpThread.start()
if __name__ == '__main__':
popupTestApp().run()
popuptest.kv file
BoxLayout:
BoxLayout:
id:LeftPane
Button:
id:MyButton
text:'Pop it up!'
on_release:app.popUpFunc()
BoxLayout:
id:RightPane
Label:
text: 'Another Pane'
Take a look at the below link where this is explained well.
Building a simple progress bar or loading animation in Kivy
Related question here
I've discovered a slave property for the runTouchApp function that prevents the Kivy's event loop from running and forces to update it from somewhere else.
Here's a part of app.py where that property is used:
# we are in a slave mode, don't do dispatching.
if slave:
return
try:
if EventLoop.window is None:
_run_mainloop()
else:
EventLoop.window.mainloop()
finally:
stopTouchApp()
Here, if the app is not being run in slave mode, we have two choices on how to run the mainloop.
The first one, _run_mainloop() function works pretty straight-forward - it simply calls the EventLoop.run(), which in turn infinitely calls EventLoop.idle().
That could lead us to believe that to keep the GUI running, we only need to call idle.
But then there's the second option, which calls the kivy.core.window.WindowSDL's method mainloop.
That method works by calling another method, the _mainloop and this is where it gets interesting. The definition of said method is huge and it handles all sorts of events.
So okay, I ran my app in slave mode:
class TestApp(App):
def start_event(self):
pass
def build(self):
return Button(text = "hello")
def run(self):
# This definition is copied from the superclass
# except for the start_event call and slave set to True
if not self.built:
self.load_config()
self.load_kv(filename=self.kv_file)
root = self.build()
if root:
self.root = root
if self.root:
Window.add_widget(self.root)
window = EventLoop.window
if window:
self._app_window = window
window.set_title(self.get_application_name())
icon = self.get_application_icon()
if icon:
window.set_icon(icon)
self._install_settings_keys(window)
self.dispatch('on_start')
runTouchApp(slave = True)
self.start_event() # Here we start updating
self.stop()
Now, if I put this in the start_event method (by expectations):
def start_event(self):
while True:
EventLoop.idle()
Guess what, the app doesn't respond to touch events and freezes.
So I tried to call the Window's mainloop instead:
def start_event(self):
EventLoop.window.mainloop()
And suddenly everything started working normally again. But the problem here is that such a call blocks forever, as it is an infinite loop, so there's no one-time update call like EventLoop.idle
How to keep the app running using such one-time calls?
Well, this is Python so assuming you want to stick to WindowSDL provider, you can always monkey patch this mainloop function so it won't be infinite:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.base import EventLoop
Builder.load_string('''
<MyWidget>:
Button:
text: 'test'
on_press: print('doing my job')
''')
# https://github.com/kivy/kivy/blob/master/kivy/core/window/window_sdl2.py#L630
def mainloop(self):
# replaced while with if
if not EventLoop.quit and EventLoop.status == 'started':
try:
self._mainloop()
except EventLoop.BaseException as inst:
# use exception manager first
r = EventLoop.ExceptionManager.handle_exception(inst)
if r == EventLoop.ExceptionManager.RAISE:
EventLoop.stopTouchApp()
raise
else:
pass
class MyWidget(BoxLayout):
pass
if __name__ == '__main__':
from kivy.base import runTouchApp
runTouchApp(MyWidget(), slave=True)
# monkey patch
EventLoop.window.mainloop = mainloop
while True:
EventLoop.window.mainloop(EventLoop.window)
print('do the other stuff')
if EventLoop.quit:
break
It's really hacky though and thus I'd not recommend running something like that in a production code.
I cannot understand how to update kivy screen.
This is my python file:
import kivy
kivy.require('1.1.1')
from kivy.app import App
from kivy.uix.widget import Widget
import time
class PongGame(Widget):
labe = ObjectProperty(None)
def settext(self,x):
self.labe.text = str(x)
print "DONE"
class PongApp(App):
def build(self):
game = PongGame()
game.settext(1)
time.sleep(3)
game.settext(5)
time.sleep(3)
game.settext(87)
time.sleep(3)
game.settext(5)
return game
if __name__ == '__main__':
PongApp().run()
this is my kv file
#:kivy 1.0.9
<PongGame>:
labe:lab
Label:
id: lab
font_size: 70
center_x: root.width / 4
top: root.top - 50
text: str("Hello")
When I run it all it freezes. Then all I see is a 5.
How can i get the others to show up?
def build(self):
game = PongGame()
game.settext(1)
time.sleep(3)
game.settext(5)
time.sleep(3)
game.settext(87)
time.sleep(3)
game.settext(5)
return game
Your problem is that sleep blocks the entire thread - your whole program just stops doing anything for the duration of the call. That includes any graphical interface that has been drawn, it can't update or even receive input because it's running in the same thread and doesn't get run again until your blocking function call has stopped.
Actually, you also have the problem that all these changes take place before the gui is even drawn (the game is displayed in the kivy window only after it's returned from build).
You have to instead think in terms of kivy's main loop and clock - in the background kivy is trying to run certain functions like touch detection and graphical updates as frequently as possible. If you run some code in this loop that takes a long time, none of the rest of kivy can work until your code terminates. This is normal in gui programming, you always have to be careful not to block the main program loop.
The easiest way in kivy to schedule something regularly is to create a function and hook into this event loop, telling the clock to run your function after some interval or after every repeat of some time period. In your case, you want to change the text every 3 seconds.
Here is a short example of how to achieve exactly what you want:
from kivy.app import App
from kivy.uix.widget import Widget
import time
from kivy.properties import ObjectProperty
from functools import partial
from kivy.lang import Builder
from kivy.clock import Clock
Builder.load_string('''
<PongGame>:
labe:lab
Label:
id: lab
font_size: 70
center_x: root.width / 4
top: root.top - 50
text: str("Hello")
''')
class PongGame(Widget):
labe = ObjectProperty(None)
def settext(self, x, *args):
self.labe.text = str(x)
print "DONE"
class PongApp(App):
def build(self):
game = PongGame()
game.settext(1)
Clock.schedule_once(partial(game.settext, 5), 3)
Clock.schedule_once(partial(game.settext, 87), 6)
Clock.schedule_once(partial(game.settext, 5), 9)
return game
if __name__ == '__main__':
PongApp().run()
There are a few important notes. One is that I used functools.partial() because you need to pass a function to Clock.schedule_once, and partial creates a function from an existing function (here game.settext) and some default arguments to use (here the numbers for the label). It was also important to add *args to PongGame.settext because the clock automatically passes some extra arguments that we don't care about.
If the meaning of that is not clear to you, experiment with these parameters to see what happens.