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
    • Login

    GeUserArea keeps returning (null) on window move even with DrawMsg returning True

    Cinema 4D SDK
    windows python 2025
    2
    3
    148
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Amazing_iKeA
      Amazing_iKe
      last edited by

      Hi everyone,

      I'm currently working with a custom GeUserArea, and I've noticed a strange behavior: when I move the window around, the area keeps returning (null) repeatedly, as if it's constantly redrawing. This causes an unclean or "flickering" appearance.

      Is there something I might be missing? Or is there a recommended way to prevent these unnecessary redraws during window movement?

      Any insight or workaround would be greatly appreciated!

      Thanks in advance.

      import c4d, os
      from c4d import gui, bitmaps
      
      class KidooShotManagerUserArea(gui.GeUserArea):
          def __init__(self):
              super(KidooShotManagerUserArea, self).__init__()
      
              self.padding = 8  # 每张图片之间的间距。
              self.base_size = 128  # 每张图片的基础大小
              self.min_size = 50  # 最小尺寸
              self.max_size = 256  # 最大尺寸
      
              self.col_count = 4  # 列数
              self.cell_size = self.base_size  # 每个格子的实际大小
      
              self.assets = []  # 存储图片
      
          def load_card(self, image_assets : list) -> list:
              """
              input:
              - image_assets: [{path, name}, {path, name}, ...] 需要输入图片的路径和名称列表。
      
              returns:
              - assets: 返回[[bitmap, path, name], ...] 的列表,包含加载的图片对象、路径和名称。
              """
              assets = []
      
              for asset in image_assets:
                  path = asset.get('path')
                  name = asset.get('name', '')
      
      
                  bitmap = bitmaps.BaseBitmap()
                  try:
                      if os.path.exists(path):
                          # 如果路径存在,尝试加载图片
                          bitmap.InitWith(path)
                      else:
                          # 如果路径不存在,尝试加载默认图片(注:确保默认图片可以被正确加载)
                          bitmap.InitWith(r'C:\Users\19252\PycharmProjects\DCP_Forge\res\icon\file-video.png')
                  except Exception as e:
                      bitmap.InitWith(r'C:\Users\19252\PycharmProjects\DCP_Forge\res\icon\file-video.png')
      
                  assets.append([bitmap, path, name])
      
              self.assets = assets
      
              self.Redraw()
              c4d.EventAdd()
              return assets
      
          def GetMinSize(self):
              return (100, 100)
      
          def _calc_grid(self, width: int):
      
              """
              input:
              - width: width 是当前绘图区域的总宽度(比如 800 像素)
      
              returns:
              - cols: 列数
              - cell: 每个格子的实际大小
      
              """
      
      
              # 1. 先估算一个最多能排几列(不一定准确,先试探)
              # 每个 cell 大小大概是 base_size,加上 padding 是单个格子的占位宽度
              tentative = max(1, int((width - self.padding) / (self.base_size + self.padding)))
      
              # 2. 从 tentative 开始尝试减少列数,看是否能得到合法的 cell 尺寸
              for cols in range(tentative, 0, -1):
                  # 用当前列数 cols 反推每个格子的实际大小 cell
                  cell = (width - self.padding) / cols - self.padding
      
                  # 如果 cell 大小在允许范围内(不太小也不太大)
                  if self.min_size <= cell <= self.max_size:
                      # ✅ 说明这个 cols 和 cell 是合适的,就返回
                      return cols, int(cell)
      
              # 3. 如果找不到任何合法的 cell,退而求其次只返回一列+最小尺寸
              return 1, self.min_size
      
      
      
          def DrawMsg(self, x1, y1, x2, y2, msg):
              self.OffScreenOn()# 开启离屏绘制
      
              self.SetClippingRegion(x1, y1, x2, y2) # 设置绘制区域
              self.DrawSetPen(c4d.COLOR_BG) # 设置绘画区域的背景颜色
              self.DrawRectangle(x1, y1, x2, y2) # 绘制背景矩形
      
      
      
              for index, (bitmap, path, name) in enumerate(self.assets):
      
                  # 获取原图大小
                  image_orig_width = bitmap.GetBw()
                  image_orig_height = bitmap.GetBh()
      
                  # 获取绘制区域大小
                  area_width = x2 - x1
                  area_height = y2 - y1
      
                  # 获取文字宽度和高度
                  text_width = self.DrawGetTextWidth(name)
                  text_height = self.DrawGetFontHeight()
      
                  # 计算网格布局
                  self.col_count, self.cell_size = self._calc_grid(area_width)
      
                  # 计算每个格子的行列位置
                  row = index // self.col_count
                  col = index % self.col_count
      
                  # print(f"Row: {row}, Col: {col}, Cell Size: {self.cell_size}")
      
                  cell = self.cell_size
                  padding = self.padding
      
                  total_cell_height = cell + text_height + padding
      
                  # 计算每个格子的实际绘制位置
                  px = padding + col * (cell + padding)
                  py = padding + row * (total_cell_height + padding)
      
                  # print(f"Drawing at: ({px}, {py}) with size {self.cell_size}")
      
                  scale = min(cell / image_orig_width, cell / image_orig_height)
                  dw, dh = int(image_orig_width * scale), int(image_orig_height * scale)
      
                  ox = px + (cell - dw) // 2
                  oy = py + (cell - dh) // 2
      
      
                  # 绘制图片背景
                  self.DrawSetPen(1001)  # 设置背景颜色
                  self.DrawRoundedRectangle(px, py, px + cell, py + cell, 5, 5)
      
                  # 绘制图片
                  self.DrawBitmapRounded(bitmap, ox, oy, dw, dh, 0, 0, image_orig_width, image_orig_height,
                      c4d.BMP_ALLOWALPHA | c4d.BMP_NORMALSCALED, 2)
      
                  # 绘制文字
      
                  text_x = px + cell // 2 - text_width //2
                  text_y = py + cell + padding // 2
      
                  self.DrawSetTextCol(1, c4d.COLOR_BG)
                  self.DrawSetFont(c4d.FONT_BIG | c4d.FONT_BIG_BOLD)
                  self.DrawText(name, text_x, text_y, c4d.DRAWTEXT_STD_ALIGN)
      
              return True
      
      
      
      
      class KidooShotManagerDialog(gui.GeDialog):
          UA_ID = 1000
      
          def __init__(self):
              super(KidooShotManagerDialog, self).__init__()
              self.user_area = KidooShotManagerUserArea()
      
          def CreateLayout(self):
              self.SetTitle("KidooShotManager 图片预览器")
              self.GroupBegin(0, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=1, rows=1)
              self.AddUserArea(self.UA_ID, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT)
              self.AttachUserArea(self.user_area, self.UA_ID)
              self.GroupEnd()
              return True
      
          def InitValues(self):
              # 只加载一张图片测试
              path_list = [{'path': r'C:\Users\19252\Desktop\test_image\preview.jpg', 'name':'001'},
                           {'path': r'C:\Users\19252\Desktop\test_image\General 3800x1584 digital art artwork illustration city cityscape.jpg', 'name':'002'},
                           {'path': r'C:\Users\19252\Desktop\test_image\85d3180dc6bf9880bd91792dd6f73f93.jpg', 'name':'003'},
                           {'path': r'C:\Users\19252\Desktop\test_image\file-video1.png', 'name':'004'},
                           {'path': r'C:\Users\19252\Desktop\test_image\preview.jpg', 'name':'001'},
                           {'path': r'C:\Users\19252\Desktop\test_image\General 3800x1584 digital art artwork illustration city cityscape.jpg', 'name':'002'},
                           {'path': r'C:\Users\19252\Desktop\test_image\85d3180dc6bf9880bd91792dd6f73f93.jpg', 'name':'003'},
                           {'path': r'C:\Users\19252\Desktop\test_image\file-video1.png', 'name':'004'}]
              self.user_area.load_card(path_list)
              return True
      
      def main():
          dlg = KidooShotManagerDialog()
          dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=800, defaulth=600)
      
      if __name__ == '__main__':
          main()
      
      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @Amazing_iKe
        last edited by ferdinand

        Hey @Amazing_iKe,

        Thank you for reaching out to us. There is a multitude of issues with your code. Find my answer below. In short, the return type of KidooShotManagerUserArea.DrawMsg was wrong, causing the (Null) being printed, and you did not keep your async dialog alive.

        Cheers,
        Ferdinand

        """The TLDR is here that you cannot have an async dialog in a synchronous context such as a script
        manager script.
        
        And you especially have to store a reference to the dialog in a persistent entity such as a command, 
        otherwise it will be garbage collected, and you will end up with a dangling dialog implementation.
        """
        
        # coding: utf-8
        
        import c4d, os
        from c4d import gui, bitmaps
        
        class KidooShotManagerUserArea(gui.GeUserArea):
            def __init__(self):
                self.padding = 8
                self.base_size = 128
                self.min_size = 50
                self.max_size = 256
        
                self.col_count = 4
                self.cell_size = self.base_size
                self.assets = []
        
            def load_card(self, image_assets : list) -> list:
                """
                """
                assets = []
        
                for asset in image_assets:
                    path = asset.get('path')
                    name = asset.get('name', '')
                    if not path or not os.path.exists(path):
                        print(f"Invalid path: {path}")
                        continue
        
                    bitmap = bitmaps.BaseBitmap()
                    bitmap.InitWith(path)
                    assets.append([bitmap, path, name])
        
                self.assets = assets
                self.Redraw()
                # c4d.EventAdd() # Big NO-NO, never call EventAdd in an async context (in a message function
                                 # of a GeDialog or GeUserArea it would be fine, but here it definitely is not)
                return assets
        
            def GetMinSize(self):
                return (100, 100)
        
            def _calc_grid(self, width: int):
                """
                """
                tentative = max(1, int((width - self.padding) / (self.base_size + self.padding)))
                for cols in range(tentative, 0, -1):
                    cell = (width - self.padding) / cols - self.padding
                    if self.min_size <= cell <= self.max_size:
                        return cols, int(cell)
                    
                return (self.min_size, self.min_size)
        
            def DrawMsg(self, x1, y1, x2, y2, msg):
                self.OffScreenOn()
        
                self.SetClippingRegion(x1, y1, x2, y2)
                self.DrawSetPen(c4d.COLOR_BG)
                self.DrawRectangle(x1, y1, x2, y2)
        
                for index, (bitmap, path, name) in enumerate(self.assets):
                    image_orig_width = bitmap.GetBw()
                    image_orig_height = bitmap.GetBh()
                    area_width = x2 - x1
                    area_height = y2 - y1
                    text_width = self.DrawGetTextWidth(name)
                    text_height = self.DrawGetFontHeight()
                    self.col_count, self.cell_size = self._calc_grid(area_width)
                    row = index // self.col_count
                    col = index % self.col_count
                    cell = self.cell_size
                    padding = self.padding
        
                    total_cell_height = cell + text_height + padding
                    px = padding + col * (cell + padding)
                    py = padding + row * (total_cell_height + padding)
        
                    scale = min(cell / image_orig_width, cell / image_orig_height)
                    dw, dh = int(image_orig_width * scale), int(image_orig_height * scale)
        
                    ox = px + (cell - dw) // 2
                    oy = py + (cell - dh) // 2
        
                    self.DrawSetPen(1001)
                    self.DrawRoundedRectangle(px, py, px + cell, py + cell, 5, 5)
        
                    self.DrawBitmapRounded(bitmap, ox, oy, dw, dh, 0, 0, image_orig_width, image_orig_height,
                        c4d.BMP_ALLOWALPHA | c4d.BMP_NORMALSCALED, 2)
        
                    text_x = px + cell // 2 - text_width //2
                    text_y = py + cell + padding // 2
        
                    self.DrawSetTextCol(1, c4d.COLOR_BG)
                    self.DrawSetFont(c4d.FONT_BIG | c4d.FONT_BIG_BOLD)
                    self.DrawText(name, text_x, text_y, c4d.DRAWTEXT_STD_ALIGN)
        
                # This not the return type of DrawMsg, it is a void function. When you implement something
                # with a wrong return type, the Python module will print (Null) in the console, we could
                # probably improve that a bit.
        
                # return True
        
        
        class KidooShotManagerDialog(gui.GeDialog):
            UA_ID = 1000
        
            def __init__(self):
                super(KidooShotManagerDialog, self).__init__()
                self.user_area = KidooShotManagerUserArea()
        
            def CreateLayout(self):
                self.SetTitle("KidooShotManager")
                self.GroupBegin(0, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=1, rows=1)
                self.AddUserArea(self.UA_ID, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT)
                self.AttachUserArea(self.user_area, self.UA_ID)
                self.GroupEnd()
                return True
        
            def InitValues(self):
                path_list = [
                    ]
                self.user_area.load_card(path_list)
                return True
        
        def main():
            """
            """
            # This does not work, as explained in many other examples on the forum.
            # 
            # You define a variable #dlg in the local scope of main. Then you call Open on it (which will 
            # not wait for dialog to be closed, because it is async). And then the main function ends, and 
            # the variable dlg is garbage collected. So, in C++ you now have a dangling dialog implementation
            # which cannot poll its implementation anymore (hence the empty window or even crashes). That 
            # things sometimes sort of work when you print to the console is because printing things can 
            # cause the Python VM store a reference to the dialog, and with that prevent it from being 
            # garbage collected. But that is not a reliable way to keep the dialog alive.
            # dlg = KidooShotManagerDialog()
        
            dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=800, defaulth=600)
        
        # A global instance of the dialog, which we use to keep the dialog alive. THIS IS VERY MUCH NOT A
        # TECHNIQUE MEANT FOR PRODUCTION CODE, it is a hack. You need a persistent entity such as a command
        # to properly handle an async dialog. Because this hack relies on the assumption that the Python
        # module of Cinema 4D will keep script modules alive indefinitely, which is not guaranteed. But for
        # testing purposes, this is sort of okay.
        dlg: KidooShotManagerDialog = KidooShotManagerDialog()
        
        if __name__ == '__main__':
            main()
        

        MAXON SDK Specialist
        developers.maxon.net

        ferdinandF 1 Reply Last reply Reply Quote 1
        • ferdinandF
          ferdinand @ferdinand
          last edited by

          And just to be clear, using a modal dialog, e.g., DLG_TYPE_MODAL, is absolutely fine in a script manager script. Because then the GeDialog.Open call will only return when the dialog is closed (and you therefore do not have to keep the dialog alive).

          The hack I showed above is only needed when you need one of the async dialog types in a script manager script for testing purposes.

          MAXON SDK Specialist
          developers.maxon.net

          1 Reply Last reply Reply Quote 1
          • First post
            Last post