1
$\begingroup$

The operator work correctly when I call it. But report error when I open a new file.

bl_info = {
"name" : "Test",
"author" : "Public",
"version" : (1, 0),
"blender" : (2, 90, 0),
"location" : "View3d > Tool",
"category" : "3D View",
}

import bpy, blf

class Test_operator(bpy.types.Operator):
    bl_idname = "object.test_operator"
    bl_label = "test"

    @staticmethod
    def draw_callback_px(self, context):
        blf.size(1, 12, 72)
        blf.position(1, 50, 50, 0)
        blf.draw(1, self.s)

    def modal(self, context, event):
        "some code..."
        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        self.s = "hello world"

        args = (self, context)
        self._handle = bpy.types.SpaceView3D.draw_handler_add(
            Test_operator.draw_callback_px, args, 'WINDOW', 'POST_PIXEL'
        )
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

def register():
    bpy.utils.register_class(Test_operator)
def unregister():
    bpy.utils.unregister_class(Test_operator)

Traceback (most recent call last): File "C:\Users\user\AppData\Roaming\Blender Foundation\Blender\2.90\scripts\addons\testcallback.py", line 20, in draw_callback_px blf.draw(1, self.s) File "C:\Program Files\Blender Foundation\Blender 2.90\2.90\scripts\modules\bpy_types.py", line 717, in getattribute properties = StructRNA.path_resolve(self, "properties") ReferenceError: StructRNA of type Test_operator has been removed

How can I fix it? Thank you very much.

$\endgroup$

1 Answer 1

1
$\begingroup$

Operator is invalid, callback isn't removed

At issue here is the draw callback belonging to the space is persistent when another file is opened, whereas a modal operator is not. The solution below removes the draw callback if the operator becomes invalid.

Asked

Removing space draw callbacks after losing the handle?

with relation to having issues like in question. If the handle is attached to the operator and the operator is "lost" there is no way to remove the draw callback. Well no one has answered how to .

If you were to put in the line

print(repr(self))

into your draw call back method,

All going well, and run from text editor (main thread)

<class '__main__.Test_operator'>

if the operator is removed will see

<bpy_struct, Test_operator invalid>

Once this is the case error shown in question will begin. Without going into great details the "rna" is how blender objects store their properties under the hood. When the instance of the blender operator is no longer required its rna is trashed.

Using repr avoids using a try catch clause. Would be keen to find out if there is a nicer way to check for this.

As a workaround to resolve issue of lost handle, when using operator as an argument, and owner of properties to draw, could set up a class variable list of operators and handles. When

repr(op).endswith("invalid>")

is true, remove its callback via handle, remove from handles... and avoid the error message.

Test script. Have made a class method to check the validity, and validate the list.

import bpy, blf

class Test_operator(bpy.types.Operator):
    bl_idname = "object.test_operator"
    bl_label = "test"
    handles = []
    
    @classmethod
    def validate(cls):
        invalids = [(op, handle) for op, handle in cls.handles if  repr(op).endswith("invalid>")]
        valid = not(invalids)
        
        while invalids:
            op, handle = invalids.pop()
            bpy.types.SpaceView3D.draw_handler_remove(handle, 'WINDOW')
            cls.handles.remove((op, handle))
        return valid
            
            
                
    @staticmethod
    def draw_callback_px(self, context):
        font_id = 0  # XXX, need to find out how best to get this.

        # check handles
        if not Test_operator.validate():
            return

        blf.position(font_id, 15, 30, 0)
        blf.size(font_id, 50, 72) 
        blf.draw(font_id, self.s)
        
    def modal(self, context, event):
        "some code..."
        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        self.s = "hello"

        args = (self, context)
        self.handles.append((self,  bpy.types.SpaceView3D.draw_handler_add(
            Test_operator.draw_callback_px, args, 'WINDOW', 'POST_PIXEL'
        )))
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

def register():
    bpy.utils.register_class(Test_operator)
def unregister():
    bpy.utils.unregister_class(Test_operator)

if __name__ == "__main__":    
    register()

Window Manager

IMO would instead make a drawing class, similarly to link atop answer, or as a mixin class or as a wrapper for the callback.

Adding further to How do you remove a draw handler after it's been added? can leverage the window manager to keep a context reference to the draw handler classes.

Here is a quick cobble together by I think you can see where this is going. Can communicate with the draw callback via context from your operator when it is run again.

import bpy
from bpy.types import PropertyGroup
from bpy.props import PointerProperty
import blf

class DrawingClass:
    def __init__(self, context, prop):
        self.prop = prop
        self.handle = bpy.types.SpaceView3D.draw_handler_add(
                   self.draw_text_callback,(context,),
                   'WINDOW', 'POST_PIXEL')

    def draw_text_callback(self, context):
        font_id = 0  # XXX, need to find out how best to get this.

        # draw some text
        blf.position(font_id, 15, 50, 0)
        blf.size(font_id, 20, 72)
        blf.draw(font_id, "%s %s" % (context.scene.name, self.prop))

    def remove_handle(self):
         bpy.types.SpaceView3D.draw_handler_remove(self.handle, 'WINDOW')


class DrawCallBacks(PropertyGroup):
    _dc = []
    def add(self, cb):
        self._dc.append(cb)
        


def register(): 
    bpy.utils.register_class(DrawCallBacks)   
    bpy.types.WindowManager.callbacks = PointerProperty(type=DrawCallBacks)
    
if __name__ == "__main__":
    register()
    
    # test 
    wm = bpy.context.window_manager
    wm.callbacks.add(DrawingClass(bpy.context, "FoogleDoodle"))

Can open a new file go to console

>>> C.window_manager.callbacks._dc
[<__main__.DrawingClass object at 0x7fed5b609a30>]

>>> for cb in C.window_manager.callbacks._dc:
...     cb.prop
...     cb.prop = "MumbleGrunt"
...     
'FoogleDoodle'

and the callback prints "MumbleGrunt" to screen.

$\endgroup$

You must log in to answer this question.

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