Eto Button - Border Type/Removal

Hello,

I’m working on a basic 3x3 grid UI (Table Layout) in which I’m setting custom image buttons.

So far it looks like this:
image

In Eto documentation I see how to remove borders of TextBox, Label, and SearchBox but can’t seem to figure out where we can change the Button border styles?

I have a custom button class I setup cleverly called MyButton.

class MyButton(forms.Button):
    def __init__(self, text, tag, on_click):
        super(MyButton, self).__init__()
        self.Text = text
        self.Tag = tag
        self.OnClick = on_click

        # Initialize default image
        self.DefaultImage = drawing.Bitmap()

        self.Click += self.on_click_handler
        self.MouseEnter += self.on_mouse_enter
        self.MouseLeave += self.on_mouse_leave

Is this where I would apply border styling?

To summarize, with a button control, can I do the following:

  1. Remove the border line
  2. Change the hover/highlight color
  3. Set a custom image for default/hover/click states (I did figure out a solution for this but I was hoping this is natively available with the button properties or methods somewhere?)

Part of the “Button State” image setting logic:

    def on_mouse_enter(self, sender, e):
        # Change image on hover
        hover_image_path = os.path.join(image_folder_path, "ICO_{}_Hover.png".format(self.Text))
        hover_image = drawing.Bitmap(hover_image_path)
        sender.Image = hover_image

    def on_mouse_leave(self, sender, e):
        # Change image to default on mouse leave
        sender.Image = self.DefaultImage


    def on_mouse_down(self, sender, e):
        if self.ClickImage:
            sender.Image = self.ClickImage

My form class where I’m setting some stuff about my buttons like giving them a tag and setting the background color to be transparent:

class MyForm(forms.Form):
    def __init__(self):
        super(MyForm, self).__init__()

        # Rhino.UI.EtoExtensions.UseRhinoStyle(self) # Follow Rhino Light/Dark Mode
 
        self.Styles.Add[forms.Panel]("transparent", self.MyFormStyler)
        self.Style = "transparent"

        # Form Properties
        self.Title = "Table Layout with Buttons"
        self.Padding = drawing.Padding(10)
        self.WindowStyle = forms.WindowStyle.None
        self.Size = drawing.Size(300, 300)
        self.Padding = drawing.Padding(40)
        self.MovableByWindowBackground = True
        self.AutoSize = False
        self.Resizable = False
        self.TopMost = True
        self.ShowActivated = False

        forms.Button()
        # Setup Buttons & Tags
        self.FloorButton = forms.Button(Text="Floor", Tag="00",BackgroundColor = drawing.Colors.Transparent)
        self.FloorButton.Click += self.OnButtonClick

Here’s the code I was using to set the button state imagery:

import Rhino
import Eto.Drawing as drawing
import Eto.Forms as forms

class MyForm(forms.Form):
    def __init__(self):
        self.Title = "Image Button Example"
        self.WindowStyle = forms.WindowStyle.None
        self.ClientSize = drawing.Size(200, 200)
        
        # Load your images
        self.normal_image_path = "default button image file path"
        self.hover_image_path = "hover button image file path"
        self.click_image_path = "click button image file path"
        
        self.normal_image = drawing.Bitmap(self.normal_image_path)
        self.hover_image = drawing.Bitmap(self.hover_image_path)
        self.click_image = drawing.Bitmap(self.click_image_path)

        # Create the image button
        self.image_button = forms.Button(Text="", Image=self.normal_image, Size=drawing.Size(100, 100))
        self.image_button.MouseEnter += self.on_button_mouse_enter
        self.image_button.MouseLeave += self.on_button_mouse_leave
        self.image_button.Click += self.on_button_click

        # Add the image button to the form
        layout = forms.DynamicLayout()
        layout.AddRow(self.image_button)
        self.Content = layout

    def on_button_mouse_enter(self, sender, e):
        self.image_button.Image = self.hover_image

    def on_button_mouse_leave(self, sender, e):
        self.image_button.Image = self.normal_image

    def on_button_click(self, sender, e):
        self.image_button.Image = self.click_image
        Rhino.RhinoApp.WriteLine("Image button clicked!")
        self.Close()

# Create and show the form
form = MyForm()
form.Show()

default button state:
image

hover button state:
image

clicked button state:
image

Thanks for your time and response!

5 Likes

You might want to read through this discussion:

But in all honesty, for this use case, I’d probably just write my own button class derived from a Drawable. In the OnMouseEnter event set a ‘hover’ flag to true, set to false on OnMouseLeave. In OnPaint draw your bitmap, and add your outline depending on the ‘hover’ state.

You’ll write it up in 15 minutes and keep ultimate control over the appearance of your custom button.

Thanks @mrhe !

Do you happen to have a “template” example of what that may look like?

I haven’t yet figured out how to turn a drawable into a button that you can click/hover on only how to make a button “look like a drawable” which is not a great solution obviously.

I see it inherits from Control so I imagine that is what allows the events to work on it.

This is the direction I’m trying to head where I have images that work as buttons but that I have more graphic control over, hence, drawable as you mentioned.

Any pointers would be greatly appreciated, thank you!

1 Like

It’s really as simple as this:

using System;
using Eto.Forms;
using Eto.Drawing;
using System.ComponentModel;

namespace UI
{
    /// <summary>
    /// Represents a custom button control with various styling options, toggle behavior, and popup functionality.
    /// </summary>
    public class CustomButton : Drawable
    {

        public event EventHandler<EventArgs> Clicked;

        public new Color BackgroundColor;
        public Color HighlightColor;
        private Color _backgroundColor;

        private Control _control;

        private bool _isHover = false;

        public CustomButton(Control control)
        {
            _control = control;
            Height = Settings.Instance.General.ButtonHeight;

            HighlightColor = Settings.Instance.Colors.HighlightColor;
        }

        protected override void OnPreLoad(EventArgs e)
        {
            base.OnPreLoad(e);
            DrawGraphics();
        }

        private void DrawGraphics()
        {
            Content = _control;

            SetBackgroundColor();
            Invalidate();
        }

        protected override void OnMouseEnter(MouseEventArgs e)
        {
            _isHover = true;
            Invalidate();
        }

        protected override void OnMouseLeave(MouseEventArgs e)
        {
            _isHover = false;
            Invalidate();
        }

        protected override void OnMouseUp(MouseEventArgs e)
        {
            if (e.Buttons == MouseButtons.Primary && _isHover)
            {
                Invalidate();

                if (Clicked != null)
                    Clicked.Invoke(this, e);
            }
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            _backgroundColor = HighlightColor;
            Invalidate();
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            // do something useful with _isHover ...
            e.Graphics.FillEllipse(_backgroundColor, 0, 0, Height, Height);
        }
    }
}

``
3 Likes

Awesome, thank you! This is very helpful, I’ll give it a go and report back with my findings. Much appreciated @mrhe

I ended up taking a slightly different approach since I moved away from clickable buttons for now but your code helped me a lot as I was working through the button stuff.

You probably saw it already but here’s the direction I went with Drawables instead of dedicated buttons.

I have some other forms I’m working through that will need proper buttons so I’ll be coming back to this again.

I appreciate your help @mrhe !

1 Like

Hi @mrhe ,

I was trying to implement a rounded button with your sample code. I am new to drawable and not quite sure about the mechanism here. I copied your code to my IDE. There are several warnings:

  1. What is the namespace for Settings.Instance.General.ButtonHeight?
  2. How do I construct a new instance of this custom button? Not sure what the argument Control is.

The documentation for eto is very sparse and I couldn’t find a working sample for rounded button. Would greatly appreciate if you can share some advice.

Best,
Vincent

HI @vincentfs,

The Settings.Instance.General.ButtonHeight is a reference to my plugin’s settings. Replace it with a local int and everything will work fine.

Control is used to pass any object derived from Eto.Forms.Control this allows me to customize the button to my liking. This way, I can pass images, complex layouts, or other custom controls to the button’s constructor. The button will inherit their appearance but extend the functionality with onClick events etc.

Hi @mrhe ,

Thanks for your reply. I will try it out.

In the meanwhile, could you point me to some resources for the ‘drawable’ class. I am not planning to do some UI as sophisticated as your terrain plugin, but I would like to at least be able to tweak the style a little bit to my likings.

regards,
Vincent

@vincentfs, think of a Drawable as a blank canvas. It inherits from Eto.Forms.Control for basic functionality, but it’s really up to you to define its appearance. Most of the magic happens in OnPaint where you can use all available methods of the Graphics Class

1 Like

@mrhe So what kind of Eto.Forms.Control would you recommend as the base to draw upon for implementing a round button? I try your sample code and pass in a Eto.Froms.Button and then tried to do the following in OnPaint(e), but nothing get draw except the original button.

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            // draw something 
            e.Graphics.FillEllipse(_backgroundColor, 0, 0, Height, Width);
        }

Most likely the original button takes up all the space of the Drawable and you can’t see the ellipse being drawn underneath.

Try passing a Label instead of the Button and adding some padding to the Drawable. You’d need to experiment with various settings to better understand the order of operations and how they stack.

Also, to draw a round button, don’t start with the Button class. Draw the round shape directly in the Drawable. You could also remove the Control argument from the constructor and do everything in the Drawable class.

@mrhe Thanks! Finally I managed to draw a round button which responds perfectly with mouse hovering/leaving etc. (Although it requires drawing 4 pies and 3 rectangles to get such a shape :grin:).

Unfortunately, now I have another glitch with this button. It works perfectly on Eto.Forms.Dialog. But when I used it on a tablelayout for modelless Panel, it could not detect mouse hovering event (i.e. not switching bg colors). Do you have any hint for this behavior? The panel class I am using is essentially the WizardPanel in the official sample code.

Github_McNeel_SampleCsWizardPanel

Would be great if @curtisw or @dale can help on this too.

There’s no known issues with this that we are aware and our UI has numerous Drawable based controls that rely on it. Perhaps if you posted or emailed what you have we can dig into why it’s not working in your particular case?

Cheers,
Curtis.

As Curtis said, please include your code if you need more meaningful help.

Although it requires drawing 4 pies and 3 rectangles to get such a shape

You can easily draw a circle like this:
e.Graphics.FillEllipse(_backgroundColor, 0, 0, Height, Width);

But when I used it on a tablelayout for modelless Panel , it could not detect mouse hovering event (i.e. not switching bg colors)

I suggest you debug it by setting breakpoints at the events you’re interested in analyzing. If you don’t know how to do it, then at least log your events to the command line to see what is happening and which order. For example:
Rhino.RhinoApp.WriteLine("MouseEnter");

But I highly recommend to step through your code in VS.

1 Like

FWIW, if it’s a rounded rect which it sounds like, you can easily do:

var path = GraphicsPath.GetRoundedRect(new Rectangle(Size), radius: 10);
e.Graphis.FillPath(_backgroundColor, path);
1 Like
var path = GraphicsPath.GetRoundedRect(new Rectangle(Size), radius: 10);
e.Graphis.FillPath(_backgroundColor, path);

Hi @curtisw ,
I managed to spot the bug by looking at your code snippet above. You are drawing with _backgroundColor rather than BackgroundColor. Originally I was drawing with BackgroundColor and changed its value base on Mouse event. But it seems that in some scenario, BackgroundColor will get overridden.

cad

Many thanks to @mrhe @curtisw ! Now I get a better sense of drawable class.

Cheers!

3 Likes

Hi all,

I’m trying to get a custom image button class established in Python as I’m still so new to C# and I’m struggling to get the basic implementation working. I’m failing to understand how to allow the Drawable to get the Click events?

The error I’m getting is the following:

AttributeError: 'CustomButton' object has no attribute 'Click'

Any help or pointers would be appreciated!

Here’s the CustomButton class I’m attempting to create in Python:

class CustomButton(Eto.Forms.Drawable):
    def __init__(self, control):
        self.BackgroundColor = Eto.Drawing.Colors.White
        self.HighlightColor = Eto.Drawing.Colors.OrangeRed
        self._backgroundColor = Eto.Drawing.Colors.LightGrey
        self._control = control
        self._isHover = False

        # Initiliaze default image
        self.DefaultImage = Eto.Drawing.Bitmap("C:\Users\micha\OneDrive - Michael Vollrath\TOYBLOCK\PROJECTS\PROJECT_ENDGAME\ICONS\UI\LAYER MANAGER\ICO_Wildcard_Hover.png")

        self.Click += self.OnMouseDown
        self.MouseEnter += self.OnMouseEnter
        self.MouseLeave += self.OnMouseLeave

        self.Height = 30  # Example height, replace with actual button height
        self.HighlightColor = Eto.Drawing.Colors.Blue  # Example highlight color, replace with actual color

    def OnMouseEnter(self, e):
        self._isHover = True
        Rhino.UI.RhinoApp.WriteLine("Hover")
        self.Invalidate()

    def OnMouseLeave(self, e):
        self._isHover = False
        Rhino.UI.RhinoApp.WriteLine("End Hover")
        self.Invalidate()

    def OnMouseUp(self, e):
        if e.Buttons == MouseButtons.Primary and self._isHover:
            self.Invalidate()
            if self.Clicked:
                self.Clicked(self, e)
                Rhino.UI.RhinoApp.WriteLine("Click")

    def OnMouseDown(self, e):
        self._backgroundColor = self.HighlightColor
        Rhino.UI.RhinoApp.WriteLine("Click Down")
        self.Invalidate()

    def OnPaint(self, e):
        super().OnPaint(e)
        e.Graphics.FillEllipse(self._backgroundColor, 0, 0, self.Height, self.Height)

Here’s an example of what my current form looks like with some rounded rectangles and an image drawn. I’ve made it so that it colorizes the UI based on if Rhino is using Dark/Light mode theme (thanks for the help on that @clement) I want to make the image in the upper right of the UI into a custom button that receives enter, leave, and click events. I tried to adapt the C# code you shared @mrhe but admittedly it’s still a bit over my head and I’m trying to learn Python first and then convert to C# as I get a better understanding of the concepts.

-I’m assuming the image for the button would be drawn as a bitmap image in the OnPaint event?

-Do I draw the graphics on PreLoad? I’ve never used PreLoad before and, while the name makes sense, I don’t quite understand how to actually utilize arguments for it properly.

-The little green “toggle button image” would be the custom image button and right now is just an image.

In the snip above I’m creating an Eto.Form that uses FillPath to create the “bar” graphics and I would like to do similar things in the OnPaint method of the button drawable as well as supplying images, text, etc.

Thank you all, I appreciate your patience and help as I’m trying to learn this new possibility within Rhino.

Hi @michaelvollrath, try if below example script brings some inspiration:

ETO_DrawableSwitchControl.py (3.1 KB)

Note that Eto.Forms.Drawable has no Click event, so you handle the click action yourself either on mouse down or up. My example uses the latter to change a state which is either True or False. It draws everything in the Graphics of the drawable.

You can draw images there too, eg. try one of these if you want to get fancy.

_
c.

1 Like

@clement amazing! That’s the exact capability I am after, thank you so much for sharing! I feel now I have the elements I need to create the UI ideas I’ve been after.

Thanks again!