22
$\begingroup$

I'm making an python script which create a lot of object and this could take a lot of time. My question is how to show to the user of my script, the "progression" of the script? For now I print an progress bar in the system console.

But you have to open the system console befor and (apparently) the system console only work with Windows.

Is there an other way to show the progression of my script with python like control the mouse percentage icon (the little icon that replace the mouse while baking) or another idea?

$\endgroup$
1

4 Answers 4

15
$\begingroup$

Currently the only (simple) way to do this is to show the progress with the mouse cursor.

import bpy
wm = bpy.context.window_manager

# progress from [0 - 1000]
tot = 1000
wm.progress_begin(0, tot)
for i in range(tot):
    wm.progress_update(i)
wm.progress_end()
$\endgroup$
3
  • $\begingroup$ Thank's for your answer. You say "the only (simple) way". Does this means that you know other (hard) ways to do it? And of course other than those indicated in other answer? $\endgroup$
    – lucblender
    Oct 11, 2013 at 13:53
  • 2
    $\begingroup$ @lucblender with python a lot of hacks are possible, like calling into operating system specific progress bars which display in the window list, or writing a modal operator that draws text in the 3d view with a timer, its possible but way overkill in almost all cases. $\endgroup$
    – ideasman42
    Mar 12, 2014 at 20:04
  • $\begingroup$ FWIW, on Windows (tested with 2.83), if the script takes too long (about 5+ seconds), as the UI does not 'respond', the cursor is replaced by the standard Windows spinning cursor - thus hiding the progress info. $\endgroup$
    – rotoglup
    Sep 15, 2020 at 10:16
25
$\begingroup$
  • Progress of script can also be printed and updated with sys module to the console:

    import sys
    from time import sleep
    
    sys.stdout.write("Some job description: ")
    sys.stdout.flush()
    some_list = [0] * 100
    for idx, item in enumerate(some_list):
        msg = "item %i of %i" % (idx, len(some_list)-1)
        sys.stdout.write(msg + chr(8) * len(msg))
        sys.stdout.flush()
        sleep(0.02)
    
    sys.stdout.write("DONE" + " "*len(msg)+"\n")
    sys.stdout.flush()
    

    This can look like this:

    enter image description here

  • You can also copy paste this next function to display a progress bar:

    import time, sys
    
    def update_progress(job_title, progress):
        length = 20 # modify this to change the length
        block = int(round(length*progress))
        msg = "\r{0}: [{1}] {2}%".format(job_title, "#"*block + "-"*(length-block), round(progress*100, 2))
        if progress >= 1: msg += " DONE\r\n"
        sys.stdout.write(msg)
        sys.stdout.flush()
    
    # Test
    for i in range(100):
        time.sleep(0.1)
        update_progress("Some job", i/100.0)
    update_progress("Some job", 1)
    

    This results in:

    enter image description here

$\endgroup$
3
$\begingroup$

Research over this subject :

I have this but it seems that working in modal do not update the gui correctly between the tasks and it might freeze the interface anyway

so this should work in theory, but in practice it seems that there's few flaws because on how blender handle the freezing process. (that perhaps could be resolved by using bpy.app.timers ?)

This technique consist of segmenting your code into multiple function, that you store in a dictionary of operations, then a modal operator will loop over your operations while updating the progress bar depending on you steps achievement.

import bpy
import time

#Operation Dict
#operation will  need to be divided in multiple steps

sleep = 0

def _00(): 
    print("f00")
    time.sleep(sleep)
    
def _01(): 
    print("f01")
    time.sleep(sleep)
    
def _02(): 
    print("f02")
    time.sleep(sleep)
    
def _03(): 
    print("f03")
    time.sleep(sleep)
    
def _04(): 
    print("f04")
    time.sleep(sleep)
     
Operations = {
    0:_00,
    1:_01,
    2:_02,
    3:_03,
    4:_04,    
}

#Modal Operator

class EXAMPLE_OT_modal_operator(bpy.types.Operator): 
    
    bl_idname = "example.modal_operator"
    bl_label = "Modal Operator"
    
    step : bpy.props.IntProperty()


    def modal(self, context, event):

        global Operations
        max_step = len(Operations.keys())

        #Update Gui
        context.object.progress = self.step/max_step*100
        context.area.tag_redraw()
        context.area.tag_redraw()
        
        #Running Operations Steps In Order 
        if self.step < max_step:
            print(f"Operations[{self.step}]()")
            Operations[self.step]()
            self.step += 1
            return {'RUNNING_MODAL'}
        
        #No More steps, Restore and Done
        print("Finished")
        self.step = 0
        context.object.progress = 0
        return {'FINISHED'}
            

    def invoke(self, context, event):
        print("Invoke")
        #self.execute(context)
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}



#Panel

class EXAMPLE_PT_panel(bpy.types.Panel):

    bl_idname      = "EXAMPLE_PT_panel"
    bl_label       = "Example"
    bl_category    = "Example"
    bl_space_type  = "VIEW_3D"
    bl_region_type = "UI"
    bl_context     = "objectmode"

    def draw(self, context):
        layout = self.layout
        obj = context.object
        
        if obj.progress: 
            layout.prop(bpy.context.object,"progress")
        else:
            ope = layout.row()
            ope.operator_context = "INVOKE_DEFAULT"
            ope.operator("example.modal_operator",text="Run Modal Operator")
        
        return 
    

#Registering Stuff...

bpy.types.Object.progress = bpy.props.FloatProperty(
    name="Progress",
    subtype="PERCENTAGE",
    soft_min=0, 
    soft_max=100, 
    precision=0,
    )
bpy.utils.register_class(EXAMPLE_PT_panel)
bpy.utils.register_class(EXAMPLE_OT_modal_operator)

Here is another test, same principle but this time using delayed function hoping that the delay between the operations might be enough to let blender "breath" so we can get a glimpse of the interface. it seems that this attempt is also failing and that blender interface simply like to keep in freeze state even if is is not supposed to

  • note that this technique might be limiting as execution context within a delay function is highly limiting

Not sure if my research are of any help. I'll try to explore more solutions sooner or later


import bpy
import time
import functools

#Operation Dict
#your operation work steps should divided into multiple function

sleep = 0

def _00(): 
    print("f00")
    time.sleep(sleep)
    
def _01(): 
    print("f01")
    time.sleep(sleep)
    
def _02(): 
    print("f02")
    time.sleep(sleep)
    
def _03(): 
    print("f03")
    time.sleep(sleep)
    
def _04(): 
    print("f04")
    time.sleep(sleep)


#then you store then all in a dict like this 
Operations = {
    0:_00,
    1:_01,
    2:_02,
    3:_03,
    4:_04,    
}

#We need a function that refresh all areas
#we need this because we can't access context.area from a timer function

def refresh_all_areas():
    for wm in bpy.data.window_managers:
        for w in wm.windows:
            for area in w.screen.areas:
                area.tag_redraw()

#Then we run all steps of our Operation Dict in this operator
#the goal of this operator is to run the function from timers
#We need to run these from timers otherwise the interface might be locked and freeze when calculating

class EXAMPLE_OT_timer_operator(bpy.types.Operator): 
    
    bl_idname = "example.timer_operator"
    bl_label = "Timer Operator"
    
    interval : bpy.props.FloatProperty(default=0.1)
            
    def execute(self, context):
        
        global Operations
        max_step = len(Operations.keys())
        
        for i,(step,operation) in enumerate(Operations.items()):
            i+=1
                
            def generate_delay_fct(stp,):
                """fct factory needed otherwise same function for all timer"""
                
                def fct():
                    """delay is needed, so interface don't freeze and we have time to display progress bar"""
                    
                    print("execute_op_with_delay ->"+str(stp))
                    Operations[stp]()
                    
                    #Update Gui
                    bpy.context.object.progress = (stp/max_step)*100
                    refresh_all_areas()
                    return None 
                return fct
            
            bpy.app.timers.register( generate_delay_fct(step,), first_interval=self.interval*i,)
            continue 
            
            
        def last_step():
            """one last function is required in orter to restore the progress bar to 0 once all operations are done"""
            bpy.context.object.progress = 0
            refresh_all_areas()
            return None
        
        bpy.app.timers.register( last_step, first_interval=self.interval*(i+1),)
        
        return {'FINISHED'}

#Panel

class EXAMPLE_PT_panel(bpy.types.Panel):

    bl_idname      = "EXAMPLE_PT_panel"
    bl_label       = "Example"
    bl_category    = "Example"
    bl_space_type  = "VIEW_3D"
    bl_region_type = "UI"
    bl_context     = "objectmode"

    def draw(self, context):
        layout = self.layout
        obj = context.object
        
        if obj.progress: 
            layout.prop(bpy.context.object,"progress")
        else:
            ope = layout.row()
            ope.operator_context = "INVOKE_DEFAULT"
            ope.operator("example.timer_operator",text="Run Timer Operator")
        
        return 
    

#Registering Stuff...

bpy.types.Object.progress = bpy.props.FloatProperty(
    name="Progress",
    subtype="PERCENTAGE",
    soft_min=0, 
    soft_max=100, 
    precision=0,
    )
    
bpy.utils.register_class(EXAMPLE_PT_panel)
bpy.utils.register_class(EXAMPLE_OT_timer_operator)

Solution Found

enter image description here

It seems that the first prototype i did was ok, and the second idea of timers was a good lead, what i needed in the end is an hybrid solution between the two.

by using a timer such as event_timer_add(), we could check for this timer in our events, then have a guaranteed unfreeze at this moment. But that's not enough, we need to extend this timeframe by just a bit! that's why i added a counter timer_count that will ignore the first 10 modal loops

in the end this is working quite fine

import bpy
import time

#Operation Dict
#operation will  need to be divided in multiple steps

sleep = 1

def _00(): 
    print("f00")
    time.sleep(sleep)
    
def _01(): 
    print("f01")
    time.sleep(sleep)
    
def _02(): 
    print("f02")
    time.sleep(sleep)
    
def _03(): 
    print("f03")
    time.sleep(sleep)
    
def _04(): 
    print("f04")
    time.sleep(sleep)
     
Operations = {
    "First Step":_00,
    "Second Step":_01,
    "Running Stuff":_02,
    "Wait a minute":_03,
    "There's a problem":_04,    
    "ah no it's ok":_04,    
    "we are done":_04,    
    }

#Modal Operator

class EXAMPLE_OT_modal_operator(bpy.types.Operator): 
    
    bl_idname = "example.modal_operator"
    bl_label = "Modal Operator"
    
    def __init__(self):
        
        self.step = 0
        self.timer = None
        self.done = False
        self.max_step = None
        
        self.timer_count = 0 #timer count, need to let a little bit of space between updates otherwise gui will not have time to update
                
    def modal(self, context, event):
        
        global Operations
        
        #update progress bar
        if not self.done:
            print(f"Updating: {self.step+1}/{self.max_step}")
            #update progess bar
            context.object.progress = ((self.step+1)/(self.max_step))*100
            #update label
            context.object.progress_label = list(Operations.keys())[self.step]
            #send update signal
            context.area.tag_redraw()
            
            
        #by running a timer at the same time of our modal operator
        #we are guaranteed that update is done correctly in the interface
        
        if event.type == 'TIMER':
            
            #but wee need a little time off between timers to ensure that blender have time to breath, so we have updated inteface
            self.timer_count +=1
            if self.timer_count==10:
                self.timer_count=0
                
                if self.done:
                    
                    print("Finished")
                    self.step = 0
                    context.object.progress = 0
                    context.window_manager.event_timer_remove(self.timer)
                    context.area.tag_redraw()
                    
                    return {'FINISHED'}
            
                if self.step < self.max_step:
                        
                    #run step function
                    list(Operations.values())[self.step]()
                    
                    self.step += 1
                    if self.step==self.max_step:
                        self.done=True
                    
                    return {'RUNNING_MODAL'}
        
        return {'RUNNING_MODAL'}
            
    def invoke(self, context, event):
        
        print("")
        print("Invoke")
        
        #terermine max step
        global Operations
        if self.max_step == None:
            self.max_step = len(Operations.keys())        

        context.window_manager.modal_handler_add(self)
        
        #run timer 
        self.timer = context.window_manager.event_timer_add(0.1, window=context.window)
        
        return {'RUNNING_MODAL'}



#Panel

class EXAMPLE_PT_panel(bpy.types.Panel):

    bl_idname      = "EXAMPLE_PT_panel"
    bl_label       = "Example"
    bl_category    = "Example"
    bl_space_type  = "VIEW_3D"
    bl_region_type = "UI"
    bl_context     = "objectmode"

    def draw(self, context):
        layout = self.layout
        obj = context.object
        
        if obj.progress: 
            progress_bar = layout.row()
            progress_bar.prop(bpy.context.object,"progress")
            progress_lbl = layout.row()
            progress_lbl.active = False
            progress_lbl.label(text=bpy.context.object.progress_label)
        else:
            ope = layout.row()
            ope.operator_context = "INVOKE_DEFAULT"
            ope.operator("example.modal_operator",text="Run Modal Operator")
        
        return 
    

#Registering Stuff...

bpy.types.Object.progress = bpy.props.FloatProperty( name="Progress", subtype="PERCENTAGE",soft_min=0, soft_max=100, precision=0,)
bpy.types.Object.progress_label = bpy.props.StringProperty()
    
bpy.utils.register_class(EXAMPLE_PT_panel)
bpy.utils.register_class(EXAMPLE_OT_modal_operator)

Prop to sybren from the Grove3d, i did dig a little bit to discover his logic

$\endgroup$
2
  • $\begingroup$ Or via modal timer operator. blender.stackexchange.com/a/47398/15543 $\endgroup$
    – batFINGER
    Jul 27, 2021 at 15:13
  • $\begingroup$ Nice! Thanks for posting, i suggest to make it more obvious and posting it as a solution to this thread. I did a lot of research and never stumble over this solution $\endgroup$
    – DB3D
    Jul 27, 2021 at 23:44
1
$\begingroup$

Solution:

I've found this two solution in the blenderartist forum:

  • Print a text in the 3D view with the blf library
  • Set progress information in the header area of a window via a modal timer
  • In 2.69 an update in the api lets the user change the cursor appearance so we could change the cursor to the "loading" cursor.

What doesn't work:

  • The solution of printing a progress bar in the console only work with windows.

This part of answer may contain errors

$\endgroup$
1
  • 2
    $\begingroup$ Two of your links are dead. Can you elaborate with some code? $\endgroup$
    – dwitvliet
    Apr 20, 2015 at 0:34

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.