Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    using customgui listview

    PYTHON Development
    0
    3
    2.0k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • H
      Helper
      last edited by

      On 23/02/2018 at 13:00, xxxxxxxx wrote:

      Hi,

      I'm new-ish to guis in python for c4d.  Looking for documentation.

      My goal is to implement a plugin with a list view in a dialog, similar to the texture manager (with the check/x buttons on each row).  I think I've successfully added a listview customgui, but can't find examples on how to interact with it.

      self.AddCustomGui(1001, c4d.CUSTOMGUI_LISTVIEW, "myList", c4d.BFH_SCALEFIT, 300, 200)
      

      Any guidance would be great.  I've checked both the python and cpp docs as sometimes you can find things in the latter to help with the former.  Might have just missed it completely, so apologies if I'm not being thorough enough.

      Thanks.

      --
      Kevin

      1 Reply Last reply Reply Quote 0
      • H
        Helper
        last edited by

        On 26/02/2018 at 04:04, xxxxxxxx wrote:

        Hello KevinW,

        First of all, this is actually not possible to create a ListView in python. But you can create a TreeView, wich is basicly a more complexe ListView.
        So with that said, let's get's started !
        The bascis concept is to create a CUSTOMGUI_TREEVIEW, then attach an instance of a c4d.gui.TreeViewFunctions.
        Our instance of c4d.gui.TreeViewFunctions will then react to different functions according c4d.gui.TreeViewFunctions

        But let's go step by step
        First things is to create a GeDialog, and add a custom GUI. Ignore ListView() for the moment

        class TestDialog(c4d.gui.GeDialog) :
            _treegui = None # Our CustomGui TreeView
            _listView = ListView() # Our Instance of c4d.gui.TreeViewFunctions
          
            def CreateLayout(self) :
                # Create the TreeView GUI.
                customgui = c4d.BaseContainer()
                customgui.SetBool(c4d.TREEVIEW_BORDER, c4d.BORDER_THIN_IN)
                customgui.SetBool(c4d.TREEVIEW_HAS_HEADER, True) # True if the tree view may have a header line.
                customgui.SetBool(c4d.TREEVIEW_HIDE_LINES, False) # True if no lines should be drawn.
                customgui.SetBool(c4d.TREEVIEW_MOVE_COLUMN, True) # True if the user can move the columns.
                customgui.SetBool(c4d.TREEVIEW_RESIZE_HEADER, True) # True if the column width can be changed by the user.
                customgui.SetBool(c4d.TREEVIEW_FIXED_LAYOUT, True) # True if all lines have the same height.
                customgui.SetBool(c4d.TREEVIEW_ALTERNATE_BG, True) # Alternate background per line.
                customgui.SetBool(c4d.TREEVIEW_CURSORKEYS, True) # True if cursor keys should be processed.
                customgui.SetBool(c4d.TREEVIEW_NOENTERRENAME, False) # Suppresses the rename popup when the user presses enter.
          
                self._treegui = self.AddCustomGui( 1000, c4d.CUSTOMGUI_TREEVIEW, "", c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 300, 300, customgui)
                if not self._treegui:
                    print "[ERROR]: Could not create TreeView"
                    return False
          
                self.AddButton(1001, c4d.BFH_CENTER, name="Add")
                return True
        

        Then we need to initializes some values to this CustomGui. So we implement InitValues within our GeDialog:

            def InitValues(self) :
                # Initialize the column layout for the TreeView.
                layout = c4d.BaseContainer()
                layout.SetLong(ID_CHECKBOX, c4d.LV_CHECKBOX)
                layout.SetLong(ID_NAME, c4d.LV_TREE)
                layout.SetLong(ID_OTHER, c4d.LV_USER)
                self._treegui.SetLayout(3, layout)
          
                # Set the header titles.
                self._treegui.SetHeaderText(ID_CHECKBOX, "Check")
                self._treegui.SetHeaderText(ID_NAME, "Name")
                self._treegui.SetHeaderText(ID_OTHER, "Other")
                self._treegui.Refresh()
          
                # Set TreeViewFunctions instance used by our CUSTOMGUI_TREEVIEW
                self._treegui.SetRoot(self._treegui, self._listView, None)
                return True
        

        Now that our GeDialog is created, we need to implement our ListView.
        The purpose of this class is to manage some objects (as the Object manager does for example).
        So we firstly need to create a custom Object which will represent an item in our list.

        class TextureObject(object) :
            """
            Class which represent a texture, aka an Item in our list
            """
            texturePath = "TexPath"
            otherData = "OtherData"
            _selected = False
          
            def __init__(self, texturePath) :
                self.texturePath = texturePath
                self.otherData += texturePath
          
            @property
            def IsSelected(self) :
                return self._selected
          
            def Select(self) :
                self._selected = True
          
            def Deselect(self) :
                self._selected = False
          
            def __repr__(self) :
                return str(self)
          
            def __str__(self) :
                return self.texturePath
        

        Note that in your case you may want to create an object from a BaseList2D, by doing RegisterNodeData, and which will allow you to support Drag and drop. But that's maybe something we can see in a future post.

        Then let's start by implemeting our class derived from c4d.gui.TreeViewFunctions

        class ListView(c4d.gui.TreeViewFunctions) :
          
            def __init__(self) :
                self.listOfTexture = list() # Store all objects we need to display in this list
          
                # Add some defaults values 
                t1 = TextureObject("T1")
                t2 = TextureObject("T2")
                t3 = TextureObject("T3")
                t4 = TextureObject("T4")
          
                self.listOfTexture.extend([t1, t2, t3, t4])
        

        The whole concept of TreeView is to override some functions. So make sure to read the documentation to know which functions to override according your need.
        So we will define some more general options for our TreeView:

            def IsResizeColAllowed(self, root, userdata, lColID) :
                return True
          
            def IsTristate(self, root, userdata) :
                return False
          
            def GetColumnWidth(self, root, userdata, obj, col, area) :
                return 80  # All have the same initial width
          
            def IsMoveColAllowed(self, root, userdata, lColID) :
                # The user is allowed to move all columns.
                # TREEVIEW_MOVE_COLUMN must be set in the container of AddCustomGui.
                return True
        

        Now, since everything is setup we have to tell which item is the first one. Then all our lists view will be process as you process the current ObjectManager (GetNext,GetDown).
        To do it we override c4d.gui.TreeViewFunctions.GetFirst

            def GetFirst(self, root, userdata) :
                """
                Return the first element in the hierarchy, or None if there is no element.
                """
                rValue = None if not self.listOfTexture else self.listOfTexture[0]
                return rValue
        

        Then we have to handle GetDown (for child object, since in our case it's a simple list, we return None).
        GetNext which is the Object after the current Object, and GetPred wich is the Object before current Object.
        We also have to override GetID in order to uniquely identify an object in our list.

            def GetDown(self, root, userdata, obj) :
                """
                Return a child of a node, since we only want a list, we return None everytime
                """
                return None
          
            def GetNext(self, root, userdata, obj) :
                """
                Returns the next Object to display after arg:'obj'
                """
                rValue = None
                currentObjIndex = self.listOfTexture.index(obj)
                nextIndex = currentObjIndex + 1
                if nextIndex < len(self.listOfTexture) :
                    rValue = self.listOfTexture[nextIndex]
          
                return rValue
          
            def GetPred(self, root, userdata, obj) :
                """
                Returns the previous Object to display before arg:'obj'
                """
                rValue = None
                currentObjIndex = self.listOfTexture.index(obj)
                predIndex = currentObjIndex - 1
                if 0 <= predIndex < len(self.listOfTexture) :
                    rValue = self.listOfTexture[predIndex]
          
                return rValue
        		
            def GetId(self, root, userdata, obj) :
                """
                Return a unique ID for the element in the TreeView.
                """
                return hash(obj)
        

        In order to handle selection we have to override Select and IsSelected

            def Select(self, root, userdata, obj, mode) :
                """
                Called when the user selects an element.
                """
                if mode == c4d.SELECTION_NEW:
                    for tex in self.listOfTexture:
                        tex.Deselect()
                    obj.Select()
                elif mode == c4d.SELECTION_ADD:
                    obj.Select()
                elif mode == c4d.SELECTION_SUB:
                    obj.Deselect()
          
            def IsSelected(self, root, userdata, obj) :
                """
                Returns: True if *obj* is selected, False if not.
                """
                return obj.IsSelected
        

        As you may be aware, in our GeDialog::InitValues we define some columns with different flags (LV_CHECKBOX, LV_TREE and LV_USER). Each one get a different implementation.
        So let's start with LV_CHECKBOX wich stand for check box.
        In order to make it work we have to override SetCheck and IsChecked
        Note that I used the same variable for selection, so if you select an item it will also check them, and if you check an item it will select it.

            def SetCheck(self, root, userdata, obj, column, checked, msg) :
                """
                Called when the user clicks on a checkbox for an object in a
                `c4d.LV_CHECKBOX` column.
                """
                if checked:
                    obj.Select()
                else:
                    obj.Deselect()
          
            def IsChecked(self, root, userdata, obj, column) :
                """
                Returns: (int) : Status of the checkbox in the specified *column* for *obj*.
                """
                if obj.IsSelected:
                    return c4d.LV_CHECKBOX_CHECKED | c4d.LV_CHECKBOX_ENABLED
                else:
                    return c4d.LV_CHECKBOX_ENABLED
        

        Then LV_TREE element will check for Object name by calling GetName

            def GetName(self, root, userdata, obj) :
                """
                Returns the name to display for arg:'obj', only called for column of type LV_TREE
                """
                return str(obj) # Or obj.texturePath
        

        And finally LV_USER allow us to do some drawing function, as you will do if you are in a GeUserArea. Use DrawCell to draw anything you want (in our case a text). Take a look at DrawInfo-Dict

            def DrawCell(self, root, userdata, obj, col, drawinfo, bgColor) :
                """
                Draw into a Cell, only called for column of type LV_USER
                """
                if col == ID_OTHER:
                    name = obj.otherData
                    geUserArea = drawinfo["frame"]
                    w = geUserArea.DrawGetTextWidth(name)
                    h = geUserArea.DrawGetFontHeight()
                    xpos = drawinfo["xpos"]
                    ypos = drawinfo["ypos"] + drawinfo["height"]
                    drawinfo["frame"].DrawText(name, xpos, ypos - h * 1.1) # *1.1 ugly trick to make it nicer
        

        Then few stuffs can be usefull, like to trigger some scripts when you double click by overriding DoubleClick
        And also manager Delete key by overriding DeletePressed

            def DoubleClick(self, root, userdata, obj, col, mouseinfo) :
                """
                Called when the user double-clicks on an entry in the TreeView.
          
                Returns:
                  (bool) : True if the double-click was handled, False if the
                    default action should kick in. The default action will invoke
                    the rename procedure for the object, causing `SetName()` to be
                    called.
                """
                c4d.gui.MessageDialog("You clicked on " + str(obj))
                return True
          
            def DeletePressed(self, root, userdata) :
                "Called when a delete event is received."
                for tex in reversed(self.listOfTexture) :
                    if tex.IsSelected:
                        self.listOfTexture.remove(tex)
        

        And that's it, for our ListView, it's pretty basic but when you understand the concept is very easy, just take a look at all functions available for TreeViewFunctions.

        Additionally you may want to trigger some changes from GeDialog to your list, then remember we Create a button in our CreateLayout, now let's create his interaction.

            def Command(self, id, msg) :
                # Click on button
                if id == 1001:
                    # Add data to our DataStructure (ListView)
                    newID = len(self._listView.listOfTexture) + 1 
                    tex = TextureObject("T{}".format(newID))
                    self._listView.listOfTexture.append(tex)
          
                    # Refresh the TreeView
                    self._treegui.Refresh()
          
                return True
        

        And voila ! 🙂

        Here the full example:
        https://pastebin.com/hp9KpZjx

        I hope everything make sense and answers to your initial question.
        For the moment it's pretty basic, but I will see how I can improve it in order to also demonstrate how to support Drag and Drop.

        Finally you can find some usefull informations in C++:
        TreeView made simple - Part 1[URL-REMOVED]
        Treeview made simple – Part 2[URL-REMOVED]

        Cheers,
        Maxime


        [URL-REMOVED] @maxon: This section contained a non-resolving link which has been removed.

        1 Reply Last reply Reply Quote 0
        • H
          Helper
          last edited by

          On 26/02/2018 at 06:16, xxxxxxxx wrote:

          This is super helpful Maxime, thank you very much!

          This more then answers all my questions, and really makes a lot things much more clear.  This is more involved then I expected, but makes sense with your explanations.  Thanks very much.  I imagine this would be very useful to others as well!

          --
          Kevin

          1 Reply Last reply Reply Quote 0
          • First post
            Last post