Solved Problem Adding description parameter by clicking button and access this

currently I am making an object plugin.
At the moment I am not sure how to solve certain things.

Screenshot 2023-02-13 232733.png ![Bild Text](Bild Link)

As you can see on the figure above, the plugin has only a few parameters in its description. It is a res.file , header.file .....
It creates buildings.
From a password protected zipfile with the models it reads the folders and add them dynamically to the cycle parameter (dropdown menu).
After you load the plugin it creates also an basement, you can also choose the type of the basement. Each building type has 2 basement types, 2 level types and 2 top types.

So now I need to add a level, when I press the "Add Level " button (ID = c4d.PY_ADD_LEVEL).
At the moment when I press the button I just add the model files.
the model structure happens in a second document. Then I return a clone of the model tree
I create a second document and add for instance the level model which corresponds to the building type for the Cycle Menu

What I need is I want to create a parameter like the Basement Type parameter on the figure. A simple Quicktabradio with two entries.
So the user clicks the "Add Level" Button and such a Entry with DESC_NAME "Level_0" will be added below
If the user clicks the button again another parameter will be added.
If the user clicks delete level the last level parameter will be deleted

I roughly know how to do this but the problem is....the button sends a message to node.Message().
I can catch the message but how to create the parameter in the description because I thought parameters must be created in node.GetDDescription() or may I add a parameter also in the node.Message() method or in an extra method in my class?

Also I am not sure what is better, first to create the parameter in the description and due to this add the model,
or first create the model and then read the models in the model tree and add then the parameters.

I also want that the user is able to switch for instance the level type of level 3 for example and it loads another level type just for this third level.

Best Regards

you are totally right; the UI should be created in the function GetDDescription.
You should create some internal data structure that will store the status of your building. There are multiple ways of doing it depending on how you want to manage it and the purpose of the plugin. I cannot answer that question for you.

The idea is the following. When you click the add level button, the Message function will update that internal data by adding or removing levels. GetDDescription would take care of updating the UI depending on those data. GetVirtualObject will take care of creating the cache of the object also based on those Data.

You have different choices to define your data structure.

You can think that you will always have a basement and a roof so only storing the number of levels will be enough. In that case, in GetDDescription you will create this number of levels. For each level you may need many parameters (type, size, texture). You need to create those parameters and calculate their DescID. For example, the first level will start at the ID 2000, the next level will have 2100 etc... that will give you 99 parameters per level. Add more if you want.
The parameters values are stored (most of the time) in the BaseContainer so be careful, you need to "clean" the BaseContainer each time you delete a level, otherwise it will reload the previous values. If you store the number of levels in the BaseContainer of the object, C4D will take care of saving everything or copy everything if you duplicate the object. The major issue with this idea is that it will be a bit difficult if you want to change the levels order.

Now you may want to store an array of levels instead so you can change the order. You will store a structure of datas in this array where you will store each parameter of your level. Now you must override the function GetDParameter/SetDParameter so the UI can display or update the parameter and store the value at the right place. If you do not store the data inside the BaseContainer you must override the function Read/Write/CopyTo so the data will be loaded with the document, saved with it, and copied when you duplicate the object. To display and change the order of level you can add an up and down button or re-create a tree displaying the levels (creating this kind of UI is a lot more work).

You can also, as we did with the Character Object (menu Character - > Character), create different objects for each level so the user can organize the hierarchy as he wants. Those objects must be created in the Message function, you can have a look at this thread where Ferdinand is talking about it.


MAXON SDK Specialist

MAXON Registered Developer

First of all, thank you for the detailed description, that's how I thought about it. I'll try to implement it like this. If I have another question about this, I'll add something. Best regards

I solved it like this now:

I create the levels with null objects which get their own icon. For each of these levels, I add a Quicktab Userdata Entry that lets you choose the type of level.
In my node, the user can now click on refresh and internally he goes through this hierarchy, checks the user data and then creates the geometry and returns the model structure.
In contrast to the character object, I want to avoid the user having access to the models.

But is it also possible instead of creating Null-Objects to create another ObjectPlugin with its description? In this case the description is just a simple Quicktabradio with 2 buttons.
I see this in X-Particles where there is just one plugin file but in the res-folder there are all res-files for other object-plugins.
I do not now how to do this....
This would be better because the plugin should also work with R19 and there you can´t give a Null-Object a custom icon.
So the "Add New Level" button should call the new ObjectPlugin with it´s ID and insert it under the Node.


The same way you created your first ObjectData there is nothing blocking you from creating another, the same way on the same directory. Then, instead of calling nullObject = c4d.BaseObject(c4d.Onull) call myObject = c4d.BaseObject(123456) where 123456 will be the pluginID of the new second ObjectData . You can change the parameters on that object just like any kind of object like so myObject[foo] = 42


MAXON SDK Specialist

MAXON Registered Developer

This post is deleted!

ok thanks,
Yes I added a second class into the .pyp file and registered a second object-plugin

Best Regards


You can also create this new objectData in a new file.

Remember that you cannot alter the document in the GetVirtualObject function. This function is not executed in the main thread. So, another object could already be reading the document.

As i said in this thread Ferdinand "demonstrates a pattern to safely modify the scene graph from a generator object'.


MAXON SDK Specialist

MAXON Registered Developer

Do you mean just the active document or also a virtual document which is not alive?
I do not exactly know if that is wrong but when the user starts the plugin for the first time, a layer is created and the node is set to this layer, the layer is saved in an hidden description Baselink and a property which was False in the init function turns to True, and also another hidden LONG Parameter turns to 1,
this if statement happens just one time in the GVO method...

or can I call functions also in the node.Init() method? because I need to get op and doc
The problem is I can´t get the active document from my _init_() method.
when i write

self.doc = c4d.documents.GetActiveDocument()

he says, document not alive.
And when I use it from outer scope that also doesn´t work.
But the plugin works pretty my opinion. Even if you delete the layer, the next time you press the "Refresh" button he adds again this layer and put all related materials back into this layer....

I would trigger my Refresh button in my node's description but then he complains "just from the main thread".

This is true for every programming language. When you have two processes accessing the same data, be careful of what you are doing. Just imagine you are looking for a book and at the same time someone else is randomly moving all the book at the same time.

So, accessing your own document, if there is only one process accessing it, it should be ok to do it inside GVO.

When you object is added in the object manager, or more precisely, when the command is called from the menu, MSG_MENUPREPARE is sent to the object. If you react to this message in the Message function of the plugin, you can create the layer at that moment. The Message function is (almost) never call outside the main thread.
Your refresh button is working better because you are reacting to the button pushed in the Message function. That is a safe place to update the scene.

To be sure your function is called from the mainThread, you can use c4d.threading.GeIsMainThread()

It is hard to predict all scenario, in some, you will have no issue in other your plugin will break fast and crash c4d.


MAXON SDK Specialist

MAXON Registered Developer

sorry manuel for another question depending this and probably annoying you.
so I want to go the save way:
so in my quicktabradio for the basement type , the user can choose between 3 different types.
At the moment the base will cloned and inserted in the virtual document under a null....I can change this so that this is not happening in the GVO method.
But I need to catch the message for the parameter change in the Quicktabradio for the basetype

I tried:

            # for key in data:
            #     print("key:", key)
            #     print("val:", data[key])

            if data['descid'][0] == c4d.PY_BASE_TYPE:

nothing happens

when I catch a button I catch id with:

        if type == c4d.MSG_DESCRIPTION_COMMAND:
            if data['id'][0].id == c4d.PY_ADD_LEVEL:

this works.
so I printed out the dict and get the key "descid" but this doesn't work, how can I catch this Quicktabradio change in my description in the message method?

Best Regards


Screenshot 2023-02-25 202007.png
So I do not really know how to catch this Quicktabradio switch correctly. At the moment I have this in my level object Message() method:
I tried to catch the parameter "PY_LEVEL_TYPE" of the Level-Plugin and I also tried to catch the MainThread. I don´t know if this is correct :grin:

        def Message(self, node, type, data):    
            parent = node.GetUp()    
            if type == c4d.MSG_DESCRIPTION_POSTSETPARAMETER:    
                if data["descid"][0].id == c4d.PY_LEVEL_TYPE:    
                    if c4d.threading.GeIsMainThread():                        
                        if parent != None:    
                            c4d.CallButton(parent, c4d.PY_REFRESH) 

it works but prints out some errors in the console:

AttributeError:'function' object has no attribute 'im_func'

I just want, when I switch the Quicktabradio, that the citybuilding-plugins "Refresh" Button is called or to send a Message to the CityBuildings plugin and catch this message and then do something.

Best Regards


We like to have different thread for different question even if it is related to the same project. I forked that thread. The question regarding the license will be discussed on this thread

@thomasb said in Problem Adding description parameter by clicking button and access this:

it works, I'd just forgotten to return True in my Message method of the Level-Object

It is sometimes a bit hard to follow people's project specially when we are bending a bit the rules. I am glad it is working.


MAXON SDK Specialist

MAXON Registered Developer

@Manuel said in Problem Adding description parameter by clicking button and access this:


The parameters values are stored (most of the time) in the BaseContainer so be careful, you need to "clean" the BaseContainer each time you delete a level, otherwise it will reload the previous values.



Sorry Manuel ,
it took a while to work out a few things, so now I am ready for your answer.
So here is an example plugin, I made it to create description elements by clicking a button. So this example is better for the answer you hopefully can give me.

Screenshot 2023-05-11 012148.png
I keeped track of the ID`s as you recommended in an array.
So when the user clicks the button Add Measurement a new ID will be created in the array. And accordingly to that, a Group with this ID will be created in the description and a Baselink, a real and a Delete Button will be added to the group. Just for example.
I have also overwritten Read(), Write() and Copy() methods.

And when the user clicks "Delete", the ID will be deleted from the array and so the group and it's parameters dissapear.
So when the user clicks again "Add Measurement" then it searches the array and the next smallest ID that is not in the array is used again. To save some ID`s :v: . God saves the ID's
But since this ID already was in the description. It is not empty as you can hardly see in the small video example.

You told me something about cleaning the BaseContainer?

How can I do that?
Here is the code, I think the res file is not necessary for the two buttons in the resfile:

Thank you in advance

import os
import sys
import c4d
from c4d import plugins, bitmaps
import os
import copy
import math

PLUGIN_ID = 1234596549

class Supi_Object(plugins.ObjectData):

    def __init__(self):
        # self.current_id = 10000
        self.id_list = []

    def Init(self, op):

        return True

    def Read(self, node, hf, level):
        count = hf.ReadInt32()
        for index in range(count):
            value = hf.ReadInt32()

        return True

    def Write(self, node, hf,):

        count = len(self.id_list)

        for desc_id in self.id_list:

        return True

    def CopyTo(self, dest, snode, dnode, flags, trn):

        dest.id_list = copy.copy(self.id_list)

        return True

    def GetVirtualObjects(self, op, hh):
        # dirty = True if a cache is dirty or if the data (any parameters) of the object changed.
        # If nothing changed and a cache is present, return the cache

        # cache deaktiveren

        return c4d.BaseObject(c4d.Osplinetext)

    def GetDDescription(self, op, description, flags):

        if not description.LoadDescription(op.GetType()):
            return False

        # Get description single ID
        singleID = description.GetSingleDescID()

        # Check if dynamic group needs to be added
        bc_group = c4d.GetCustomDataTypeDefault(c4d.DTYPE_GROUP)
        bc_group.SetBool(c4d.DESC_GUIOPEN, True)

        # Baselink for Measure
        bc_baselink = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BASELISTLINK)

        # Declare REAL parameter container
        bc_real = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
        bc_real.SetInt32(c4d.DESC_CUSTOMGUI, c4d.CUSTOMGUI_REALSLIDER)
        bc_real.SetFloat(c4d.DESC_MIN, 0.0)
        bc_real.SetFloat(c4d.DESC_MAX, 1.0)
        bc_real.SetFloat(c4d.DESC_MINSLIDER, 0.0)
        bc_real.SetFloat(c4d.DESC_MAXSLIDER, 1.0)
        bc_real.SetFloat(c4d.DESC_STEP, 0.01)
        bc_real.SetInt32(c4d.DESC_UNIT, c4d.DESC_UNIT_FLOAT)
        bc_real.SetInt32(c4d.DESC_ANIMATE, c4d.DESC_ANIMATE_ON)
        bc_real.SetBool(c4d.DESC_REMOVEABLE, False)

        # Declare Delete Button
        bc_button = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BUTTON)
        # bc_button[c4d.DESC_NAME] = "Delete"

        id_counter = 1
        for desc_id in self.id_list:
            # if child.GetType() == 200000082:
            main_group = c4d.DescID(c4d.DescLevel(desc_id, c4d.DTYPE_GROUP, 0))

            pm_real = c4d.DescID(c4d.DescLevel(desc_id + id_counter, c4d.DTYPE_REAL, op.GetType()))
            id_counter += 1

            measure_link = c4d.DescID(c4d.DescLevel(desc_id + id_counter, c4d.DTYPE_BASELISTLINK, op.GetType()))
            id_counter += 1

            button_delete = c4d.DescID(c4d.DescLevel(desc_id + id_counter, c4d.DTYPE_BUTTON, op.GetType()))
            id_counter = 1

            group_check = singleID is None
            if not group_check:
                group_check = main_group.IsPartOf(singleID)[0]

            if group_check:
                bc_group.SetString(c4d.DESC_NAME, "Messung" + str(desc_id))
                if not description.SetParameter(main_group, bc_group,
                    return False

            pm_check = singleID is None
            if not pm_check:
                pm_check = pm_real.IsPartOf(singleID)[0]

            if pm_check:
                name = "Dynamic REAL "
                bc_real.SetString(c4d.DESC_NAME, name)
                bc_real.SetString(c4d.DESC_SHORT_NAME, name)
                if not description.SetParameter(pm_real, bc_real, main_group):
                    return False

            pm_check = singleID is None
            if not pm_check:
                pm_check = measure_link.IsPartOf(singleID)[0]

            if pm_check:
                bc_baselink.SetString(c4d.DESC_NAME, "Measure-Object")
                if not description.SetParameter(measure_link, bc_baselink, main_group):
                    return False

            pm_check = singleID is None
            if not pm_check:
                pm_check = button_delete.IsPartOf(singleID)[0]

            if pm_check:
                bc_button.SetString(c4d.DESC_NAME, "Delete")
                bc_button.SetString(c4d.DESC_SHORT_NAME, "Delete")
                bc_button.SetInt32(c4d.DESC_CUSTOMGUI, c4d.CUSTOMGUI_BUTTON)
                if not description.SetParameter(button_delete, bc_button, main_group):
                    return False

        return True, flags | c4d.DESCFLAGS_DESC_LOADED

    def Message(self, op, typo, data):
        if typo == c4d.MSG_DESCRIPTION_COMMAND:
            if data["id"][0].id == c4d.PY_ADD_MEASUREMENT:
                start = 10000
                while start in self.id_list:
                    start += 100

                # self.id_list.append(self.current_id)
                # self.current_id += 100

                if c4d.threading.GeIsMainThread():
                    tool = plugins.FindPlugin(op.GetDocument().GetAction(), c4d.PLUGINTYPE_TOOL)
                if tool:
                    objects = op.GetDocument().GetObjects()

                    c4d.CallButton(tool, c4d.MDATA_MEASURE_NEW)
                    c4d.CallButton(tool, c4d.MDATA_MEASURE_CREATEOBJECT)
                    for obj in op.GetDocument().GetObjects():
                        if obj not in objects:


            elif data["id"][0].id == c4d.PY_ADD_TOOL:

                if c4d.threading.GeIsMainThread():

            elif data["id"][0].id - 3 in self.id_list:
                desc_id = data["id"][0].id - 3

        return True

if __name__ == "__main__":

    path, file = os.path.split(__file__)
    file = "icon.tif"
    new_path = os.path.join(path, "res", file)
    bitmap = bitmaps.BaseBitmap()
    plugins.RegisterObjectPlugin(id=PLUGIN_ID, str="DM-Measure 2023", g=Supi_Object, description="supi_object",

Hey @ThomasB,

I would not reuse the identifiers of previously removed parameters (when avoidable) as this does violate the Cinema 4D design pattern that parameter identifiers express a purpose. I.e., a parameter Foo with the ID 1000 should not be relabeled as the parameter Bar (with also the ID 1000) . Which is effectively what you are doing here.

As you have discovered yourself, you will then run into value history problems. Other things that can cause problems are undo states that are inexplicable for the user and the preset system.

C4DAtom parameter identifiers below the value 1,000 are reserved for Cinema 4D. Plugin identifiers start at the value 1,000,000, we should not go above this value because there could be other plugins which write data under such plugin ID into the data container of our node. Static parameter identifiers for a plugin, i.e., what you define in a header file, usually do not exceed the value 20,000. Which means that we have 980,000 identifier slots left. There is no need for any house keeping with our identifiers, even when we reserve them in blocks.

A good pattern to setup header files for dynamic descriptions is this:

// Header for Oexample object hook description.

#ifndef _OEXAMPLE_H__
#define _OEXAMPLE_H__

  // The identifiers for static parameters.
  // ...

  // This just a normal enum, we can define here whatever we want, including values that are not 
  // referenced in the res or str files.

  // These values express the range in which dynamic IDs can be found, i.e., the lowest ID can
  // be 10,000 and the highest 20,000. Your plugin has still to conform to that, but it is a good
  // idea to express such information in the header file of the resource.

  // The stride with which "logical parameter blocks" are placed. By just reading the header file
  // we now know that there are 10,000 dynamic IDs going from 10,000 to 20,000, placed with a stride
  // of 100, resulting in up to 100 dynamic parameter groups. When this is not enough, you can easily
  // also set #ID_EXAMPLE_DYNAMIC_IDS_END to 200,000 to have 1000 item groups. Or also make your 
  // stride smaller. A value of 100 is probably a bit wasteful for a use case of only a handful of
  // parameters.


#endif // _OEXAMPLE_H__

You should also remember to initialize your dynamically added parameters. Which you do not do at the moment in GetDDescription. Doing this will get rid of the problem of 'lingering' values with your current design, but the other problems would remain.


MAXON SDK Specialist

@ferdinand said in Problem Adding description parameter by clicking button and access this:

Plugin identifiers start at the value 1,000,000,

you probably meant end or?

ok this was my first idea, but my ids starting from 10000
Manuel just said above that you have to empty the BaseContainer, so thought not a bad idea at all

can you still tell me how to empty the container?
if not also ok
I mean I certainly need that in other situations too.

The user deletes the ID, all parameters in the group must be reset.

Have a nice day.

Hi @ThomasB,

you probably meant end or?

No, I meant start. The upper limit of plugin IDs, i.e., "end", is simply +2147483648, the maximum value a signed 32 bit integer can take. The largest plugin ID assigned at the time of writing is 1061082:


Internally, there are exceptions and Cinema 4D ships with some plugins with an ID below 1,000,000 but we can ignore them. The reason why we must respect plugin IDs is that they can also be used as a globally unique address for data containers. I.e., someone could register ID_MY_DATA: int = 1061083 to write his or her data into the data container of a node implemented by you. The plugin ID is then meant to ensure that your plugin does not accidently overwrite that data. From which follows that parameter identifiers should not exceed the value 999,999.

can you still tell me how to empty the container?

You are not really meant to delete a value of a node, you are only meant to modify the data model, i.e., add, remove, or modify parameters. You can call BaseContainer.RemoveData on the data container of a node to remove an entry. But that is not really deleting the value (history).

As stated before, setting a default value once a "new" parameter has been created will fix your problem, even with your current design. No need for deletion. I am not quite sure why Manuel did recommend this pattern, only he can explain that.


MAXON SDK Specialist


Ah plugin ID. Sorry, I thought you meant the IDs for the description parameters. Yes, PluginID. Yes, I didn't take that into account when writing the example code

Ok, yes, that would be the best solution to add new unused ids for dynamic ids. I will definitely do that.
But if the user would press a dynamically created reset button, could I use BaseContainer.FlushAll() to only reset the values in that ID group?
Or do I have to use the SetParameter() Method for all parameters.

So if the user has made some settings that don't quite fit and he wants to reset all dynamic parameters in the dynamic group... So they have to be set to the default value.

Best Regards

@ThomasB said in Problem Adding description parameter by clicking button and access this:

You told me something about cleaning the BaseContainer?
2023-05-11 01-11-45.mp4

hi, that is exactly why i was talking about cleaning the BaseContainer, or to initialise correctly the values. It is even worse if you mix datatype.

I was thinking of the morph tag and the way he does add or remove morph target using the same IDs.

And of course, removing data that you are not using anymore is a good idea.


MAXON SDK Specialist

MAXON Registered Developer

and how do I initialise the parameter if the user presses for instance reset.
With Atom.SetParameter(), right?