10

I am writing a Kivy UI for cmd line utility I have developed. Everything works fine, but some of the processes can take from a few seconds to a few minutes to process and I would like to provide some indication to the user that the process is running. Ideally, this would be in the form of a spinning wheel or loading bar or something, but even if I could update my display to show the user that a process is running, it would be better than what I have now.

Currently, the user presses a button in the main UI. This brings up a popup that verifies some key information with the user, and if they are happy with those options, they press a 'run' button. I have tried opening a new popup to tell them that the process is running, but because the display doesn't update until the process finishes, this doesn't work.

I have a lot of coding experience, but mostly in the context of math and engineering, so I am very new to the designing of UIs and having to handle events and threads. A simple self-contained example would be greatly appreciated.

Ratan Uday Kumar
  • 5,738
  • 6
  • 35
  • 54
David K
  • 213
  • 1
  • 2
  • 12
  • See [this example](https://groups.google.com/forum/#!topic/kivy-users/QM0e_bmtlbU). Maybe you can use it in your own app. – doru Jun 02 '15 at 20:19

3 Answers3

11

I was recently tackling the problem you described: display doesn't update until the process finishes

Here is a complete example that I got working with the help of @andy_s in the #Kivy IRC channel:

My main.py:

from kivy.app import App
from kivy.uix.popup import Popup
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.clock import Clock

import time, threading

class PopupBox(Popup):
    pop_up_text = ObjectProperty()
    def update_pop_up_text(self, p_message):
        self.pop_up_text.text = p_message

class ExampleApp(App):
    def show_popup(self):
        self.pop_up = Factory.PopupBox()
        self.pop_up.update_pop_up_text('Running some task...')
        self.pop_up.open()

    def process_button_click(self):
        # Open the pop up
        self.show_popup()

        # Call some method that may take a while to run.
        # I'm using a thread to simulate this
        mythread = threading.Thread(target=self.something_that_takes_5_seconds_to_run)
        mythread.start()

    def something_that_takes_5_seconds_to_run(self):
        thistime = time.time() 
        while thistime + 5 > time.time(): # 5 seconds
            time.sleep(1)

        # Once the long running task is done, close the pop up.
        self.pop_up.dismiss()

if __name__ == "__main__":
    ExampleApp().run()

My example.kv:

AnchorLayout:
    anchor_x: 'center'
    anchor_y: 'center'
    Button:
        height: 40
        width: 100
        size_hint: (None, None)
        text: 'Click Me'
        on_press: app.process_button_click()

<PopupBox>:
    pop_up_text: _pop_up_text
    size_hint: .5, .5
    auto_dismiss: True
    title: 'Status'   

    BoxLayout:
        orientation: "vertical"
        Label:
            id: _pop_up_text
            text: ''

If you run this example, you can click the Click Me button, which should open up a "progress bar" in the form of a modal/pop-up. This pop up will remain open for 5 seconds without blocking the main window. After 5 seconds, the pop up will automatically be dismissed.

Dirty Penguin
  • 4,212
  • 9
  • 45
  • 69
  • Thanks for the advice @Dirty Penguin. I have am playing around with this, starting with the threading and have a very strange issue. Rather than call my function when I hit my in-app run button, I call a starter which generates a 'running' popup and starts a thread with my function. The issue is that when run as a thread, the function does weird stuff. Basically, my function generates a table of buttons based on a csv, and when run as a thread, some of these buttons are white and unclickable. It is different buttons each time, and it goes away if i don't run it as a separate thread. Thoughts? – David K Jun 04 '15 at 12:40
  • Turns out these odd buttons are clickable, they just look inexplicably different from the rest of the buttons in my grid. it is usually either 1 or 4 buttons (out of aprox 60) that does this. All the other buttons have the default fill colour, but these strange buttons have a white fill and don't highlight when clicked. The only way I was able to tell they were clickable was by binding a print to them. – David K Jun 04 '15 at 12:54
  • You should change widgets only in the main thread. https://github.com/kivy/kivy/issues/6244 – clfaster Jun 14 '19 at 07:24
1

I've dealt with similar problem and creating new thread didn't do the trick. I had to use Clock.schedule_once(new_func) function. It schedules function call to the next frame, so it is going to run almost immediately after callback ends.

0

I had this issue but found the discussion here a bit beyond my understanding.

I looked at a LOT of answers but nothing made sense until I researched the threading module a bit more. The Kivy support channel on discord also were incredibly helpful so props to them.

My workaround has been to create a loading screen. I will attach the code here for people who similarly need a simple reproducible code snippet :

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.properties import ObjectProperty, ListProperty, StringProperty, 
NumericProperty
import threading
import time
from kivy.clock import mainthread


class WindowManager(ScreenManager):
    pass

class WelcomeWindow(Screen):

    def generateData(self):
        data = []
        for i in range (100):
            #print(i)
            data.append(i)
        time.sleep(3)
        self.set_screen()
        return data

def executeFunc(self):
    self.manager.current = 'Loading' # Here is where I have tried to move to loading screen while func runs
    t1 = threading.Thread(target=self.generateData)# Here is where I have tried to thread the function
    t1.start()
    #t1.join() #Here is where I have tried to wait until func finished before changing screen
    #self.manager.current = 'Final'

@mainthread
def set_screen(self):
    self.manager.current = 'Final'



class LoadingWindow(Screen):
    pass

class FinalWindow(Screen):
    pass

KV = '''
WindowManager:
    WelcomeWindow:
    LoadingWindow:
    FinalWindow:

<WelcomeWindow>:
    name:'Welcome'
    BoxLayout:
        Label:
            text: "JUST SOME TEXT"
        Button:
            text: "Generate Data"
            font_size: sp(30)
            size_hint: .4,.4
            on_release:
                root.executeFunc()
                #app.root.current = "Loading"
                root.manager.transition.direction = "left"

<LoadingWindow>:
    name: 'Loading'
    BoxLayout:
        Label: 
            text: "LOADING SCREEN"
        Button:
            text: "Go Back"
            on_release:
                app.root.current = "Welcome"
                root.manager.transition.direction = "right"
                
<FinalWindow>:
    name: 'Final'
    BoxLayout:
        Label: 
            text: "FINISHED"
'''''

class TestApp(App):
    def build(self):
        return Builder.load_string(KV)

TestApp().run()