10
$\begingroup$

The Goal

I'm writing a Blender add-on that extracts a point cloud from multiple photos. This operation is implemented as an Operator that is invoked from a Panel button. This Operator, once started takes a lot of time to finish.

I would like to inform the user about the progress of the operation, via some kind of status text. What is the best way to script this?

What I've Tried Already

Currently, I have a status message label in the Panel which invokes the "Start SFM" Operator. This displays the text stored in a Scene Property called currentStatus.

Panel snapshot

In my Operator's Execute method, I do this:

def execute(self, context):
    thr = threading.Thread(target=self.foo, args=(context,))
    thr.start()
    return {'FINISHED'}

And in the time-consuming method foo, I update currentStatus:

def foo(self, context):
    context.scene.currentStatus = "Starting..."
    # Time consuming operation 1
    context.scene.currentStatus = "Operation 1 Completed"
    # Time consuming operation 2
    context.scene.currentStatus = "Operation 2 Completed"
    ...

The problem with this is that the label only updates when it is moused over or if the user clicks on the scene. It does not redraw automatically. I have failed at several attempts to force a redraw through python. I have tried polling, but it behaves similarly.

Am I missing something, or is this a completely wrong way to approach the problem?

$\endgroup$
3
  • $\begingroup$ Related: blender.stackexchange.com/q/3219/599 $\endgroup$
    – gandalf3
    Mar 12, 2014 at 19:54
  • $\begingroup$ I tried the progress cursor method. The cursor doesn't update without moving it. So basically the user has to wave around the cursor just to get notified of progress. Not really satisfactory in my case because my operation can take a while. $\endgroup$
    – ApoorvaJ
    Mar 14, 2014 at 13:35
  • $\begingroup$ That's odd.. It was working okay for me. Anyway, I agree the cursor method is not the best. :/ $\endgroup$
    – gandalf3
    Mar 14, 2014 at 19:26

2 Answers 2

8
+50
$\begingroup$

This is a hack, at best. So, when an image in blender is rendered, it provides a convenient status bar at the top of the window. The problem is a script has to be a renderEngine to take advantage of this. The solution? Make your script a custom renderEngine, then have your button invoke the render function.

Here's a very basic example of updating the status bar, and adding a status string to the render window. You would put the code you want run in the myCode function.

import bpy
import time

class CustomRenderEngine(bpy.types.RenderEngine):
    # These three members are used by blender to set up the
    # RenderEngine; define its internal name, visible name and capabilities.
    bl_idname = 'custom_renderer2'
    bl_label = 'Progress Bar Example2'
    bl_use_preview = True
    # This is an example function, put your code here
    def myCode(engine):
        engine.update_stats("Starting","")        #update the stats in render window
        time.sleep(1)
        engine.update_stats("Running","halfway")
        engine.update_progress(0.5)               #update the progress bar
        time.sleep(1)

    # This is the method called by blender
    def render(self, scene):
        self.myCode()

# Register the RenderEngine
bpy.utils.register_class(CustomRenderEngine)

Then select your new renderEngine. From the top of the blender window click "Blender Render" and select "Progress Bar Example." Now you can run your script by pressing F12. One other method that might be useful is test_break, which checks to see if the render(or script, in this case) has been cancelled.
One last thing to note, directly executing a script (eg. with altP) doesn't display the progress bar, however when the render function (byp.render.render) is bound to a panel it displays correctly.

See these for more information: Blender artists, setting progress bar values Blender Python Api, RenderEngine

$\endgroup$
4
  • $\begingroup$ Fixed the link. $\endgroup$
    – user2699
    Mar 14, 2014 at 19:36
  • $\begingroup$ Thank you for the answer. I wanted text-based progress updates instead of a progress bar, but that seems to be surprisingly difficult, and impossible without any hacks. I'll mark this as the answer at give the bounty after six more days, if there's no text-based notification answer till then. $\endgroup$
    – ApoorvaJ
    Mar 15, 2014 at 15:46
  • $\begingroup$ @ApoorvaJ, I'm not sure how I missed that in your question. I've added a couple lines to show how a text status can be shown in the render window using update_stats. $\endgroup$
    – user2699
    Mar 15, 2014 at 17:22
  • 1
    $\begingroup$ It's a hack, but it's the closest implementation to my requirements. Thanks! $\endgroup$
    – ApoorvaJ
    Mar 17, 2014 at 13:08
1
$\begingroup$

Here's a minimal working example that displays a progress bar filling in the UI. I used a modal operator and a timer but it can be driven by any property. You'll need Version 4.0 and bpy.types.UILayout.progress. You're kind of obligated to use a modal operator since a regular operator's execution will block updating the UI until it finishes. Note autosave is disabled while a modal operator runs.

enter image description here

import bpy


class ModalTimerOperator(bpy.types.Operator):
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"

    _timer = None

    def modal(self, context, event):
        [a.tag_redraw() for a in context.screen.areas]
        if self._timer.time_duration > 3:
            context.window_manager.progress = 1
            return {'FINISHED'}
        context.window_manager.progress = self._timer.time_duration / 3
        return {'PASS_THROUGH'}

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


def progress_bar(self, context):
    row = self.layout.row()
    row.progress(
        factor=context.window_manager.progress,
        type="BAR",
        text="Operation in progress..." if context.window_manager.progress < 1 else "Operation Finished !"
    )
    row.scale_x = 2
    

def register():
    bpy.types.WindowManager.progress = bpy.props.FloatProperty()
    bpy.utils.register_class(ModalTimerOperator)
    bpy.types.TEXT_HT_header.append(progress_bar)


if __name__ == "__main__":
    register()
    bpy.ops.wm.modal_timer_operator()
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .