user_area_drag¶
Creates and attaches a GeUserArea to a Dialog. Creates a series of aligned squares, that can be dragged and swapped together.
user_area_drag.py¶
"""Creates and attaches a GeUserArea to a Dialog. Creates a series of aligned squares, that can be dragged and swapped together.
Topics:
* c4d.gui.GeUserArea
* GeUserArea.DrawMsg
* GeUserArea.InputEvent
* GeUserArea.MouseDragStart
* GeUserArea.MouseDrag
* GeUserArea.MouseDragEnd
* c4d.gui.GeDialog
* GeDialog.CreateLayout
* GeDialog.AddUserArea
* GeDialog.AttachUserArea
Note:
- This example uses weakref, I encourage you to read https://pymotw.com/2/weakref/.
"""
__author__ = "Maxime Adam"
__copyright__ = "Copyright (C) 2026 MAXON Computer GmbH"
__license__ = "Apache-2.0 License"
__version__ = "R13"
import c4d
import weakref
# Global variable to determine the Size of our Square.
SIZE = 100
class Square(object):
"""Abstract class to represent a Square in a GeUserArea."""
def __init__(self, geUserArea, index):
self.index = index # The initial square index (only used to differentiate square between each others)
self.w = SIZE # The width of the square
self.h = SIZE # The height of the square
self.col = c4d.Vector(0.5) # The default color of the square
self.parentGeUserArea = weakref.ref(geUserArea) # A weak reference to the host GeUserArea
def GetParentedIndex(self):
"""Returns the current index in the parent list.
Returns:
int: The index or c4d.NOTOK if there is no parent.
"""
parent = self.GetParent()
if parent is None:
return c4d.NOTOK
return parent._squareList.index(self)
def GetParent(self):
"""Retrieves the parent instance, stored in the weakreaf self.parentGeUserArea.
Returns:
c4d.gui.GeUserArea: The parent instance of the Square.
"""
if self.parentGeUserArea:
geUserArea = self.parentGeUserArea()
if geUserArea is None:
raise RuntimeError("GeUserArea parent is not valid.")
return geUserArea
return None
def DrawNormal(self, x, y):
"""Called by the parent GeUserArea to draw the Square normally.
Args:
x: X position to draw.
y: Y position to draw.
"""
geUserArea = self.GetParent()
geUserArea.DrawSetPen(self.col)
geUserArea.DrawRectangle(x, y,
x + self.w, y + self.h)
geUserArea.DrawText(str(self.index), x, y)
def DrawDraggedInitial(self, x, y):
"""Called by the parent GeUserArea when the Square is dragged
with the initial position (same coordinate than DrawNormal).
Args:
x: X position to draw.
y: Y position to draw.
"""
geUserArea = self.GetParent()
geUserArea.DrawBorder(c4d.BORDER_ACTIVE_1,
x, y,
x + self.w, y + self.h)
def DrawDragged(self, x, y):
"""Called by the parent GeUserArea when the Square is dragged
with the current mouse position.
Args:
x: X position to draw.
y: Y position to draw.
"""
geUserArea = self.GetParent()
geUserArea.DrawSetPen(c4d.Vector(1))
geUserArea.DrawRectangle(int(x), int(y),
int(x + self.w), int(y + self.h))
geUserArea.DrawText(str(self.index), int(x + (SIZE / 2.0)), int(y + (SIZE / 2.0)))
class DraggingArea(c4d.gui.GeUserArea):
"""Custom implementation of a GeUserArea that creates 4 squares and lets you drag them."""
def __init__(self):
self._squareList = [] # Stores a list of Square that will be draw in the GeUserArea.
self.draggedObj = None # None if not dragging, Square if dragged
self.clickedPos = None # None if not dragging, tuple(X, Y) if dragged
# Creates 4 squares
self.CreateSquare()
self.CreateSquare()
self.CreateSquare()
self.CreateSquare()
# ===============================
# Square management
# ===============================
def CreateSquare(self):
"""Creates a square that will be draw later.
Returns:
Square: The created square
"""
square = Square(self, len(self._squareList))
self._squareList.append(square)
return square
def GetXYFromId(self, index):
"""Retrieves the X, Y op, left position according to an index in order.
This produces an array of Square correctly aligned.
Args:
index (int): The index to retrieve X, Y from.
Returns:
tuple(x left position, y top position).
"""
x = SIZE * index
xPadding = 5 * index
x += xPadding
y = 5
return x, y
def GetIdFromXY(self, xIn, yIn):
"""Retrieves the square id stored in self._squareList according to its normal (not dragged) position.
Args:
xIn (int): The position in x.
yIn (int): The position in y.
Returns:
int: The id or c4d.NOTOK (-1) if not found.
"""
# We could optimize the method by reversing the algorithm from GetXYFromID,
# But for now we just iterate all squares and see which one is correct.
for squareId, square in enumerate(self._squareList):
x, y = self.GetXYFromId(squareId)
if x < xIn < x + SIZE and y < yIn < y + SIZE:
return squareId
return c4d.NOTOK
# ===============================
# Drawing management
# ===============================
def DrawSquares(self):
"""Called in DrawMsg.
Draws all squares contained in self._squareList
"""
for squareId, square in enumerate(self._squareList):
x, y = self.GetXYFromId(squareId)
if square is not self.draggedObj:
square.DrawNormal(x, y)
else:
square.DrawDraggedInitial(x, y)
def DrawDraggedSquare(self):
"""Called in DrawMsg.
Draws the dragged squares
"""
if self.draggedObj is None or self.clickedPos is None:
return
x, y = self.clickedPos
self.draggedObj.DrawDragged(x, y)
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_ref (c4d.BaseContainer): The original mesage container.
msg:
"""
# Initializes draw region
self.OffScreenOn()
self.SetClippingRegion(x1, y1, x2, y2)
# Get default Background color
defaultColorRgbDict = self.GetColorRGB(c4d.COLOR_BG)
defaultColorRgb = c4d.Vector(defaultColorRgbDict["r"], defaultColorRgbDict["g"], defaultColorRgbDict["b"])
defaultColor = defaultColorRgb / 255.0
self.DrawSetPen(defaultColor)
self.DrawRectangle(x1, y1, x2, y2)
# First draw pass, we draw all not dragged object
self.DrawSquares()
# Last draw pass, we draw the dragged object, this way dragged square is drawn on top of everything.
self.DrawDraggedSquare()
# ===============================
# Dragging management
# ===============================
@property
def isCurrentlyDragged(self):
"""Checks if a dragging operation currently occurs.
Returns:
bool: True if a dragging operation currently occurs otherwise False.
"""
return self.clickedPos is not None and self.draggedObj is not None
def GetDraggedSquareWithPosition(self):
"""Retrieves the clicked square during a drag event from the click position.
Returns:
Union[Square, None]: The square or None if there is nothing dragged.
"""
if self.clickedPos is None:
return None
x, y = self.clickedPos
squareId = self.GetIdFromXY(x, y)
if squareId == c4d.NOTOK:
return None
return self._squareList[squareId]
def InputEvent(self, msg):
"""Called by Cinema 4D, when there is a user interaction (click) on the GeUserArea.
This is the place to catch and handle drag interaction.
Args:
msg (c4d.BaseContainer): The event container.
Returns:
bool: True if the event was handled, otherwise False.
"""
# Do nothing if it's not a left mouse click event
if msg[c4d.BFM_INPUT_DEVICE] != c4d.BFM_INPUT_MOUSE and msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT:
return True
# Retrieves the initial position of the click
mouseX = msg[c4d.BFM_INPUT_X]
mouseY = msg[c4d.BFM_INPUT_Y]
# Initializes the start of the dragging process (needs to be initialized with the original mouseX, mouseY).
self.MouseDragStart(c4d.KEY_MLEFT, mouseX, mouseY, c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE | c4d.MOUSEDRAGFLAGS_NOMOVE)
isFirstTick = True
# MouseDrag needs to be called all time to update information about the current drag process.
# This allow to catch when the mouse is released and leave the infinite loop.
while True:
# Updates the current mouse information
result, deltaX, deltaY, channels = self.MouseDrag()
if result != c4d.MOUSEDRAGRESULT_CONTINUE:
break
# The first tick is ignored as deltaX/Y include the mouse clicking behavior with a deltaX/Y always equal to 4.0.
# However it can be useful to do some initialization or even trigger single click event
if isFirstTick:
isFirstTick = False
continue
# If the mouse didn't move, don't need to do anything
if deltaX == 0.0 and deltaY == 0.0:
continue
# Updates mouse position with the updated delta
mouseX -= deltaX
mouseY -= deltaY
self.clickedPos = mouseX, mouseY
# Retrieves the clicked square
square = self.GetDraggedSquareWithPosition()
# Defines the draggedObj only if the user clicked on a square and is not yet already defined
if square is not None and self.draggedObj is None:
self.draggedObj = square
# Redraw the GeUserArea (it will call DrawMsg)
self.Redraw()
# Asks why we leave the while loop
endState = self.MouseDragEnd()
# If the drag process was ended because the user releases the mouse.
# Note that while we are not anymore really in the Drag Pooling, from our implementation we consider we are still
# and don't clear directly the data, so self.clickedPos and self.draggedObj still refer to the last tick of the
# MouseDrag pool and we will clear it once we don't need anymore those data (after this if statement).
if endState == c4d.MOUSEDRAGRESULT_FINISHED:
# Checks a dragged object is set
# in case of a simple click without mouse movement nothing has to be done.
if self.isCurrentlyDragged:
# Retrieves the initial index of the dragged object.
currentIndex = self.draggedObj.GetParentedIndex()
# Retrieves the index where the drag operation ended. If we find an ID, swap both items.
releasedSquare = self.GetDraggedSquareWithPosition()
if releasedSquare is not None:
targetIndex = releasedSquare.GetParentedIndex()
# Swap items only if source index and target index are different
if targetIndex != currentIndex:
self._squareList[currentIndex], self._squareList[targetIndex] = self._squareList[targetIndex], \
self._squareList[currentIndex]
# In case the user release the mouse not on another square.
# Swaps the current square to either the first position or last position.
else:
# if current Index is already the first one, make no sense to inserts it
if currentIndex != 0:
# If the X position is before the X position of the first square
# Removes and inserts it back to the first position.
if self.clickedPos[0] < self.GetXYFromId(0)[0]:
self._squareList.remove(self.draggedObj)
self._squareList.insert(0, self.draggedObj)
# Retrieves the last index
lastIndex = len(self._squareList) - 1
# if current Index is already the last one, make no sense to insert it
if currentIndex != lastIndex:
if self.clickedPos[0] > self.GetXYFromId(lastIndex)[0] + SIZE:
# If the X position is after the X position of the last square (and its size)
# Removes and inserts it back to the last position.
self._squareList.remove(self.draggedObj)
self._squareList.insert(lastIndex, self.draggedObj)
# Cleanup and refresh information if we dragged something
if self.clickedPos is not None or self.draggedObj is not None:
self.clickedPos = None
self.draggedObj = None
self.Redraw()
return True
class MyDialog(c4d.gui.GeDialog):
"""Creates a Dialog with only a GeUserArea within."""
def __init__(self):
# It's important to stores our Python implementation instance of the GeUserArea in class variable,
# This way we are sure the GeUserArea instance live as long as the GeDialog.
self.area = DraggingArea()
def CreateLayout(self):
"""This method is called automatically when Cinema 4D Create the Layout (display) of the Dialog."""
self.AddUserArea(1000, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT)
self.AttachUserArea(self.area, 1000)
return True
def main():
# Creates a new dialog
dialog = MyDialog()
# Opens it
dialog.Open(dlgtype=c4d.DLG_TYPE_MODAL_RESIZEABLE, defaultw=500, defaulth=500)
if __name__ == '__main__':
main()