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] Click detection selects wrong item when clicking on lower part of cell (image + text area)

    Cinema 4D SDK
    windows python 2025 2024
    2
    4
    69
    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

      Hello,

      I am implementing a custom thumbnail grid browser inside a GeUserArea.
      The grid cell contains an image + text label.

      I calculate the rectangle of each cell using _get_cell_rect(),
      and then check whether the mouse click is inside that rectangle.

      Here is the simplified code:

          def __init__(self, dialog=None):
              super(KidooShotManagerUserArea, self).__init__()
              self.dialog = dialog
      
              # 我自己的库实例
              self.GetC4dCont = GetC4DContent()
              self.PathPro = PathProcess()
              self.GuiUtils = GuiUtils()
      
      
              self.padding = 8  # 每张图片之间的间距。
              self.base_size = 128  # 每张图片的基础大小
              self.min_size = 50  # 最小尺寸
              self.max_size = 256  # 最大尺寸
              self.scale_factor = 1.0  # 缩放因子
              
              self.col_count = 4  # 列数
              self.cell_size = self.base_size  # 每个格子的实际大小
      
              self.rounded_radius = 10  # 圆角半径
      
              self.assets = []  # 存储图片
      
              self.selected_index = 0 # 用于记录选中卡片
      
              # 基础颜色变量
              self.bg_color = c4d.Vector(43/255.0, 43/255.0, 43/255.0)  # 背景颜色
              self.bg_color_01 = c4d.Vector(20/255.0, 20/255.0, 20/255.0)  # 背景颜色
          def _calc_grid(
              self,
              width: int,
              base_size: int,
              padding: int,
              min_size: int,
              max_size: int,
              scale_factor: float = 1.0,
              mode: str = "auto",
              fixed_cols: int = None,
              fixed_cell: int = None
          ):
              """
              根据区域宽度和配置计算网格列数与 cell 尺寸
              :param width: 总区域宽度
              :param base_size: 基础尺寸(设计稿默认卡片大小)
              :param padding: 内边距
              :param min_size: 允许的最小尺寸
              :param max_size: 允许的最大尺寸
              :param scale_factor: 缩放因子(用户滑块控制,默认1.0)
              :param mode: "auto" | "fixed_cols" | "fixed_cell"
              :param fixed_cols: 固定列数
              :param fixed_cell: 固定格子大小
              """
              if mode == "fixed_cols" and fixed_cols is not None:
                  cols = fixed_cols
                  cell = (width - (cols + 1) * padding) / cols
                  return cols, max(min(cell, max_size), min_size)
      
              if mode == "fixed_cell" and fixed_cell is not None:
                  cols = max(1, int((width + padding) / (fixed_cell + padding)))
                  return cols, fixed_cell
      
              # --- 改进的 auto 模式 ---
              # 用 scale_factor 控制缩放
              scaled_size = base_size * scale_factor
              cell = max(min(scaled_size, max_size), min_size)
      
              cols = max(1, int((width + padding) / (cell + padding)))
              return cols, cell
      
      def _get_cell_rect(
          self, index, area_width, base_size, padding,
          min_size, max_size, text_height, scale_factor=1.0
      ):
          cols, cell = self._calc_grid(area_width, base_size, padding, min_size, max_size, scale_factor)
          total_cell_height = cell + text_height + padding
          row = index // cols
          col = index % cols
      
          px = padding + col * (cell + padding)
          py = padding + row * (total_cell_height + padding)
      
          rect_x1 = px
          rect_y1 = py
          rect_x2 = px + cell
          rect_y2 = py + cell + text_height + padding  # <- include text height
      
          return int(rect_x1), int(rect_y1), int(rect_x2), int(rect_y2)
      
          def DrawMsg(self, x1, y1, x2, y2, msg):
              # 绘制背景
              self.SetClippingRegion(x1, y1, x2, y2) # 设置裁剪区域
              self.OffScreenOn() # 开启屏幕裁剪
              self.DrawSetPen(c4d.COLOR_BG) #设置背景颜色
              self.DrawRectangle(x1, y1, x2, y2) # 绘制背景
              self.DrawSetPen(1) #设置背景颜色
              
              # 计算容器大小
              container_width  = x2 - x1
              container_height = y2 - y1
              
              self.DrawSetFont(c4d.FONT_DEFAULT)
              text_height = self.DrawGetFontHeight()
              
              for index, (bitmap, _, name) in enumerate(self.assets):
                  image_orig_width = bitmap.GetBw()
                  image_orig_height = bitmap.GetBh()
                  
                  
                  
                  # 获取绘制位置
                  # 获取绘制位置(当前索引 index 对应卡片的矩形区域)
                  px, py, px2, py2 = self._get_cell_rect(
                      index=index,                        # 当前要绘制的卡片索引
                      area_width=self.GetWidth(),         # 当前用户区域的总宽度(UI 绘制区域宽度)
                      base_size=self.base_size,           # 期望的基础 cell 大小(默认 128px)
                      padding=self.padding,               # cell 之间的间距(默认 8px)
                      min_size=self.min_size,             # cell 允许的最小尺寸(避免过小)
                      max_size=self.max_size,             # cell 允许的最大尺寸(避免过大)
                      text_height=text_height, # 当前字体的高度,用于计算文字占位
                      scale_factor=self.scale_factor, # 当前缩放因子(用户滑块控制,默认 1.0)
                  )
                  self.DrawSetPen(c4d.Vector(0.5, 0.5, 0.5))
                  self.DrawRectangle(px, py, px2, py2)
                  
                  
                  
                  
                  
                  
                  
                  cell = self.cell_size
                  padding = self.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
      
                  # 鼠标选中边缘高光绘制
                  if index == self.selected_index:
                      border_thickness = 2  # 边框线宽,可调整
                      border_color = c4d.Vector(1.0, 1.0, 1.0)  # 例如白色边框,可根据需要更改颜色
                      self.DrawSetPen(border_color)  # 设置绘制颜色为高亮边框颜色
                      # 绘制矩形边框,范围比卡片区域每边扩大2像素
                      self.DrawFrame(px, py, px2, py2, lineWidth=border_thickness)
      def on_click(self, x, y):
          self.DrawSetFont(c4d.FONT_DEFAULT)
          text_height = self.DrawGetFontHeight()
      
          for index, (_, _, name) in enumerate(self.assets):
              px, py, px2, py2 = self._get_cell_rect(
                  index=index,
                  area_width=self.GetWidth(),
                  base_size=self.base_size,
                  padding=self.padding,
                  min_size=self.min_size,
                  max_size=self.max_size,
                  text_height=text_height,
                  scale_factor=self.scale_factor
              )
      
              if px <= x <= px2 and py <= y <= py2:
                  self.selected_index = index
                  self.Redraw()
                  break
      
      

      When I click on the lower part of a cell (in the text label area),
      sometimes the next cell below gets selected instead of the one I clicked.
      
      So visually I click "inside" the bottom of cell A,
      but on_click reports cell B (the one below A) as selected.
      
      Question
      
      Am I calculating the rectangle wrong?
      
      Should the text label be excluded from the clickable area?
      
      Or is this expected behavior of GeUserArea and I need to treat the image and text separately?

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

        Hey @Amazing_iKe,

        thank you for reaching out to us. I cannot execute your code example like this. Please provide an executable example. And as far as I can see, that also seems to be your own code which fails there and not one of our API calls? We cannot debug your code for you. Or is there a specific API call which is failing for you?

        My recommendation would be more abstraction, all this function shuffling especially in the context of GUIs gets really confusing really quick. I would write myself a Grid and a GridItem class which encapsulate the items and their hit logic. What matters also is how you feed def on_click(self, x, y). There are local (e.g. within a user area) and global (e.g., within a dialog or screen) coordinate systems.

        Cheers,
        Ferdinand

        MAXON SDK Specialist
        developers.maxon.net

        Amazing_iKeA 1 Reply Last reply Reply Quote 0
        • Amazing_iKeA
          Amazing_iKe @ferdinand
          last edited by

          @ferdinand
          This is my GeUserArea class

          class KidooShotManagerUserArea(gui.GeUserArea):
              def __init__(self, dialog=None):
                  super(KidooShotManagerUserArea, self).__init__()
                  self.dialog = dialog
          
                  # 我自己的库实例
                  self.GetC4dCont = GetC4DContent()
                  self.PathPro = PathProcess()
                  self.GuiUtils = GuiUtils()
          
          
                  self.padding = 8  # 每张图片之间的间距。
                  self.base_size = 128  # 每张图片的基础大小
                  self.min_size = 50  # 最小尺寸
                  self.max_size = 256  # 最大尺寸
                  self.scale_factor = 1.0  # 缩放因子
                  
                  self.col_count = 4  # 列数
                  self.cell_size = self.base_size  # 每个格子的实际大小
          
                  self.rounded_radius = 10  # 圆角半径
          
                  self.assets = []  # 存储图片
          
                  self.selected_index = 0 # 用于记录选中卡片
          
                  # 基础颜色变量
                  self.bg_color = c4d.Vector(43/255.0, 43/255.0, 43/255.0)  # 背景颜色
                  self.bg_color_01 = c4d.Vector(20/255.0, 20/255.0, 20/255.0)  # 背景颜色
          
              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', '')
                      default_image = os.path.join(_ICONPATH, 'photo-film_white.png')
                      bitmap = None
                      try:
                          if os.path.exists(path):
                              # 如果路径存在,尝试加载图片
                              bitmap = self.GuiUtils.load_bitmap(path)
                          else:
                              # 如果路径不存在,尝试加载默认图片(注:确保默认图片可以被正确加载)
                              bitmap = self.GuiUtils.load_bitmap(default_image)
                      except Exception as e:
                          bitmap = self.GuiUtils.load_bitmap(default_image)
          
                      assets.append([bitmap, path, name])
          
                  self.assets = assets
          
                  self.Redraw()
          
          
                  return assets
          
              def GetMinSize(self):
                  """
                  动态计算最小尺寸,基于卡片数量和布局参数
                  """
                  if not self.assets:
                      return 0, 100  # 没有资产时返回最小高度
                  
                  # 获取当前区域宽度(如果还没有初始化,使用默认值)
                  area_width = self.GetWidth() if hasattr(self, 'GetWidth') and self.GetWidth() > 0 else 400
                  
                  # 计算字体高度
                  self.DrawSetFont(c4d.FONT_DEFAULT)
                  text_height = self.DrawGetFontHeight()
                  
                  # 计算网格布局
                  cols, cell = self._calc_grid(
                      area_width,
                      self.base_size,
                      self.padding,
                      self.min_size,
                      self.max_size,
                      self.scale_factor
                  )
                  
                  # 计算总行数
                  total_cards = len(self.assets)
                  rows = (total_cards + cols - 1) // cols  # 向上取整
                  
                  # 计算总高度
                  total_cell_height = cell + text_height + self.padding
                  total_height = (rows * (total_cell_height + self.padding)) + self.padding
                  
                  # 确保最小高度
                  min_height = max(int(total_height), 100)
                  
                  return 0, min_height
          
              def _calc_grid(
                  self,
                  width: int,
                  base_size: int,
                  padding: int,
                  min_size: int,
                  max_size: int,
                  scale_factor: float = 1.0,
                  mode: str = "auto",
                  fixed_cols: int = None,
                  fixed_cell: int = None
              ):
                  """
                  根据区域宽度和配置计算网格列数与 cell 尺寸
                  :param width: 总区域宽度
                  :param base_size: 基础尺寸(设计稿默认卡片大小)
                  :param padding: 内边距
                  :param min_size: 允许的最小尺寸
                  :param max_size: 允许的最大尺寸
                  :param scale_factor: 缩放因子(用户滑块控制,默认1.0)
                  :param mode: "auto" | "fixed_cols" | "fixed_cell"
                  :param fixed_cols: 固定列数
                  :param fixed_cell: 固定格子大小
                  """
                  if mode == "fixed_cols" and fixed_cols is not None:
                      cols = fixed_cols
                      cell = (width - (cols + 1) * padding) / cols
                      return cols, max(min(cell, max_size), min_size)
          
                  if mode == "fixed_cell" and fixed_cell is not None:
                      cols = max(1, int((width + padding) / (fixed_cell + padding)))
                      return cols, fixed_cell
          
                  # --- 改进的 auto 模式 ---
                  # 用 scale_factor 控制缩放
                  scaled_size = base_size * scale_factor
                  cell = max(min(scaled_size, max_size), min_size)
          
                  cols = max(1, int((width + padding) / (cell + padding)))
                  return cols, cell
          
              def _get_cell_rect(
                  self,
                  index: int,
                  area_width: int,
                  base_size: int,
                  padding: int,
                  min_size: int,
                  max_size: int,
                  text_height: int,
                  scale_factor: float = 1.0,
                  mode: str = "auto",
                  fixed_cols: int = None,
                  fixed_cell: int = None
              ):
                  """
                  获取指定 index 的 cell 坐标区域 (含文字区域)
                  """
                  cols, cell = self._calc_grid(
                      area_width,
                      base_size,
                      padding,
                      min_size,
                      max_size,
                      scale_factor,
                      mode,
                      fixed_cols=fixed_cols,
                      fixed_cell=fixed_cell
                  )
          
                  total_cell_height = cell + text_height + padding
                  row = index // cols
                  col = index % cols
          
                  px = padding + col * (cell + padding)
                  py = padding + row * (total_cell_height + padding)
          
                  rect_x1 = px
                  rect_y1 = py
                  rect_x2 = px + cell
                  rect_y2 = py + cell + text_height + padding
          
                  return int(rect_x1), int(rect_y1), int(rect_x2), int(rect_y2)
          
              def set_cell_size(self, size: int = None):
                  if size is None:
                      size = self.base_size
                  self.scale_factor = size
                  
                  self.Redraw()
                  
              def DrawMsg(self, x1, y1, x2, y2, msg):
                  # 绘制背景
                  self.SetClippingRegion(x1, y1, x2, y2) # 设置裁剪区域
                  self.OffScreenOn() # 开启屏幕裁剪
                  self.DrawSetPen(c4d.COLOR_BG) #设置背景颜色
                  self.DrawRectangle(x1, y1, x2, y2) # 绘制背景
                  self.DrawSetPen(1) #设置背景颜色
                  
                  
                  self.DrawSetFont(c4d.FONT_DEFAULT)
                  text_height = self.DrawGetFontHeight()
                  
                  for index, (bitmap, _, name) in enumerate(self.assets):
                      image_orig_width = bitmap.GetBw()
                      image_orig_height = bitmap.GetBh()
                      
                      
                      
                      # 获取绘制位置
                      # 获取绘制位置(当前索引 index 对应卡片的矩形区域)
                      px, py, px2, py2 = self._get_cell_rect(
                          index=index,                        # 当前要绘制的卡片索引
                          area_width=self.GetWidth(),         # 当前用户区域的总宽度(UI 绘制区域宽度)
                          base_size=self.base_size,           # 期望的基础 cell 大小(默认 128px)
                          padding=self.padding,               # cell 之间的间距(默认 8px)
                          min_size=self.min_size,             # cell 允许的最小尺寸(避免过小)
                          max_size=self.max_size,             # cell 允许的最大尺寸(避免过大)
                          text_height=text_height, # 当前字体的高度,用于计算文字占位
                          scale_factor=self.scale_factor, # 当前缩放因子(用户滑块控制,默认 1.0)
                      )
                      # 绘制背景
                      self.DrawSetPen(self.bg_color_01)
                      self.DrawRectangle(px, py, px2, py2)
                      
                      
                      
                      cols, actual_cell = self._calc_grid(
                      self.GetWidth(),
                      self.base_size,
                      self.padding,
                      self.min_size,
                      self.max_size,
                      self.scale_factor
                  )
                      
                      
                      
                      cell = int(actual_cell)
          
          
                      # 缩放计算
                      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
          
                      # 鼠标选中边缘高光绘制
                      if index == self.selected_index:
                          border_thickness = 2  # 边框线宽,可调整
                          border_color = c4d.Vector(1.0, 1.0, 1.0)  # 例如白色边框,可根据需要更改颜色
                          self.DrawSetPen(border_color)  # 设置绘制颜色为高亮边框颜色
                          # 绘制矩形边框,范围比卡片区域每边扩大2像素
                          expand = border_thickness + 2
                          self.DrawFrame(px - expand, py - expand, px2 + expand, py2 + expand, lineWidth=border_thickness)
          
          
          
                      # 绘制图片背景
                      self.DrawSetPen(1001)
                      self.DrawRectangle(px, py, px + cell, py + cell)
          
                      # 绘制图片
                      self.DrawBitmap(bitmap, ox, oy, dw, dh, 0, 0, image_orig_width, image_orig_height,
                                      c4d.BMP_ALLOWALPHA | c4d.BMP_NORMALSCALED)
          
                      # 绘制文字
                      text_width = self.DrawGetTextWidth(name)
                      text_x = px + cell // 2 - text_width // 2
                      text_y = py + cell + self.padding // 2
          
                      self.DrawSetTextCol(1, self.bg_color_01)
                      self.DrawSetFont(c4d.FONT_DEFAULT | c4d.FONT_BIG | c4d.FONT_BIG_BOLD)
                      self.DrawText(name, text_x, text_y, c4d.DRAWTEXT_STD_ALIGN)
          
          
              def InputEvent(self, msg):
          
                  
                  # 判断事件是否为鼠标滚轮,并且有修饰键按下(QUALIFIER,例如 Ctrl/Shift)
                  if msg[c4d.BFM_INPUT_CHANNEL] == c4d.BFM_INPUT_MOUSEWHEEL and msg[c4d.BFM_INPUT_QUALIFIER]:
                      delta = msg.GetInt32(c4d.BFM_INPUT_VALUE)  # 获取滚轮的数值(正=向上,负=向下)
          
                      size = self.scale_factor  # 当前缩放因子
          
                      # 根据滚轮方向调整缩放因子
                      if delta < 0:
                          size -= 0.05  # 滚轮向下 → 缩小
                      else:
                          size += 0.05  # 滚轮向上 → 放大
          
                      # 限制缩放范围在 [0.4, 2.0] 之间
                      size = max(0.4, min(2.0, size))
          
                      # 只有在新值和旧值不同的时候才更新(避免重复刷新)
                      if size != self.scale_factor:
                          self.scale_factor = size
                          self.set_cell_size(self.scale_factor)  # 应用新的缩放因子到 UI
                          
                  if msg[c4d.BFM_INPUT_DEVICE] == c4d.BFM_INPUT_MOUSE \
                     and msg[c4d.BFM_INPUT_CHANNEL] == c4d.BFM_INPUT_MOUSELEFT \
                     and msg[c4d.BFM_INPUT_VALUE]:
          
                      local_x = msg[c4d.BFM_INPUT_X]
                      local_y = msg[c4d.BFM_INPUT_Y]
          
                      scroll_y = self.dialog.get_scroll_offset()
          
                      actual_x = local_x
                      actual_y = local_y + scroll_y
          
                      self.on_click(actual_x, actual_y)
                  return True
          
              def on_click(self, x, y):
                  self.DrawSetFont(c4d.FONT_DEFAULT)
                  text_height = self.DrawGetFontHeight()
                  for index, (_, _, name) in enumerate(self.assets):
          
                      # 使用完整的 _get_cell_rect 公式来计算卡片矩形
                      px, py, px2, py2 = self._get_cell_rect(
                          index=index,                        # 当前卡片索引
                          area_width=self.GetWidth(),         # 用户区域宽度
                          base_size=self.base_size,           # 基础 cell 大小(默认 128px)
                          padding=self.padding,               # cell 间距
                          min_size=self.min_size,             # 最小 cell 尺寸
                          max_size=self.max_size,             # 最大 cell 尺寸
                          text_height=text_height,       # 文字高度(需要在类里存好)
                          scale_factor=self.scale_factor      # 缩放因子(用户滑动控制)
                      )
          
                      # 判断点击坐标是否在当前卡片矩形内
                      if px <= x <= px2 and py <= y <= py2:
                          self.selected_index = index
                          self.Redraw()  # 触发重绘,显示选中效果
          
                          # 发送点击消息给父级
                          magContainer = BaseContainer(ID_MSG_KIDOO_SHOT_MANAGER_IMAGE_CLICKED)
                          magContainer.SetInt32(0, index)
                          magContainer.SetString(1, name)
                          self.SendParentMessage(magContainer)
                          break
          
          
          
          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand @Amazing_iKe
            last edited by ferdinand

            Hey,

            thanks for the extended code, but I still cannot run this, as it is only a fragment 😉 So, I only have a very rough understanding of what is going wrong.

            A likely issue apart from you just having a bug in your hit logic, is that you use the wrong coordinate system.

            if msg[c4d.BFM_INPUT_DEVICE] == c4d.BFM_INPUT_MOUSE \
                and msg[c4d.BFM_INPUT_CHANNEL] == c4d.BFM_INPUT_MOUSELEFT \
                and msg[c4d.BFM_INPUT_VALUE]:
            
                local_x = msg[c4d.BFM_INPUT_X]
                local_y = msg[c4d.BFM_INPUT_Y]
            
                scroll_y = self.dialog.get_scroll_offset()
            

            I would have to check myself, but there is some inconsistency with which dialogs send mouse input messages regarding the used coordinate system. Sometimes they send messages in the coordinate system of the dialog and sometimes messages in the coordinate system of the gadget. Just print out your (local_x and local_y) and check if the values make sense as local coordinates as you seem to treat them. On GeUserArea are multiple coordinate conversion methods. I think BFM_INPUT in this context is in local user area coordinates but I am not sure, it could also be that you have to convert them.

            The other thing is of course that you add this self.dialog.get_scroll_offset() on top of things. I assume this is sourced by something like GeDialog.GetVisibleArea? There you could also unintentionally mix coordinate systems.

            Cheers,
            Ferdinand

            MAXON SDK Specialist
            developers.maxon.net

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