GeUserArea keeps returning (null) on window move even with DrawMsg returning True
-
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()
-
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()
-
And just to be clear, using a modal dialog, e.g.,
DLG_TYPE_MODAL
, is absolutely fine in a script manager script. Because then theGeDialog.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.