Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush Python API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Add "Clickable" Text in a GUI?

    Cinema 4D SDK
    r25 python
    2
    5
    626
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • B
      bentraje
      last edited by

      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?

      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @bentraje
        last edited by ferdinand

        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

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 0
        • B
          bentraje
          last edited by bentraje

          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

          1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand
            last edited by ferdinand

            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)
            

            MAXON SDK Specialist
            developers.maxon.net

            1 Reply Last reply Reply Quote 1
            • B
              bentraje
              last edited by bentraje

              @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.

              1 Reply Last reply Reply Quote 0
              • First post
                Last post