Sorry for the delay here the full example.
"""
Copyright: MAXON Computer GmbH
Author: Maxime Adam
Description:
- Creates and attaches a GeUserArea to a Dialog.
- Creates a series of aligned squares, that can be dragged and swapped together.
Note:
- This example uses weakref, I encourage you to read https://pymotw.com/2/weakref/.
Class/method highlighted:
- c4d.gui.GeUserArea
- GeUserArea.DrawMsg
- GeUserArea.InputEvent
- GeUserArea.MouseDragStart
- GeUserArea.MouseDrag
- GeUserArea.MouseDragEnd
- c4d.gui.GeDialog
- GeDialog.CreateLayout
- GeDialog.AddUserArea
- GeDialog.AttachUserArea
Compatible:
- Win / Mac
- R13, R14, R15, R16, R17, R18, R19, R20
"""
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.
:return: The index or c4d.NOTOK if there is no parent.
:rtype: int
"""
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.
:return: The parent instance of the Square.
:rtype: c4d.gui.GeUserArea
"""
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.
:param x: X position to draw.
:param 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).
:param x: X position to draw.
:param 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.
:param x: X position to draw.
:param 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.
:return: The created square
:rtype: 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.
:param index: The index to retrieve X, Y from.
:type index: int
:return: tuple(x left position, y top position).
:return: tuple(int, int)
"""
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.
:param xIn: The position in x.
:type xIn: int
:param yIn: The position in y.
:type yIn: int
:return: The id or c4d.NOTOK (-1) if not found.
:rtype: int
"""
# 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.
:param x1: The upper left x coordinate.
:type x1: int
:param y1: The upper left y coordinate.
:type y1: int
:param x2: The lower right x coordinate.
:type x2: int
:param y2: The lower right y coordinate.
:type y2: int
:param msg_ref: The original mesage container.
:type msg_ref: c4d.BaseContainer
"""
# 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.
:return: True if a dragging operation currently occurs otherwise False.
:rtype: bool
"""
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.
:return: The square or None if there is nothing dragged.
:rtype: Union[Square, None]
"""
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.
:param msg: The event container.
:type msg: c4d.BaseContainer
:return: True if the event was handled, otherwise False.
:rtype: bool
"""
# Do nothing if its 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()
It will be included soon on Github.
Cheers,
Maxime