DrawRoundedRectangle and DrawBitmapRounded cause UI overflow and artifacts even with SetClippingRegion
-
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()
-
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!
-
Hey @Amazing_iKe,
Thank you for reaching out to us.
- You do the same thing you did incorrectly in your other thread. You create an async dialog and you do not keep it alive.
- 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:
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 noCommandData.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,
Ferdinandedit: 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.