27
$\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

9 Answers 9

18
$\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
    Commented 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
    Commented Mar 12, 2014 at 20:04
  • 1
    $\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
    Commented Sep 15, 2020 at 10:16
29
$\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$
10
$\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
    Commented 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$
    – Fox
    Commented Jul 27, 2021 at 23:44
9
$\begingroup$

Blender 4.0 now has a built-in progress bar.

Example usage of Blender progress bar

Docs: https://docs.blender.org/api/current/bpy.types.UILayout.html#bpy.types.UILayout.progress

Usage:

layout.progress(factor=0.5)
layout.progress(factor=0.8, type="RING", text="Updating")
  • Currently type can be "RING" or "BAR"
  • Factor is from 0.0 to 1.0

Blender 4.0 Changelog


More information:

Pull request with more info

Here is the Blender commit

$\endgroup$
3
$\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
    Commented Apr 20, 2015 at 0:34
3
$\begingroup$

Could use text to show progress bar anywhere you can have text in the UI:

import bpy
from functools import partial

def progress(area):
    if progress.i >= 100:
        area.header_text_set(None)
        return
    else:
        area.header_text_set('Loading ' + progress.bar + " " + str(progress.i) +"%")
        progress.i += 0.5
        progress.bar = "█"*int(progress.i)+"░"*(100-int(progress.i))
        return 0.01
    
progress.bar = ""
progress.i = 0

bpy.app.timers.register(partial(progress, bpy.context.area))
$\endgroup$
3
$\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()

Here's a second technique using a non-modal operator. You'll notice your script takes significantly longer if you update the UI a lot. I suggest doing it sparsingly. Also the cursor flickers a lot, at least on windows.

import bpy


class TimerOperator(bpy.types.Operator):
    bl_idname = "wm.timer_operator"
    bl_label = "Timer Operator"

    def execute(self, context):
        progress_bar.progress = 0
        for i in range(100):
            progress_bar.progress += 0.01
            bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
            bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
        return {'FINISHED'}


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

progress_bar.progress = 0
    

def register():
    bpy.utils.register_class(TimerOperator)
    bpy.types.TEXT_HT_header.append(progress_bar)


if __name__ == "__main__":
    register()
    bpy.ops.wm.timer_operator()
$\endgroup$
2
  • $\begingroup$ Is there a way to show progress while subprocess.run(command) is running? I mean there is no way for percentage. Just something like this since the progress depends, sometimes it can take very long so I need feedback to the user that it's in progress. Currently there is no way but looks as if it hangs. That's bad feedback :( $\endgroup$
    – Harry McKenzie
    Commented Jan 11 at 12:05
  • 1
    $\begingroup$ @HarryMcKenzie You can, but you need to run it in a separate thread and use a queue to receive updates docs.blender.org/api/current/… $\endgroup$
    – Gorgious
    Commented Jan 11 at 12:31
2
$\begingroup$

This is a complete example of updating a UI progress bar using multithreading :) by Blender 4.1

enter image description here

import bpy
import threading
import time

bl_info = {
    "name": "LaserScanningModeling",
    "author": "ShaderFallback",
    "description": "",
    "blender": (2, 80, 0),
    "version": (0, 0, 1),
    "location": "",
    "warning": "",
    "category": "Generic"
}

# Global variables to save progress
progress = 0.0
is_scanning = False
stop_requested = False

def remap(value, from_min, from_max, to_min, to_max):
    from_span = from_max - from_min
    to_span = to_max - to_min
    value_scaled = float(value - from_min) / float(from_span)
    return to_min + (value_scaled * to_span)

def update_ui():
    # Force UI update
    for window in bpy.context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                area.tag_redraw()
    if is_scanning:
        return 0.1  # Call every 0.1 seconds
    else:
        return None  # Stop the timer

def scan_task():
    global progress, is_scanning, stop_requested
    for i in range(30):
        if stop_requested:
            progress = 0.0
            break
        progress = (i + 1) * 3.33  # Progress from 0 to 100
        time.sleep(1)  # Simulate time-consuming task
    if not stop_requested:
        progress = 100.0  # Set to 100% when done
    is_scanning = False
    stop_requested = False
    bpy.app.timers.register(update_ui)

class buttonExplode(bpy.types.Operator):
    bl_idname = "button.explode"
    bl_label = "Start Scan"

    def execute(self, context):
        global is_scanning, stop_requested
        if not is_scanning:
            is_scanning = True
            self.report({'INFO'}, "Starting scan...")
            my_thread = threading.Thread(target=scan_task)
            my_thread.start()
            bpy.app.timers.register(update_ui)
            buttonExplode.bl_label = "Stop Scan"
        else:
            self.report({'INFO'}, "Stopping scan...")
            stop_requested = True
            buttonExplode.bl_label = "Start Scan"
        return {'FINISHED'}

class HelloWorld(bpy.types.Panel):
    bl_idname = "TEST_SCRIPTS"
    bl_label = "LaserScanningTool"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'LaserScanningTool'

    def draw(self, context):
        global progress, is_scanning
        layout = self.layout
        layout.label(text="Scan Plugin")
        layout.operator("button.explode", text="Stop Scan" if is_scanning else "Start Scan")
        layout.label(text=f"Progress: {progress:.1f}%")
        value = remap(progress, 0, 100, 0, 1)
        layout.progress(factor=value)

def register():
    bpy.utils.register_class(HelloWorld)
    bpy.utils.register_class(buttonExplode)
    bpy.types.Scene.scan_progress = bpy.props.FloatProperty(name="Scan Progress", min=0, max=100, default=0.0, get=lambda self: progress)

def unregister():
    bpy.utils.unregister_class(HelloWorld)
    bpy.utils.unregister_class(buttonExplode)
    del bpy.types.Scene.scan_progress

if __name__ == "__main__":
    register()
$\endgroup$
-1
$\begingroup$

How to show a progress bar and load the cpu in the background

demo featuring

  • Progress Bar!
  • Responsive GUI
  • Background Thread (long uninterrupted compute time allowed!)
  • Signals (user cancel-able pattern)
  • Memory Safety
  • Developer Comments
  • State Reset (user preferences respected)

tested on 2.82 manually :✔️:

# blender progress bar
# License: AGPL

import bpy
import bpy.props

import time
import threading

class ThreadExample:
    def __init__(self, context):
        self.thread = threading.Thread(target=self.main)
        self.tid = None

        # self.state is protected by this lock
        self.lock = threading.Lock()
        self.state = None

        # blender memory is protected by this lock
        self.context_lock = threading.Lock()
        self.context = context

    def reset(self):
        self.state = {
            'tid': None,
            'state': None,
            'progress': None,
            'signal': None,
            'rc': None,
        }

    def start(self):
        self.reset()
        self.thread.start()

    def signal(self):
        """ react to user signals (0=RUN, 2=INTERRUPT, 15=EXIT)
        """
        with self.lock:
            if self.state['signal'] != 0:
                self.state.update(dict(state='DONE', rc=1, signal=None))
        return False

    def progress(self, part, total_parts):
        """ inform blender of our progress
        """
        with self.lock:
            self.state.update(dict(state='RUN', progress=part/total_parts))

    def main(self):
        """ your workload here
        """
        # get local ro state copy
        state = dict()
        with self.lock:
            state.update(self.state)

        self.enter_()
        
        with self.context_lock:
            scene_objects = [i.name for i in self.context.scene.objects]
            scene_objects_len = len(scene_objects)
            
        for idx, obj_name in enumerate(scene_objects):
            # check for user abort regularly, the thread is atomic between self.signal() calls
            # if the user requests quit, blender will hang until we react
            if self.signal(): return

            with self.context_lock:
                # your workload here
                obj = self.context.scene.objects[obj_name]
                print("%r" % ((self.state['tid'], obj),))
                
            # report progress
            self.progress(idx, scene_objects_len)
        self.exit_()

    def join(self, *args):
        self.thread.join(*args)

    def enter_(self):
        tid = threading.get_ident()
        with self.lock:
            self.state.update(dict(tid=tid, state='RUN', progress=0.0, signal=0))

    def exit_(self):
        with self.lock:
            self.state.update(dict(state='DONE', progress=1.0))

class ModalOperator(bpy.types.Operator):
    """Move an object with the mouse, example"""
    bl_idname = "object.modal_operator"
    bl_label = "Simple Modal Operator"

    singleton_p = [True]
    dirty_timer = None
    focus_reset = None

    @classmethod
    def poll(cls, context):
        return cls.singleton_p[0]

    def _enter(self, context):
        bpy.context.window.cursor_set("WAIT")
        self.focus_reset = self.focus()
        self.singleton_p[0] = False
        bpy.context.window_manager.modal_handler_add(self)
        self.dirty_timer = bpy.app.timers.register(self.dirty_interval, first_interval=2.0)
        self.thread = ThreadExample(context)
        self.thread.context_lock.__enter__()
        self.thread.start()

    def _exit(self):
        bpy.context.window.cursor_set("DEFAULT")
        for area, context in self.focus_reset:
            area.spaces.active.context = context
            self.focus_reset = None
        with self.thread.lock:
            self.thread.state['signal'] = 15
        self.singleton_p[0] = True
        if bpy.app.timers.is_registered(self.dirty_timer):
            bpy.app.timers.unregister(self.dirty_timer)

        self.thread.join()
        self.thread.reset()
        self._progress(self.thread.state)

    def cancel_(self):
        """ request thread exit
        """
        with self.thread.lock:
            self.thread.state['signal'] = 2

        self._exit()
        self.report({'WARNING'}, "operation stopping ...")
        return {'CANCELLED'}

    def finish_(self):
        """ thread finished
        """
        self._exit()
        self.thread.state['progress'] = 1.0
        self._progress(self.thread.state)
        return {'FINISHED'}

    def _progress(self, state):
        """ copy progress from thread to blender
        """
        if state['progress'] is not None:
            bpy.context.scene['modal_progress'] = state['progress']
        else:
            del bpy.context.scene['modal_progress']

    def dirty_interval(self):
        """ blender only redraws if something happened, the thread doesn't count
            so we artificially "do something" regularly, so the progress bar
            will get redrawn
        """
        try:
            state = dict()
            with self.thread.lock:
                state.update(self.thread.state)

            self._progress(state)
            self.dirty()

            return 1.0/12 # fps

        except ReferenceError:
            # modal finished, "self" is deallocated
            pass

        return None

    @staticmethod
    def get_screens():
        screens = bpy.data.screens
        if bpy.context.screen:
            screens = [bpy.context.screen]
        return screens

    @staticmethod
    def dirty(areas={'PROPERTIES'}, regions={'WINDOW'}):
        """ progress bar is drawn as a scene custom property
        """
        for screen in ModalOperator.get_screens():
            for area in screen.areas:
                for region in area.regions:
                    if area.type in areas and region.type in regions:
                        region.tag_redraw()

    def focus(context='SCENE'):
        """ try to make sure the progress bar is on screen
        """
        ret = []
        for screen in ModalOperator.get_screens():
            for area in screen.areas:
                if area.type != 'PROPERTIES': continue

                ret.append((area, area.spaces.active.context))
                area.spaces.active.context = 'SCENE'
        return ret


    def modal(self, context, event):
        # get read-only copy of thread state
        state = dict()
        with self.thread.lock:
            state.update(self.thread.state)

        # redraw progress indicator
        self.dirty()

        # check if user abort requested
        cancel = event.type in {'ESC'} #, 'RIGHTMOUSE'}
        if cancel:
            return self.cancel_()

        # check if thread finished
        if state['state'] == 'DONE':
            return self.finish_()

        assert self.thread.state['state'] == 'RUN', 'unhandled thread state %r' % self.thread.state['state']

        # report progress from thread to blender
        self._progress(state)
        
        # dedicated time for thread to read blender state
        self.thread.context_lock.__exit__()
        time.sleep(1/24)  # gui min 24 fps - threat read time
        self.thread.context_lock.__enter__()
        
        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self._enter(context)
        return {'RUNNING_MODAL'}

class ProgressPanel(bpy.types.Panel):
    """Progress Bar"""
    bl_label = "Progress Panel"
    bl_idname = "SCENE_PT_layout"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    # try to make sure the progress bar is on screen
    bl_options = {'HIDE_HEADER'}

    def draw(self, context):
        layout = self.layout

        if context.scene.modal_progress < 1.0:
            text = "Progress: %3.2f%% (ESC to cancel)" % (context.scene.modal_progress*100.0)
        else:
            text = "Progress: Done"
        layout.label(text=text)

        row = layout.row()
        row.prop(context.scene, "modal_progress")

def register():
    bpy.utils.register_class(ModalOperator)
    bpy.utils.register_class(ProgressPanel)
    bpy.types.Scene.modal_progress = bpy.props.FloatProperty(name="Modal Progress", min=0.0, max=1.0, subtype='FACTOR')

def unregister():
    bpy.utils.unregister_class(ModalOperator)
    bpy.utils.unregister_class(ProgressPanel)
    del bpy.types.Scene.modal_progress


if __name__ == "__main__":
    register()
    # test call
    bpy.ops.object.modal_operator('INVOKE_DEFAULT')

Mirror opencode.net: https://www.opencode.net/-/snippets/3378 Mirror archive.org: https://web.archive.org/web/20230705035959/https://www.opencode.net/-/snippets/3378/raw/main/blender_progress_bar.py


well, the progress state/status reporting is working, drawing the progressbar is trickier, possible future work

  • create a progress window (seems impossible currently)
  • to ensure progress bar in on screen, or reset the current window to a specific predictable gui element (possible, but, too invasive)
  • drawing a progress bar, with import gpu overtop of some existing ui element
  • find a way to place gui elements more concretely (with many plugins installs, this progress could be pushed off screen)
$\endgroup$
4
  • $\begingroup$ This doesn't work in 3.6.0 it just freezes until you press escape. Leaves a useless property in the Scene tab, Properties Editor... $\endgroup$ Commented Jul 5, 2023 at 6:39
  • $\begingroup$ Could simply use something like this $\endgroup$ Commented Jul 5, 2023 at 7:14
  • $\begingroup$ hmm, i just tried it on blender 3.6.0 (Linux) and has no issues, seemed to work as intended. you might be talking about blender's Modal mode where it doesn't let you intact with the UI while a modal operator is running, you can still however interrupt the background job by pressing escape, or bind more hotkeys if you like. i think that is what you are talking about @MartynasŽiemys - this method allows the ui to be updated/painted while the thread is running $\endgroup$ Commented Jul 8, 2023 at 21:56
  • $\begingroup$ hmm, i'll think on doing a non-modal version, that will break blender's undo feature, but some people might like to trade "blender being usable" for complicating their background blender<->thread edge cases/synchronization $\endgroup$ Commented Jul 8, 2023 at 22:00

You must log in to answer this question.

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