Embed Rhino.Viewport In Eto.Forms?

Hello everyone,

I can successfully embed a Rhino.UI.Controls.ViewportControl into an Eto.Forms.Form or Dialog but the behavior of said ViewportControl is unexpected to me.

I’ve found these posts here:

What I am trying to achieve…

-embed an interactive Rhino.Viewport into an Eto.Forms.Form
-have this LOOK like (A)
-have this ACT like (B)

To clarify, when I say “interactive” I mean in the way the free floating viewport works (outside of Eto) where I can run Rhino commands, change the Display Mode, Change the Camera, etc.

When the ViewportControl is added to an Eto.Forms layout, it allows me to navigate (orbit, pan, zoom) the viewport but not draw anything or have the additional controls such as “changing the camera, display mode, drawing geometry, etc.”

The ViewportControl in Eto also has different “navigation mouse settings” than the CustomViewport

I cannot find Rhino.UI.Controls.ViewportControl in the Rhino Common API Documentation…

Am I missing something obvious? Is there an argument I can pass on the ViewportControl to show the additional options similar to the freestanding CustomViewport

Is the lack of full interactivity stemming from Eto or Rhinocommon when used in this way? @curtisw, @CallumSykes, @stevebaer I can’t debug the root cause and am curious if there are some arguments I can use to achieve the functionality I seek.

Thank you in advance for any help!

Here’s some sample code I modified and borrowed from @clement:

#! python3
import scriptcontext as sc
import Rhino.UI
import Eto


class MainUIWindow(Eto.Forms.Form):
    def __init__(self):
        super().__init__()
        self.ClientSize = Eto.Drawing.Size(500, 500)
        self.vpc = Rhino.UI.Controls.ViewportControl()
        self.button = Eto.Forms.Button()
        self.button.Text = "Click"
        self.button.Click += self.SetViewport
        self.layout = Eto.Forms.DynamicLayout()
        self.layout.AddRow(self.button)
        self.layout.AddRow(self.vpc)
        self.Content = self.layout

    def SetViewport(self, sender, e):
        target_location = Rhino.Geometry.Point3d(0, 0, 0)
        camera_location = Rhino.Geometry.Point3d(100, 100, 50)
        self.vpc.Viewport.SetCameraLocations(target_location, camera_location)
        self.vpc.Viewport.Camera35mmLensLength = 20.0
        Rhino.UI.Controls.ViewportControl.Focus()
        #self.vpc.Invalidate()

    def OnFormClosed(self, sender, e):
        if 'main_ui_form' in sc.sticky:
            form = sc.sticky['main_ui_form']
            if form:
                form.Dispose()
                form = None
            del sc.sticky['main_ui_form']


def CreateMainForm():
    if 'main_ui_form' in sc.sticky:
        return

    form = MainUIWindow()
    form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    form.Show()
    sc.sticky['main_ui_form'] = form


if __name__ == '__main__':
    CreateMainForm()
3 Likes

What is a CustomViewport?

Hi @stevebaer ,

When you add a new ViewportControl outside of Eto instead of saying “Perspective” or “Top” in the upper left corner of the viewport it says “CustomViewport”

EDIT:

Sorry it says “Custom view”. Here is what it looks like:

Currently I have custom Eto UI existing on top of/within the Rhino Viewports.

I want to flip the table and have a floating Eto “window” form that has a Rhino Viewport within it in addition to my other custom Eto UI elements.

@stevebaer, apart from Michael’s questions, please run the example script above from the new PythonScript editor and note that there seems to be something wrong. If i run the script a few times and just close the dialog, i get a lot of “Rhino Viewport” entries in my taskbar which cannot be closed and which i cannot get focus to once clicked:

This did not happen in Rhino 7 using IronPython and above script.

btw. @michaelvollrath, i can repeat the unusual navigation in the form’s viewport control, this is something which was asked for in the past but never got a lot of attention.

_
c.

1 Like

Thanks for sharing that @clement , I noticed that behavior as well with Python 3 but forgot to mention it.

Separately, to expand a bit more on the embedded viewport control capabilities I did find a little bit more info digging through some old forum posts.

There is a youtrack for documentation to be published on this particular control here:
https://mcneel.myjetbrains.com/youtrack/issue/RH-65901

And a post detailing some additional ways to be able to set the display mode and top/perspective view orientation of said viewport control:

Unfortunately that seems to be the extend of the capabilities.

The methods of mouse navigation are still different and ideally I would like to be able to set those to match the main Rhino instance. Meaning if Right Mouse rotates/orbits, i would expect the viewport control to also be rotated/orbited with Right Mouse.

Any additional leads are appreciated, thank you all!

EDIT:

@clement and @stevebaer, I did manage to remove the “rogue/duplicate” viewports that get created in Python 3 with the following code:

How are tools like Artisan doing this?

It looks like they made a bunch of UI.Panels that are docked on the sides of the viewport and these panels have their custom UI in it but obviously you don’t “see” Rhino default UI anywhere…

Is that handled by turning off all the default Rhino.UI.Toolbars and having the Artisan ones show instead?

Thanks everyone!

Yep. If you really want you can really strip Rhino down until it’s just a viewport, the menu bar, and the status bar. You can even hide the command line, but it’ll take focus of your inputs still so it’s best to keep it around :slight_smile:

1 Like

Thanks @CallumSykes ,

Good to know, and that does make sense!

I still am trying to figure out if there’s a viable way to have a floating viewport with the main eto UI there so in other words…

screen 1 could have vanilla/default Rhino with it’s typical UI
screen 2 (or floating viewport not maximized) would just be a single monolithic viewport with UI on top of it as needed.

In testing the screen 2 version the floating viewport seems to show the Window.Style.Utility (single red X close button)

I can add Eto on top of this and handle close/minimize/maximize in a custom way but I’m curious if one can just set the Window.Style to be .Default on a floating viewport so that you could see the minimize/maximize/and title bar? OR set Window.Style to Window.Style.NONE and then again… have the eto UI handle the min, max, close, and titlebar graphics/functions.

Here’s a VERY silly “sketch” of a test attempt to have Eto on top of a floating viewport (obviously the viewport titlebar/close button would be hidden if possible)

This code has the Eto.Form driving the viewport size/position maybe i’m thinking about it the exact opposite of how I should be :upside_down_face::

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


class MyEtoForm(forms.Form):
    def __init__(self):
        super().__init__()
        self.Title = "    Standard Viewport in Eto Form"
        self.Size = drawing.Size(800, 600)
        self.Location = drawing.Point(0, 0)
        self.MovableByWindowBackground = True
        self.WindowStyle = forms.WindowStyle.NONE

        self.window_button_size = drawing.Size(32, 32)
        self.window_button_bg_color = drawing.Color(180, 180, 180)

        self.viewport = None
        self.AddViewport()
        self.CreateWindowButtons()

        # Add Transparent Style To The Form Background Panel
        self.Styles.Add[forms.Panel]("transparent", self.MyFormStyler)
        self.Style = "transparent"

        # Subscribe to Eto form events
        self.LocationChanged += self.OnFormMove
        self.SizeChanged += self.OnFormResize

    def OnMinimizeButtonClick(self, sender, e):
        try:
            self.Minimize()  # Minimize Eto Form

        except Exception as ex:
            Rhino.RhinoApp.WriteLine(f"Minimize Button Exception: {ex}")

    def OnMaximizeButtonClick(self, sender, e):
        try:
            self.Maximize()  # Maximize Eto Form

        except Exception as ex:
            Rhino.RhinoApp.WriteLine(f"Maximize Button Exception: {ex}")

    def OnFormClose(self, sender, e):
        try:
            if self.viewport:
                self.viewport.Close()  # Close "Embedded" Viewport
            self.Close()  # Close Eto Form

        except Exception as ex:
            Rhino.RhinoApp.WriteLine(f"Close Button Exception: {ex}")

    def CreateWindowButtons(self):



        self.header_label = forms.Label()
        self.header_label.Size = drawing.Size(300, self.window_button_size.Height)
        self.header_label.Text = self.Title
        self.header_label.BackgroundColor = self.window_button_bg_color

        self.MinimizeButton = forms.Button()
        self.MinimizeButton.Size = self.window_button_size
        self.MinimizeButton.Text = "-"
        self.MinimizeButton.BackgroundColor = self.window_button_bg_color

        self.MaximizeButton = forms.Button()
        self.MaximizeButton.Size = self.window_button_size
        self.MaximizeButton.Text = "[ ]"
        self.MaximizeButton.BackgroundColor = self.window_button_bg_color

        self.CloseButton = forms.Button()
        self.CloseButton.Size = self.window_button_size
        self.CloseButton.Text = "X"
        self.CloseButton.BackgroundColor = self.window_button_bg_color

        self.MinimizeButton.Click += self.OnMinimizeButtonClick
        self.MaximizeButton.Click += self.OnMaximizeButtonClick
        self.CloseButton.Click += self.OnFormClose

        self.layout = forms.DynamicLayout()
        self.layout.Height = self.window_button_size.Height
        self.layout.AddRow(self.header_label, self.MinimizeButton, self.MaximizeButton, self.CloseButton)
        self.layout.AddRow(None)

        self.Content = self.layout

    def AddViewport(self):
        if self.viewport:
            self.viewport.Close()
        self.display_mode = Rhino.Display.DisplayModeDescription.FindByName("Rendered")
        # Create a Rhino viewport (floating)
        view_rectangle = sdrawing.Rectangle(self.Location.X, (self.Location.Y + self.window_button_size.Height), self.Size.Width, (self.Size.Height - self.window_button_size.Height))
        self.viewport = Rhino.RhinoDoc.ActiveDoc.Views.Add("embedded_viewport", Rhino.Display.DefinedViewportProjection.Perspective, view_rectangle, True)
        self.viewport.TitleVisible = False
        # handle = self.viewport.Handle
        # Rhino.RhinoApp.WriteLine(f"viewport handle: {handle}")
        # self.viewport.Topmost = True
        # self.SendToBack = False

    def OnFormMove(self, sender, e):
        self.UpdateViewportPosition()

    def OnFormResize(self, sender, e):
        self.UpdateViewportPosition()

    def UpdateViewportPosition(self):
        if self.viewport:
            self.AddViewport()
            # Rhino.RhinoApp.WriteLine("Update Viewport Location On Form Move")
            # Get the form's screen position and size
            form_rect = self.Bounds

            # Convert form position to screen coordinates
            screen_position = forms.Screen.PrimaryScreen.Bounds.Location
            new_position = sdrawing.Point(form_rect.X + screen_position.X, form_rect.Y + screen_position.Y)
            new_size = sdrawing.Size(form_rect.Width - 16, form_rect.Height - 36)

            # Update viewport position and size
            self.viewport.Position = new_position
            Rhino.RhinoApp.WriteLine(f"New Pos: {new_position}")
            self.viewport.Size = new_size
            self.viewport.DisplayMode = self.display_mode

    # UI STYLE CODE -------------------------------------------------------
    # Handle Overall Background Transparency
    def MyFormStyler(self, control):
        self.BackgroundColor = drawing.Colors.Transparent
        window = control.ControlObject
        if hasattr(window, "AllowsTransparency"):
            window.AllowsTransparency = True
        if hasattr(window, "Background"):
            brush = window.Background.Clone()
            brush.Opacity = 0
            window.Background = brush
        else:
            color = window.BackgroundColor
            window.BackgroundColor = color.FromRgba(0, 0, 0, 0)


# Create and show the Eto form
def EstablishForm():
    main_toolbar = MyEtoForm()
    main_toolbar.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    main_toolbar.Show()


if __name__ == "__main__":
    EstablishForm()

EDIT:

In looking at this again, I think having the ability to set the floating viewport Window.Style and Titlebar text is actually what I am after.

I can’t seem to find methods exposed to do that though?

Off of the top of my head I’m not exactly sure.
I will need to spend time digging into this, and I will try and do that soon-ish

Okay thanks @CallumSykes .

Here is the “most minimal” floating viewport arguments I can set so far:

import Rhino
import System.Drawing as sd


class CreateViewport():
    def __init__(self):
        self.Location = sd.Point(0, 0)  # Example location
        self.Size = sd.Size(800, 600)       # Example size
        self.window_button_size = sd.Size(100, 30)  # Example button size
        self.viewport = None

    def AddViewport(self):
        if self.viewport:
            self.viewport.Close()

        # Create a Rhino viewport (floating)
        view_rectangle = sd.Rectangle(self.Location.X, self.Location.Y + self.window_button_size.Height,
                                      self.Size.Width, self.Size.Height - self.window_button_size.Height)

        self.viewport = Rhino.RhinoDoc.ActiveDoc.Views.Add("MySpecialFloatingViewport",
                                                           Rhino.Display.DefinedViewportProjection.Perspective,
                                                           view_rectangle,
                                                           floating=True)
        Rhino.RhinoApp.WriteLine(str(self.viewport))

        self.viewport.ActiveViewport.DisplayMode = Rhino.Display.DisplayModeDescription.FindByName("Rendered")  # This Does Not Seem To Work On A Floating Viewport?
        # self.viewport.ActiveViewport.Name = "Test Name"  # Set The Name After Creation

        self.viewport.TitleVisible = False  # If The Title Is Visible, You Can Set Various Options For Floating Viewport Such As Display Mode, View Projection, Etc.

if __name__ == "__main__":
    # Example usage
    viewport_creator = CreateViewport()
    viewport_creator.AddViewport()

Here’s the additional functionality I would like to have on a floating viewport:

Ideally, I would like the floating viewport to have the “Default” window style that shows the Rhino Icon, Window Title, Min, Max, and Close Button but then be able to set the icon and title from code like I can on an Eto.Form window (The Current Floating Viewport seems to be hardcoded to use the “Utility” Window Style:

        self.Title = "MyFloatingViewportTitle"
        self.Icon = drawing.Icon(icon_path_to_my_special_icon)
        Rhino.UI.EtoExtensions.UseRhinoStyle(self) # Support Rhino Theme Matching
        self.Resizable = True
        self.WindowState = forms.WindowState.Maximized  # Set It To Maximized On Creation
        self.Maximizable = True
        self.Minimizable = False
        self.MovableByWindowBackground = True
        self.ShowInTaskbar = True  # Be Able To Close The Floating Window From Taskbar
        self.BackgroundColor = drawing.Colors.White
        """self.WindowStyle = forms.WindowStyle.NONE  #Set Window Style To Default Or None 
        For Custom UI Overlays/title bar drawing code"""
        self.MinimumSize = drawing.Size(800, 600) # Safe "Minimum" Size To Load Window

That’s my wish list :slight_smile:

Thanks for considerations and looking into it!

Could you please tell me how to create a style for a window with rounded corners?

Hi @taraskydon, that’s just the default Eto.Forms window style on Windows 11 as far as I know

Kindly bumping this again…

Really need a way to remove or style a floating rhino view please even if I need to tap into window handles to do so.

Any help? Thank you!

EDIT:

making some “progress” here but failing to set the styling… is Eto in Rhino overriding the changes I’m making maybe?

I see this tiny rounded top right and top left corner show up when i run the code so i think “something” is taking but have a feeling rhino is taking back over on the rest.

TLDR; since there’s not Rhino View embed in Eto.Forms in site, I am overlaying Eto.Forms as floating UI elements on top of a floating viewport and it works actually pretty well… I just need to style the floating viewport which Rhino does not have exposed API methods for that I can see?

Thanks for your help and tagging you @curtisw and pestering you @stevebaer again on this to see if I can tweak something in my code… many thanks!

import Rhino
import scriptcontext as sc

import System.Windows.Forms as Forms  # For Windows OS only
import System

import Eto
import ctypes
import time


def CreateFloatingViewport(name="MyFloatingViewportTest"):
    """Create a new floating viewport to test on"""
    view_rectangle = System.Drawing.Rectangle(0, 0, 800, 600)
    floating_viewport = Rhino.RhinoDoc.ActiveDoc.Views.Add(
        name,
        Rhino.Display.DefinedViewportProjection.Perspective,
        view_rectangle,
        floating=True)
    floating_viewport.TitleVisible = True  # Changed to True to show the title bar
    return floating_viewport

def GetFloatingViewport():
    """Find an existing floating viewport"""
    for view in sc.doc.Views:
        if view.ActiveViewport.Name == "MyExistingFloatingViewport" and view.Floating:
            return view
            # Found the floating viewport

def UpdateFloatingViewportStyling(view):
    if not view:
        print("Valid view not found")
        return

    # Get the RhinoView object
    rhinoView = view.ActiveViewport.ParentView
    view_id = view.ActiveViewportID
    print(view_id)
    print(view.Handle)

    # Show all open windows in Rhino for debugging
    for window in Eto.Forms.Application.Instance.Windows:
        print(f"Window Type: {type(window)} - {window}")

    hwnd = int(str(rhinoView.Handle))  # Get the handle

    if hwnd:
        print(f"Searching for window with handle: {hwnd}")

        # Try to get the window title to confirm it's the right one
        GetWindowText = ctypes.windll.user32.GetWindowTextW
        GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW

        text_length = GetWindowTextLength(hwnd)
        buffer = ctypes.create_unicode_buffer(text_length + 1)
        GetWindowText(hwnd, buffer, text_length + 1)

        print(f"Found Window Title: {buffer.value}")

        # Set the window to standard style with min/max/close buttons
        GWL_STYLE = -16
        WS_OVERLAPPEDWINDOW = 0x00CF0000  # Standard window style with min/max/close
        WS_VISIBLE = 0x10000000  # Ensures the window stays visible

        GetWindowLong = ctypes.windll.user32.GetWindowLongW
        SetWindowLong = ctypes.windll.user32.SetWindowLongW
        SetWindowPos = ctypes.windll.user32.SetWindowPos

        # Apply the standard overlapped window style
        new_style = WS_OVERLAPPEDWINDOW | WS_VISIBLE

        SetWindowLong(hwnd, GWL_STYLE, new_style)

        # Redraw the window
        SetWindowPos(hwnd, None, 0, 0, 0, 0, 0x0027)  # SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED

        print("Window style updated to standard style with min/max/close buttons.")

if __name__ == "__main__":
    view = CreateFloatingViewport()  # GetFloatingViewport()
    UpdateFloatingViewportStyling(view)
1 Like