SOLVED Add "Clickable" Text in a GUI?

Hi,

Is there a way to have a clickable text in a GUI?
I was thinking of like the Object Manager where if you click an item, you can then set several commends like SetActiveObject, etc.

I understand that can be done with the TreeViewFunctions but I'm not after a hierarchy. I just really want a simple
self.AddClickableText line or something like that for my UI.

I checked the documentation. The closest is the AddStaticText but it does not have a parameter to be clicked.

Is there a way around this?

Hello @bentraje,

Thank you for reaching out to us. I do not fully understand your question, especially the part 'the closest is the AddStaticText but it does not have a parameter to be clicked'. GUI gadgets do not really have parameters in the sense Cinema 4D is using the term, so I assume when you say 'parameter', you mean an interaction event/message, i.e., you want to be informed when the user has interacted with the gadget.

Just to get the basics out of the way: Gadget interactions are routed through GeDialog.Command (direct interactions as clicks) and GeDialog.Message (lower-level events). So, when you have some gadget with the ID ID_MY_CLICK_TEXT, you must listen in .Command() for this event ID. There are then three options I see here:

  1. Simply use a button, i.e., GeDialog.AddButton. This might not be what you want visually, but this is how Cinema 4D usually realizes a 'clickable text'. If you want a 'flatter' look, you could use a c4d.gui.BitmapButtonCustomGui. There you would have to either render the text inside the button yourself by rendering it into a BaseBitmap, using GeClipMap, or just use an icon instead.
  2. There is also c4d.gui.HyperLinkCustomGui which does more or less the same. Important is here its flag HYPERLINK_IS_LINK with which you can turn on and off the gadget attempting to open the link in a browser on click events.
  3. Finally, there is GeDialog.AddStaticText. If I remember correctly, static texts do not fire off on click events in GeDialog.Command, probably the reason why you are asking this question here. You could however check if an interaction message is sent via GeDialog.Message when a static text gadget is being clicked. The event will likely be sent as BFM_ACTION where then the message data container holds the ID of the triggering element as BFM_ACTION_ID. See GUI Messages for an overview of the messages sent to dialogs.

I personally would just go with a button or bitmap button because that is usually how Cinema 4D solves the task of 'clickable text', but you can likely make all three options work.

Cheers,
Ferdinand

Hi @ferdinand

Thanks for the response. I'm trying the #2 route you presented but I'm having a problem on implementing it.
There is the AddCustomGui which I believe is just an equivalent of the AddStaticText but for some custom stuff (like the c4d.gui.HyperLinkCustomGui).

My only concern is that in the documentation under SymbolID. The HyperlinkCustomGui is not listed.

How do I pass that to the AddCustomGui?

P.S. As you can see, the HyperlinkCustomGui is not listed.
P.P.S. I already search the forum for c4d.gui.HyperLinkCustomGui but its only this thread that appears in the search result huehue
735fcb99-ac50-4224-8041-6462ba7363c0-image.png .S

Hey @bentraje,

The id is CUSTOMGUI_HYPER_LINK_STATIC, you can find it on the page of the custom GUI. However, I just tried it myself, and HYPERLINK_IS_LINK unfortunately turns the link effectively into a static text. Even more so, static texts do not send any interaction messages at all (at least I did not find any). So, the only route which remains is BitmapButton, find an example below.

Bitmaps containing text to be shown in GUIs are always tricky because one must combat interpolation issues. My example renders at twice the required resolution but is far from perfect.

Cheers,
Ferdinand

The result:
Screenshot 2022-11-14 at 21.27.55.png
The code:

"""Demonstrates how to render "clickable text" with a bitmap button.

Can be run as a Script Manger script.
"""

import c4d

class MyDialog(c4d.gui.GeDialog):
    """Implements a dialog using a bitmap button, a hyper link, and a static text gadget.
    """
    ID_GRP_MAIN: int = 1000

    ID_BTN_BITMAP: int = 2000
    ID_BTN_HYPERLINK: int = 2001
    ID_BTN_STATICTEXT: int = 2002
    
    def AddClickableText(self, gid: int, label: str):
        """Adds a bitmap button containing the text #label.

        Content is rendered at twice the output resolution to combat interpolation issues with the 
        text.
        """
        font: c4d.BaseContainer = c4d.bitmaps.GeClipMap.GetDefaultFont(c4d.GE_FONT_DEFAULT_SYSTEM)
        canvas: c4d.bitmaps.GeClipMap = c4d.bitmaps.GeClipMap()

        # Fake init the canvas with a generous size so that we can measure the size of #label.
        canvas.Init(1000, 100, 32)
        canvas.BeginDraw()
        canvas.SetFont(font, 0)
        # We render with a margin of 3, 3, 3, 3 and at twice the size to combat blurry text.
        w, h = (canvas.TextWidth(label) + 6) * 2, (canvas.TextHeight() + 6) * 2
        fSize: int = canvas.GetFontSize(font, c4d.GE_FONT_SIZE_INTERNAL)
        canvas.EndDraw()
        
        # Get the drawing colors for the gadget (bg) and text (fg)
        bg: c4d.Vector = c4d.gui.GetGuiWorldColor(c4d.COLOR_BG) * 255
        fg: c4d.Vector = c4d.gui.GetGuiWorldColor(c4d.COLOR_TEXT) * 255
        bg: tuple[int] = int(bg.x), int(bg.y), int(bg.z)
        fg: tuple[int] = int(fg.x), int(fg.y), int(fg.z)

        # Re-init the canvas at twice the final display size, fill the background and render the
        # text (also at twice the size).
        canvas.Init(w, h, 32)
        canvas.BeginDraw()
        canvas.SetFont(font, fSize * 2)
        canvas.SetColor(*bg)
        canvas.FillRect(0, 0, w, h)
        canvas.SetColor(*fg)
        canvas.TextAt(6, 6, label)
        canvas.EndDraw()

        # Prepare the bitmap button container, we force the button to be drawn at half its size.
        bc: c4d.BaseContainer = c4d.BaseContainer()
        bc.SetInt32(c4d.BITMAPBUTTON_BACKCOLOR, c4d.COLOR_BG)
        bc.SetInt32(c4d.BITMAPBUTTON_FORCE_SIZE, int(canvas.GetBw() / 2))
        bc.SetInt32(c4d.BITMAPBUTTON_FORCE_SIZE_Y, int(canvas.GetBh() / 2))
        bc.SetBool(c4d.BITMAPBUTTON_DISABLE_FADING, True)

        # Add the button and set the image.
        button: c4d.gui.BitmapButtonCustomGui = self.AddCustomGui(
            gid, c4d.CUSTOMGUI_BITMAPBUTTON, "", c4d.BFH_SCALE, 0, 0, bc)
        if not button:
            raise MemoryError("Could not allocate bitmap button.")
        
        button.SetImage(canvas.GetBitmap())



    def CreateLayout(self) -> bool:
        """Adds the three gadgets to the dialog.
        """
        self.SetTitle("Clickable Text Examples")

        self.GroupBegin(id=self.ID_GRP_MAIN, flags=c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=1)
        self.GroupBorderSpace(5, 5, 5, 5)

        # Add the bitmap button.
        self.AddClickableText(self.ID_BTN_BITMAP, "Bitmap Button")

        # Add the static text and the hyper-link, both won't work regarding raising click events.
        self.AddStaticText(self.ID_BTN_STATICTEXT, c4d.BFH_SCALE, name="Static Text")

        bc: c4d.BaseContainer = c4d.BaseContainer()
        bc.SetString(c4d.HYPERLINK_LINK_TEXT, "HyperLink")
        bc.SetString(c4d.HYPERLINK_LINK_DEST, "")
        bc.SetBool(c4d.HYPERLINK_IS_LINK, False)
        bc.SetBool(c4d.HYPERLINK_NO_UNDERLINE, True)
        self.AddCustomGui(self.ID_BTN_HYPERLINK, c4d.CUSTOMGUI_HYPER_LINK_STATIC, "", 
            c4d.BFH_SCALE, 0, 0, bc)

        self.GroupEnd()

        return super().CreateLayout()


    def Command(self, cid: int, msg: c4d.BaseContainer) -> bool:
        """Called by Cinema 4D to signal gadget interactions.
        """
        if cid == self.ID_BTN_BITMAP:
            print ("Command: Bitmap Button")
        # Never emitted once HYPERLINK_IS_LINK is set to false.
        elif cid == self.ID_BTN_HYPERLINK:
            print ("Command: Hyper Link")
        # Never emitted
        elif cid == self.ID_BTN_STATICTEXT:
            print ("Command: Static Text")

        return super().Command(cid, msg)

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

        This also includes gadget interactions with BFM_ACTION.
        """
        # Only emitted for ID_BTN_BITMAP, i.e., the bitmap button which shows up in Command()
        # anyways.
        if msg.GetId() == c4d.BFM_ACTION:
            print (f"Action from: {msg.GetInt32(c4d.BFM_ACTION_ID)}")
        # In fact, ID_BTN_HYPERLINK and ID_BTN_STATICTEXT never appear in any message data.
        elif MyDialog.ContainsIds(msg, (self.ID_BTN_HYPERLINK, self.ID_BTN_STATICTEXT)):
            print (f"{msg.GetId() = }")

        return super().Message(msg, result)

    @staticmethod
    def ContainsIds(bc: c4d.BaseContainer, idCollection: tuple[int]) -> bool:
        """Returns if an element of #idCollection is contained in #bc or one of its descendant
        containers.
        """
        for key, value in bc:
            if value in idCollection:
                return True
            if bc.GetType(key) == c4d.DA_CONTAINER:
                if MyDialog.ContainsIds(value, idCollection):
                    return True

        return False

DIALOG: MyDialog = MyDialog()

if __name__ == "__main__":
    DIALOG.Open(c4d.DLG_TYPE_ASYNC, defaultw=300, default=200)

@ferdinand

RE: The id is CUSTOMGUI_HYPER_LINK_STATIC, you can find it on the page of the custom GUI
Gotcha. Thanks for the confirmation.
Just wondering, why is that ID not also on the list of the Custom GUIs in the documentation. Along with other CUSTOMGUIs.
Is it because the list will be too long already?

RE: code
Can confirm it works as expected. Although had to revised this default=200 part to defaultw=200.