Getting/Setting "description" / Tool Tip value in Properties

I’m trying to use Blender PropertyGroup classes in the same way that I’d use regular Python classes … to encapsulate data and functionality.

For example, I might want a class that can represent two floating point values. The values might represent x,y coordinates, they might represent the length and width of a window, or they might represent the height and weight of a person. The same class should work for all of these cases. I’d also like it to be able to draw its own user interface. Here’s a simple class that can do that:

class TwoFloatGroup(bpy.types.PropertyGroup):
    v1 = FloatProperty ( name="Val1", description="Float Val 1", default=0.0 )
    v2 = FloatProperty ( name="Val2", description="Float Val 2", default=0.0 )
    def draw ( self, row ):
        row.column().prop(self, "v1")
        row.column().prop(self, "v2")

Now that I have this class, I’d like to be able to use it in dozens (or even hundreds) of places in my application. But for each instance, I’d like to be able to give it different values for things like name, description, and even default value. Does anyone know how this can be done?

The only way I know to create an instance of a PropertyGroup class is using a PointerProperty. But PointerProperties don’t provide a mechanism for setting the names, descriptions, and defaults for the Properties inside a PropertyGroup. I don’t want to have to create a new PropertyGroup for every possible instance that I create. Any ideas?

For reference, I’ve attached a complete addon example. It produces this output:


As you can see, the labels on the values are the “generic” default names in the class, and the same is true of the Tool Tip help message (“Float Val 1”). I’d like to be able to set these to more meaningful values in my addon, and that’s the heart of my question. Thanks in advance.

"""
This program explores setting of Properties in Property Groups
"""

bl_info = {
  "version": "0.1",
  "name": "Changing Name and/or Description",
  'author': 'BlenderHawk',
  "location": "Properties > Scene",
  "category": "Blender Experiments"
  }

import bpy
from bpy.props import *


class TwoFloatGroup(bpy.types.PropertyGroup):
    v1 = FloatProperty ( name="Val1", description="Float Val 1", default=0.0 )
    v2 = FloatProperty ( name="Val2", description="Float Val 2", default=0.0 )
    def draw ( self, row ):
        row.column().prop(self, "v1")
        row.column().prop(self, "v2")


class APP_PT_Test_Props(bpy.types.Panel):
    bl_label = "Examples: (x,y), (length,width) and (height,weight)"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "scene"

    def draw(self, context):
        context.scene.x_y.draw(self.layout.row())
        context.scene.length_width.draw(self.layout.row())
        context.scene.height_weight.draw(self.layout.row())


def register():
    print ("Registering ", __name__)
    bpy.utils.register_module(__name__)
    bpy.types.Scene.x_y           = bpy.props.PointerProperty(type=TwoFloatGroup,description="x,y")
    bpy.types.Scene.length_width  = bpy.props.PointerProperty(type=TwoFloatGroup,description="l,w")
    bpy.types.Scene.height_weight = bpy.props.PointerProperty(type=TwoFloatGroup,description="h,w")

def unregister():
    print ("Unregistering ", __name__)
    del bpy.types.Scene.height_weight
    del bpy.types.Scene.length_width
    del bpy.types.Scene.x_y
    bpy.utils.unregister_module(__name__)


if __name__ == "__main__":
    register()

Hi

As alluded to in a previous post you can register classes on the fly (for most part) in blender. Especially the properties as they are basically for UI and data decorating.

Here is a simple example of rereg’ing the PropGroup class and then any class that uses it.

When the value of v2 is updated it changes the description accordingly. Notice how the update can be used as a “pseudo” class here.

Once again I suggest you set up your data model as a dictionary, then you can register / unregister classes for UI manipulation on the fly.


"""
This program explores setting of Properties in Property Groups
"""


bl_info = {
  "version": "0.1",
  "name": "Changing Name and/or Description",
  'author': 'BlenderHawk',
  "location": "Properties > Scene",
  "category": "Blender Experiments"
  }


import bpy
from bpy.props import *


def draw ( self, row ):
    row.column().prop(self, "v1")
    row.column().prop(self, "v2")




def blonk(self, context):
    def desc(key):
        return "(CHANGED) %s : %5.2f" % (key, getattr(self, key, 0.00))
     #print(self, type(self))
    
    dic = {'v1': FloatProperty ( name="Val1", description=desc('v1'), default=0.0 ),
           'v2': FloatProperty ( name="Val2", description=desc('v2'), update=blonk),
           'draw': draw,
           }
    <b>TwoFloatGroup = type('TwoFloatGroup', (bpy.types.PropertyGroup, ), dic)</b>
    if hasattr(bpy.types, "TwoFloatGroup"):
        bpy.utils.unregister_class(bpy.types.TwoFloatGroup)
    bpy.utils.register_class(TwoFloatGroup)
    
    # reregister the types involved
    
    bpy.types.Scene.x_y           = bpy.props.PointerProperty(type=TwoFloatGroup,description="x,y")
    bpy.types.Scene.length_width  = bpy.props.PointerProperty(type=TwoFloatGroup,description="l,w")
    bpy.types.Scene.height_weight = bpy.props.PointerProperty(type=TwoFloatGroup,description="h,w")
    


    
class TwoFloatGroup(bpy.types.PropertyGroup):
    v1 = FloatProperty ( name="Val1", description="Float Val 1", default=0.0 )
    v2 = FloatProperty ( name="Val2", description="Float Val 2", default=0.0, update=blonk)
    def draw ( self, row ):
        row.column().prop(self, "v1")
        row.column().prop(self, "v2")




class APP_PT_Test_Props(bpy.types.Panel):
    bl_label = "Examples: (x,y), (length,width) and (height,weight)"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "scene"


    def draw(self, context):
        context.scene.x_y.draw(self.layout.row())
        context.scene.length_width.draw(self.layout.row())
        context.scene.height_weight.draw(self.layout.row())




def register():
    print ("Registering ", __name__)
    bpy.utils.register_module(__name__)
    bpy.types.Scene.x_y           = bpy.props.PointerProperty(type=TwoFloatGroup,description="x,y")
    bpy.types.Scene.length_width  = bpy.props.PointerProperty(type=TwoFloatGroup,description="l,w")
    bpy.types.Scene.height_weight = bpy.props.PointerProperty(type=TwoFloatGroup,description="h,w")


def unregister():
    print ("Unregistering ", __name__)
    del bpy.types.Scene.height_weight
    del bpy.types.Scene.length_width
    del bpy.types.Scene.x_y
    bpy.utils.unregister_module(__name__)




if __name__ == "__main__":
    register()

Hello batFINGER,

I’ve finally dug into this enough to understand your example a bit better. Creating classes on the fly is powerful, but it’s still somewhat new to me!

Following your example, I was able to mostly get the effect that I wanted:


Each of those rows is a dynamically generated “TwoFloatGroup” under different names and with different descriptions. The first two represent the “x,y” and “length,width” examples (separate classes generated), and the last three are all “height,weight” examples which share the same dynamically generated class. The trickiest part was using locals()[class_name] for the dynamically generated types which were needed for registration and for creating the PointerProperty returned.

There are two things that I still haven’t been able to figure out.

First, I could not get any of those dynamically generated PropertyGroups to display themselves without putting in the “junk” references first (junk=str(type(context.scene.propgroup))). Any ideas on why that was needed? Is there a better way to force them into existence?

Second, I found that the last setting of the defaults was applied to all of the properties created of the same type. So in my example, the creation of bpy.types.Scene.patient3 with height of 5.11 and weight of 200 caused those defaults to be used for all of the other objects which were of type App_HW_Coord. I suppose that gives an insight into how Blender handles the creation of objects from registered types. If you have any thoughts on that, they’d be welcome as well.

Thanks again for providing that code. It opens up a lot of possibilities.

P.S. Here’s my current code (sorry, not PEP8 compliant):

"""
This program explores dynamic creation of Property Groups
"""


bl_info = {
  "version": "0.011",
  "name": "More Changing Name and/or Description 011",
  'author': 'batFINGER, BlenderHawkBob',
  "location": "Properties &gt; Scene",
  "category": "Blender Experiments"
  }


import bpy
from bpy.props import *


def draw ( self, row ):
    row.column().prop(self, "v1")
    row.column().prop(self, "v2")


def new_two_float_group ( class_name, v1_name, v1_desc, v1_default, v2_name, v2_desc, v2_default ):

    dic = {'v1': FloatProperty ( name=v1_name, description=v1_desc, default=v1_default ),
           'v2': FloatProperty ( name=v2_name, description=v2_desc, default=v2_default ),
           'draw': draw,
           }

    locals()[class_name] = type(class_name, (bpy.types.PropertyGroup, ), dic)

    if hasattr(bpy.types, class_name):
        print ( "Class name [" + class_name + "] found, unregistering first" )
        bpy.utils.unregister_class( eval("bpy.types." + class_name) )
    else:
        print ( "Class name [" + class_name + "] not found" )
        
    # Register or re-register the type
    bpy.utils.register_class(locals()[class_name])
    
    return bpy.props.PointerProperty(type=locals()[class_name])
    


class PP_OT_init_app(bpy.types.Operator):
    bl_idname = "app.init"
    bl_label = "Init App"
    bl_description = "Initialize Application"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        print ( "Initialize Application" )

        bpy.types.Scene.x_y = new_two_float_group ("App_XY_Coord", "x", "x coordinate of point", 1, "y", "y coordinate of point", 2)
        junk = str(type(context.scene.x_y))  # Some reference needs to be made to get these to display ... don't know why???

        bpy.types.Scene.l_w = new_two_float_group ("App_LW_Coord", "Length", "Length of Selection", 1, "Width", "Width of Selection", 1)
        junk = str(type(context.scene.l_w))  # Some reference needs to be made to get these to display ... don't know why???

        bpy.types.Scene.patient1 = new_two_float_group ("App_HW_Coord", "Height", "Height of Patient", 5.10, "Weight", "Weight of Patient", 165.0)
        context.scene.patient1.v1 = 5.10
        context.scene.patient1.v2 = 165

        bpy.types.Scene.patient2 = new_two_float_group ("App_HW_Coord", "Height", "Height of Patient", 5.9, "Weight", "Weight of Patient", 155.0)
        junk = str(type(context.scene.patient2.v1))  # Some reference needs to be made to get these to display ... don't know why???

        bpy.types.Scene.patient3 = new_two_float_group ("App_HW_Coord", "Height", "Height of Patient", 5.11, "Weight", "Weight of Patient", 200.0)
        junk = str(type(context.scene.patient3))  # Some reference needs to be made to get these to display ... don't know why???
        
        return {'FINISHED'}


class APP_PT_Test_Props(bpy.types.Panel):
    bl_label = "2 Float Examples: (x,y), (length,width) and (height,weight)"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "scene"


    def draw(self, context):
        row = self.layout.row()
        row.operator ( "app.init", text="Initialize" )
        if "x_y" in context.scene:
            context.scene.x_y.draw(self.layout.row())
        if "l_w" in context.scene:
            context.scene.l_w.draw(self.layout.row())
        if "patient1" in context.scene:
            context.scene.patient1.draw(self.layout.row())
        if "patient2" in context.scene:
            context.scene.patient2.draw(self.layout.row())
        if "patient3" in context.scene:
            context.scene.patient3.draw(self.layout.row())


def register():
    print ("Registering ", __name__)
    bpy.utils.register_module(__name__)


def unregister():
    print ("Unregistering ", __name__)
    del bpy.types.Scene.height_weight
    del bpy.types.Scene.length_width
    del bpy.types.Scene.x_y
    bpy.utils.unregister_module(__name__)


if __name__ == "__main__":
    register()

Please delete this post.

Hi Bob,

In a previous post we discussed unitialised registered types. If the type is registered its name will be a member of object.bl_rna.properties.keys(), where object is the type of object that the prop is defined on, in this case the scene object. Once it has been initialised it will also be in object.keys(). (keys gives a list of the property names, better practice to use this than name in object IMO)


&gt;&gt;&gt; "patient2" in C.scene
False


&gt;&gt;&gt; "patient2" in C.scene.bl_rna.properties.keys()
True

Test this in your panel draw, and you can take out the “junk”

If you give the classes the same name and access them via locals()[same_name] you’d expect to get the values of the last one registered wouldn’t you?

I’m still a bit confused by what you want to do here, If you want a patient class, define it once, then set the values like height and weight on each instance. (Collection would be the go) To have a customized class per patient, with differing default values, you will need to create a unique class for each.