py_tool_ui¶
Demonstrates how to implement a ToolData with a linkBox where it’s possible to drag and drop an object, which is then cloned and added to the document in a random position.
DETAILS
The tool accepts a dragged object, clones it when selected, and inserts the clone into the document at a random position.
To run this example, place the file in the Cinema 4D plugins folder and restart Cinema 4D.
Topics:
- Building a ToolData link-box interface
- Cloning dropped objects into the document
Files¶
py_tool_ui.pyp¶
"""Demonstrates how to implement a ToolData with a linkBox where it's possible to drag and drop an object, which is then cloned and added to the document in a random position.
The tool accepts a dragged object, clones it when selected, and inserts the clone into the document at a random position.
To run this example, place the file in the Cinema 4D plugins folder and restart Cinema 4D.
Topics:
- Building a ToolData link-box interface
- Cloning dropped objects into the document
"""
__copyright__ = "Copyright 2026, MAXON Computer"
__author__ = "Manuel MAGALHAES, Maxime Adam"
__date__ = "25/08/2026"
__license__ = "Apache-2.0 license"
__version__ = "15.0.0"
import c4d
import random
# Be sure to use a unique ID obtained from https://plugincafe.maxon.net/c4dpluginid_cp
TOOLDATA_UI_PLUGIN_ID = 1054512
ID_CUSTOM_LINK = 1000
ID_BUTTON_APPLY = 1001
ID_BUTTON_RESET = 1002
class ToolDemoDialog(c4d.gui.SubDialog):
"""Creates a Dialog to show the ToolData options.
This dialog will be displayed in the Attribute Manager so this means the ToolDemoDialog
will be instantiate each time the tool is activate and destruct when the AM change its mode.
"""
def __init__(self, sharedDict):
super(ToolDemoDialog, self).__init__()
# Checks if the argument passed is a dictionary
if not isinstance(sharedDict, dict):
raise TypeError("sharedDict is not a dict.")
# Since its a dictionary it will not be a copy of the initial dict, but a reference of it.
# This means if we change the data in the SubDialog it will also change it
# in the tool data and vise versa.
self.sharedDict = sharedDict
# This variable will store the LinkBox custom gui defined in CreateLayout.
self.linkGadget = None
def CreateLayout(self):
"""This Method is called automatically when Cinema 4D Create the Layout (display) of the Dialog."""
if self.GroupBegin(0, c4d.BFH_SCALEFIT, 1, 0, ""):
if self.GroupBegin(0, c4d.BFH_SCALEFIT, 2, 0, ""):
self.AddStaticText(0, c4d.BFH_RIGHT, 0, 12, "Object:", c4d.BORDER_WITH_TITLE_BOLD)
# Adds a BaseContainer so the linkBox will only accept sphere and cube as
# dropped objects.
bc = c4d.BaseContainer()
accept = c4d.BaseContainer()
accept.SetInt32(c4d.Ocube, 1)
accept.SetInt32(c4d.Osphere, 1)
bc.SetContainer(c4d.DESC_ACCEPT, accept)
self.linkGadget = self.AddCustomGui(
ID_CUSTOM_LINK, c4d.CUSTOMGUI_LINKBOX, "", c4d.BFH_LEFT, 240, 12, bc)
self.GroupEnd()
# Adds some buttons
if self.GroupBegin(0, c4d.BFH_SCALEFIT, 3, 0, ""):
self.AddButton(ID_BUTTON_APPLY, c4d.BFH_LEFT,
c4d.gui.SizePix(50), c4d.gui.SizePix(10), "Apply")
self.AddButton(ID_BUTTON_RESET, c4d.BFH_LEFT,
c4d.gui.SizePix(50), c4d.gui.SizePix(10), "Reset")
self.GroupEnd()
self.GroupEnd()
return True
def InitValues(self):
"""This Method is called automatically after the GUI is initialized."""
# This is important to init the LinkBox gadget with the link stored in the tool data.
# Otherwise, the link will be erase when the ui is refreshed.
self.linkGadget.SetLink(self.sharedDict['link'])
return True
def Command(self, commandID, msg):
"""This Method is called automatically when the user clicks on a gadget and/or changes its value this function will be called.
It is also called when a string menu item is selected.
Args:
commandID (int): The ID of the gadget that triggered the event.
msg (c4d.BaseContainer): The original message container
Returns:
bool: False if there was an error, otherwise True.
"""
# If the user interact with the LinkBox
if commandID == ID_CUSTOM_LINK:
# Retrieves the data from the LinkBox
obj, objName, objID = self.GetDataFromLinkBox()
# Stores them in the shared dict
self.sharedDict['link'] = obj
self.sharedDict['linkGUID'] = objID
# If the user press the Apply Button we send a Message to the tool to
# inform to perform an action
elif commandID == ID_BUTTON_APPLY:
if self.sharedDict['link'] is None:
c4d.gui.MessageDialog("No object is currently defined.")
return True
# Retrieve the c4d.plugins.BasePlugin corresponding to our ToolData.
# Of course we could have stored our instance of ToolData in the shared dict.
tool = c4d.plugins.FindPlugin(TOOLDATA_UI_PLUGIN_ID, c4d.PLUGINTYPE_TOOL)
if tool is None:
raise RuntimeError("Unable to find the registered tool.")
# Sends a message to the ToolData with a unique ID
# Ideally a new plugin should be used so the ToolData plugin ID and the Message Plugin ID is very distinct.
# In the end it does not matter, you could use whatever kind of message
# you want such as MSG_BASECONTAINER.
tool.Message(TOOLDATA_UI_PLUGIN_ID)
# If the user press the Reset Button
elif commandID == ID_BUTTON_RESET:
# Resets the data and the UI.
self.linkGadget.SetLink(None)
self.sharedDict["link"] = None
self.sharedDict["linkGUID"] = None
return True
def GetDataFromLinkBox(self):
"""Retrieves the object linked on the linkBox, its name, and its GUID
Returns:
tuple(c4d.BaseObject, str, int): A tuple with the linked object, the object's Name and the object's GUID.
"""
# Retrieves the gadget added in CreateLayout
if self.linkGadget is None:
raise RuntimeError("couldn't retrieve the linkBox gadget")
# Retrieves the object linked
linkedObject = self.linkGadget.GetLink()
if linkedObject is None:
return None, None, None
# Retrieves object's information.
objName = linkedObject.GetName()
objGUID = linkedObject.GetGUID()
return linkedObject, objName, objGUID
class ToolDataWithUiExample(c4d.plugins.ToolData):
"""Inherit from ToolData to create your own tool"""
def __init__(self):
# This dictionary will be passed to the SubDialog. This will let us storing persistent data
# This means if yo change the data in the SubDialog it will also change it
# in the tool data and vise versa.
self.sharedDict = {'link': None, 'linkGUID': None}
def GetState(self, doc):
"""Called by Cinema 4D to know if the tool can be used currently
Args:
doc (c4d.documents.BaseDocument): The current active document.
Returns:
c4d.CMD_ENABLED to enable, or **False** to disable.
"""
return c4d.CMD_ENABLED
def Message(self, doc, data, msgType, t_data):
"""Called when the tool receives messages.
Args:
doc (c4d.documents.BaseDocument): The current document.
data (c4d.BaseContainer): The message data.
msgType (int): The Message type.
t_data (Any): Depends on `type`.
Returns:
any: Depends on the message `type`.
"""
# If we received a message with the plugin ID(since its a unique ID it
# come from us, see line 115)
if msgType == TOOLDATA_UI_PLUGIN_ID:
# Creates a clones with the linked object
self.CreateAClone(self.sharedDict["link"], createUndo=True)
return True
def MouseInput(self, doc, data, bd, win, msg):
"""Called by Cinema 4D, when the user click on the viewport and the tool is active.
The main place to do mouse interaction from the viewport.
Args:
doc (c4d.documents.BaseDocument): The current active document.
data (c4d.BaseContainer): The tool settings container.
bd (c4d.BaseDraw): The BaseDraw object of the active editor view.
win (c4d.gui.EditorWindow): The EditorWindow object for the active editor view.
msg (c4d.BaseContainer): The original message container.
Returns:
bool: False** if a problem occurred during this function.
"""
# Leaves if its not the left mouse click that occurred
if msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT:
return True
# Check if there's an object linked in the ui
if self.sharedDict["link"] is None or not self.sharedDict["link"].IsAlive():
return True
# Creates a clones with the linked object.
# Since we are in the MouseInput we don't need to call doc.StartUndo and doc.EndUndo.
self.CreateAClone(self.sharedDict["link"], createUndo=False)
return True
def AllocSubDialog(self, bc):
"""Called by Cinema 4D To allocate the Tool Dialog Option.
Args:
bc (c4d.BaseContainer): Currently not used.
Returns:
ToolDemoDialog: The allocated sub dialog.
"""
return ToolDemoDialog(self.sharedDict)
def CreateAClone(self, op, createUndo):
"""Creates a clone of the passed object and insert it in the document.
Args:
op (Union[c4d.BaseObject, None]): The object that must be cloned, if None nothing happens.
createUndo (bool): True if the method should call StartUndo/EndUndo otherwise False.
Returns:
c4d.BaseObject: The cloned object already inserted.
"""
if op is None:
return
doc = op.GetDocument()
if doc is None:
raise RuntimeError("The object is not into a document.")
# Creates a clone of the linked Object
cloned = op.GetClone()
if cloned is None:
raise RuntimeError("Failed to copy the object.")
if createUndo:
doc.StartUndo()
# Adds an undo step
doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, cloned)
# Inserts the cloned object in the active document.
doc.InsertObject(cloned)
# Creates some random position
x = round(random.uniform(-1000.0, 1000.0), 3)
y = round(random.uniform(-1000.0, 1000.0), 3)
z = round(random.uniform(-1000.0, 1000.0), 3)
# Sets the position of the object
newPos = c4d.Vector(x, y, z)
cloned.SetRelPos(newPos)
if createUndo:
doc.EndUndo()
# Pushes an update event to Cinema 4D
c4d.EventAdd()
if __name__ == "__main__":
# Registers the tool plugin. PLUGINFLAG_HIDEPLUGINMENU hides the natural menu entry of this
# plugin. Omit this flag or replace it by 0 when you do not have custom menu handling as the SDK
# has(see py_menu).
c4d.plugins.RegisterToolPlugin(id=TOOLDATA_UI_PLUGIN_ID,
str="Py SDK - ToolData With UI",
info=c4d.PLUGINFLAG_HIDEPLUGINMENU,
icon=None,
help="A Tool with a SubDialog UI",
dat=ToolDataWithUiExample())