using customgui listview



  • 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



  • 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
    Treeview made simple – Part 2

    Cheers,
    Maxime



  • 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


Log in to reply