How to implement an image control that can receive and generate image data drag events
-
Dear community,
We recently got a non-public support request about handling drag and drop events for a custom image control in a dialog. And while we have shown this, drag event handling, multiple times before, I thought it does not hurt to answer this publicly, as this very case - an image control - is quite common, and it does not hurt having a bit more verbose public example for it.
edit: I have updated this topic with new code and a new video, to also handle outgoing asset drag events, and talk a bit about the pros and cons of different approaches, and what you could do when not implementing a code example as I did here, and therefore have a bit more leverage room when it comes to complexity.
In general, doing all this is possible. It is just that not everything is a ready-made solution for you, where you just call a function. At some point you have to "swim" here yourself, as we cannot write your code for you, but I hope the example helps shedding some light on the probably uncessarily complicated drag handling in Cinema 4D.
Cheers,
FerdinandResult
Code
"""Provides an example for implementing a custom control that can generate and receive drag events. The example implements a dialog which holds multiple "cards" (BitmapCard) that display an image and can receive drag events. The user can drag images from the file system into these cards (file paths) or drag texture assets from the Asset Browser into these cards. The cards can also generate drag events themselves, e.g., when the user drags from a card, it will generate a drag event. Showcased (selectable via the combo box in the menu of the dialog) are three drag types: 1. File paths: Dragging a card will generate an image file path drag event, which for example could be dragged into a file path field of a shader, onto an object in the viewport, or onto other BitmapCard controls. The advantage of this approach is that we do not have to create materials or assets, but can just use the file path directly. 2. Materials: Dragging a card will generate an atom drag event, here populated with a Redshift material that has the texture of the card as an input. This material can then be dragged onto anything that accepts materials, like objects in the viewport. The disadvantage of this approach is that we have to create a material, and that we have to exactly define how the material is constructed. 3. Assets: Dragging a card will generate an asset drag event, here populated with a texture asset that has the texture of the card as an input. This asset can then be dragged onto anything that texture accepts assets, such as an object in the viewport. The disadvantage of this approach is that we have to create an asset. - The example also showcases a variation oof this approach, where we exploit a bit how the texture asset drag handling works, to avoid having to create an asset. Overview: - BitmapCard: A user area control that can receive drag events and generate drag events itself. Here you will find almost all the relevant code for drag and drop handling - BitmapStackDialog: A dialog that holds multiple BitmapCard controls and allows the user to select the drag type via a combo box. This dialog is the main entry point of the example but does not contain much relevant code itself. """ __author__ = "Ferdinand Hoppe" __copyright__ = "Copyright 2025 Maxon Computer GmbH" import os import re import c4d import maxon import mxutils # The file types that are supported to be dragged into a BitmapCard control. DRAG_IN_FILE_TYPES: list[str] = [".png", ".jpg", ".jpeg", ".tif"] # A regex to check of a string (which is meant to be a path/url) already as a scheme (like file:/// # or http:///). The protocol we are mostly checking for is "asset:///", but we keep this regex generic # to allow for other schemes as well. RE_URL_HAS_SCHEME = re.compile("^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/\/") # A non-public core message which need when we want to properly generate asset drag events. COREMSG_SETCOMMANDEDITMODE: int = 300001000 class BitmapCard(c4d.gui.GeUserArea): """Implements a control that can receive bitmap path drag events and generates drag events itself. """ def __init__(self, host: "BitmapStackDialog") -> None: """Constructs a new BitmapCard object. """ # The host dialog that holds this card. This is used to access the drag type selected in the # combo box of the dialog. self._host: BitmapStackDialog = host # The image file path and the bitmap that is displayed in this card. self._path: str | None = "" self._bitmap: c4d.bitmaps.BaseBitmap | None = None def GetMinSize(self) -> tuple[int, int]: """Called by Cinema 4d to evaluate the minimum size of the user area. """ return 250, 100 def DrawMsg(self, x1, y1, x2, y2, msg_ref): """Called by Cinema 4D to let the user area draw itself. """ self.OffScreenOn() self.SetClippingRegion(x1, y1, x2, y2) # Draw the background and then the bitmap if it exists. In the real world, we would have to # either adapt #GetMinSize to the size of the bitmap, or draw the bitmap in a way that it # fits into the user area, e.g., by scaling it down. Here we just draw it at a fixed size. self.DrawSetPen(c4d.gui.GetGuiWorldColor(c4d.COLOR_BGGADGET)) self.DrawRectangle(x1, y1, x2, y2) if self._bitmap: w, h = self._bitmap.GetSize() self.DrawBitmap(self._bitmap, 5, 5, 240, 90, 0, 0, w, h, c4d.BMP_NORMALSCALED) def Message(self, msg: c4d.BaseContainer, result: c4d.BaseContainer) -> int: """Called by Cinema 4D to handle messages sent to the user area. Here we implement receiving drag events when the user drags something onto this user area. """ # A drag event is coming in. if msg.GetId() == c4d.BFM_DRAGRECEIVE: # Get out when the drag event has been discarded. if msg.GetInt32(c4d.BFM_DRAG_LOST) or msg.GetInt32(c4d.BFM_DRAG_ESC): return self.SetDragDestination(c4d.MOUSE_FORBIDDEN) # Get out when this is not a drag type we support (we just support images). Note that we # cannot make up drag types ourselves, so when we want to send/receive complex data, we # must use the DRAGTYPE_ATOMARRAY type and pack our data into a BaseContainer attached # to the nodes we drag/send. data: dict = self.GetDragObject(msg) dragType: int = data.get("type", 0) if dragType not in [c4d.DRAGTYPE_FILENAME_IMAGE, maxon.DRAGTYPE_ASSET]: return self.SetDragDestination(c4d.MOUSE_FORBIDDEN) # Here we could optionally check the drag event hitting some target area. # yPos: float = self.GetDragPosition(msg).get('y', 0.0) # if not 0 < yPos < 50 or not self.CheckDropArea(msg, True, True): # return self.SetDragDestination(c4d.MOUSE_FORBIDDEN) # After this point, we are dealing with a valid drag event. # The drag is still going on, we just set the mouse cursor to indicate that we are # ready to receive the drag event. if msg.GetInt32(c4d.BFM_DRAG_FINISHED) == 0: return self.SetDragDestination(c4d.MOUSE_MOVE) # The drag event is finished, we can now process the data. else: # Unpack a file being dragged directly. path: str | None = None if dragType == c4d.DRAGTYPE_FILENAME_IMAGE: path = data.get("object", None) # Unpack an asset being dragged. elif dragType == maxon.DRAGTYPE_ASSET: array: maxon.DragAndDropDataAssetArray = data.get( "object", None) descriptions: tuple[tuple[maxon.AssetDescription, maxon.Url, maxon.String]] = ( array.GetAssetDescriptions()) if not descriptions: # Invalid asset but we are not in an error state. return True # Check that we are dealing with an image asset. asset: maxon.AssetDescription = descriptions[0][0] metadata: maxon.BaseContainer = asset.GetMetaData() subType: maxon.Id = metadata.Get( maxon.ASSETMETADATA.SubType, maxon.Id()) if (subType != maxon.ASSETMETADATA.SubType_ENUM_MediaImage): # Invalid asset but we are not in an error state. return True path = str(maxon.AssetInterface.GetAssetUrl(asset, True)) if not isinstance(path, str): return False # Critical failure. if (dragType == c4d.DRAGTYPE_FILENAME_IMAGE and (not os.path.exists(path) or os.path.splitext(path)[1].lower() not in DRAG_IN_FILE_TYPES)): # Invalid file type but we are not in an error state. return True self._path = path self._bitmap = c4d.bitmaps.BaseBitmap() if self._bitmap.InitWith(path)[0] != c4d.IMAGERESULT_OK: return False # Critical failure. self.Redraw() # Redraw the user area to show the new bitmap. return True # Process other messages, doing this is very important, as we otherwise break the # message chain. return c4d.gui.GeUserArea.Message(self, msg, result) def InputEvent(self, msg: c4d.BaseContainer) -> bool: """Called by Cinema 4D when the user area receives input events. Here we implement creating drag events when the user drags from this user area. The type of drag event which is initiated is determined by the drag type selected in the combo box of the dialog. """ # When this is not a left mouse button event on this user area, we just get out without # consuming the event (by returning False). if (msg.GetInt32(c4d.BFM_INPUT_DEVICE) != c4d.BFM_INPUT_MOUSE or msg.GetInt32(c4d.BFM_INPUT_CHANNEL) != c4d.BFM_INPUT_MOUSELEFT): return False # Get the type of drag event that should be generated, and handle it. dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE) return self.HandleDragEvent(msg, dragType) # --- Custom drag handling methods ------------------------------------------------------------- # I have split up thing into three methods for readability, this all could also be done directly # in the InputEvent method. def HandleDragEvent(self, event: c4d.BaseContainer, dragType: int) -> bool: """Handles starting a drag event by generating the drag data and sending it to the system. This is called when the user starts dragging from this user area. """ # This requires us to modify the document, so we must be on the main thread (technically # not true when the type is DRAGTYPE_FILENAME_IMAGE, but that would be for you to optimize). if not c4d.threading.GeIsMainThread(): raise False # Generate our drag data, either a file path, a material, or an asset. doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument() data: object = self.GenerateDragData(doc, dragType) # Now we set off the drag event. When we are dragging assets, we have to sandwich the event # in these core message calls, as otherwise the palette edit mode toggling will not work # correctly. if dragType == maxon.DRAGTYPE_ASSET: bc: c4d.BaseContainer = c4d.BaseContainer( COREMSG_SETCOMMANDEDITMODE) bc.SetInt32(1, True) c4d.SendCoreMessage(c4d.COREMSG_CINEMA, bc, 0) # When #HandleMouseDrag returns #False, this means that the user has cancelled the drag # event, one case could be that the user actually did not intend to drag, but just # clicked on the user area. # # In this case we remove the drag data we generated, as it is not needed anymore. Note that # this will NOT catch the case that the drag event is cancelled by the recipient, e.g., the # user drags a material onto something that does not accept materials. if not self.HandleMouseDrag(event, dragType, data, 0): self.RemoveDragData(doc, data) return True # Other half of the sandwich, we toggle the palette edit mode back to normal. if dragType == maxon.DRAGTYPE_ASSET: bc: c4d.BaseContainer = c4d.BaseContainer( COREMSG_SETCOMMANDEDITMODE) bc.SetInt32(1, False) c4d.SendCoreMessage(c4d.COREMSG_CINEMA, bc, 0) return True def GenerateDragData(self, doc: c4d.documents.BaseDocument, dragType ) -> str | c4d.BaseMaterial | maxon.DragAndDropDataAssetArray: """Generates the drag data for the given drag type. Each tile just encapsulates a file path, but we realize dragging them as file paths, materials, or assets. So, when the type is material or asset, the drag data must be generated from the file path. Which is why we have this method here. """ if not c4d.threading.GeIsMainThread(): raise RuntimeError( "GenerateDragData must be called from the main thread.") # The user has selected "File" as the drag type, we just return the file path. if dragType == c4d.DRAGTYPE_FILENAME_IMAGE: return self._path # The user has selected "Material" as the drag type, we create a Redshift material with the # texture as input and return that material. elif dragType == c4d.DRAGTYPE_ATOMARRAY: material: c4d.BaseMaterial = mxutils.CheckType( c4d.BaseMaterial(c4d.Mmaterial)) # Create a simple graph using that texture, and insert the material into the # active document. maxon.GraphDescription.ApplyDescription( maxon.GraphDescription.GetGraph( material, maxon.NodeSpaceIdentifiers.RedshiftMaterial), { "$type": "Output", "Surface": { "$type": "Standard Material", "Base/Color": { "$type": "Texture", # Set the texture. When our source is a file we will just have a plain # path, e.g. "C:/path/to/file.png" and we have to prefix with a scheme # (file) to make it a valid URL. When we are dealing with an asset, the # texture path will already be a valid URL in the "asset:///" scheme. "Image/Filename/Path": (maxon.Url(f"{self._path}") if RE_URL_HAS_SCHEME.match(self._path) else maxon.Url(f"file:///{self._path}")) } } }) doc.InsertMaterial(material) return [material] # The user has selected "Asset" as the drag type, we create a texture asset and return that. elif dragType == maxon.DRAGTYPE_ASSET: # So we have to cases here, either that we want truly want to generate an asset drag # event or that we just want to piggy-back onto the texture asset drag and drop # mechanism of Cinema 4D, which is what we do here. # Code for truly generating an asset drag event, where we would store the asset in the # scene repository. generateAssets: bool = False if generateAssets: # Get the scene repository and create an asset storage structure for the asset. repo: maxon.AssetRepository = doc.GetSceneRepository(True) store: maxon.StoreAssetStruct = maxon.StoreAssetStruct( maxon.Id(), repo, repo) # Now save the texture asset to the repository. url: maxon.Url = maxon.Url(self._path) name: str = url.GetName() asset, _ = maxon.AssetCreationInterface.SaveTextureAsset( url, name, store, (), True) # Create a drag array for that asset. It is important that we use the URL of the # asset (maxon.AssetInterface.GetAssetUrl) and not #url or asset.GetUrl(), as both # are physical file paths, and will then cause the drag and drop handling to use # these physical files, including the popup asking for wether the file should be # copied. We will exploit exactly this behavior in the second case. dragArray: maxon.DragAndDropDataAssetArray = maxon.DragAndDropDataAssetArray() dragArray.SetLookupRepository(repo) dragArray.SetAssetDescriptions(( (asset, maxon.AssetInterface.GetAssetUrl(asset, True), maxon.String(name)), )) return dragArray # With this case we can piggy-back onto the existing drag and drop texture asset # handling of Cinema 4D, without actually having to create an asset. THIS IS A # WORKAROUND, we could decide at any moment to change how the drag and drop # handling of assets works, possibly rendering this approach obsolete. else: # The slight disadvantage of this approach (besides it being a hack and us possibly # removing it at some point) is that we have to pay the download cost of # #self._host._dummyAssetId once. I.e., the user has to download that texture once # either by using it in the Asset Browser or by running this code. Once the asset has # been cached, this will not happen again. # # But since we search below in a 'whoever comes first' manner, what is the first # texture asset could change, and this could then be an asset which has not been # downloaded yet. So, in a more robust world we would pick a fixed asset below. But # this comes then with the burden of maintenance, as we could remove one of the # builtin texture assets at any time. So, the super advanced solution would be to # ship the plugin with its own asset database, and mount and use that. Here we could # use a hard-coded asset ID, and would have to have never pay download costs, as # the repository would be local. # Find the asset by its Id, which we have stored in the dialog. repo: maxon.AssetRepositoryInterface = maxon.AssetInterface.GetUserPrefsRepository() dummy: maxon.AssetDescription = repo.FindLatestAsset( maxon.AssetTypes.File().GetId(), self._host._dummyAssetId, maxon.Id(), maxon.ASSET_FIND_MODE.LATEST) # Setup the drag array with the asset. Note that #asset must be a valid texture # asset, so just passing maxon.AssetDescription() will not work. But the backend # for drag and drop handling will actually ignore the asset except for type checking # it, and use the URL we provided instead. dragArray: maxon.DragAndDropDataAssetArray = maxon.DragAndDropDataAssetArray() dragArray.SetLookupRepository(repo) dragArray.SetAssetDescriptions(( (dummy, maxon.Url(self._path), maxon.String(os.path.basename(self._path))), )) return dragArray else: raise ValueError(f"Unsupported drag type: {dragType}") def RemoveDragData(self, doc: c4d.documents.BaseDocument, data: str | list[c4d.BaseMaterial] | maxon.DragAndDropDataAssetArray) -> bool: """Removes generated content when the user cancels a drag event. Sometimes the user starts a drag event, but then cancels it, e.g., by pressing the escape key. In this case, we have to remove the generated content, e.g., the material or asset that we created for the drag event. """ if not c4d.threading.GeIsMainThread(): return False if isinstance(data, list): # Remove the dragged materials from the document. Since we are always generating a new # material, we can just remove them. That is in general probably not the best design, # and a real world application should avoid duplicating materials. for item in data: if isinstance(item, c4d.BaseList2D): item.Remove() elif isinstance(data, str): # Here we could technically remove a file. pass elif isinstance(data, maxon.DragAndDropDataAssetArray): # We could remove the asset, but other than for the material, there is no guarantee # that we are the only user of it, as the Asset API has a duplicate preventing # mechanism. So, the #SaveTextureAsset call above could have returned an already # existing asset. Since we could operate with a document bound repository, we could # search the whole document for asset references and only when we find none, remove # the asset. # # We use here the little hack that we actually do not create an asset, to just to piggy- # back onto the material drag and drop mechanism of assets, so we can just ignore # this case. pass return True class BitmapStackDialog(c4d.gui.GeDialog): """Implements a a simple dialog that stacks multiple BitmapCard controls. """ ID_DRAG_TYPE: int = 1002 def __init__(self): """Constructs a new ExampleDialog object. """ # The user area controls that will generate and receive drag events. self._cards: list[BitmapCard] = [ BitmapCard(host=self) for _ in range(4)] # The id for the dummy asset that used to generate 'fake' texture asset drag events. We # search for it here once, so that we do not have to do it in the __init__ of each # BitmapCard, or even worse, in the HandleDragEvent of each BitmapCard. # We could technically also store here the AssetDescription instead of the Id, but that # reference can go stale, so we just store the Id and then grab with it the asset when we # need it. Which is much faster than searching asset broadly as we do here. In the end, we # could probably also do all this in the drag handling of the BitmapCard, but a little bit # of optimization does not hurt. self._dummyAssetId: maxon.Id | None = None if not maxon.AssetDataBasesInterface.WaitForDatabaseLoading(): raise RuntimeError("Could not initialize the asset databases.") def findTexture(asset: maxon.AssetDescription) -> bool: """Finds the first texture asset in the user preferences repository. """ # When this is not a texture asset, we just continue searching. meta: maxon.AssetMetaData = asset.GetMetaData() if (meta.Get(maxon.ASSETMETADATA.SubType, maxon.Id()) != maxon.ASSETMETADATA.SubType_ENUM_MediaImage): return True # When it is a texture asset, we store it as the dummy asset and stop searching. self._dummyAssetId = asset.GetId() return False # Search for our dummy asset in the user preferences repository. repo: maxon.AssetRepositoryInterface = maxon.AssetInterface.GetUserPrefsRepository() repo.FindAssets(maxon.AssetTypes.File().GetId(), maxon.Id(), maxon.Id(), maxon.ASSET_FIND_MODE.LATEST, findTexture) def CreateLayout(self): """Called by Cinema 4D to populate the dialog with controls. """ self.SetTitle("Drag and Drop Example") self.GroupSpace(5, 5) self.GroupBorderSpace(5, 5, 5, 5) # Build the combo box in the menu bar of the dialog. self.GroupBeginInMenuLine() self.GroupBegin(1000, c4d.BFH_LEFT | c4d.BFV_TOP, cols=2) self.GroupBorderSpace(5, 5, 5, 5) self.GroupSpace(5, 5) self.AddStaticText(1001, c4d.BFH_LEFT | c4d.BFV_CENTER, name="Drag Events as:") self.AddComboBox(1002, c4d.BFH_RIGHT | c4d.BFV_TOP) self.GroupEnd() self.GroupEnd() # Add the BitmapCard controls to the dialog. for i, card in enumerate(self._cards): self.AddUserArea(2000 + i, c4d.BFH_LEFT | c4d.BFV_TOP) self.AttachUserArea(card, 2000 + i) # Add items to the combo box to select the drag type. self.AddChild(self.ID_DRAG_TYPE, c4d.DRAGTYPE_FILENAME_IMAGE, "Files") self.AddChild(self.ID_DRAG_TYPE, c4d.DRAGTYPE_ATOMARRAY, "Materials") self.AddChild(self.ID_DRAG_TYPE, maxon.DRAGTYPE_ASSET, "Assets") self.SetInt32(self.ID_DRAG_TYPE, c4d.DRAGTYPE_FILENAME_IMAGE) return True # Define a global variable of the dialog to keep it alive in a Script Manager script. ASYNC dialogs # should not be opened in production code from a Script Manager script, as this results in a # dangling dialog. Implement a command plugin when you need async dialogs in production. dlg: BitmapStackDialog = BitmapStackDialog() if __name__ == "__main__": dlg.Open(dlgtype=c4d.DLG_TYPE_ASYNC, defaultw=150, defaulth=250)
-
I cleaned up the thread, removed older code and videos, and updated the first posting, to keep this readable for future readers. The example now also discusses and realizes outgoing asset drag events.
-
What a detailed explanation! Love these technical videos!
-
So detailed example as always, this is really helpfull!
-
Hey @ferdinand ,
An extension issue, which is actually the original intention of this issue, I followed your suggestion and implemented "user seamless interaction" using a timer, but there is still one issue that troubles me.
How to exclude mouse clicks before dragging event.
- The reason I need to do this is that before clicking, there may not be any data on the hard drive to generate the material. After dragging and dropping, I want to perform a data check, but I don't want to trigger a download task every time I click
- I tried using MouseDragStart polling to determine the position of the mouse when it is released, in order to determine whether the action should be considered a click, but at this point, the print ('try to start drag event ') did trigger, but there was no HandleMouseDrag action, and there was no interaction when the mouse moved over the object.
- I followed the suggestion here and tried using GetInputState to determine mouse release, but I couldn't get rid of this limitation
Uncertain alternative methods require testing
- Generate empty material, if it is a valid dragging, check data and replace materials after successful execution, which may be feasible but requires a lot of additional code.
- Generate an empty material, but download the address in the texture path after success, and then refresh the material.
These methods certainly cannot separate clicking and dragging intuitively. Is it possible to achieve this?
get idea from https://developers.maxon.net/forum/topic/16267/marquee-selection-of-items-in-geuserarea
Cheers~
DunHoudef InputEvent(self, msg: c4d.BaseContainer) -> bool: mx = int(msg[c4d.BFM_INPUT_X]) my = int(msg[c4d.BFM_INPUT_Y]) gx,gy = mx,my mx -= self.Local2Global()["x"] my -= self.Local2Global()["y"] channel = msg[c4d.BFM_INPUT_CHANNEL] state = c4d.BaseContainer() mousex = mx mousey = my if channel == c4d.BFM_INPUT_MOUSELEFT: # res, dx, dy, channels = self.MouseDrag() # print(res, dx, dy, channels) self.MouseDragStart( c4d.BFM_INPUT_MOUSELEFT, mx, my, c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE, ) while True: res, dx, dy, channels = self.MouseDrag() if res == c4d.MOUSEDRAGRESULT_ESCAPE: self._drag_enabled = False break elif res == c4d.MOUSEDRAGRESULT_CONTINUE: mx -= dx my -= dy if not self.GetInputState(c4d.BFM_INPUT_MOUSE, c4d.BFM_INPUT_MOUSELEFT, state): break if state[c4d.BFM_INPUT_VALUE] == 0: print ("Released Left Mouse") break if dx == 0 and dy == 0: continue mousex += dx mousey += dy print("Mouse Dragging at position [%f,%f]" % (mousex, mousey)) if res == c4d.MOUSEDRAGRESULT_FINISHED: print("dra finished") # print(mx,my) # print(f"inside: {self.is_inside(mx, my)}") if self.is_inside(mx, my): print("end inside") if min(abs(gx-mx), abs(gy-my)) < 20: print("move little, treat as click") self._drag_enabled = False return True break else: self._drag_enabled = True print("end outside, drag enabled") break self.MouseDragEnd() if self._drag_enabled: print('try to start drag event') dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE) return self.HandleDragEvent(msg, dragType) return True
-
Hey @Dunhou,
I am not sure if I understand the question. For
HandleMouseDrag
, this applies:# When #HandleMouseDrag returns #False, this means that the user has cancelled the drag # event, one case could be that the user actually did not intend to drag, but just # clicked on the user area. # # In this case we remove the drag data we generated, as it is not needed anymore. Note that # this will NOT catch the case that the drag event is cancelled by the recipient, e.g., the # user drags a material onto something that does not accept materials. if not self.HandleMouseDrag(event, dragType, data, 0): self.RemoveDragData(doc, data) return True
For normal input polling, i.e., an
BFM_INPUT
event there isBFM_ACTION_INDRAG
(which is at least emitted for sliders) andBFM_DRAGSTART
andBFM_DRAGEND
. See : Gui Messages. But you are doing continuous drag polling viaMouseDragStart
anyway.So, when I boil things down here, a normal click BFM_INPUT event does not contain any drag related markers/data. Which is what you are asking? I.e., when you do input handling in your dialog, and want to sort out your own dragging events, you should look for one of these drag event markers (or their absence)
This might help: getting-only-the-last-value-of-the-slider-slide. But as stated above, I think
BFM_ACTION_INDRAG
is only emitted for sliders.Cheers,
Ferdinand -
Hey @ferdinand ,
Sorry for silence, quite busy last days, anyway, back to the question.
It should be noted that from the results, I have found an alternative way to implement it as mentioned above. I just want to review the diagram to understand the logic of the input event
I'm not questioning the rationality of this setting, I just want to know if there's a way to control it more finelyA more precise question may be control over clicking and dragging, e.g. :
- Assume Cinema consider a click down and release under 0.1sencond and mouse position didn't change more than 2 pixels as a
BFM_INPUT_MOUSELEFT
, and hold on more than 0.1s and mouse move asHandleMouseDrag
. - Under this premise, if the user accidentally touches and causes the mouse to quickly click and the cursor to move some distance, this behavior will be considered as dragging.
- I hope this behavior is seen as a click (I know this is a bit strange, I just want to try and see if it can be achieved)
- I tried to use c4d.BFM-INPUT_LUE to retrieve mouse release, but as far as I know, this needs to be executed in a while loop.
- When there is a loop in
InputEvent
, the conditions I set are judged correctly, but the drag time is masked, which means that the drag behavior cannot be triggered(e.g., no plus under the cursor, and no material created).
like our asset browser, click or drag between the green arrow will not trigger the download progress. only click download/double click/drag outside will download and assign the asset.
Cheers~
DunHou - Assume Cinema consider a click down and release under 0.1sencond and mouse position didn't change more than 2 pixels as a
-
Did you had a look at geuserarea_drag_r13.py? There is no else statement but that would be the perfect place to do something specific on single click. But there is no built-in solution you have to kind of track all by yourself as done line 294.
Cheers,
Maxime. -
Hey @m_adam ,
Yes , I had a look at this example, but I'm confusing to understand how it can apply to my case, sorry for my foolish brain.
If I keep
InputEvent
as @ferdinand did in the example,HandleMouseDrag
will return True if I have a little move, I try to stop this,
so I want to executeHandleDragEvent
only when mouse "obvious interaction outside the ua",to avoid the accompanying effect of dragging when lightly clicked (download) .def InputEvent(self, msg: c4d.BaseContainer) -> bool: """Called by Cinema 4D when the user area receives input events. Here we implement creating drag events when the user drags from this user area. The type of drag event which is initiated is determined by the drag type selected in the combo box of the dialog. """ # When this is not a left mouse button event on this user area, we just get out without # consuming the event (by returning False). if (msg.GetInt32(c4d.BFM_INPUT_DEVICE) != c4d.BFM_INPUT_MOUSE or msg.GetInt32(c4d.BFM_INPUT_CHANNEL) != c4d.BFM_INPUT_MOUSELEFT): return False # Get the type of drag event that should be generated, and handle it. dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE) return self.HandleDragEvent(msg, dragType)
my code did stop the
HandleDragEvent
execute if I didn't want to.but in this situation, even if I print and execute
HandleDragEvent
successfully, C4D does not accept drag events, which means the material cannot be assigned to the object. I don't know what is preventing this.def InputEvent(self, msg: c4d.BaseContainer) -> bool: """Called by Cinema 4D when the user area receives input events. Here we implement creating drag events when the user drags from this user area. The type of drag event which is initiated is determined by the drag type selected in the combo box of the dialog. """ # When this is not a left mouse button event on this user area, we just get out without # consuming the event (by returning False). if msg[c4d.BFM_INPUT_DEVICE] != c4d.BFM_INPUT_MOUSE and msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT: return False dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE) if msg.GetBool(c4d.BFM_INPUT_DOUBLECLICK): print("Double click detected, generating drag event") return True mx = int(msg[c4d.BFM_INPUT_X]) my = int(msg[c4d.BFM_INPUT_Y]) mx -= self.Local2Global()["x"] my -= self.Local2Global()["y"] # print(f"Start mouse: {mx}, {my}") state = c4d.BaseContainer() self.MouseDragStart(c4d.BFM_INPUT_MOUSELEFT,mx,my,c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE|c4d.MOUSEDRAGFLAGS_NOMOVE) isFirstTick = True s = 0 dua = 0 while True: res, dx, dy, channels = self.MouseDrag() if res != c4d.MOUSEDRAGRESULT_CONTINUE: break mx -= dx my -= dy self.GetInputState(c4d.BFM_INPUT_MOUSE, c4d.BFM_INPUT_MOUSELEFT, state) # mouse released, this can triggered by a click or a drag end if state[c4d.BFM_INPUT_VALUE] == 0: dua = time.perf_counter() - s print (f"Released Mouse in {dua:.4f} seconds") # drag too short, not starting drag event if dua < 0.15: return False break if isFirstTick: isFirstTick = False s = time.perf_counter() print(f"\t-- first click : {mx}, {my}") continue endState = self.MouseDragEnd() if endState == c4d.MOUSEDRAGRESULT_FINISHED: print(f"\t-- drag finished : {mx}, {my}") # drag end inside ua, not generating drag event if (0 <= mx <= 0 + self.width and 0 <= my <= 0 + self.height): return False print('Now ,try to start drag event') return self.HandleDragEvent(msg, dragType)
Cheers~
DunHou -
Hey @Dunhou,
I am still not 100% clear about what you are trying to do. But I guess what you want to do is distinguish a single-drag -click, i.e., the user is dragging something, from a single click. The issue with that is that we are in your code inside a while loop which just polls the input state as fast as it can and not in message stream, where we only get events for state changes. So, this means unless there is Speedy Gonzales at the mouse, even the quickest of single clicks will produce more than one iteration in the loop.
What is still unclear to me why you are doing all this, as knowing that the mouse is outside of the UA does not mean that we know if the user dropped the payload on an object. But this is how I would solve distinguishing a 'light click' (a single click) from a drag event.
A cleaner solution might be to let the convenance function
InputEvent
be a convenance function and move to the sourceMessage
. There you should be issue start and stop events for drag operations. But since you want to start it yourself, we are sort of in a pickle. I would have to play around a bit with the code to see if there is a better way withMessage
,Cheers,
Ferdinanddef InputEvent(self, msg: c4d.BaseContainer) -> bool: """Called by Cinema 4D when the user area receives input events. Here we implement creating drag events when the user drags from this user area. The type of drag event which is initiated is determined by the drag type selected in the combo box of the dialog. """ # When this is not a left mouse button event on this user area, we just get out without # consuming the event (by returning False). if msg[c4d.BFM_INPUT_DEVICE] != c4d.BFM_INPUT_MOUSE and msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT: return False dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE) mx = int(msg[c4d.BFM_INPUT_X]) my = int(msg[c4d.BFM_INPUT_Y]) mx -= self.Local2Global()["x"] my -= self.Local2Global()["y"] state = c4d.BaseContainer() self.MouseDragStart(c4d.BFM_INPUT_MOUSELEFT,mx,my,c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE|c4d.MOUSEDRAGFLAGS_NOMOVE) lastPos: tuple[float, float] | None = None while True: res, dx, dy, channels = self.MouseDrag() if res != c4d.MOUSEDRAGRESULT_CONTINUE: break self.GetInputState(c4d.BFM_INPUT_MOUSE, c4d.BFM_INPUT_MOUSELEFT, state) # This is how I debugged this, GetContainerTreeString (in the beta it might be already # contained) is a feature of a future version of the SDK. # print(f"{mxutils.GetContainerTreeString(state, 'BFM_')}") # State: Root (None , id = -1): # ├── BFM_INPUT_QUALIFIER (DTYPE_LONG): 0 # ├── BFM_INPUT_MODIFIERS (DTYPE_LONG): 0 # ├── BFM_INPUT_DEVICE (DTYPE_LONG): 1836021107 # ├── BFM_INPUT_CHANNEL (DTYPE_LONG): 1 # ├── BFM_INPUT_VALUE (DTYPE_LONG): 1 # ├── BFM_INPUT_VALUE_REAL (DTYPE_REAL): 0.0001 # ├── BFM_INPUT_X (DTYPE_REAL): 203.13671875 # ├── BFM_INPUT_Y (DTYPE_REAL): 88.0390625 # ├── BFM_INPUT_Z (DTYPE_REAL): 0.0 # ├── BFM_INPUT_ORIENTATION (DTYPE_REAL): 0.0 # ├── 1768977011 (DTYPE_REAL): 1.0 # ├── BFM_INPUT_TILT (DTYPE_REAL): 0.0 # ├── BFM_INPUT_FINGERWHEEL (DTYPE_REAL): 0.0 # ├── BFM_INPUT_P_ROTATION (DTYPE_REAL): 0.0 # └── BFM_INPUT_DOUBLECLICK (DTYPE_LONG): 0 # I.e., we are unfortunately neither being issued a BFM_DRAGSTART nor an # c4d.BFM_INTERACTSTART, I assume both or only emitted in the direct Message() loop. # But we can write code like this. # if state[c4d.BFM_INPUT_DOUBLECLICK]: # print(f"Double click detected at {mx}, {my}") # break # elif state[c4d.BFM_INPUT_VALUE] != 1: # print(f"Mouse button not pressed anymore at {mx}, {my}") # break # else: # print(f"Non double click at {mx}, {my}") # The issue with this is that we are here just in a loop polling the current left button # state, not inside a message function where we get a state stream. So, for a single # click, we end up with somewhat like this, and here I made sure to click really fast # Non double click at 96.8515625, 58.37109375 # Non double click at 96.8515625, 58.37109375 # Non double click at 96.8515625, 58.37109375 # Non double click at 96.8515625, 58.37109375 # Mouse button not pressed anymore at 96.8515625, 58.37109375 # And this is a short drag event. # Non double click at 84.875, 56.5859375 # Non double click at 84.875, 56.5859375 # Non double click at 84.875, 56.5859375 # Non double click at 84.875, 56.5859375 # Non double click at 84.59765625, 56.5859375 # Non double click at 83.49609375, 56.94921875 # Non double click at 83.49609375, 56.94921875 # Non double click at 82.39453125, 57.3125 # Non double click at 82.39453125, 57.3125 # Non double click at 80.74609375, 58.1328125 # Non double click at 80.74609375, 58.1328125 # Non double click at 77.7265625, 58.6328125 # ... # Non double click at -8.35546875, 80.16796875 # Non double click at -8.35546875, 80.16796875 # Non double click at -8.35546875, 80.16796875 # Mouse button not pressed anymore at -8.35546875, 80.16796875 # So they are very similar, and we cannot go by the pure logic "when the coordinates # do not change, we are in a drag event" because this is not an event stream, i.e., we # might poll the same input state multiple times, depending on how fast our #while loop # runs. # But what we could do, is postpone all actions until we see a change. In extreme cases, # where the user is swiping very fast with the mouse and then clicks on a tile, this might # fail. mx -= dx my -= dy currentPos: tuple[float, float] = (mx, my) if lastPos is None and currentPos != lastPos: lastPos = currentPos # The mouse is not being pressed anymore. if not state[c4d.BFM_INPUT_VALUE]: if currentPos != lastPos: print("Drag event") else: print("Click event") break return True
Click event Drag event Click event Click event Click event Drag event