Solved some problem with CUSTOMGUI_BITMAPBUTTON

there is some problem with CUSTOMGUI_BITMAPBUTTON, I want to use it to create a file browser.
my envirment: c4d R26, Python
1.width self-adaptive,when I change the dialog width,the number of BitmapButton in row could be changed by the dialog width。
GIF 2022-9-2 10-29-09.gif
just like this.
here is my demo picture:face_with_thermometer:
GIF 2022-9-2 10-29-44.gif

2.can I capture the double click event Message with bitmapbutton? I want entry the directory when I double click the directory bitmapbutton。

3.the bottom of the directory bitmapbutton Text
when the text is too long, it will be hide in the end

exp. Text is “electric wire”
I want Is Shows "delctric..."
but is Shows "electric w"
how could I measure the width,or Is there have some other solution?

thanks very much for reading! have a good day!

here is my demo code

import c4d

class Test1Dialog(c4d.gui.GeDialog):
    ID_BITMAP_BUTTON = 10000
    ID_BITMAP_TEXT = 20000

    def CreateLayout(self):
        """Creates the layout for the dialog.
        if self.ScrollGroupBegin(0, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, c4d.SCROLLGROUP_VERT  |  c4d.SCROLLGROUP_AUTOVERT, 80, 80):
                for i in range(30):
                    bc = c4d.BaseContainer()
                    bc[c4d.BITMAPBUTTON_BUTTON] = True
                    bc[c4d.BITMAPBUTTON_ICONID1] = 1052837
                    bc[c4d.BITMAPBUTTON_BACKCOLOR] = c4d.COLOR_BG
                    bc[c4d.BITMAPBUTTON_DISABLE_FADING] = False
                    bc[c4d.BITMAPBUTTON_FORCE_SIZE] = Test1Dialog.BITMAP_WIDTH
                    if self.GroupBegin(0, c4d.BFH_LEFT, 1, 2):
                        self.AddCustomGui(Test1Dialog.ID_BITMAP_BUTTON + i, c4d.CUSTOMGUI_BITMAPBUTTON, "", c4d.BFH_CENTER | c4d.BFV_CENTER, 0, 0, bc)
                        self.AddStaticText(id= Test1Dialog.ID_BITMAP_TEXT + i,flags=c4d.BFH_SCALEFIT,initw=Test1Dialog.BITMAP_WIDTH,inith=0,name="test string number" + str(i) ,borderstyle=0)
        self.AddDlgGroup(c4d.DLG_OK | c4d.DLG_CANCEL)
        return True

def main():
    global dialog
    dialog = Test1Dialog()
    dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, defaultw=-2, defaulth=-2)

if __name__ == '__main__':

Hello @mogli,

Thank you for reaching out to us. Please refer to our Forum Guidelines for the scope of support. Multiple questions constitute multiple topics.

So, to sort out the easy things first:

  • (2.) You can detect also bitmap button clicks just as all gadget interactions with GeDialog::Command. When you want to detect a double click, it would be up to you to associate two clicks in quick succession on one gadget as a single action.
  • (3.) That your text was cut off was caused by you using the incorrect layout flags in the if self.GroupBegin(0, c4d.BFH_LEFT, 1, 2): calls. Since you set here BFH_LEFT, the group could not scale to the required size of its static text child. You were also reusing here a gadget ID (0) since this line of code is called thirty times.

But your major problem, question (1.), has nothing to do with CUSTOMGUI_BITMAPBUTTON and more with your general approach to the problem and how you treat dialog groups. Groups cannot dynamically resize their column count in Cinema 4D and you never define the column count in your code. In general, I would also say that the approach you want to use here, place multiple items in a group, and then dynamically rearrange them there, is very labor intensive, because you have then to interact with and respect all the dynamic GUI handling Cinema 4D does. I would strongly recommend implementing a folder view as a single GeUserArea gadget, where you draw all items manually and have full control over spacing and other things.

When you are hell-bent on doing it in this manner, you can find below a draft. You must flush parts of the dialog on resize events and then rebuild the relevant group(s) with a column count which fits the new dialog width. But there are multiple hoops to jump through, especially when this all must happen inside a scroll view.


The result:
The code:

"""Example for a dialog which dynamically manages its gadgets based on its width.

The pattern shown here works, but is not very advisable. It would be better to implement such things
with GeUserArea, where the area renders all buttons and is wrapped by a scroll group. The solution
shown here is harder to handle and less clean, and has therefore still some glitches here and there,
especially when scaling back the width of dialog (you would have to communicate the scaling 
direction to RebuildFolderView() and handle the item row count differently based on that).

The major problem with your code was that you assumed dialog groups in Cinema 4D to work like a 
StackPanel, i.e., that things are dynamically arranged in that box. And while they have that quality
to some degree, they are in essence more like a grid gadget. When you set a group to have 10 columns,
Cinema 4D will always just place 10 items in each row, no matter if each item is just 1px wide, and
the group having a width of 1000px. The row and column count of groups is also static. So, no matter
how much you resize a group, its row and column count will always stay the same, even when items 
could be placed more densely with more columns and less rows.

In your code you used the row and column count (0, 0) which will cause Cinema 4D to place the items
in  a fit fashion towards the default size of a dialog. But as lined out above, this will not 
dynamically change based on the current dialog size.
import c4d
import math

class MyDialog(c4d.gui.GeDialog):
    """Implements a dialog which dynamically manages its gadgets based on its width.
    # The gadget IDs, more or less the same as yours. ID_GRP_FOLDER_SCROLL is the scroll group,
    # ID_GRP_FOLDER_CONTAINER the container in the scroll group, and ID_GDT_BASE is then the
    # base ID for all gadgets in that container.
    ID_GRP_SCROLL: int = 1000
    ID_GRP_CONTAINER: int = 1001
    ID_GDT_BASE: int = 2000

    # The icon width, number of items and item width. As explained below, 'guessing' the item width 
    # is just a lazy approach, GeDialog.GetItemDim would be more precise.
    ICON_WIDTH: int = 50
    ITEM_COUNT: int = 42
    ITEM_WIDTH: int = 55

    # The bitmap button data container, always the same, can therefore be attached to the class.
    DATA_BITMAPBUTTON: c4d.BaseContainer = c4d.BaseContainer()

    def __init__(self) -> None:
        # The dialog width for which a dialog state has been built.
        self._widthCache: int = c4d.NOTOK

    def CreateLayout(self):
        """Creates the layout for the dialog.
        # The scroll group, this gadget is never being removed. It is important to allow here both
        # horizontal and vertical scrolling because otherwise it will be impossible the scale the
        # dialog horizontally down.
        self.ScrollGroupBegin(MyDialog.ID_GRP_SCROLL, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 
                              c4d.SCROLLGROUP_HORIZ | c4d.SCROLLGROUP_VERT, 80, 80)
        self.GroupBorderSpace(5, 5, 5, 5)
        # The container which is being flushed when the dialog is resized, this gadget is never
        # being removed.
        self.GroupBegin(MyDialog.ID_GRP_CONTAINER, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT)
        # Build the dynamically crated content.
        return True

    def RebuildFolderView(self, width: int = 400) -> None:
        """Builds the dynamic folder list.
        # Calculate the number of items which fit into the width of the dialog. MyDialog.ITEM_WIDTH
        # is sort of a cheap hack here. MyDialog.ICON_WIDTH is not the correct value, because there
        # is space being added around items which must be accounted for. A more precise approach 
        # would be GeDialog.GetItemDim called on one of the button label container created below.
        itemsPerRow: int = int(math.floor(width / MyDialog.ITEM_WIDTH))
        print(f"\t{width = }, {itemsPerRow = }")

        # Flush all gadgets in ID_GRP_CONTAINER and place the gadget cursor after ID_GRP_CONTAINER. 
        # When this method is being called from CreateLayout, this will not do anything.

        # Create a group inside ID_GRP_CONTAINER where we can set the number of items per row, i.e.,
        # it looks like this:
        #   ID_GRP_SCROLL
        #       ID_GRP_CONTAINER
        #           [ID_GDT_BASE]   <--- The group here created.
        #               ID_BTN_1
        #               ID_BTN_2
        #               ...
        # This is necessary because (A) scroll groups itself cannot be flushed and (B) we must
        # dynamically change the number of rows in the group where we place the buttons. Since
        # we flush ID_GRP_CONTAINER (i.e., that gadget itself is static), we need this group here.
        self.GroupBegin(MyDialog.ID_GDT_BASE, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, itemsPerRow)

        # More or less your code.
        for i in range(MyDialog.ITEM_COUNT):
            idBase: int = MyDialog.ID_GDT_BASE + 1 + (i * 3)
            label: str = f"Label {i + 1}"
            self.GroupBegin(idBase, c4d.BFH_LEFT, 1, 2)
            self.AddCustomGui(idBase + 1, c4d.CUSTOMGUI_BITMAPBUTTON, "", 
                              c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 0, 0, MyDialog.DATA_BITMAPBUTTON)
            self.AddStaticText(idBase + 2, c4d.BFH_SCALEFIT,
                               MyDialog.ICON_WIDTH, name=label)

    def IsUpdateEvent(self, width: int) -> bool:
        """Determines if the dynamically created gadgets of the dialog should be rebuilt based on
        the passed dialog width.

        Doing this is necessary, especially the `(abs(self._widthCache - width) >= self.ITEM_WIDTH)`
        part, because otherwise `RebuildFolderView()` will introduce a feedback loop where the
        dialog changes its size, then RebuildFolderView() is called, which will cause gadgets to
        resize, which in turn will cause the dialog resize (to the same or marginally larger size),
        which then will cause RebuildFolderView() to be called, ...
        # Something did pass garbage.
        if not isinstance(width, int):
            return False
        # either the width cache has not been set yet, i.e., the dialog has never been resized, or
        # the current dialog width requires the gadgets to be rebuild.
        elif (self._widthCache == c4d.NOTOK) or (abs(self._widthCache - width) >= self.ITEM_WIDTH):
            self._widthCache = width
            return True
        # The current gadget layout is still the optimal one for this dialog width.
        return False

    def Message(self, msg: c4d.BaseContainer, result: c4d.BaseContainer) -> int:
        """Called by Cinema 4D to convey events to the dialog.

        Here used to react to size changes of the dialog.
        # When the dialog has been resized, ...
        if msg.GetId() == c4d.BFM_ADJUSTSIZE:
            # and the new width requires the gadgets to be rebuilt, ...
            width: int = msg[c4d.BFM_ADJUSTSIZE_WIDTH]
            if self.IsUpdateEvent(width):
                print(f"Update: {width}")
                # Then rebuild the dynamic gadgets and invoke an update event on #ID_GRP_CONTAINER.
                print(f"No update: {width}")
        return super().Message(msg, result)

def main():
    global dialog
    dialog = MyDialog()
    dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, defaultw=500, defaulth=500)

if __name__ == '__main__':

MAXON SDK Specialist

thank you very much! It's help me a lot! sorry about the multiple topics, I'll take them apart next time