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

    DrawRoundedRectangle and DrawBitmapRounded cause UI overflow and artifacts even with SetClippingRegion

    Cinema 4D SDK
    windows python 2025
    2
    3
    177
    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 ferdinand

      Hi everyone,
      
      I'm encountering an issue when using DrawRoundedRectangle and DrawBitmapRounded in my custom GeUserArea.
      
      Whenever I draw elements with rounded corners, the UI content seems to overflow its intended boundary, and sometimes leaves visual artifacts (residual pixels). I've already added SetClippingRegion at the beginning of the DrawMsg
      However, it doesn't seem to help with the overflow or visual glitches when using the rounded drawing functions.
      
      Interestingly, when I switch back to DrawRectangle and DrawBitmap (without rounded corners), the problem goes away completely.
      
      Is this a known issue? Or is there any recommended way to safely use rounded drawing functions without causing overflow or artifacts?
      
      Thanks a lot for your help!!

      edit: @ferdinand -- redacted --

      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 = []  # 存储图片
              print("KidooShotManagerUserArea initialized.")
          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\Desktop\test\alexander-thieme-2.jpg')
                  except Exception as e:
                      bitmap.InitWith(r'C:\Users\19252\Desktop\test\alexander-thieme-2.jpg')
      
                  assets.append([bitmap, path, name])
      
              self.assets = assets
      
              self.Redraw()
      
              return assets
      
          def GetMinSize(self):
              return (100, 10000)
      
          def _calc_grid(self, width: int):
      
              """
              input:
              - width: width 是当前绘图区域的总宽度(比如 800 像素)
      
              returns:
              - cols: 列数
              - cell: 每个格子的实际大小
      
              """
      
      
              # 每个 cell 大小大概是 base_size,加上 padding 是单个格子的占位宽度
              tentative = max(1, int((width - self.padding) / (self.base_size + self.padding)))
      
              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)
              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)
      
      
      
      
      
      
      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.ScrollGroupBegin(0, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT,c4d.SCROLLGROUP_VERT)
              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\alexander-thieme-1.jpg', 'name':'001'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'002'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'003'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'004'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'005'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'006'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'007'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'008'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'002'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'003'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'004'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'005'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'006'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'007'},
                           {'path': r'C:\Users\19252\Desktop\test\alexander-thieme-1.jpg', 'name':'008'}]
              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
      • Amazing_iKeA
        Amazing_iKe
        last edited by

        Hi everyone,

        I'm implementing mouse interaction in a custom GeUserArea to select thumbnail items by index.

        I'm currently using mouse coordinates inside MouseInput() and calculating the selected index based on the position and layout of the grid. However, when the user scrolls the view down (for example, inside a scroll group), the calculated index starts to become incorrect — it seems to be offset or accumulated incorrectly.

        Here's a simplified explanation of what I'm doing:

        I calculate the row and column based on the mouse x and y

        Then I compute the index: index = row * col_count + col

        But when I scroll the UI, the y coordinate doesn't account for the scroll offset correctly, so the index becomes wrong

        Is there a proper way to get the scroll offset or compensate for it in GeUserArea, so that the mouse position corresponds correctly to the layout and gives me the accurate index?

        Any help or suggestions would be really appreciated. Thank you!

        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.

          1. You do the same thing you did incorrectly in your other thread. You create an async dialog and you do not keep it alive.
          2. We would prefer it to keep things G-rated here. I.e., no nudity, no violence, and no weapons. I removed the image in your posting.

          With that being said, and when I try out your script (with the dlg thing fixed). I end up with this:
          fc83d223-abc0-4568-98fc-deb82596b059-image.png

          I.e., it just works. But you docked your dialog (and so did I imitating what you did). I now cannot reproduce this anymore, but when I did something in my layout, I think I dragged a palette, I got a similar result as yours. That is not too surprising though as you implement here an async dialog in a hacky manner. You have no CommandData.RestoreLayout which would handle layout events for your dialog. Something getting out of whack with scroll areas is not out of question for such irregular cases.

          Please follow the patterns shown in our GUI examples I would recommend to follow py-cmd_gui_simple_2024, it also contains code for handling layout events. I.e., this:

              def RestoreLayout(self, secret: any) -> bool:
                  """Restores the dialog on layout changes.
          
                  Implementing this is absolutely necessary, as otherwise the dialog will not be
                  restored when the user changes the layout of Cinema 4D.
                  """
                  return self.Dialog.Restore(self.ID_PLUGIN, secret)
          

          And to be clear, it is absolutely fine to share plugins with us here. It does not have to be a script manager script. Things should just not get too long. Feel free to anonymize your plugin IDs when you feel skittish about sharing them in public.

          Cheers,
          Ferdinand

          edit: Okay, there it is again. Not exactly the same as yours but close. But I would still have to ask you to provide an example where this happens inside a valid async dialog implementation (with a command or a similar owner). It is not out of question that there is a bug but we still need a non-hacky example.

          df89038f-124d-434f-8cde-3442bd9aebba-image.png

          MAXON SDK Specialist
          developers.maxon.net

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