20
$\begingroup$

The following question might've been asked several times, but I've found no satisfactory answer.

Short question: How can I show progress of background task in Blender UI?

Explanation: I have a long running task which I want to run in the background. I know there's some gotcha while using threads in python script, but since the task does not affect blender objects or data in any way, there's no reason the user should wait for it to complete (they can continue with other things in their pipeline).

My thread works perfectly fine without any crashes, and I can show the progress on console. However to show the progress in the UI I do the following:

  • Have an integer/float property with subtype 'PERCENTAGE' which will be set to current progress. This might crash blender, since I'm actually modifying blender data. I've even tried this with some Blender versions and it kinda worked for pre-2.65. However it does not work for latest Blender builds.

  • In recent Blender version, I've found a new api named window_manager.progress_begin, window_manager.progress_update and window_manager.progress_end. I'm not sure if they are really intended to show progress, and if that is the case, how to use them.

  • EDIT: I've found an add-on template Always On Blank Render Engine by @Atom which seems to do the trick. It basically extends RenderEngine so all events can be captured. This is the first time I've tried it, and it can show progress similar to rendering progress (which is exactly what I want). However with a very quick test it seems that it'll also open a render preview window when the task is run, and also it shows the label as "Rendering" near the progressbar :-( Has anyone got any experience with extending RenderEngine?

Is there any option I can show progress in the UI? Or, is there any future plan regarding this?

$\endgroup$
  • 1
    $\begingroup$ A bit of a hack could be to use BGL and draw a small overlay onto 3dview? $\endgroup$ – zeffii Jun 14 '13 at 10:16
  • $\begingroup$ Well.. good idea. But since it also accesses blender data I doubt it'll work from other thread. $\endgroup$ – mg007 Jun 14 '13 at 10:45
  • 2
    $\begingroup$ If you are happy with just a text-based progress bar at the top of the window, maybe this can help: dalaifelinto.com/?p=645 $\endgroup$ – Mike Pan Jun 15 '13 at 3:51
  • $\begingroup$ That's interesting! I'll take a look into that. Thanks @MikePan! $\endgroup$ – mg007 Jun 15 '13 at 5:46
12
$\begingroup$

The general mechanism to solve UI thread safety issues is to pass data from the worker thread to the main thread and then update the UI in the main thread using that data.

In this case you would create a modal operator that gets called regularly with a timer, see Text Editor > Templates > Python > Operator Modal Timer for an example. Each time the operator modal function is called, you would check if the data has changed and update the UI accordingly.

How you pass the data between threads depends on the multithreading mechanism you are using. In Python there are a queue and multiprocessing.Queue modules that can be used with the threading and multiprocessing modules respectively. You put data in the queue in the worker thread and then get it out in the main thread.

$\endgroup$
  • $\begingroup$ So updating the PERCENTAGE property seems the only way, isn't it? $\endgroup$ – mg007 Jun 14 '13 at 13:33
  • $\begingroup$ I don't know of a way to create a progress bar exactly like you get for rendering or baking physics from a Python script. Using a percentage property is one way to get something that looks similar, but you could also use a text label or do custom OpenGL drawing in the 3D view or timeline. Maybe someone else knows a better way to do it, I only intended to explain how you can safely update the UI when working with multiple threads. $\endgroup$ – brecht Jun 14 '13 at 15:08
  • $\begingroup$ Actually I already knew about modal operators, but was confused that it'd crash due to reason I mentioned in the question. Your statement about queue is promising, and I'll test it now. Meanwhile I've also found a one more trick that we can use (which includes creating RenderEngine). I'll update my question or add new answer after a bit more research. $\endgroup$ – mg007 Jun 14 '13 at 15:46
  • $\begingroup$ This is the core of the answer! Even if I use any of the options I mentioned in question, I WILL have to manage my thread with this mechanism. $\endgroup$ – mg007 Jun 15 '13 at 5:44
2
$\begingroup$

I've been able to create a progress bar in the top header for a long-running method. It looks as follows and it disappears once progress reaches 100%.

enter image description here

It does update a custom Scene 'PERCENTAGE' property, but using threading.Thread in combination with a timer-modal operator, I have not encountered any crashes.

This is the code I used:

ProgressWidget

import bpy
import time

class ProgressWidget(object):
    update_every = 0.2 # seconds

    widget_visible = False
    last_updated = 0

    @staticmethod
    def update_widget(context, force=False):
        sec_since_update = time.time() - ProgressWidget.last_updated

        if not force and sec_since_update < ProgressWidget.update_every:
            return

        # Update the top header        
        for area in bpy.context.screen.areas:
            if area.type == 'INFO':
                area.tag_redraw()

        ProgressWidget.last_updated = time.time()

    @staticmethod
    def draw(self, context):    
        if ProgressWidget.get_progress(context) < 100:

            # Shows the custom progress property as a "bar"
            self.layout.prop(context.scene, "ProgressWidget_progress", text="Progress", slider=True)

        else:
            ProgressWidget.hide()

    @staticmethod
    def create_progress_property():
        bpy.types.Scene.ProgressWidget_progress = bpy.props.IntProperty(default=0, min=0, max=100, step=1, subtype='PERCENTAGE')    

    @staticmethod
    def set_progress(context, value):
        if ProgressWidget.widget_visible:
            context.scene.ProgressWidget_progress = value

    @staticmethod
    def get_progress(context):
        if ProgressWidget.widget_visible:
            return context.scene.ProgressWidget_progress    
        else:
            return 0

    @staticmethod
    def show(context):    
        if not ProgressWidget.widget_visible:
            ProgressWidget.create_progress_property()

            bpy.app.handlers.scene_update_pre.append(ProgressWidget.update_widget)
            bpy.types.INFO_HT_header.append(ProgressWidget.draw)

            ProgressWidget.widget_visible = True

            ProgressWidget.set_progress(context, 0)

    @staticmethod
    def hide():    
        bpy.types.INFO_HT_header.remove(ProgressWidget.draw)
        bpy.app.handlers.scene_update_pre.remove(ProgressWidget.update_widget)

        ProgressWidget.widget_visible = False


# Creates a widget simulator in the text editor side panel
# The code below is only used for widget development

class CUSTOM_PT_testPanel(bpy.types.Panel):
    """Adds a custom panel to the TEXT_EDITOR"""
    bl_idname = 'TEXT_PT_testPanel'
    bl_space_type = "TEXT_EDITOR"
    bl_region_type = "UI"
    bl_label = "Progress Simulator"

    def draw(self, context):
        self.layout.operator("custom.show_progress_widget")
        self.layout.operator("custom.make_progress")


class CUSTOM_OT_show_progress_widget(bpy.types.Operator):
    bl_idname = "custom.show_progress_widget"
    bl_label="Show Widget in Header"

    def execute(self, context):

        ProgressWidget.show(context) # Call this to start showing progress

        return {'FINISHED'}  

class CUSTOM_OT_make_progress(bpy.types.Operator):
    bl_idname = "custom.make_progress"
    bl_label="Make progress"

    def execute(self, context):
        new_progress_value = (ProgressWidget.get_progress(context) + 25) % 125

        # This line will update the progress widget
        # call it from your long-running modal operator (0-100)
        # When progress is 100 or more, the widget disappears
        ProgressWidget.set_progress(context, new_progress_value)

        return {'FINISHED'}

if __name__ == "__main__":    
    bpy.utils.register_class(CUSTOM_PT_testPanel)
    bpy.utils.register_class(CUSTOM_OT_show_progress_widget)
    bpy.utils.register_class(CUSTOM_OT_make_progress)



Tested with a Timer Modal Operator + Long Running Background Thread:

import bpy


class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"

    _timer = None
    th = None
    prog = 0
    stop_early = False

    def modal(self, context, event):
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            self.cancel(context)

            self.stop_early = True
            self.th.join()
            print('DONE EARLY')

            return {'CANCELLED'}

        if event.type == 'TIMER':
            context.scene.ProgressWidget_progress = self.prog

            if not self.th.isAlive():
                self.th.join()
                print('DONE')
                return {'FINISHED'}

        return {'PASS_THROUGH'}

    def execute(self, context):
        import threading

        def long_task(self):
            import time
            for i in range(10):
                if self.stop_early:
                    return

                time.sleep(.5)
                self.prog += 10

        self.th = threading.Thread(target=long_task, args=(self,))

        bpy.ops.custom.show_progress_widget()
        self.th.start()

        wm = context.window_manager
        self._timer = wm.event_timer_add(0.1, context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def cancel(self, context):
        wm = context.window_manager
        wm.event_timer_remove(self._timer)


def register():
    bpy.utils.register_class(ModalTimerOperator)


def unregister():
    bpy.utils.unregister_class(ModalTimerOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.wm.modal_timer_operator()

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.