Help Updating Eto.Drawable Graphics - Cross-Class Communication

Hello,

I’m using drawables for different parts of a UI “toolbar” called “MainToolbar” in my code.

I have a drawable “button” called “TrayToggle” that updates a global boolean called “show_tray” when clicked.

When I click this button it runs the following function (irrelevant code removed):

    def DrawTrayGraphics(self, show_tray):

        Rhino.RhinoApp.WriteLine("Draw Tray Graphics Executed")
        Rhino.RhinoApp.WriteLine("show tray: " + str(show_tray))

        self.show_tray = show_tray

        # Create Background Tray Graphics

        try:
            if show_tray:
                # Draw Background Tray Graphics
                self.graphics.FillPath(brush_3, path_3)  # Draw Tray
                self.graphics.FillPath(g_brush_tb, path_3)  # Draw Main Toolbar Shadow On Tray
            else:
                pass

        except Exception as ex:
            print(ex)

        Rhino.RhinoApp.WriteLine("Draw Tray Graphics Completed")

MainToolbar Class (relevant code):

# Code For The Main Toolbar UI
class MainToolbar(Eto.Forms.Form):

    def __init__(self):
        super().__init__()
        # Set Form General Settings
        self.Title = "Main Toolbar"

        self.tray_toggle = TrayToggle(self)  # Create Instance Of Tray Toggle Tab/Button
        
        # Subscribe DrawTrayGraphics to the OnMouseDown event of TrayToggle
        self.tray_toggle.MouseDown += self.DrawTrayGraphics


        def CreateBackgroundUI():

            self.DrawTrayGraphics(show_tray)
            self.DrawBackgroundGraphics()  # Call The Function That Creates The Graphics

            # Add Items To Layout
            self.layout = Eto.Forms.PixelLayout()

            # Add the Tray Toggle Control
            self.layout.Add(self.tray_toggle, ((self.Width / 2) - (self.tray_toggle.Size.Width / 2)), tray_height - self.tray_toggle.Size.Height / 2) # Center On Form And Pass In Tray Toggle Button Size

            self.Content = self.layout

        CreateBackgroundUI()

DrawTrayGraphics relationship to MainToolbar Class:

# Code For The Main Toolbar UI
class MainToolbar(Eto.Forms.Form):

    def __init__(self):
        super().__init__()
        # Set Form General Settings
        self.Title = "Main Toolbar"

        def CreateBackgroundUI():

    def DrawTrayGraphics(self, show_tray):

However, I’m struggling in this regard:
my layout content exists in my MainToolbar class.

So how do I update the MainToolbar class layout to show/hide the tray graphics?

Or is the issue I need to have a seperate layout in my DrawTrayGraphics def and hide/show that instead?

Or do I just need to subscribe to the event somehow? I’m unsure how do this across classes.

I’m a bit confused and have tried a lot of things without success.

Currently in my code the show_tray boolean does work but the graphics are only draw either shown or “hidden” aka not drawn the 1st time the script is run but not dynamically each time the show_tray boolean changes it’s value from the TrayToggle button press.

The code is rather long which is why I only included these pieces where the “communication/update” needs to happen.

Here’s what the hidden tray state looks like:
image

Here’s the hover state of the TrayToggle button:
image

Here’s what the shown tray state looks like:
image

Thank you all for the help!

1 Like

Hey @michaelvollrath, the tray looks very nice!

What’s supposed to be shown in the expanded state? Only graphics drawn in OnPaint, or other controls?

If it’s the latter, you’d have to replace the Content of your tray when the state changes and invalidate it. In your case, rebuild your PixelLayout and reassign it as Content.

In c#, I’d implement an observer pattern on the Boolean to automatically trigger this change, but you can also manually call it each time you click any of the buttons.

1 Like

Hi @mrhe thank you and thanks for your response!

The tray will be contextual or rather have different controls shown at different times.

So currently I’m just drawing the “background” graphics of the tray which will have a mix of drawable and standard Eto.Controls on top of the background.

Controls like a SearchBox, TableLayout with thumbnails will likely be shown, etc.

If I’m understanding you correctly let’s say the tray is shown and I added SearchBox1 and Label2 to it, then when I toggle the hide state of the tray I would Invalidate() SearchBox1 and Label2 is that correct?

Does invalidate remove them or just hide them?

I’m not quite clear on how that method works yet.

Do I do this via an event subscription? Like call the DrawTrayGraphics via an event bound to TrayToggle.OnMouseUp?

Thanks so much for your help!

When hiding the tray, you need to remove SearchBox and Label from the Content property of your Drawable. The easiest way is to rebuild the PixelLayout you are using and reassign it to Content.

Then you trigger invalidate on the Drawable itself. It simply flags it as outdated and lets the dispatcher know to redraw it.

Ideally, you’d have an observer pattern implemented, but to keep things simple, you can introduce a public method in your Drawable e.g. Expand() and call it from your buttons’ OnClick event.

1 Like

Thanks @mrhe ,

Sorry I just realized I never posted that part of the code but I’m doing something similar here in the OnMouseUp that follows my OnMouseDown of the TrayToggle drawable button class:

    def OnMouseUp(self, e):
        try:
            if e.Buttons == Eto.Forms.MouseButtons.Primary and self._hover:
                global show_tray
                self._state = not self._state
                show_tray = self._state
                # Rhino.RhinoApp.WriteLine("show tray: " + str(show_tray))
                self.main_toolbar.DrawTrayGraphics(show_tray)  # Use the instance to call the method

                self.Invalidate()

this line calls my DrawTrayGraphics and passes the boolean:

self.main_toolbar.DrawTrayGraphics(show_tray)

but within my MainToolbar class inside the DrawTrayGraphics I’m missing something fundamental about clearing/updating the pixellayout. I’ll look at what you mentioned about rebuilding it.

EDIT:

Alright so I added this definition to “Refresh” the UI by clearing/invalidating/disposing of the self.layout and while I can get the layout to disappear on initial toggle, i can’t get it to reappear again.

    def RefreshUI(self):
        Rhino.RhinoApp.WriteLine("Graphics Refreshed")
        if self.layout:
            Rhino.RhinoApp.WriteLine("layout valid")
            # self.UpdateLayout()  # Doesn't Seem To Work For This Case, Perhaps Now How This Is Intended To Be Used
            # self.layout.Invalidate()  # Creates Infinite Recursion
            # self.layout.Clear()  # Doesn't Actually Remove/Clear Anything?
            self.layout.Dispose()  # Clears The UI
            # self.Close()  # Closes The Whole Form
            self.CreateBackgroundUI() # Attempt To Call The Function That Draws The UI Elements Again

Can’t figure out how to clear the layout and then redraw via CreateBackgroundUI def. hmm…

EDIT 2:

The closest I’ve gotten is to close the form and recreate it by calling “EstablishForm” and by passing the toggle state but that seems silly and of course “flashes” the UI which isn’t ideal either. Furthermore it wouldnt save the states of any other specific buttons unless specified so that’s not great either… and I’m sure it’s way easier than that…

    def RefreshUI(self):
        Rhino.RhinoApp.WriteLine("Graphics Refreshed")
        if self.layout:
            Rhino.RhinoApp.WriteLine("layout valid")
            self.Close()  # Closes The Whole Form
            EstablishForm(self.show_tray)  # Recreate the form with the current show_tray state

def EstablishForm(show_tray=False):  # Set a default value for show_tray
    global form_instance  # Declare form_instance as global
    form_instance = MainToolbar()
    form_instance.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    form_instance.Show()
    form_instance.tray_toggle._state = show_tray  # Set the state of tray_toggle based on show_tray

Okay I finally figured it out. As usual I was overthinking and overcomplicating it.

When I toggled my “TrayToggle button” I was calling the definition that was creating the graphics for the tray but not the definition that is updating the PixelLayout and handling the layout/content.

I simply switched it so the tray toggle calls the main definition handling the layout now and then within that def I call the def that draws the tray graphics. Since the tray graphics handles the conditional logic inside of it (whether to draw or not draw the tray) it now works as expected and is super snappy of course.

Furthermore I’m not hacking it by force closing the whole form. Now all the other controls keep their states regardless of whether the tray is being opened or closed.

TrayToggle OnMouseUp:

    def OnMouseUp(self, e):
        try:
            if e.Buttons == Eto.Forms.MouseButtons.Primary and self._hover:
                global show_tray
                self._state = not self._state
                show_tray = self._state

                self.main_toolbar.CreateBackgroundUI()  # Use The Main Toolbar Instance To Call The Function That Handles The Graphics/Pixel Layout

                self.Invalidate()

Calling the DrawTrayGraphics from within CreateBackgroundUI:

def CreateBackgroundUI(self):

        # Add Items To Layout
        self.layout = Eto.Forms.PixelLayout()

        # Setup bitmap to have background graphics drawn on
        pixelformat = Eto.Drawing.PixelFormat.Format32bppRgba
        bitmap = Eto.Drawing.Bitmap(self.Size, pixelformat)
        self.graphics = Eto.Drawing.Graphics(bitmap)

        self.DrawTrayGraphics(show_tray)  # Call The Function That Creates The Tray Graphics

        self.DrawBackgroundGraphics()  # Call The Function That Creates The Main Toolbar Background Graphics

DrawTrayGraphics:

    def DrawTrayGraphics(self, show_tray):

        brush_3 = Eto.Drawing.SolidBrush(background_color)
        rect_3 = Eto.Drawing.Rectangle(0, 0, self.Width, self.Height)
        g_rect_3 = Eto.Drawing.Rectangle(0, (tray_height - shadow_height), self.Width, self.Height - tray_height)
        path_3 = Eto.Drawing.GraphicsPath.GetRoundRect(rect_3, self.radius_1, self.radius_1, self.radius_1, self.radius_1)
        g_brush_tb = Eto.Drawing.LinearGradientBrush(g_rect_3, shadow_color, background_color, 270)

        try:
            if show_tray:
                # Draw Background Tray Graphics
                self.graphics.FillPath(brush_3, path_3)  # Draw Tray
                self.graphics.FillPath(g_brush_tb, path_3)  # Draw Main Toolbar Shadow On Tray
            else:
                pass

        except Exception as ex:
            print(ex)

It seems so easy this way now… and when I add other controls I’ll remove those within the CreateBackgroundUI if the show_tray value changes essentially.

I think I understand it now. It certainly works smoothly so that’s a plus at the very least.

Thanks for your help @mrhe !

4 Likes

Glad you’ve figured this out!

Out of curiosity, did you find a good way of moving the tray when the Rhino window is resized or moved around?

It took @Wiley and me a lot of effort to get this right and our current solution feels a bit hacky. Would love to learn if there is a simple way of doing this.

@mrhe I have not yet. Currently I get the MainViewport and center the form at the bottom with a vertical offset.

I saw what you and @Wiley had done with the .dll wrapper and I love that it works but I need to figure out a cross platform way of achieving the positioning.

@AndersDeleuran recently shared a .PDF with a method for drawing graphics at specific screen corners or following the mouse corner.

I was going to explore this method where I get the main viewport frustrum bounds, deconstruct it for the plane and screen coordinates and spawn the form using that positioning.

Then I was going to subscribe to a viewport resize event so that if the viewport size changes the positional logic is updated and redraws/moves the form to the new location.

Since this would be based off of Rhino Common and the viewport itself my hope is that it will work cross compatibly but I have not yet tested.

Here’s a link to the .PDF I’m referencing. I believe the last page is the one I’m referring to with the viewport frustrum.

1 Like

I probably didn’t make it very obvious, but the source code from the slideshow/examples is also on the GitHub. Here’s that last example file with the relevant line (I think):

Edit: Also note the jank warning provided for those particular examples. The bounding box methods doesn’t quite/always seem to work, but it’s what I managed to get ready for the workshop :wink:

1 Like

@AndersDeleuran oh thank you! I was going to reference the screen grab but the source is a welcomed addition haha

When you say it doesn’t always work did you happen to figure out why it fails sometimes?

Thanks for your help!

In reference to this example file, the jank in question is that the method used to get the clipping box does not appear to work that well when one zooms out or in quite a bit (i.e. the component stops drawing):

When one recomputes the component it starts drawing again, so it might simply be a question of re-expiring it again within the code. But honestly it was a very quickly thrown together example, I just didn’t want to leave out this topic from the workshop! I suspect @mrhe might be able to provide tips for how to properly define/return the bounding box of 3D geometry drawn in 2D/screen space, considering their fantastic looking GUI/HUDs :raised_hands:

2 Likes

Very pretty!

1 Like

@AndersDeleuran thanks for the breakdown! I always appreciate your shares no matter how quickly they are put together, I always learn important things!

I’ll dig in and see what I can dig up. I’m sure between us all we can find a dynamic viewport position method for UI stuff.

EDIT:

Based on your video Anders that, (to my eye at least)looks like it may be a culling volume or Camera Near/Far Clipping Plane issue when either too close or too far.

You can see similar behavior in older versions of SketchUp and Unreal Engine depending on Camera Clipping Plane settings.

Thanks @CallumSykes!

1 Like

Hi @mrhe, @Wiley , and @AndersDeleuran ,

I was playing around this morning with trying to get consistent screen coordinates and it appears that the following method returns pixel coordinates that can be used in combination with an event listener to always get the active viewport screen position/size.

# Get Rhino Active Window Dimensions (Pixels)
screen_size = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Size
w = screen_size.Width
h = screen_size.Height

Here you can see it spawns a form with 30 pixels of padding on bottom and right side:

And here is the same form when the layers/panels are expanded and the viewport has shrunk:

image

And here if a smaller viewport is activated:

I’m now going to search through the API docs to see what would be the best event listener for the active view changing so that I can update the forms position anytime the the active view has changed.

@CallumSykes , do you happen to know of that one off the top of your head?

Anders, I’m not quite sure if this is more or less stable that the method you were using but so far in my testing it seems solid but I’d love for someone else to test.

Thanks guys!

2 Likes

Off of the top of my head?

This should do it, but I don’t know the exact modifications that will invoke it, resizing seems likely.

RhinoView.Modified += OnModified;

private void OnModified(object? sender, ViewEventArgs args)
{
    if (args.View != RhinoDoc.Views.ActiveView) return;
    // The Active View has changed! All hands on deck!
}

What you’re creating is an Eto (or other UI library) form and not being drawn in the pipeline, correct? If it’s a pipeline it’ll redraw every time the view changes whether you like it or not.

– cs

1 Like

Cool! I’ll give this a go and yes I’m using Eto in Python 3 for this test.

I’ll report back in a moment

EDIT:

Trying to pythonize this @CallumSykes and I think I’m missing something as I can’t get it to execute the definition OnViewUpdate. “view modified” never prints

# Initialize width and height variables
w = 0
h = 0


def OnViewUpdate(self, sender, e):
    Rhino.RhinoApp.WriteLine("view modified")
    # global w, h
    # Rhino.RhinoApp.WriteLine(str(e))
    # # Check if the modified view is the active view
    # if e == Rhino.RhinoDoc.ActiveDoc.Views.ActiveView:
    #     # Get the size of the active view
    #     s = sender.Size
    #     w = s.Width
    #     h = s.Height
    #     # Print or do something else with the dimensions
    #     print("Active View Dimensions:" + str(w) + "x" + str(h))

Rhino.Display.RhinoView.Modified += OnViewUpdate  # Subscribe To Viewport Update / Resizing

EDIT:

Okay I got it working with modifying this code:

You should also add a busy flag and peg OnViewUpdate to the RhinoApp.Idle event to avoid the event handler from firing multiple times when the view is being modified. This would improve performance as well.