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, 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