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%.
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()