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.TreeViewFunctionsBut let's go step by step
First things is to create a GeDialog, and add a custom GUI. Ignore ListView() for the momentclass 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.GetFirstdef 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 DeletePresseddef 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/hp9KpZjxI 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.
-
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