UserArea drag and drop example?
-
Hi community,
I have recently started learning UserArea modules, but there are some principle documents that are not very easy to understand, especially regarding drag and drop, can you provide an example of this and explain more details about the usage of related functions?
abstraction
- how do we drag a BaseList2d into a UA and get some data
- how do we drag a UA item back to Cinema and create BaseList2d depends on the context?
concretion
If the abstract problem is relatively vague, presenting the simplest case may be more clear:
- we create a dialog with UA,
- we can drag a Cube and a Material into UA, create UA item.
- we can drag the UA item(Cube) back to OM, and drag the UA item(Material) into Material Manager or OM(on the selected object)
Hope this can explain my problem clearly, and hope Python can handle these things.
Cheers~
DunHou -
Hi @Dunhou speaking more generally about drag and drop in GeUserArea, I would invite you to read the Drag&Drop an image in a gui field topic which covers the basics to receive and user area - drop finished for receiving.
@Dunhou said in UserArea drag and drop example?:
- How do we drag a BaseList2d into a UA and get some data
- how do we drag a UA item back to Cinema and create BaseList2d depends on the context?
import c4d class CustomUserArea(c4d.gui.GeUserArea): def __init__(self): self.receivedObject = [] def GetMinSize(self): """Called by Cinema 4D to know the minimum size of the GeUserArea""" return 50, 100 def DrawMsg(self, x1, y1, x2, y2, msg_ref): """This Method is called automatically when Cinema 4D Draw the Gadget. Args: x1 (int): The upper left x coordinate. y1 (int): The upper left y coordinate. x2 (int): The lower right x coordinate. y2 (int): The lower right y coordinate. msg_ref (c4d.BaseContainer): The original message container. """ self.OffScreenOn() self.SetClippingRegion(x1, y1, x2, y2) # From 0 pixel to 50 draw a red box. This one will be used to receive drop operation. self.DrawSetPen(c4d.Vector(1, 0,0)) self.DrawRectangle(0, 0, 50, 50) # From 50 pixel to 100 draw a green box. This one will be used to start the drag operation. # Look at the InputEvent method to see how this is done self.DrawSetPen(c4d.Vector(0, 1,0)) self.DrawRectangle(0, 50, 50, 100) def Message(self, msg, result) : if msg.GetId()==c4d.BFM_DRAGRECEIVE: # Discard if lost drag or if it has been escaped if msg.GetInt32(c4d.BFM_DRAG_LOST) or msg.GetInt32(c4d.BFM_DRAG_ESC): return self.SetDragDestination(c4d.MOUSE_FORBIDDEN) # Check drop area and discard if not on the red box or not on the user area mouseY = self.GetDragPosition(msg)["y"] if not 0 < mouseY < 50 or not self.CheckDropArea(msg, True, True): return self.SetDragDestination(c4d.MOUSE_FORBIDDEN) # Check if drag is finished (=drop) if msg.GetInt32(c4d.BFM_DRAG_FINISHED) == 1: # Get drag object type and data dragInfo = self.GetDragObject(msg) self.receivedObject = dragInfo['object'] print(f"Dropped: {self.receivedObject}") return True # Return current mouse cursor for valid drag operation return self.SetDragDestination(c4d.MOUSE_MOVE) # Call GeUserAre.Message() implementation so that it can handle all the other messages return c4d.gui.GeUserArea.Message(self, msg, result) def InputEvent(self, msg): """Called by Cinema 4D when a click occurs""" mouseY = msg[c4d.BFM_INPUT_Y] + self.Global2Local()["y"] if 50 < mouseY < 100: # Retrieve the first object in the scene to use or otherwise use a newly created cube if self.receivedObject is None: self.receivedObject = c4d.BaseObject(c4d.Ocube) # Create a list of C4DAtom and pass that to the HandleMouseDrag function that will generate all the drag information self.HandleMouseDrag(msg, c4d.DRAGTYPE_ATOMARRAY, self.receivedObject, 0) return True class ExampleDialog(c4d.gui.GeDialog): def __init__(self): super().__init__() self.ua = CustomUserArea() def CreateLayout(self): """This Method is called automatically when Cinema 4D Create the Layout (display) of the Dialog.""" self.AddUserArea(10000, c4d.BFH_LEFT | c4d.BFV_TOP) self.AttachUserArea(self.ua, 10000) return True # main if __name__ == "__main__": global dlg dlg = ExampleDialog() dlg.Open(dlgtype=c4d.DLG_TYPE_ASYNC, defaultw=50, defaulth=100)
Sadly there is no way for the part that generate the drag operation to be context specific. The GeUserArea generate the drag information, then Cinema 4D forward it to the appropriate UI element, this one receive a BFM_DRAGRECEIVE and then it handle it as it want and have no clue from where it comes.
Hope it answers your questions,
Cheers,
Maxime. -
Hi @m_adam ,
Thanks for your reply! After some research, I had expanded the code for this case to adapt to the requirements I mentioned earlier:
- we can drag c4d.BaseObject , c4d.BaseMaterial or Image into UA, create their icon.
- we can drag the UA item(Object) back to OM, and drag the UA item(Material) into Material Manager or OM(on the selected object), or drag the UA item(ImageRef) into a shader link.
The UserArea Accept:
- c4d.BaseObject
- c4d.BaseMaterial
- Image file outside Cinema 4D.
Here is the code in case anyone is interested in this topic in the future.Hope it can be helpful.
Cheers~
DunHouimport c4d from c4d import Vector from c4d.gui import GeUserArea, GeDialog GADGET_ID_GEUSERAREA = 10000 class DropArea(GeUserArea): def __init__(self): # data host self.receivedObject: list[c4d.BaseList2D, str] = [] # bg color of the pen self.color: Vector = self._get_color() def _get_color(self, colorId: int = c4d.COLOR_BG) -> Vector: """ Get a rgb color from c4d color id """ color = self.GetColorRGB(colorId) r = color['r']/255 g = color['g']/255 b = color['b']/255 return c4d.Vector(r,g,b) def DrawMsg(self, x1, y1, x2, y2, msg): """This Method is called automatically when Cinema 4D Draw the Gadget. Args: x1 (int): The upper left x coordinate. y1 (int): The upper left y coordinate. x2 (int): The lower right x coordinate. y2 (int): The lower right y coordinate. msg (c4d.BaseContainer): The original message container. """ # Initializes draw region self.OffScreenOn() self.SetClippingRegion(x1, y1, x2, y2) # Defines the color used in draw operation, use c4d backgroud color here. self.DrawSetPen(self.color) # Draws a rectangle filling the whole UI self.DrawRectangle(x1, y1, x2, y2) # Draw a Icon with the drop object, only consider c4d.BaseList2D and ImageRef here if msg.GetInt32(c4d.BFM_DRAG_FINISHED) == 1: # If drag info recive a image, draw a icon of image if isinstance(self.receivedObject, str): icon = c4d.bitmaps.InitResourceBitmap(1050500) self.DrawBitmap(icon, x1, y1, x2, y2, 0, 0, icon.GetBw(), icon.GetBh(), mode= c4d.BMP_ALLOWALPHA) # If drag info recive a list of BaseList2D, draw a icon of first one. else: if self.receivedObject: # Draw the first drop object's Icon, # for BaseMaterial, we can get the preview image. # for BaseObject, we can get the icon. if isinstance(self.receivedObject[0], c4d.BaseList2D): icon = self.receivedObject[0].GetIcon() self.DrawBitmap(icon['bmp'], x1, y1, x2, y2, icon['x'], icon['y'], icon['w'], icon['h'],mode= c4d.BMP_ALLOWALPHA) def GetMinSize(self): # do a calculation here, min size. return 200, 200 def Message(self, msg, result) : if msg.GetId()==c4d.BFM_DRAGRECEIVE: # Discard if lost drag or if it has been escaped if msg.GetInt32(c4d.BFM_DRAG_LOST) or msg.GetInt32(c4d.BFM_DRAG_ESC): return self.SetDragDestination(c4d.MOUSE_FORBIDDEN) if not self.CheckDropArea(msg, True, True): return self.SetDragDestination(c4d.MOUSE_FORBIDDEN) # Check if drag is finished (=drop) if msg.GetInt32(c4d.BFM_DRAG_FINISHED) == 1: # Get drag object type and data dragInfo = self.GetDragObject(msg) # Redraw the GeUserArea (will call DrawMsg) self.Redraw() #print(type(dragInfo['object'])) self.receivedObject = dragInfo['object'] print(f"Dropped: {self.receivedObject}") return True # Return current mouse cursor for valid drag operation return self.SetDragDestination(c4d.MOUSE_MOVE) # Call GeUserAre.Message() implementation so that it can handle all the other messages return c4d.gui.GeUserArea.Message(self, msg, result) def InputEvent(self, msg): """Called by Cinema 4D when a click occurs""" # If the #self.receivedObject host the file path of the image, we generate the file path. # this means we can drag it into a image link like a shader link. if isinstance(self.receivedObject, str): self.HandleMouseDrag(msg, c4d.DRAGTYPE_FILENAME_IMAGE, self.receivedObject, 0) else: # Create a list of C4DAtom and pass that to the HandleMouseDrag function that will generate all the drag information self.HandleMouseDrag(msg, c4d.DRAGTYPE_ATOMARRAY, self.receivedObject, 0) return True class ExampleDialog(GeDialog): # The GeUserArea need to be stored somewhere, we will need this instance to be attached to the current Layout geUserArea = DropArea() def CreateLayout(self): """This Method is called automatically when Cinema 4D Create the Layout (display) of the Dialog.""" self.SetTitle("Drag Area") if self.GroupBegin(0, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=1, rows=0, title="", groupflags=0, initw=100, inith=100): self.GroupBorderSpace(8,8,8,8) self.GroupSpace(2, 2) # Adds a Gadget that will host a GeUserArea self.AddUserArea(GADGET_ID_GEUSERAREA, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 200, 200) # Attaches the stored GeUserArea to the Gadget previously created self.AttachUserArea(self.geUserArea, GADGET_ID_GEUSERAREA) self.GroupEnd() return True if __name__ == "__main__": global dlg dlg = ExampleDialog() dlg.Open(dlgtype=c4d.DLG_TYPE_ASYNC, defaultw=200, defaulth=200)
-
Hi @Dunhou ,Hi @m_adam , thank you for providing the code. I have also been researching drag and drop methods in GeUserArea recently. I noticed that in the DragObject () method, when retrieving an external file, a single file path is returned. May I ask if it is possible to return a list containing multiple file paths when dragging multiple external files at the same time, similar to the DRAGTYPE-ATOMARRAY
-
This post is deleted! -
I tried using a timer to solve this problem, but I still want to know if there is a more direct way
import c4d import threading from c4d.gui import GeUserArea, GeDialog GADGET_ID_GEUSERAREA = 10000 class DropArea(GeUserArea): def __init__(self): # Used to store all objects involved in the drag-and-drop operation self.currentDragObjects = [] # Flag to indicate whether a drag operation is in progress self.isDragging = False # Define a timer to delay the handling of the drag completion self.dragTimer = None def Message(self, msg, result): # Handle drag-and-drop messages if msg.GetId() == c4d.BFM_DRAGRECEIVE: # Check if the drag was lost or canceled if msg.GetInt32(c4d.BFM_DRAG_LOST) or msg.GetInt32(c4d.BFM_DRAG_ESC): self.isDragging = False return self.SetDragDestination(c4d.MOUSE_FORBIDDEN) # If the drag just started, clear the previous object list if not self.isDragging: self.currentDragObjects = [] # Initialize the storage list self.isDragging = True # Mark the beginning of the drag # Verify if it is a valid drop area if not self.CheckDropArea(msg, True, True): return self.SetDragDestination(c4d.MOUSE_FORBIDDEN) # Get the dragged file object dragInfo = self.GetDragObject(msg) if dragInfo is not None: dragObject = dragInfo['object'] # Check if the object already exists in the list to avoid duplicates if dragObject not in self.currentDragObjects: self.currentDragObjects.append(dragObject) # Reset the timer to delay the handling of drag completion if self.dragTimer is not None: self.dragTimer.cancel() # Set a short timer (e.g., 0.2 seconds) to determine if the drag operation is complete self.dragTimer = threading.Timer(0.2, self._finalize_drag) self.dragTimer.start() # Set the mouse cursor to a valid state return self.SetDragDestination(c4d.MOUSE_MOVE) # Call the base class Message() method to handle other messages return c4d.gui.GeUserArea.Message(self, msg, result) def _finalize_drag(self): # Delayed execution to ensure all dragged objects have been received self.isDragging = False if self.currentDragObjects: # Print all dropped files print(f"Dropped files: {self.currentDragObjects}") # Additional logic can be executed here, e.g., handling file paths or other content # Clear the object list for the next drag-and-drop operation self.currentDragObjects = [] # Redraw the user area (if UI update is needed) self.Redraw() class ExampleDialog(GeDialog): geUserArea = DropArea() def CreateLayout(self): self.SetTitle("Drag Area") if self.GroupBegin(0, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=1, rows=0, title="", groupflags=0, initw=100, inith=100): self.GroupBorderSpace(8, 8, 8, 8) self.GroupSpace(2, 2) # Add the user area gadget self.AddUserArea(GADGET_ID_GEUSERAREA, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 200, 200) # Attach the user area to the gadget self.AttachUserArea(self.geUserArea, GADGET_ID_GEUSERAREA) self.GroupEnd() return True if __name__ == "__main__": global dlg dlg = ExampleDialog() dlg.Open(dlgtype=c4d.DLG_TYPE_ASYNC, defaultw=200, defaulth=200)