Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Register
    • Login
    1. Home
    2. Neekoe
    N
    • Profile
    • Following 3
    • Followers 0
    • Topics 8
    • Posts 26
    • Best 0
    • Controversial 0
    • Groups 0

    Neekoe

    @Neekoe

    0
    Reputation
    8
    Profile views
    26
    Posts
    0
    Followers
    3
    Following
    Joined Last Online

    Neekoe Unfollow Follow

    Latest posts made by Neekoe

    • RE: Storage issues with plugin settings

      Thank you for @Dunhou answer. It was ultimately completed in JSON format

      posted in Cinema 4D SDK
      N
      Neekoe
    • Storage issues with plugin settings

      Hello everyone, I have encountered a problem. When I select a folder through the button, it will update the. c4d type files in the folder to TREELIST, including the path in an EDITNEXT that I set. However, after I restart the plugin or C4D, these contents are missing. Do you have any methods or documentation to refer to? be deeply grateful
      Snipaste_2025-02-19_00-15-10.png
      Snipaste_2025-02-19_00-16-40.png

      posted in Cinema 4D SDK 2023 python windows
      N
      Neekoe
    • RE: Retrieve. c4d from folders and subfolders and update tree list

      @Neekoe
      I seem to have solved my problem again. Next, I will share my code, which is complete. If you bind it to a button, it will be a good list refresh (although it is very poor quality)

      folder_path = c4d.storage.LoadDialog(c4d.FILESELECT_DIRECTORY, "Select Folder", c4d.FILESELECT_DIRECTORY)
                  
                      if folder_path:
                          # 如果选择了文件夹,更新列表
                          # self.UpdateTreeView(folder_path)
                          # 更新显示的文件夹路径
                          self.SetString(self.ID_FILEPATH, folder_path)
                          
      
                          self._listView.listOfProjectPreset = list()  # 刷新列表,因此我们只想加载一次内容
                          self.folderpath = folder_path
                          # 检查文件夹路径有效性
                          if self.folderpath is not None and self.folderpath != "" and os.path.exists(self.folderpath):
                              # 获取指定文件夹内的所有文件
                              filelist = os.listdir(self.folderpath)
      
                              # 用来存储最终文件名列表
                              final_names = []
                              # 使用 os.walk 遍历文件夹及子文件夹
                              for root, dirs, files in os.walk(self.folderpath):
                                  for file in files:
                                      if file.endswith(".c4d"):  # 判断文件扩展名
                                          # 获取文件的完整路径
                                          file_path = os.path.join(root, file)
                                          # 获取文件的修改时间
                                          mtime = os.path.getmtime(file_path)
                                          final_names.append(file)  # 将文件名添加到 final_names 列表
      
                              # 按文件修改时间排序,使用 mtime 作为排序的依据
                              final_names.sort(key=lambda x: x[1])  # 按修改时间升序排序
                              # 如果想要按降序排序,可以使用:
                              # final_names.sort(key=lambda x: x[1], reverse=True)
                              
                              # 打印最终的文件名列表
                              for name in final_names:
                                  print(name)  # 逐个打印文件名
      
      
                              # 将 ProjectPreset 对象添加到 listOfProjectPreset 列表
                              # 假设 ProjectPreset 是一个类,用来包装文件名
                              for name in final_names:
                                  projecname = ProjectPreset(name)  # 为每个文件名创建一个 ProjectPreset 对象
                                  self._listView.listOfProjectPreset.append(projecname)  # 将对象添加到 listOfProjectPreset 列表中
      
                          # 刷新 TreeView
                          self._treegui.Refresh()
                      return True
      
      posted in Cinema 4D SDK
      N
      Neekoe
    • Retrieve. c4d from folders and subfolders and update tree list

      Hello everyone, I have a question about treeview. I have set a button (ID_TREEBUTTON2) to select the path, and the tree list will retrieve the. c4d file from the folder (including subfolders) of the set path, and update the file name to the list
      In addition, I found that every time I open the plugin, the files in my list are missing (I wrote some content, and when I use the (IDUTREEBUTTON1) button to save the project, the current project name will be automatically updated and updated to the list, so I will find this problem)
      I have written a loadfile and printed it out, but I am unable to add it to the tree list, which confuses me

      import c4d
      import os
      from c4d import gui, plugins, storage
      
      PLUGINPATH = os.path.dirname(__file__)
      PLUGINID = 1064765
      PLUGINNAME = "Work"
      PLUGINHELP = ""
      
      
      ID_OTHER = 1124
      ID_ICON = 1125
      ID_NAME = 1126
      
      # ///////////////// 插件界面图片加载
      class LogoUserArea(c4d.gui.GeUserArea):
          def __init__(self):
              super().__init__()
              self.bmp = c4d.bitmaps.BaseBitmap()
              self.image_path = os.path.join(PLUGINPATH, "res", "logo.png")  # 确保路径正确
              result, ismovie = self.bmp.InitWith(self.image_path)
      
              if result != c4d.IMAGERESULT_OK:
                  self.bmp = None  # 如果加载失败,设置为空
              else:
                  self.img_width = self.bmp.GetBw()
                  self.img_height = self.bmp.GetBh()
                  self.aspect_ratio = self.img_width / self.img_height  # 计算图片宽高比
      
          def GetMinSize(self):
              """返回最小尺寸,确保图片初始化时的大小"""
              return 300, int(300 / self.aspect_ratio)  # 按比例设置初始高度
      
          def DrawMsg(self, x1, y1, x2, y2, msg):
                  """按比例缩放图片,确保不变形"""
                  if not self.bmp:
                      return
      
                  area_width = x2 - x1
                  area_height = y2 - y1
                  area_ratio = area_width / area_height
      
                  if area_ratio > self.aspect_ratio:
                      new_height = area_height
                      new_width = int(new_height * self.aspect_ratio)
                  else:
                      new_width = area_width
                      new_height = int(new_width / self.aspect_ratio)
      
                  x_offset = (area_width - new_width) // 2
                  y_offset = (area_height - new_height) // 2
      
                  new_width = min(new_width, area_width)
                  new_height = min(new_height, area_height)
      
                  self.DrawBitmap(self.bmp, x_offset, y_offset, new_width, new_height, 0, 0, self.img_width, self.img_height, c4d.BMP_NORMALSCALED | c4d.BMP_ALLOWALPHA)
      
      
      # ///////////////// TreeView界面
      class ProjectPreset(object):
      # 类,它表示一个项目,也就是我们列表中的 Item
          projectptPath = "projectptPath"
          otherData = "OtherData"
          
          _selected = False
        
          def __init__(self, projectptPath) :
              self.projectptPath = projectptPath
              self.otherData += projectptPath
              listName = os.path.basename(projectptPath)
              self.ProName = projectptPath  # 确保存在 name 属性
        
          @property
          def IsSelected(self) :
              return self._selected
        
          def Select(self) :
              self._selected = True
        
          def Deselect(self) :
              self._selected = False
        
          def __repr__(self) :
              return str(self)
        
          def __str__(self) :
              return self.projectptPath
          
      
      class ListView(c4d.gui.TreeViewFunctions) :
          
          def __init__(self) :
              
      
              self.listOfProjectPreset = list()# 存储我们需要在此列表中显示的所有对象
              self.c4dPath = r"C:\Users\86159\Desktop\test"  # 设置路径
              """
          def __init__(self):
              self.listOfProjectPreset = list() # Store all objects we need to display in this list
      
          #TreeView 的整个概念是覆盖一些函数。因此,请务必阅读文档以了解根据您的需要覆盖哪些函数。
          # 因此,我们将为 TreeView 定义一些更通用的选项
          def IsResizeColAllowed(self, root, userdata, lColID) :
              return True
        
          def IsTristate(self, root, userdata) :
              return False
        
          def GetColumnWidth(self, root, userdata, obj, col, area) :
              return 80  # 所有的初始宽度都相同
        
          def IsMoveColAllowed(self, root, userdata, lColID) :
              # 允许用户移动所有列。
              # 必须在 AddCustomGui 的容器中设置 TREEVIEW_MOVE_COLUMN。
              return True
          def GetFirst(self, root, userdata) :
              # 返回层次结构中的第一个元素,如果没有元素,则返回 None
              rValue = None if not self.listOfProjectPreset else self.listOfProjectPreset[0]
              return rValue
          # 然后我们必须处理 GetDown(对于子对象,因为在我们的例子中它是一个简单的列表,我们返回 None)
          # GetNext 是当前 Object 之后的 Object,GetPred 是当前 Object 之前的 Object。
          # 我们还必须覆盖 GetID 才能唯一标识列表中的对象
      
          def GetDown(self, root, userdata, obj) :
              # 返回节点的子节点,因为我们只需要一个列表,所以每次都返回 None
              return None
        
          def GetNext(self, root, userdata, obj) :
              # 返回 arg:'obj' 之后要显示的下一个对象
              rValue = None
              currentObjIndex = self.listOfProjectPreset.index(obj)
              nextIndex = currentObjIndex + 1
              if nextIndex < len(self.listOfProjectPreset) :
                  rValue = self.listOfProjectPreset[nextIndex]
        
              return rValue
        
          def GetPred(self, root, userdata, obj) :
              # 返回要在 arg 之前显示的上一个 Object:'obj' 
              rValue = None
              currentObjIndex = self.listOfProjectPreset.index(obj)
              predIndex = currentObjIndex - 1
              if 0 <= predIndex < len(self.listOfProjectPreset) :
                  rValue = self.listOfProjectPreset[predIndex]
        
              return rValue
      		
          def GetId(self, root, userdata, obj) :
              # 返回 TreeView 中元素的唯一 ID
              return obj.GetUniqueID()
          
          def Select(self, root, userdata, obj, mode) :
              # 在用户选择元素时调用
              if mode == c4d.SELECTION_NEW:
                  for tex in self.listOfProjectPreset:
                      tex.Deselect()
                  obj.Select()
              elif mode == c4d.SELECTION_ADD:
                  obj.Select()
              elif mode == c4d.SELECTION_SUB:
                  obj.Deselect()
        
          def IsSelected(self, root, userdata, obj) :
              # 返回: 如果选择了 *obj*,则为 True,否则为 False
              return obj.IsSelected
          
          def InsertObject(self, root, userdata, obj, dragtype, dragobject, insertmode, bCopy):
              self.listOfProjectPreset.append(dragobject)
              
              return True
          
          def SetCheck(self, root, userdata, obj, column, checked, msg) :
              # 当用户单击'c4d.LV_CHECKBOX' 列中对象的复选框时调用
              if checked:
                  obj.Select()
              else:
                  obj.Deselect()
        
          def IsChecked(self, root, userdata, obj, column) :
              # 返回: (int) : *obj* 的指定 *column* 中复选框的状态
              if obj.IsSelected:
                  return c4d.LV_CHECKBOX_CHECKED | c4d.LV_CHECKBOX_ENABLED
              else:
                  return c4d.LV_CHECKBOX_ENABLED
      
          # 然后LV_TREE元素将通过调用 GetName 来检查 Object name    
          def GetName(self, root, userdata, obj) :
              # 返回要为 arg:'obj' 显示的名称, 仅对type LV_TREE类型的列调用
              
              return str(obj) # Or obj.texturePath
      
          # 最后,LV_USER允许我们执行一些绘图功能,就像您在 GeUserArea 中所做的那样
          # 使用 DrawCell 绘制您想要的任何内容(在我们的例子中是文本)。看看 DrawInfo-Dict
          def DrawCell(self, root, userdata, obj, col, drawinfo, bgColor):
              # 在 TreeView 的单元格中绘制内容 LV_USER
              if col == ID_NAME:
                  # print("Drawcell obj: ", str(obj))
                  geUserArea = drawinfo["frame"]
                  ICON_SIZE = drawinfo["height"]
                  TEXT_SPACER = 6
                  bgColor = c4d.COLOR_SB_TEXTHG1 if drawinfo["line"] % 2 else c4d.COLOR_SB_TEXTHG2
      
                  # 使用自定义图标
                  icon = c4d.bitmaps.BaseBitmap()
                  path = os.path.join(PLUGINPATH, "res", "icon.tif")
                  if not icon.InitWith(path):
                      print("图标加载失败,请检查路径!")
                      return False  # 图标加载失败,退出
      
                  geUserArea.DrawSetPen(bgColor)
                  geUserArea.DrawBitmap(icon, drawinfo["xpos"], drawinfo["ypos"], ICON_SIZE, ICON_SIZE, 0, 0, icon.GetBw(), icon.GetBh(), c4d.BMP_ALLOWALPHA)
      
                  # 绘制文本
                  name = str(obj)
                  fontHeight = geUserArea.DrawGetFontHeight()
                  fontWidth = geUserArea.DrawGetTextWidth(name)
      
                  x = drawinfo["xpos"] + ICON_SIZE + TEXT_SPACER
                  y = drawinfo["ypos"] + (ICON_SIZE - fontHeight) / 2
                  
                  txtColor = c4d.COLOR_SB_TEXT_ACTIVE1 if obj.IsSelected else c4d.COLOR_SB_TEXT
                  geUserArea.DrawSetTextCol(txtColor, bgColor)
      
      
      
      
          
          # 处理保存工程操作
          def save_project(self, doc, file_path):
              doc = c4d.documents.GetActiveDocument()
      
              # 弹出保存文件对话框,获取文件路径
              save_path = c4d.gui.SaveFileDialog(c4d.FILESELECTTYPE_SCENE, "Save Scene As", "", "")
              if not save_path:
                  return  # 如果用户没有选择路径,退出
      
              # 保存文档(包含资源)
              c4d.documents.SaveDocument(doc, save_path, c4d.SAVEDOCUMENTFLAGS_SAVE_TEXTURES, c4d.FORMAT_C4DEXPORT)
      
              # 获取保存后的工程名称(文件名部分)
              project_name = save_path.split("\\")[-1]  # 提取文件名部分
      
              # 创建一个新的 ProjectPreset 对象并将其添加到列表中
              t1 = ProjectPreset(project_name)
              self.listOfProjectPreset.append(t1)
      
              # 可选:输出保存的工程名称,以便调试
              print(f"Project '{project_name}' has been saved and added to the list.")
      
          def on_button_click(self, msg, doc):
              # 响应按钮点击事件,弹出保存对话框
              file_path = c4d.storage.SaveDialog(c4d.FILESELECTTYPE_SCENE, "选择保存位置")
              
              if file_path:
                  self.save_project(doc, file_path)  # 执行保存工程操作
      
      
      
          def get_list(self):
              return self.listOfProjectPreset
      
          
      
          # Performing a rename on any item in the Treeview is crashing Cinema reliably. 
          def SetName(self, root, userdata, obj, name):
              # 当用户重命名元素时调用`DoubleClick()`必须返回False才能正常工作
        
      
              doc = c4d.documents.GetActiveDocument()
              doc.StartUndo()
              doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj)
              obj.SetName(name)
              doc.EndUndo()
              c4d.EventAdd()
      
          def EmptyText(self, root, userdata):
              # 判断是否为空,如果为空返回"???",否则返回默认的文本
              if not root:  # 如果根节点为空
                  return "工程名字"  # 返回显示的文本
              return "预设显示列表"  # 返回空文本,不显示内容
      
          # 然后很少有有用的东西,比如当你双击时通过覆盖 DoubleClick
          # 触发一些脚本,以及通过覆盖 DeletePressed 来管理器 Delete 键
          def DoubleClick(self, root, userdata, obj, col, mouseinfo) :
              # 当用户双击 TreeView 中的条目时调用。返回:返回:(bool) :如果双击
              # 已处理,则为 True,如果default作应启动,则为 False。默认作将调用对
              # 象的重命名过程,启用 'SetName()' 
              if not isinstance(obj, c4d.BaseList2D):
                  return True
      
              obj.SetName(c4d.gui.RenameDialog(obj.GetName()))
              return True
              #c4d.gui.MessageDialog("你确定要合并 " + str(obj)+"到当前项目吗?(增加确定和取消按钮)")
              #return True
      
          def IsResizeColAllowed(self, root, userdata, lColID):
              if lColID > 2:
                  return True
              return False
      
      
          def IsTristate(self, root, userdata):
              return False
      
      
          def GetDragType(self, root, userdata, node):
              # 返回:(int):拖动数据类型。
      
              return c4d.NOTOK
        
          def DeletePressed(self, root, userdata) :
              # 收到删除事件时调用
              for tex in reversed(self.listOfProjectPreset) :
                  if tex.IsSelected:
                      self.listOfProjectPreset.remove(tex)
      
          def HeaderClick(self, root, userdata, column, channel, is_double_click, mouseX, mouseY, ua):
              # 在单击 TreeView 标头时调用。返回:结果(bool):如果事件已处理,则为 True,否则为 False
      
              # c4d.gui.MessageDialog("You clicked on the '%i' header." % (column))
              return True
      
      
          def AcceptDragObject(self, root, userdata, node, dragtype, dragobject):
              # 当拖放作悬停在 TreeView 上以检查是否可以接受拖动时调用。返回:(整数,布尔值)
      
              # if dragtype != c4d.DRAGTYPE_ATOMARRAY:
              #     return 0
      
              # return c4d.INSERT_BEFORE | c4d.INSERT_AFTER | c4d.INSERT_UNDER, True
      
              return 0
      
      
          def GenerateDragArray(self, root, userdata, node):
              # 返回:(c4d 列表。BaseList2D):生成一个对象列表,这些对象可以从 'c4d.DRAGTYPE_ATOMARRAY' 类型的 TreeView 中拖动
      
              if node.GetBit(c4d.BIT_ACTIVE):
                  return [node, ]
      
      
          def InsertObject(self, root, userdata, node, dragtype, dragobject, insertmode, bCopy):
              # 在 TreeView 上放置拖动时调用
      
              if dragtype != c4d.DRAGTYPE_ATOMARRAY:
                  return # 不应该发生,我们在 AcceptDragObject 中捕获了它
      
              for op in dragobject:
                  op.Remove()
      
                  if insertmode == c4d.INSERT_BEFORE:
                      op.InsertBefore(node)
                  elif insertmode == c4d.INSERT_AFTER:
                      op.InsertAfter(node)
                  elif insertmode == c4d.INSERT_UNDER:
                      op.InsertUnder(node)
              return
          
          def IsMoveColAllowed(self, root, userdata, lColID):
              # 允许用户移动所有列。
              # 必须在 AddCustomGui 的容器中设置 TREEVIEW_MOVE_COLUMN。
              return False
      
          def update_treeview_with_c4d_files(self, path):
              # 先清空现有的列表
              self.listOfProjectPreset.clear()
      
              # 检查路径是否存在
              if not os.path.exists(path):
                  c4d.gui.MessageDialog("路径不存在!")
                  return
      
              # 遍历该路径及其子文件夹中的所有 .c4d 文件
              for root, dirs, files in os.walk(path):
                  for file in files:
                      if file.endswith(".c4d"):
                          # 获取文件的完整路径并添加到列表
                          full_path = os.path.join(root, file)
                          project = ProjectPreset(full_path)
                          self.listOfProjectPreset.append(project)
      
              # 刷新 TreeView
              self.refresh_treeview()
              
          def add_projects_from_path(self, path):
              # 获取所有 .c4d 文件
              c4d_files = self.get_all_c4d_files(path)
              
              # 清空旧的列表内容
              self.clear_treeview()  # 清空TreeView内容
      
              # 打印路径内容,检查文件夹是否正确
              print(f"读取路径: {path}")
              
              # 将每个找到的 .c4d 文件添加到 TreeView 中
              for c4d_file in c4d_files:
                  project_name = os.path.basename(c4d_file)  # 获取文件名
                  self.add_to_treeview(project_name)  # 添加文件到TreeView
                  
              print(f"已加载项目:{self.listOfProjectPreset}")
      
          def get_all_c4d_files(self, path):
              c4d_files = []
              
              # 使用 os.walk 遍历目录及子目录
              for root, dirs, files in os.walk(path):
                  for file in files:
                      if file.lower().endswith(".c4d"):  # 检查文件扩展名
                          full_path = os.path.join(root, file)  # 获取文件的完整路径
                          c4d_files.append(full_path)  # 添加到列表
                          
              return c4d_files
      
          def clear_treeview(self):
              # 清空TreeView的所有项
              item = self.GetFirst(None, None)  # 获取TreeView中的第一个项目,root和userdata设置为None
              while item:
                  next_item = self.GetNextItem(item)  # 获取下一个项目
                  self.RemoveItem(item)  # 移除当前项目
                  item = next_item  # 继续遍历下一个项目
      
          def add_to_treeview(self, project_name):
              # 向TreeView添加项目
              item_id = self.AddItem(None, project_name)  # 添加项目到TreeView
              self.Refresh()  # 刷新TreeView,显示新的内容
              self.listOfProjectPreset.append(project_name)  # 将项目添加到 list
          
      
          
          def refresh_treeview(self):
              # 这里是刷新 TreeView 的逻辑
              print("刷新 TreeView")
      
      # ///////////////// 插件界面
      class DL_MainDialog(c4d.gui.GeDialog):
      
              ID_MENU = 10
              ID_MENU1 = ID_MENU+1
              ID_MENU2 = ID_MENU+2
              ID_GROUP_START = 100
              ID_GROUP_NONE1 = ID_GROUP_START+1
              ID_TABGROUP = 1
              ID_TREEBUTTON1 = 1501
              ID_TREEBUTTON2 = 1502
              ID_TREEBUTTON3 = 1503
              ID_STATICETEXT1 = 2000
              ID_STATICETEXT2 = 2001 #未引用
              ID_FILEPATH = 2501
      
              ID_LINE = 4000
              ID_LINE1 = ID_LINE+1
              ID_LINE2 = ID_LINE+2
              ID_USERAREA = 5000
              ID_USERAREA1 = ID_USERAREA+1
              ID_TREEVIEW = 6001
              ID_NAME = 6501
              ID_RENDER = 6502
              ID_TREEICON1 = 7001
      
              _treegui = None # 我们的 CustomGui TreeView
              _listView = ListView() # 我们的 c4d.gui.TreeViewFunctions 实例
              _listView2 = []
              
      
      
              def __init__(self):
                  super().__init__()
                  self.logo_area = LogoUserArea()
      
      
              def CreateLayout(self):
                  self.SetTitle(PLUGINNAME) 
      
                  # 加载并显示封面图片
                  cover_image_path = os.path.join(PLUGINPATH, "res", "logo.png")  # 图片路径
                  cover_image = c4d.bitmaps.BaseBitmap()  # 创建图片对象
      
      
                  if self.GroupBegin(self.ID_GROUP_NONE1, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT,  cols=1, rows=0, title = PLUGINNAME, groupflags=0, initw=0, inith=0):
                      self.GroupBegin(self.ID_GROUP_NONE1, c4d.BFH_CENTER | c4d.BFV_CENTER, 0, 0, "Logo")
                      self.GroupBorderNoTitle(c4d.BORDER_NONE)
                      self.AddUserArea(self.ID_USERAREA1, c4d.BFH_CENTER | c4d.BFV_CENTER)
                      self.AttachUserArea(self.logo_area, self.ID_USERAREA1)
                      self.GroupEnd()
      
                  if self.GroupBegin(self.ID_GROUP_NONE1, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 0, 1, "项目预设", groupflags=0, initw=0, inith=0):  
                      self.GroupBorder(c4d.BORDER_ACTIVE_1)
                      self.GroupBorderSpace(6,6,6,6)
                      customdata = c4d.BaseContainer()
                      customdata.SetBool(c4d.TREEVIEW_BORDER,c4d.BORDER_ROUND)
                      customdata.SetBool(c4d.TREEVIEW_HAS_HEADER, True)
                      customdata.SetBool(c4d.TREEVIEW_HIDE_LINES, True)
                      customdata.SetBool(c4d.TREEVIEW_MOVE_COLUMN, True)
                      customdata.SetBool(c4d.TREEVIEW_RESIZE_HEADER, True)
                      customdata.SetBool(c4d.TREEVIEW_FIXED_LAYOUT, True)
                      customdata.SetBool(c4d.TREEVIEW_ALTERNATE_BG, True)
                      customdata.SetBool(c4d.TREEVIEW_OUTSIDE_DROP, True)
                      customdata.SetBool(c4d.TREEVIEW_NO_MULTISELECT, True)
                      customdata.SetBool(c4d.TREEVIEW_NO_OPEN_CTRLCLK, False)
                      self._treegui = self.AddCustomGui(self.ID_TREEVIEW,c4d.CUSTOMGUI_TREEVIEW,"",c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT,0,0,customdata)
                      self.AddButton(self.ID_TREEBUTTON1, c4d.BFH_CENTER | c4d.BFV_SCALE, 50, 20, name="添加预设工程")
                      self.AddButton(self.ID_TREEBUTTON2, c4d.BFH_CENTER | c4d.BFV_SCALE, 100, 20, name="选择保存工程路径")
                      # self.AddButton(self.ID_TREEBUTTON3, c4d.BFH_CENTER | c4d.BFV_SCALE, 50, 20, name="保存工程")
                      self.AddEditText(self.ID_FILEPATH, c4d.BFH_SCALEFIT, 0, 20)
                      # self.AddCustomGui(self.ID_FILEPATH, c4d.CUSTOMGUI_STATICTEXT, "", c4d.BFH_LEFT, 0, 0)
                      self.SetString(self.ID_FILEPATH, "C:/path/to/your/project.c4d")  # 设置默认路径
                      # self.AddMultiLineEditText(1541, c4d.BFH_SCALEFIT | c4d.BFV_SCALE, 0, 40)
                      # self.Hierarchy(self,tv,headlist = ["Name","test"])
                      # 添加列标题(这里添加了三列)
                      if not self._treegui:
                          print ("[ERROR]: 不能生成TreeView")
                          return False
                      self.GroupEnd()
                  self.GroupEnd()
                  return True
      
              def InitValues(self) :
      
                  icon = c4d.bitmaps.BaseBitmap()
                  path = os.path.join(PLUGINPATH, "res", "icon.tif")
                  if not icon.InitWith(path):
                      print("图标加载失败,请检查路径!")
                      return False   # 图标加载失败,退出
                  icon.InitWith(path)
      
                  # Initialize the column layout for the TreeView.
                  layout = c4d.BaseContainer()
                  # layout.SetLong(self.ID_TREEICON1, c4d.LV_CHECKBOX)
                  layout.SetInt32(ID_NAME, c4d.LV_USERTREE)
                  layout.SetLong(self.ID_TREEICON1, c4d.LV_TREE )
                  layout.SetLong(self.ID_RENDER, c4d.LV_DROPDOWN)
                  self._treegui.SetLayout(3, layout)
          
                  # 设置头部标题
                  self._treegui.SetHeaderText(self.ID_NAME, "Icon")
                  self._treegui.SetHeaderText(self.ID_TREEICON1, "项目名")
                  self._treegui.SetHeaderText(self.ID_RENDER, "使用渲染器")
                  
                  self._treegui.Refresh()
      
                  # # 设置根节点
                  self._treegui.SetRoot(self._treegui, self._listView, None)
      
                  return True
      
      
              def Command(self, id, msg):
                  if id == self.ID_TREEBUTTON1:
                      # 执行 Cinema 4D 内置的保存功能(保存工程并包含资源)
                      c4d.CallCommand(12255)  # 触发保存工程(包含资源)功能
      
                      # 获取当前文档
                      doc = c4d.documents.GetActiveDocument()
      
                      # 获取保存后的文件路径
                      doc_path = doc.GetDocumentPath()
      
                      # 检查文件是否保存成功
                      if doc_path:  # 如果路径有效,说明文件已保存
                          project_name = doc.GetDocumentName()  # 获取当前文档的名称
      
                          # 检查是否已经存在相同名称的工程文件
                          # 遍历 ProjectPreset 列表,确保我们只对 ProjectPreset 对象操作
                          if not any(isinstance(prjname, ProjectPreset) and prjname.ProName == project_name 
                                  for prjname in self._listView.listOfProjectPreset):
                              # 创建一个新的 ProjectPreset 对象,并将其添加到列表中
                              prjname = ProjectPreset(project_name)
                              self._listView.listOfProjectPreset.append(prjname)
      
                              # 刷新 TreeView 以显示更新后的列表
                              self._treegui.Refresh()
                          else:
                              print(f"工程 '{project_name}' 已经存在,不会添加。")
                      else:
                          print("文件未保存或保存失败!")
      
                      return True
      
      
                  if id == self.ID_TREEBUTTON2:  # 如果点击了按钮
                      folder_path = c4d.storage.LoadDialog(c4d.FILESELECT_DIRECTORY, "Select Folder", c4d.FILESELECT_DIRECTORY)
                  
                      if folder_path:
                          # 如果选择了文件夹,更新列表
                          self.UpdateTreeView(folder_path)
                          # 更新显示的文件夹路径
                          self.SetString(self.ID_FILEPATH, folder_path)
          
                      return True
              
              def UpdateTreeView(self, folder_path):
      
                  # 遍历文件夹,查找所有 .c4d 文件(包括子文件夹),并更新列表
      
                  # 清空现有的列表
                  self._listView = []
      
                  # 递归遍历文件夹,查找 .c4d 文件
                  self.ScanFolderForC4DFiles(folder_path)
      
                  # 在这里你可以更新 TreeView 或者 ListView 来显示文件
                  for item in self._listView:
                      print(f"Found .c4d file: {item.ProName}")
      
                  # 你可以根据需要刷新 GUI 元素,例如 TreeView
      
              def ScanFolderForC4DFiles(self, folder_path):
      
                  # 递归遍历文件夹,查找所有 .c4d 文件,并将其添加到列表
      
                  for root, dirs, files in os.walk(folder_path):
                      for file in files:
                          if file.lower().endswith(".c4d"):  # 只处理 .c4d 文件
                              full_path = os.path.join(root, file)
                              # 创建 ProjectPreset 对象并加入列表
                              project = ProjectPreset(full_path)
                              self._listView.append(project)
      
              def set_save_directory(self):
                  # 打开文件夹选择对话框,让用户选择路径
                  selected_dir = c4d.gui.FileSelectionDialog(c4d.FILESELECTTYPE_ANYTHING, "选择保存目录", self.saved_projects_dir)
                  
                  if selected_dir:
                      self.saved_projects_dir = selected_dir  # 保存用户选择的路径
                      print("已选择的路径:", self.saved_projects_dir)
      
              def expand_all_nodes(self, node):
                  # 递归展开所有节点
                  self.treeview.expanded_nodes.add(node)
                  if isinstance(node, dict) and node.get("children"):
                      for child in node["children"]:
                          self.expand_all_nodes(child)
      
           
      # ///////////////// 插件界面
      class DL_MAIN(c4d.plugins.CommandData):
          def __init__(self):
              self.dialog = None
      
      if __name__ == "__main__":
          icon = c4d.bitmaps.BaseBitmap()
          icon_path = os.path.join(PLUGINPATH,"res\icon.tif")
          icon.InitWith(icon_path)
          c4d.plugins.RegisterCommandPlugin(PLUGINID,
                                            PLUGINNAME,
                                            0,
                                            icon,
                                            PLUGINHELP,
                                            DL_MAIN(),
                                            )
      
      
      posted in Cinema 4D SDK 2023 python windows
      N
      Neekoe
    • RE: Add icons to treeview

      @Dunhou said in Add icons to treeview:

      boghma

      I did refer to the teaching of Jack, a member of Boghma, which involved some methods and pre written libraries for calling. However, I would prefer to start from scratch because it is not as simple as the add button. The code snippets use parts of the function from Git and Boghma respectively. I can copy the parts that I have already learned. Thank you to the members of Boghma

      posted in Cinema 4D SDK
      N
      Neekoe
    • RE: Add icons to treeview

      Thank you again to MAXON's friends for providing forum materials

      posted in Cinema 4D SDK
      N
      Neekoe
    • RE: Add icons to treeview

      The problem has been resolved, and the reason why the icon does not appear is that I used layout in 'def InitValues (self):' SetLong(self.ID_NAME, c4d.LV_TREE ), The correct one should be layout SetInt32(ID_NAME, c4d.LV_USERTREE)

      posted in Cinema 4D SDK
      N
      Neekoe
    • Add icons to treeview

      I'm sorry, I saw a lot of information about treeview on the forum, but I still can't understand it. I'm too stupid, and I feel like I've done a lot, but still can't get the answer
      A.png B.png
      I hope to get a custom icon, similar to the green custom icon in image 2, and the treeview result will let me know what difficulty is

      import c4d
      PLUGINID = 1064765
      
      ID_OTHER = 1124
      ID_ICON = 1125
      ID_NAME = 1126
      # ///////////////// TreeView界面
      class ProjectPreset(object):
      # 类,它表示一个项目,也就是我们列表中的 Item
          projectptPath = "projectptPath"
          otherData = "OtherData"
          _selected = False
        
          def __init__(self, projectptPath) :
              self.projectptPath = projectptPath
              self.otherData += projectptPath
        
          @property
          def IsSelected(self) :
              return self._selected
        
          def Select(self) :
              self._selected = True
        
          def Deselect(self) :
              self._selected = False
        
          def __repr__(self) :
              return str(self)
        
          def __str__(self) :
              return self.projectptPath
          
      
      class ListView(c4d.gui.TreeViewFunctions) :
          
        
          def __init__(self) :
              self.listOfProjectPreset = list() # 存储我们需要在此列表中显示的所有对象
        
              # Add some defaults values 
              t1 = ProjectPreset("T1")
              t2 = ProjectPreset("T2")
              t3 = ProjectPreset("T3")
              t4 = ProjectPreset("T4")
              t5 = ProjectPreset("T5")
        
              self.listOfProjectPreset.extend([t1, t2, t3, t4])
      
          #TreeView 的整个概念是覆盖一些函数。因此,请务必阅读文档以了解根据您的需要覆盖哪些函数。
          # 因此,我们将为 TreeView 定义一些更通用的选项
          def IsResizeColAllowed(self, root, userdata, lColID) :
              return True
        
          def IsTristate(self, root, userdata) :
              return False
        
          def GetColumnWidth(self, root, userdata, obj, col, area) :
              return 80  # 所有的初始宽度都相同
        
          def IsMoveColAllowed(self, root, userdata, lColID) :
              # 允许用户移动所有列。
              # 必须在 AddCustomGui 的容器中设置 TREEVIEW_MOVE_COLUMN。
              return True
          def GetFirst(self, root, userdata) :
              # 返回层次结构中的第一个元素,如果没有元素,则返回 None
              rValue = None if not self.listOfProjectPreset else self.listOfProjectPreset[0]
              return rValue
          # 然后我们必须处理 GetDown(对于子对象,因为在我们的例子中它是一个简单的列表,我们返回 None)
          # GetNext 是当前 Object 之后的 Object,GetPred 是当前 Object 之前的 Object。
          # 我们还必须覆盖 GetID 才能唯一标识列表中的对象
      
          def GetDown(self, root, userdata, obj) :
              # 返回节点的子节点,因为我们只需要一个列表,所以每次都返回 None
              return None
        
          def GetNext(self, root, userdata, obj) :
              # 返回 arg:'obj' 之后要显示的下一个对象
              rValue = None
              currentObjIndex = self.listOfProjectPreset.index(obj)
              nextIndex = currentObjIndex + 1
              if nextIndex < len(self.listOfProjectPreset) :
                  rValue = self.listOfProjectPreset[nextIndex]
        
              return rValue
        
          def GetPred(self, root, userdata, obj) :
              # 返回要在 arg 之前显示的上一个 Object:'obj' 
              rValue = None
              currentObjIndex = self.listOfProjectPreset.index(obj)
              predIndex = currentObjIndex - 1
              if 0 <= predIndex < len(self.listOfProjectPreset) :
                  rValue = self.listOfProjectPreset[predIndex]
        
              return rValue
      		
          def GetId(self, root, userdata, obj) :
              # 返回 TreeView 中元素的唯一 ID
              return hash(obj)
          
          def Select(self, root, userdata, obj, mode) :
              # 在用户选择元素时调用
              if mode == c4d.SELECTION_NEW:
                  for tex in self.listOfProjectPreset:
                      tex.Deselect()
                  obj.Select()
              elif mode == c4d.SELECTION_ADD:
                  obj.Select()
              elif mode == c4d.SELECTION_SUB:
                  obj.Deselect()
        
          def IsSelected(self, root, userdata, obj) :
              # 返回: 如果选择了 *obj*,则为 True,否则为 False
              return obj.IsSelected
          
          def InsertObject(self, root, userdata, obj, dragtype, dragobject, insertmode, bCopy):
              self.listOfProjectPreset.append(dragobject)
              
              return True
          
          def SetCheck(self, root, userdata, obj, column, checked, msg) :
              # 当用户单击'c4d.LV_CHECKBOX' 列中对象的复选框时调用
              if checked:
                  obj.Select()
              else:
                  obj.Deselect()
        
          def IsChecked(self, root, userdata, obj, column) :
              # 返回: (int) : *obj* 的指定 *column* 中复选框的状态
              if obj.IsSelected:
                  return c4d.LV_CHECKBOX_CHECKED | c4d.LV_CHECKBOX_ENABLED
              else:
                  return c4d.LV_CHECKBOX_ENABLED
      
          # 然后LV_TREE元素将通过调用 GetName 来检查 Object name    
          def GetName(self, root, userdata, obj) :
              # 返回要为 arg:'obj' 显示的名称, 仅对type LV_TREE类型的列调用
              
              return str(obj) # Or obj.texturePath
      
          # 最后,LV_USER允许我们执行一些绘图功能,就像您在 GeUserArea 中所做的那样
          # 使用 DrawCell 绘制您想要的任何内容(在我们的例子中是文本)。看看 DrawInfo-Dict
          def DrawCell(self, root, userdata, obj, col, drawinfo, bgColor):
              # 在 TreeView 的单元格中绘制内容 LV_USER
              if col == ID_NAME:
                  print("Drawcell obj: ", str(obj))
                  geUserArea = drawinfo["frame"]
                  ICON_SIZE = drawinfo["height"]
                  TEXT_SPACER = 6
                  bgColor = c4d.COLOR_SB_TEXTHG1 if drawinfo["line"] % 2 else c4d.COLOR_SB_TEXTHG2
      
                  # 使用自定义图标
                  icon = c4d.bitmaps.BaseBitmap()
                  path = os.path.join(PLUGINPATH, "res", "icon.tif")
                  if not icon.InitWith(path):
                      print("图标加载失败,请检查路径!")
                      return False  # 图标加载失败,退出
      
                  geUserArea.DrawSetPen(bgColor)
                  geUserArea.DrawBitmap(icon, drawinfo["xpos"], drawinfo["ypos"], ICON_SIZE, ICON_SIZE, 0, 0, icon.GetBw(), icon.GetBh(), c4d.BMP_ALLOWALPHA)
      
                  # 绘制文本
                  name = str(obj)
                  fontHeight = geUserArea.DrawGetFontHeight()
                  fontWidth = geUserArea.DrawGetTextWidth(name)
      
                  x = drawinfo["xpos"] + ICON_SIZE + TEXT_SPACER
                  y = drawinfo["ypos"] + (ICON_SIZE - fontHeight) / 2
                  
                  txtColor = c4d.COLOR_SB_TEXT_ACTIVE1 if obj.IsSelected else c4d.COLOR_SB_TEXT
                  geUserArea.DrawSetTextCol(txtColor, bgColor)
                  
                  geUserArea.DrawText(name, x, y)
      
      
          
      
          # Performing a rename on any item in the Treeview is crashing Cinema reliably. 
          def SetName(self, root, userdata, obj, name):
              # 当用户重命名元素时调用`DoubleClick()`必须返回False才能正常工作
        
      
              doc = c4d.documents.GetActiveDocument()
              doc.StartUndo()
              doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj)
              obj.SetName(name)
              doc.EndUndo()
              c4d.EventAdd()
      
      
          # 然后很少有有用的东西,比如当你双击时通过覆盖 DoubleClick
          # 触发一些脚本,以及通过覆盖 DeletePressed 来管理器 Delete 键
          def DoubleClick(self, root, userdata, obj, col, mouseinfo) :
              # 当用户双击 TreeView 中的条目时调用。返回:返回:(bool) :如果双击
              # 已处理,则为 True,如果default作应启动,则为 False。默认作将调用对
              # 象的重命名过程,启用 'SetName()' 
              if not isinstance(obj, c4d.BaseList2D):
                  return True
      
              obj.SetName(c4d.gui.RenameDialog(obj.GetName()))
              return True
              #c4d.gui.MessageDialog("你确定要合并 " + str(obj)+"到当前项目吗?(增加确定和取消按钮)")
              #return True
        
          def DeletePressed(self, root, userdata) :
              # 收到删除事件时调用
              for tex in reversed(self.listOfProjectPreset) :
                  if tex.IsSelected:
                      self.listOfProjectPreset.remove(tex)
      
      class DL_MainDialog(c4d.gui.GeDialog):
      
      
              ID_TREEVIEW = 6001
              ID_NAME = 6501
              ID_RENDER = 6502
              ID_TREEICON1 = 7001
      
              _treegui = None # 我们的 CustomGui TreeView
              _listView = ListView() # 我们的 c4d.gui.TreeViewFunctions 实例
              
      
      
              def __init__(self):
                  super().__init__()
                  # treeview 容器预设
                  # self.tv_manager = TV_Manager()
      
      
              def CreateLayout(self):
                  self.SetTitle(PLUGINNAME) 
                  
                  # 添加菜单
                  self.MenuSubBegin("File")
                  self.MenuAddString(self.ID_MENU1,"What")
                  self.MenuSubEnd()
      
                  self.MenuSubBegin("Edit")
                  self.MenuAddString(self.ID_MENU2,"What")
                  self.MenuSubEnd()
      
                
                  if self.GroupBegin(self.ID_GROUP_NONE1, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 0, 1, "项目预设", groupflags=0, initw=0, inith=0):  
                      self.GroupBorder(c4d.BORDER_ACTIVE_1)
                      self.GroupBorderSpace(6,6,6,6)
                      customdata = c4d.BaseContainer()
                      customdata.SetBool(c4d.TREEVIEW_BORDER,c4d.BORDER_ROUND)
                      customdata.SetBool(c4d.TREEVIEW_HAS_HEADER, True)
                      customdata.SetBool(c4d.TREEVIEW_HIDE_LINES, True)
                      customdata.SetBool(c4d.TREEVIEW_MOVE_COLUMN, True)
                      customdata.SetBool(c4d.TREEVIEW_RESIZE_HEADER, True)
                      customdata.SetBool(c4d.TREEVIEW_FIXED_LAYOUT, True)
                      customdata.SetBool(c4d.TREEVIEW_ALTERNATE_BG, True)
                      customdata.SetBool(c4d.TREEVIEW_OUTSIDE_DROP, True)
                      customdata.SetBool(c4d.TREEVIEW_NO_MULTISELECT, True)
                      customdata.SetBool(c4d.TREEVIEW_NO_OPEN_CTRLCLK, False)
                      self._treegui = self.AddCustomGui(self.ID_TREEVIEW,c4d.CUSTOMGUI_TREEVIEW,"",c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT,0,0,customdata)
                      self.AddButton(self.ID_TREEBUTTON1, c4d.BFH_CENTER | c4d.BFH_SCALEFIT | c4d.BFV_SCALE, 100, 20, name="展开所有")
                      # self.AddMultiLineEditText(1541, c4d.BFH_SCALEFIT | c4d.BFV_SCALE, 0, 40)
                      # self.AddEditText(1842, c4d.BFH_SCALEFIT, 0, 25)
                      # self.Hierarchy(self,tv,headlist = ["Name","test"])
                      # 添加列标题(这里添加了三列)
                      if not self._treegui:
                          print ("[ERROR]: 不能生成TreeView")
                          return False
                      self.GroupEnd()
                  return True
      
              def InitValues(self) :
      
                  icon = c4d.bitmaps.BaseBitmap()
                  path = os.path.join(PLUGINPATH, "res", "icon.tif")
                  if not icon.InitWith(path):
                      print("图标加载失败,请检查路径!")
                      return False  # 图标加载失败,退出
                  icon.InitWith(path)
      
                  # Initialize the column layout for the TreeView.
                  layout = c4d.BaseContainer()
                  layout.SetLong(self.ID_TREEICON1, c4d.LV_DROPDOWN)
                  layout.SetLong(self.ID_NAME, c4d.LV_TREE )
                  layout.SetLong(self.ID_RENDER, c4d.LV_CHECKBOX)
                  self._treegui.SetLayout(3, layout)
          
                  # 设置头部标题
                  
                  self._treegui.SetHeaderText(self.ID_TREEICON1, "Icon")
                  self._treegui.SetHeaderText(self.ID_NAME, "Name")
                  self._treegui.SetHeaderText(self.ID_RENDER, "使用渲染器")
                  
                  self._treegui.Refresh()
      
                  # # 设置根节点
                  self._treegui.SetRoot(self._treegui, self._listView, None)
      
                  return True
      
      posted in Cinema 4D SDK 2023 python windows
      N
      Neekoe
    • RE: C4D uses Python to search for referenced objects

      @ferdinand Thank you so much. He perfectly solved all my problems

      posted in Cinema 4D SDK
      N
      Neekoe
    • C4D uses Python to search for referenced objects

      This is my Python code. I want to write a button to delete invalid null objects, even if they are at any level. However, this code is invalid in check_deference because executing the code will delete the linked null objects together. How can I find the referenced objects?

      In addition, I have attached an image where I want to remove the red null object and keep the green null object (green object 1111 is linked to the target label, hoping to handle different levels), as it points to the target label. Other default colors are used for various working conditions and have already met the code settings..

      Thank you.

      Snipaste_2025-02-13_03-01-41.png

      def check_references(obj):
          references = []
          
          # 检查标签引用
          for tag in obj.GetTags():
              tag_type = tag.GetType()
              if tag_type in [c4d.Tphong, c4d.Ttexture, c4d.Texpresso]:  # 更新为 Texpresso(表达式标签)
                  references.append(f"Tag link: {tag_type}")
          
          if references:
              print(f"Object {obj.GetName()} has references: {', '.join(references)}")
          return references
      
      # 判断 Null 对象是否为空
      def is_empty_null(obj):
          if not obj or obj.GetType() != c4d.Onull:
              return False
      
          if obj.GetTags():
              return False
      
          child = obj.GetDown()
          while child:
              if child.GetType() != c4d.Onull:
                  return False
              if not is_empty_null(child):
                  return False
              child = child.GetNext()
      
          return True
      
      # 获取场景中所有对象
      def get_all_objects(obj):
          all_objects = []
          while obj:
              all_objects.append(obj)
              all_objects.extend(get_all_objects(obj.GetDown()))
              obj = obj.GetNext()
          return all_objects
      
      # 删除无效 Null 对象
      def delete_null_objects():
          doc = c4d.documents.GetActiveDocument()
          if not doc:
              return
      
          doc.StartUndo()
      
          # 获取场景中的所有对象
          objects = doc.GetObjects()
      
          for obj in objects:
              if obj.GetType() == c4d.Onull:
      
                  if check_references(obj):
                      continue  # 被引用的对象跳过
      
                  # 只有 `is_empty_null` 返回 True 才会删除
                  if not is_empty_null(obj):
                      continue
      
                  # 记录删除操作的撤销
                  doc.AddUndo(c4d.UNDOTYPE_DELETE, obj)
      
                  # 获取 Null 对象的父对象
                  parent = obj.GetUp()
      
                  # 获取子对象并移到父对象下
                  child = obj.GetDown()
                  while child:
                      next_child = child.GetNext()
                      child.Remove()
      
                      # 记录撤销(对于每个子对象的改变)
                      if parent:
                          doc.AddUndo(c4d.UNDOTYPE_CHANGE, child)
                          parent.InsertUnder(child)
      
                      child = next_child
      
                  # 删除 Null 对象
                  obj.Remove()
      
          # 更新对象列表,因为在删除过程中可能会修改它
          objects = doc.GetObjects()
      
          # 额外逻辑:删除非 `Null` 对象下的空 `Null` 子对象
          for obj in objects:
              child = obj.GetDown()
              while child:
                  next_child = child.GetNext()
      
                  # 如果子对象是 `Null` 且为空,并且没有标签,则递归删除该 `Null` 对象及其子对象
                  if child.GetType() == c4d.Onull:
                      if is_empty_null(child):
                          doc.AddUndo(c4d.UNDOTYPE_DELETE, child)
                          child.Remove()
                      else:
                          # 如果子对象不是空 `Null`,递归检查其子级
                          delete_empty_null_children(child)
      
                  child = next_child
      
          # 更新界面
          c4d.EventAdd()
      
          doc.EndUndo()
      
      # 递归删除子级空 Null 对象
      def delete_empty_null_children(parent):
          doc = c4d.documents.GetActiveDocument()
          child = parent.GetDown()
          while child:
              next_child = child.GetNext()
              if child.GetType() == c4d.Onull:
                  if is_empty_null(child):
                      # 记录撤销
                      doc.AddUndo(c4d.UNDOTYPE_DELETE, child)
                      child.Remove()
                  else:
                      delete_empty_null_children(child)
      
              child = next_child
      
      posted in Cinema 4D SDK 2023 python windows
      N
      Neekoe