[GeUserArea] Click detection selects wrong item when clicking on lower part of cell (image + text area)
-
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? -
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 aGridItem
class which encapsulate the items and their hit logic. What matters also is how you feeddef 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 -
@ferdinand
This is my GeUserArea classclass 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
-
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 thinkBFM_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 likeGeDialog.GetVisibleArea
? There you could also unintentionally mix coordinate systems.Cheers,
Ferdinand