How to drag rows in Treeview
-
Hi,
I have read many posts about the dragging behavior of treeview, all of which are examples of dragging from the outside into treeview. What I want is to drag the rows of treeview to rearrange them, just like an object manager dragging objects.
This post also mentioned some explanations about the drag and drop process, but I am still confused about how to start.I found an example of code in this post. Can we implement the drag behavior of treeview rows on this basis
import c4d import weakref # Be sure to use a unique ID obtained from [URL-REMOVED] PLUGIN_ID = 1000010 # TEST ID ONLY # TreeView Column IDs. ID_CHECKBOX = 1 ID_NAME = 2 ID_OTHER = 3 def TextureObjectIterator(lst): for parentTex in lst: yield parentTex for childTex in TextureObjectIterator(parentTex.GetChildren()): yield childTex class TextureObject(object): """ Class which represent a texture, aka an Item in our list """ texturePath = "TexPath" otherData = "OtherData" _selected = False _open = True def __init__(self, texturePath): self.texturePath = texturePath self.otherData += texturePath self.children = [] self._parent = None @property def IsSelected(self): return self._selected def Select(self): self._selected = True def Deselect(self): self._selected = False @property def IsOpened(self): return self._open def Open(self): self._open = True def Close(self): self._open = False def AddChild(self, obj): obj._parent = weakref.ref(self) self.children.append(obj) def GetChildren(self): return self.children def GetParent(self): if self._parent: return self._parent() return None def __repr__(self): return str(self) def __str__(self): return self.texturePath 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]) 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 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 def GetDown(self, root, userdata, obj): """ Return a child of a node, since we only want a list, we return None everytime """ children = obj.GetChildren() if children: return children[0] return None def GetNext(self, root, userdata, obj): """ Returns the next Object to display after arg:'obj' """ rValue = None # If does have a child it means it's a child. objParent = obj.GetParent() listToSearch = objParent.GetChildren() if objParent is not None else self.listOfTexture currentObjIndex = listToSearch.index(obj) nextIndex = currentObjIndex + 1 if nextIndex < len(listToSearch): rValue = listToSearch[nextIndex] return rValue def GetPred(self, root, userdata, obj): """ Returns the previous Object to display before arg:'obj' """ rValue = None # If does have a child it means it's a child. objParent = obj.GetParent() listToSearch = objParent.GetChildren() if objParent is not None else self.listOfTexture currentObjIndex = listToSearch.index(obj) predIndex = currentObjIndex - 1 if 0 <= predIndex < len(listToSearch): rValue = listToSearch[predIndex] return rValue def GetId(self, root, userdata, obj): """ Return a unique ID for the element in the TreeView. """ return hash(obj) 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 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 def IsOpened(self, root, userdata, obj): """ Returns: (bool): Status If it's opened = True (folded) or closed = False. """ # If there is some children return obj.IsOpened def Open(self, root, userdata, obj, onoff): """ Called when the user clicks on a folding state of an object to display/hide its children """ if onoff: obj.Open() else: obj.Close() 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 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, int(ypos - h * 1.1)) 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(list(TextureObjectIterator(self.listOfTexture))): if tex.IsSelected: listToRemove = objParent.GetChildren() if objParent is not None else self.listOfTexture listToRemove.remove(tex) def DragStart(self, root: object, userdata: object, obj: object) -> int: print("drag") c4d.gui.SetMousePointer(c4d.MOUSE_INSERTMOVE) return c4d.TREEVIEW_DRAGSTART_ALLOW | c4d.TREEVIEW_DRAGSTART_SELECT def SetDragObject(self, root: object, userdata: object, obj: object) -> None: pass 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. customgui.SetBool(c4d.TREEVIEW_NO_MULTISELECT, True) 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") self.AddButton(1002, c4d.BFH_CENTER, name="Add Child to selected") return True 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 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() elif id == 1002: for parentTex in TextureObjectIterator(self._listView.listOfTexture): if not parentTex.IsSelected: continue newID = len(parentTex.GetChildren()) + 1 tex = TextureObject("T{0}.{1}".format(str(parentTex), newID)) parentTex.AddChild(tex) # Refresh the TreeView self._treegui.Refresh() return True def main(): global dialog dialog = TestDialog() dialog.Open(c4d.DLG_TYPE_ASYNC, defaulth=600, defaultw=600) if __name__ == "__main__": main()
Thanks for your help
-
Hey @chuanzhen,
Thank you for reaching out to us. Well, you must implement the documented drag methods which you already sort of started to implement in your code. The reason why your
DragStart
did not do anything, is because you need at leastDragStart
andGetDragType
to get drag handling going.What is also not so good in your example is that you implement your own hierarchy type, which complicates things when you want to drag data, as you cannot pack up arbitrary data as drag data. I solved this by packing up you
TextUreObject
trees asBaseList2D
trees for dragging.See also Handling Treeview File Drag and Drop Events (this topic is about drag and drop events from the outside and not internal ones).
Cheers,
FerdinandResult
Code
""" I put --- snip (FH) --- markers around the places I modified. """ import c4d import weakref import mxutils # Be sure to use a unique ID obtained from [URL-REMOVED] PLUGIN_ID = 1000010 # TEST ID ONLY # TreeView Column IDs. ID_CHECKBOX = 1 ID_NAME = 2 ID_OTHER = 3 def TextureObjectIterator(lst): for parentTex in lst: yield parentTex for childTex in TextureObjectIterator(parentTex.GetChildren()): yield childTex class TextureObject(object): """ Class which represent a texture, aka an Item in our list """ texturePath = "TexPath" otherData = "OtherData" _selected = False _open = True def __init__(self, texturePath): self.texturePath = texturePath self.otherData += texturePath self.children = [] self._parent = None @property def IsSelected(self): return self._selected def Select(self): self._selected = True def Deselect(self): self._selected = False @property def IsOpened(self): return self._open def Open(self): self._open = True def Close(self): self._open = False def AddChild(self, obj): obj._parent = weakref.ref(self) self.children.append(obj) def GetChildren(self): return self.children def GetParent(self): if self._parent: return self._parent() return None def __repr__(self): return str(self) def __str__(self): return self.texturePath # --- snip (FH) --- # We must be able to pack up the to be dragged things in a data format the C++ core can deal # with, a pure Python object such as TextureObject cannot be the content of a drag event. So, # we realize converting TextureObject from and to to BaseList2D. Generally it would be better # to use BaseList2D directly as your tree elements. # Conversions for the fields of #TextureObject into the data container of a node. ID_TEXTURE: int = 10000 ID_OTHER: int = 10001 ID_SELECTED: int = 10002 ID_OPEN: int = 10003 def ToBaseList2D(self) -> c4d.BaseList2D: """Serializes #self into a tree of BaseList2D instances. """ # We use null objects to store our data but also could use any other node. node: c4d.BaseList2D = c4d.BaseList2D(c4d.Onull) node[self.ID_TEXTURE] = self.texturePath node[self.ID_OTHER] = self.otherData node[self.ID_SELECTED] = False node[self.ID_OPEN] = self.IsOpened for child in self.children: child.ToBaseList2D().InsertUnderLast(node) return node @classmethod def FromBaseList2D(cls, node: c4d.BaseList2D) -> "TextureObject": """Deserializes #node in a #TextureObject tree. """ obj = cls(node[cls.ID_TEXTURE]) obj.otherData = node[cls.ID_OTHER] obj._selected = node[cls.ID_SELECTED] obj._open = node[cls.ID_OPEN] for child in node.GetChildren(): obj.AddChild(cls.FromBaseList2D(child)) return obj # --- snip end (FH) --- 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]) # --- snip (FH) --- # You made some really weird choices for root and userdata. The root is the TreeViewFunctions # itself, and userdata is None. The root is usually the root element of your displayed content, # e.g., ListView.listOfTexture in your case. # def GetDragType(self, root: "ListView", userdata: None, obj: TextureObject) -> int: """Define what data type is being dragged, such as scene elements or strings. """ # There is no way to drag arbitrary Python data, so we chose atom arrays, i.e., scene # elements and pack up your TextureObject instances as scene elements. return c4d.DRAGTYPE_ATOMARRAY def DragStart(self, root: "ListView", userdata: None, obj: TextureObject) -> int: """Called when the user starts dragging an element and returns the allowed drag actions. """ return c4d.TREEVIEW_DRAGSTART_ALLOW | c4d.TREEVIEW_DRAGSTART_SELECT def GenerateDragArray(self, root: "ListView", userdata: None, obj: TextureObject) -> list[c4d.BaseList2D]: """Called by Cinema 4D to let the treeview pack up the drag data for a drag event on #obj. """ # We just pack up #obj and send it as a node. I did not deal with multi selections here. return [obj.ToBaseList2D()] def AcceptDragObject(self, root: "ListView", userData: None, obj: TextureObject, dragType: int, dragData: any) -> tuple[bool, int]: """Called by Cinema 4D to decide if #dragData is valid data to be dragged onto #item. Since all rows accept textures being dragged onto them, we can ignore #item here. """ # Not the most through check, this for example will also "allow" drag events from the OM, # which then later on will fail. Here should be a more through check if #dragData is valid # data. if (dragType != c4d.DRAGTYPE_ATOMARRAY or not mxutils.CheckIterable(dragData, c4d.BaseList2D, minCount=1)): return False return c4d.INSERT_UNDER, False def InsertObject(self, root: "ListView", userData: None, obj: TextureObject, dragType: int, dragData: any, insertMode: int, doCopy: bool) -> None: """Called by Cinema 4D once a drag event has finished which before has been indicated as valid by #AcceptDragObject. """ # Just unpack our drag data and insert it. node: c4d.BaseList2D = mxutils.CheckIterable(dragData, c4d.BaseList2D, minCount=1)[0] obj.AddChild(TextureObject.FromBaseList2D(node)) # --- snip end (FH) --- 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 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 def GetDown(self, root, userdata, obj): """ Return a child of a node, since we only want a list, we return None everytime """ children = obj.GetChildren() if children: return children[0] return None def GetNext(self, root, userdata, obj): """ Returns the next Object to display after arg:'obj' """ rValue = None # If does have a child it means it's a child. objParent = obj.GetParent() listToSearch = objParent.GetChildren() if objParent is not None else self.listOfTexture currentObjIndex = listToSearch.index(obj) nextIndex = currentObjIndex + 1 if nextIndex < len(listToSearch): rValue = listToSearch[nextIndex] return rValue def GetPred(self, root, userdata, obj): """ Returns the previous Object to display before arg:'obj' """ rValue = None # If does have a child it means it's a child. objParent = obj.GetParent() listToSearch = objParent.GetChildren() if objParent is not None else self.listOfTexture currentObjIndex = listToSearch.index(obj) predIndex = currentObjIndex - 1 if 0 <= predIndex < len(listToSearch): rValue = listToSearch[predIndex] return rValue def GetId(self, root, userdata, obj): """ Return a unique ID for the element in the TreeView. """ return hash(obj) 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 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 def IsOpened(self, root, userdata, obj): """ Returns: (bool): Status If it's opened = True (folded) or closed = False. """ # If there is some children return obj.IsOpened def Open(self, root, userdata, obj, onoff): """ Called when the user clicks on a folding state of an object to display/hide its children """ if onoff: obj.Open() else: obj.Close() 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 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, int(ypos - h * 1.1)) 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(list(TextureObjectIterator(self.listOfTexture))): if tex.IsSelected: listToRemove = objParent.GetChildren() if objParent is not None else self.listOfTexture listToRemove.remove(tex) 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. customgui.SetBool(c4d.TREEVIEW_NO_MULTISELECT, True) 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") self.AddButton(1002, c4d.BFH_CENTER, name="Add Child to selected") return True 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 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() elif id == 1002: for parentTex in TextureObjectIterator(self._listView.listOfTexture): if not parentTex.IsSelected: continue newID = len(parentTex.GetChildren()) + 1 tex = TextureObject("T{0}.{1}".format(str(parentTex), newID)) parentTex.AddChild(tex) # Refresh the TreeView self._treegui.Refresh() return True def main(): global dialog dialog = TestDialog() dialog.Open(c4d.DLG_TYPE_ASYNC, defaulth=600, defaultw=600) if __name__ == "__main__": main()
-
Hey,
And I forgot: I see this
weakref
stuff floating around for a while inTreeViewFunctions
code. I do not know who brought it into circulation. It does not really hurt in a technical sense but makes such code more intimidating. You can remove it and writeobj._parent = self
instead ofobj._parent = weakref.ref(self)
. A weak reference is a reference that does not prevent the referenced object from being garbage collected.When there is a child with a ref to a parent, what would be a scenario where one wants the parent to be deleted without the child being deleted or the child having its parent set to
None
. A child in CS has by definition always a parent, so having the child to weak-ref its parent seems pretty pointless (unless I am overlooking something here).Cheers,
Ferdinand -
@ferdinand Thanks for your help!