SOLVED Convenience function for Combobox

Hello,

Thanks for looking into this! I'm attempting to create a script that adds User Data. Below is a snippet from the larger script, which uses convenience functions for Real, Integer, and Vector user data.

I'm interested to see if there's a convenience function for the combobox user data (CUSTOMGUI_CYCLE), since the script will add multiple comboboxes to the user data container. I've searched high and low for this without any luck -- admittedly, I'm new to python and not exactly sure how "high" and "low" I'm actually searching.

The solution would clean up the code, as well as give me some more insight into using python in C4D. Thanks much for your time. Any help is greatly appreciated!

Thanks,

Eric

import c4d
from c4d import gui
#Welcome to the world of Python
 
def CreateUserDataGroup(obj, name, parentGroup=None, columns=None, shortname=None):
    if obj is None: return False
    if shortname is None: shortname = name
    bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_GROUP)
    bc[c4d.DESC_NAME] = name
    bc[c4d.DESC_SHORT_NAME] = shortname
    bc[c4d.DESC_TITLEBAR] = 1
    bc[c4d.DESC_GUIOPEN] = 1
    if parentGroup is not None:
        bc[c4d.DESC_PARENTGROUP] = parentGroup
    if columns is not None:
        #DESC_COLUMNS VALUE IS WRONG IN 15.057 - SHOULD BE 22
        bc[22] = columns
          
    return obj.AddUserData(bc) 
  
def CreateUserDataFloat(obj, name, val=0, parentGroup=None, unit=c4d.DESC_UNIT_REAL, sliderMin = 0, sliderMax = 0, step = 0):
    if obj is None: return False
    bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_REAL)
    bc[c4d.DESC_NAME] = name
    bc[c4d.DESC_SHORT_NAME] = name
    bc[c4d.DESC_DEFAULT] = val
    bc[c4d.DESC_ANIMATE] = c4d.DESC_ANIMATE_ON
    bc[c4d.DESC_UNIT] = unit
    bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_REALSLIDER
    bc[c4d.DESC_MINSLIDER] = sliderMin
    bc[c4d.DESC_MAXSLIDER] = sliderMax
    bc[c4d.DESC_MAX] = sliderMax
    bc[c4d.DESC_MIN] = sliderMin
    bc[c4d.DESC_STEP] = step      
    if parentGroup is not None:
        bc[c4d.DESC_PARENTGROUP] = parentGroup
  
    element = obj.AddUserData(bc)
    obj[element] = val
    return element


def main():
        
    for op in doc.GetActiveObjects(0):
        if (len(op.GetUserDataContainer()) == 0):

            #creates user data
            layerGroup = CreateUserDataGroup(op,"Shader Controls",c4d.DescID(0))

            #SHADER subgroup
            subGroup2 = CreateUserDataGroup(op,"Shader",layerGroup,1)
            
            #NOISE combobox
            noiseType = c4d.GetCustomDataTypeDefault(c4d.DTYPE_LONG)
            noiseType[c4d.DESC_PARENTGROUP] = subGroup2
            noiseType[c4d.DESC_NAME] = "Noise Type"
            noiseType[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_CYCLE
            names = c4d.BaseContainer()
            names.SetString(0,"Turbulence")
            names.SetString(1,"Fractal")
            names.SetString(2,"Cell")
            noiseType.SetContainer(c4d.DESC_CYCLE, names)
            entries = op.AddUserData(noiseType)
            op[entries] = 0
            c4d.SendCoreMessage(c4d.COREMSG_CINEMA, c4d.BaseContainer(c4d.COREMSG_CINEMA_FORCE_AM_UPDATE))
            
            noiseScale = CreateUserDataFloat(op,"Noise Scale",.5,subGroup2, c4d.DESC_UNIT_FLOAT, 0, 1, .001)
            
            c4d.EventAdd()

if __name__=='__main__':
    main()

Hi @saputello2,

The pythonic way would be to use *args and **kwargs, for more information, please read *args and **kwargs in python explained.
This will give us something like.

def CreateUserDataCycle(obj, name, val, *argv, **kargs):
    if obj is None: return False        
    bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_LONG)
    bc[c4d.DESC_NAME] = name
    bc[c4d.DESC_SHORT_NAME] = name
    bc[c4d.DESC_DEFAULT] = 1
    bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_CYCLE
    names = c4d.BaseContainer()
    
    # Iterates and enumerate the list of addition no named arguments
    for i, arg in enumerate(*argv):
        names.SetString(i, arg)

    bc.SetContainer(c4d.DESC_CYCLE, names)
    
    # Check if a named argument "parentGroup" as been filled
    parentGroup = kargs.get("parentGroup", None)
    if parentGroup is not None:
        bc[c4d.DESC_PARENTGROUP] = parentGroup
  
    element = obj.AddUserData(bc)
    obj[element] = val
    return element 

The only drawback of this is that the if you want to set parentGroup, you have to explicitly do it in the call of the method like so
CreateUserDataCycle(obj, name, val,"String1", "String2", "String3", parentGroup=GroupId)

The other way could be to simply use a list to store all the data, this is probably the way a C++ developer would have choosen.

def CreateUserDataCycle(obj, name, val, entryList, parentGroup=None):
    if obj is None: return False        
    bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_LONG)
    bc[c4d.DESC_NAME] = name
    bc[c4d.DESC_SHORT_NAME] = name
    bc[c4d.DESC_DEFAULT] = 1
    bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_CYCLE
    names = c4d.BaseContainer()
    
    for i, entry in enumerate(entryList):
        names.SetString(i, entry)

    bc.SetContainer(c4d.DESC_CYCLE, names)
    if parentGroup is not None:
        bc[c4d.DESC_PARENTGROUP] = parentGroup
  
    element = obj.AddUserData(bc)
    obj[element] = val
    return element 

And the method will be called this way [] bracket operator create a list, so in this case, we create a list of string.
CreateUserDataCycle(obj, name, val, ["String1", "String2", "String3"])

If you have any question, please let me know.
Cheers,
Maxime.

Hi @saputello2, I'm not sure in which way you would expect this "convenience" to work. You can for sure build a convenience function to creates your combobox as you did for the CreateUserDataFloat. But their is nothing like that already made in Cinema 4D.
With that's said, if you want to build a combobox with all kinds of noise you can use c4d.utils.noise.C4DNoise.CreateMenuContainer.

# NOISE combobox
noiseType = c4d.GetCustomDataTypeDefault(c4d.DTYPE_LONG)
noiseType[c4d.DESC_PARENTGROUP] = subGroup2
noiseType[c4d.DESC_NAME] = "Noise Type"
noiseType[c4d.DESC_CYCLE] = c4d.utils.noise.C4DNoise.CreateMenuContainer() 
entries = op.AddUserData(noiseType)
op[entries] = c4d.NOISE_LUKA

Finally, please read Q&A New Functionality in order to set up correctly your post (I've done it for you this time, don't worry since it's your first topic).

Cheers,
Maxime.

Thanks much for the reply, @m_adam. I'm creating a script for a Redshift material using Redshift's native noises, but that's a fantastic tip on c4d.utils.noise.C4DNoise.CreateMenuContainer. And apologies for missing the post rules -- I'll be more mindful in the future.

Really just looking for a way to clean up the code (and greater understand how the function works).

This is what I came up with:

def CreateUserDataCycle(obj, name, val, entry1, entry2, entry3, entry4, entry5, entry6, parentGroup=None):
    if obj is None: return False        
    bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_LONG)
    bc[c4d.DESC_NAME] = name
    bc[c4d.DESC_SHORT_NAME] = name
    bc[c4d.DESC_DEFAULT] = 1
    bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_CYCLE
    names = c4d.BaseContainer()
    names.SetString(0,entry1)
    names.SetString(1,entry2)
    names.SetString(2,entry3)
    names.SetString(3,entry4)
    names.SetString(4,entry5)
    names.SetString(5,entry6)
    bc.SetContainer(c4d.DESC_CYCLE, names)
    if parentGroup is not None:
        bc[c4d.DESC_PARENTGROUP] = parentGroup
  
    element = obj.AddUserData(bc)
    obj[element] = val
    return element  

and...

noiseType = CreateUserDataCycle(op,"Noise Type",2,"Type01", "Type02", "Type03", "Type04", "", "",subGroup2)
textureType = CreateUserDataCycle(op, "Texture", 1, "Concrete", "Scratches", "Grunge", "", "", "", subGroup2)

So far it works, though I'm still working on how to manage the number of "entries" so there aren't blank strings: "".

Thanks again, your advice pushed me in the right direction.

Eric

Hi @saputello2,

The pythonic way would be to use *args and **kwargs, for more information, please read *args and **kwargs in python explained.
This will give us something like.

def CreateUserDataCycle(obj, name, val, *argv, **kargs):
    if obj is None: return False        
    bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_LONG)
    bc[c4d.DESC_NAME] = name
    bc[c4d.DESC_SHORT_NAME] = name
    bc[c4d.DESC_DEFAULT] = 1
    bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_CYCLE
    names = c4d.BaseContainer()
    
    # Iterates and enumerate the list of addition no named arguments
    for i, arg in enumerate(*argv):
        names.SetString(i, arg)

    bc.SetContainer(c4d.DESC_CYCLE, names)
    
    # Check if a named argument "parentGroup" as been filled
    parentGroup = kargs.get("parentGroup", None)
    if parentGroup is not None:
        bc[c4d.DESC_PARENTGROUP] = parentGroup
  
    element = obj.AddUserData(bc)
    obj[element] = val
    return element 

The only drawback of this is that the if you want to set parentGroup, you have to explicitly do it in the call of the method like so
CreateUserDataCycle(obj, name, val,"String1", "String2", "String3", parentGroup=GroupId)

The other way could be to simply use a list to store all the data, this is probably the way a C++ developer would have choosen.

def CreateUserDataCycle(obj, name, val, entryList, parentGroup=None):
    if obj is None: return False        
    bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_LONG)
    bc[c4d.DESC_NAME] = name
    bc[c4d.DESC_SHORT_NAME] = name
    bc[c4d.DESC_DEFAULT] = 1
    bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_CYCLE
    names = c4d.BaseContainer()
    
    for i, entry in enumerate(entryList):
        names.SetString(i, entry)

    bc.SetContainer(c4d.DESC_CYCLE, names)
    if parentGroup is not None:
        bc[c4d.DESC_PARENTGROUP] = parentGroup
  
    element = obj.AddUserData(bc)
    obj[element] = val
    return element 

And the method will be called this way [] bracket operator create a list, so in this case, we create a list of string.
CreateUserDataCycle(obj, name, val, ["String1", "String2", "String3"])

If you have any question, please let me know.
Cheers,
Maxime.

Thank you again, @m_adam! I ended up using the C++ method, and it worked like a charm.

Also, I appreciate the link to the *args and **kwargs at pythontips.com. It was very helpful to understand those concepts.

I do have (many) more questions, but I'll start new posts since they're not related to this topic.

My Best,

Eric