How to create a dockable form using Eto?

I was able to make a popup by drawing from one of the given sample forms, as shown by the code below. I was wondering how to make it so that the form is dockable? I have a draft of how it might be done commented right near the bottom of the code, but if that is correct, how would I go about finding a DockBarId?

Thank you so much in advance!

import System
import Rhino
import Rhino.UI
import Eto.Drawing as drawing
import Eto.Forms as forms
import scriptcontext as sc

################################################################################
# SampleEtoModelessForm class
################################################################################
class SampleEtoModelessForm(forms.Form):
    
    # Initializer
    def __init__(self):
        self.m_selecting = False
        # Basic form initialization
        self.Initialize()
        # Create the form's controls
        self.CreateFormControls()
        # Fill the form's listbox
        self.FillListBox()
        # Create Rhino event handlers
        self.CreateEvents()
        print("Done initilaizing")
    
    # Basic form initialization
    def Initialize(self):
        self.Title = 'Sample Modeless Form'
        self.Padding = drawing.Padding(5)
        self.Resizable = True
        self.Maximizable = False
        self.Minimizable = False
        self.ShowInTaskbar = True
        self.MinimumSize = drawing.Size(200, 150)
        # FormClosed event handler
        self.Closed += self.OnFormClosed
    
    # Adds a listitem to the listbox
    def AddObject(self, obj):
        item = forms.ListItem()
        item.Text = obj.ShortDescription(False)
        if obj.Name:
            item.Text += " - " + obj.Name
        item.Tag = obj.Id
        self.m_listbox.Items.Add(item)
        
    # Fills the listbox with document objects
    def FillListBox(self):
        iter = Rhino.DocObjects.ObjectEnumeratorSettings()
        iter.NormalObjects = True
        iter.LockedObjects = False
        iter.IncludeLights = True
        iter.IncludeGrips = False
        objects = sc.doc.Objects.GetObjectList(iter)
        for obj in objects:
            self.AddObject(obj)
     
    # CloseDocument event handler
    def OnCloseDocument(self, sender, e):
        self.m_listbox.Items.Clear()
        
    # NewDocument event handler
    def OnNewDocument(self, sender, e):
        self.FillListBox()
        
    # EndOpenDocument event handler
    def OnEndOpenDocument(self, sender, e):
        self.FillListBox()
        
    # OnAddRhinoObject event handler
    def OnAddRhinoObject(self, sender, e):
        self.AddObject(e.TheObject)
    
    # OnDeleteRhinoObject event handler
    def OnDeleteRhinoObject(self, sender, e):
        for item in self.m_listbox.Items:
            if item.Tag == e.ObjectId:
                self.m_listbox.Items.Remove(item)
                break
    
    # OnSelectObjects event handler
    def OnSelectObjects(self, sender, e):
        if self.m_selecting == True:
            return
        if e.RhinoObjects.Length == 1:
            i = 0
            for item in self.m_listbox.Items:
                if item.Tag == e.RhinoObjects[0].Id:
                    self.m_listbox.SelectedIndex = i
                    break
                else:
                    i += 1
        else:
            self.m_listbox.SelectedIndex = -1
    
    # OnDeselectAllObjects event handler
    def OnDeselectAllObjects(self, sender, e):
        if self.m_selecting == True:
            return
        self.m_listbox.SelectedIndex = -1
    
    # Create Rhino event handlers
    def CreateEvents(self):
        Rhino.RhinoDoc.CloseDocument += self.OnCloseDocument
        Rhino.RhinoDoc.NewDocument += self.OnNewDocument
        Rhino.RhinoDoc.EndOpenDocument += self.OnEndOpenDocument
        Rhino.RhinoDoc.AddRhinoObject += self.OnAddRhinoObject
        Rhino.RhinoDoc.DeleteRhinoObject += self.OnDeleteRhinoObject
        Rhino.RhinoDoc.SelectObjects += self.OnSelectObjects
        Rhino.RhinoDoc.DeselectAllObjects += self.OnDeselectAllObjects
        
    # Remove Rhino event handlers
    def RemoveEvents(self):        
        Rhino.RhinoDoc.CloseDocument -= self.OnCloseDocument
        Rhino.RhinoDoc.NewDocument -= self.OnNewDocument
        Rhino.RhinoDoc.EndOpenDocument -= self.OnEndOpenDocument
        Rhino.RhinoDoc.AddRhinoObject -= self.OnAddRhinoObject
        Rhino.RhinoDoc.DeleteRhinoObject -= self.OnDeleteRhinoObject
        Rhino.RhinoDoc.SelectObjects -= self.OnSelectObjects
        Rhino.RhinoDoc.DeselectAllObjects -= self.OnDeselectAllObjects
        
    # Create all of the controls used by the form
    def CreateFormControls(self):
        # Create table layout
        layout = forms.TableLayout()
        layout.Padding = drawing.Padding(10)
        layout.Spacing = drawing.Size(5, 5)
        # Add controls to layout
        layout.Rows.Add(forms.Label(Text = 'Rhino Objects:'))
        layout.Rows.Add(self.CreateListBoxRow())
        layout.Rows.Add(self.CreateButtonRow())
        # Set the content
        self.Content = layout
    
    # Listbox.SelectedIndexChanged event handler
    def OnSelectedIndexChanged(self, sender, e):
        index = self.m_listbox.SelectedIndex
        if index >= 0:
            self.m_selecting = True
            item = self.m_listbox.Items[index]
            Rhino.RhinoApp.RunScript("_SelNone", False)
            Rhino.RhinoApp.RunScript("_SelId " + item.Tag.ToString() + " _Enter", False)
            self.m_selecting = False
    
    # Called by CreateFormControls to creates the
    # table row that contains the listbox
    def CreateListBoxRow(self):
        # Create the listbox
        self.m_listbox = forms.ListBox()
        self.m_listbox.Size = drawing.Size(200, 100)
        self.m_listbox.SelectedIndexChanged += self.OnSelectedIndexChanged
        # Create the table row
        table_row = forms.TableRow()
        table_row.ScaleHeight = True
        table_row.Cells.Add(self.m_listbox)
        return table_row
    
    # 'Select' button click handler
    def OnSelectClick(self, sender, e):
        self.m_selecting = True
        self.m_listbox.SelectedIndex = -1
        Rhino.RhinoApp.RunScript("_SelAll", False)
        self.m_selecting = False
    
    # 'Clear' button click handler
    def OnClearClick(self, sender, e):
        self.m_selecting = True
        self.m_listbox.SelectedIndex = -1
        Rhino.RhinoApp.RunScript("_SelNone", False)
        self.m_selecting = False
    
    # Called by CreateFormControls to creates the
    # table row that contains the button controls.
    def CreateButtonRow(self):
        # Select button
        select_button = forms.Button(Text = 'Select All')
        select_button.Click += self.OnSelectClick
        # Clear button
        clear_button = forms.Button(Text = 'Clear')
        clear_button.Click += self.OnClearClick
        # Create layout
        layout = forms.TableLayout(Spacing = drawing.Size(5, 5))
        layout.Rows.Add(forms.TableRow(None, select_button, clear_button, None))
        return layout
    
    # Form Closed event handler
    def OnFormClosed(self, sender, e):
        # Remove the events added in the initializer
        self.RemoveEvents()
        # Dispose of the form and remove it from the sticky dictionary
        if sc.sticky.has_key('sample_modeless_form'):
            form = sc.sticky['sample_modeless_form']
            if form:
                form.Dispose()
                form = None
            sc.sticky.Remove('sample_modeless_form')
    
################################################################################
# TestSampleEtoModelessForm function
################################################################################
def TestSampleEtoModelessForm():
    
    # See if the form is already visible
    print("Testing if form is already visible")
    if sc.sticky.has_key('sample_modeless_form'):
        return
    print("It was not available, so the code is continuing. ")
    
    # Create and show form
    form = SampleEtoModelessForm()
    #Rhino.UI.Panels.RegisterPanel(Rhino.UI.DockBarId.ObjectProperties, form, "MyDockableForm", System.Drawing.SystemIcons.Information.ToBitmap())
    #Rhino.UI.Panels.ShowPanel(Rhino.UI.DockBarId.ObjectProperties)
    form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    form.Show()
    # Add the form to the sticky dictionary so it
    # survives when the main function ends.
    sc.sticky['sample_modeless_form'] = form
    

if __name__ == '__main__':
    TestSampleEtoModelessForm()

Moved to Developer category

Hi @Jefferson_Cooper,

To create a tabbed dockbar, you’ll need to create a plug-in.

The samples repo on GitHub has example Eto-based tabbed dockbar.

– Dale

Thank you, making a plugin worked for me!

I tried a couple of the examples from github and they don’t seem to work with Rhino 8.

Error message:

System.NullReferenceException: Object reference not set to an instance of an object.
   at Rhino.Runtime.Code.Languages.PythonNet.CPythonCode.Execute(RunContext context)
   at Rhino.Runtime.Code.Code.ExecTry(RunContext context, IPlatformDocument& doc, Object& docState)
   at Rhino.Runtime.Code.Code.Run(RunContext context)

This from the SampleEtoTabbedDialog and SampleEtoPushPickButtonDialog. I stopped trying after that.

@Henry_Wede - in the samples you mention, add this at the top of each file:

#! python 2

Does this help?

– Dale

No, it doesn’t seem to help. The error message and code is below.

It isn’t surprising that 7 year old examples have issues. But why are we using ETO as the official GUI framework? I searched for “using ETO with Python” and found nothing except Rhino references. It seems that Rhino is the only major program using ETO with Python. That means that we are 100% dependent on McNeel for examples and documentation. None of that “the docs are in C++ so the Python people can figure it out” business.

Regardless of the reason for ETO, can we just agree to use something else like tkinter as a GUI framework? It will still create GUIs that look like they came from the 80s, but at least it is a standard module and has a lot of examples and documentation.

Just my two cents - I think we need to revisit this decision.

Error Message:

System.NullReferenceException: Object reference not set to an instance of an object.
   at Rhino.Runtime.Code.Languages.PythonNet.CPythonCode.Execute(RunContext context)
   at Rhino.Runtime.Code.Code.ExecTry(RunContext context, IPlatformDocument& doc, Object& docState)
   at Rhino.Runtime.Code.Code.Run(RunContext context)

Here is the code that I tried to revise using your suggestion. I could not figure out how to attach it, so here comes a bunch of text.

#! python 2

################################################################################
# SampleEtoTabbedDialog.py
# Copyright (c) 2018 Robert McNeel & Associates.
# See License.md in the root of this repository for details.
################################################################################

# Imports
import System
import Rhino.UI
import Eto.Drawing as drawing
import Eto.Forms as forms

################################################################################
# Creates a panel that displays a text label
################################################################################
class LabelPanel(forms.Panel):
    # Initializer
    def __init__(self):
        # create a control
        label = forms.Label()
        label.Text = "Text Label"
        # create a layout
        layout = forms.DynamicLayout()
        layout.DefaultSpacing = drawing.Size(5, 5)
        layout.Padding = drawing.Padding(10)
        # add the control to the layout
        layout.Add(label)
        # set the panel content
        self.Content = layout

################################################################################
# Creates a panel that displays a text area control
################################################################################
class TextAreaPanel(forms.Scrollable):
    # Initializer
    def __init__(self):
        # create a control
        text = forms.TextArea()
        text.Text = "Every Good Boy Deserves Fudge."
        # create a layout
        layout = forms.TableLayout()
        layout.Padding = drawing.Padding(10)
        layout.Spacing = drawing.Size(5, 5)
        layout.Rows.Add(text)
        # set the panel content
        self.Content = layout

################################################################################
# SampleEtoTabbedDialog dialog class
################################################################################
class SampleEtoTabbedDialog(forms.Dialog):
    
    # Initializer
    def __init__(self):
        self.Rnd = System.Random()
        self.Title = "Sample Eto Tabbed Dialog"
        self.Padding = drawing.Padding(10)
        self.Resizable = True
        self.Content = self.Create()
    
    # Create the dialog content
    def Create(self):
        # create default tabs
        self.TabControl = self.DefaultTabs()
        # create stack layout item for tabs
        tab_items = forms.StackLayoutItem(self.TabControl, True)
        # create layout for buttons
        button_layout = forms.StackLayout()
        button_layout.Orientation = forms.Orientation.Horizontal
        button_layout.Items.Add(None)
        button_layout.Items.Add(self.AddTab())
        button_layout.Items.Add(self.RemoveTab())
        button_layout.Items.Add(self.SelectTab())
        button_layout.Items.Add(None)
        # create stack layout for content
        layout = forms.StackLayout()
        layout.Spacing = 5
        layout.HorizontalContentAlignment = forms.HorizontalAlignment.Stretch
        # add the stuff above to this layout
        layout.Items.Add(button_layout)
        layout.Items.Add(tab_items)
        return layout
    
    # Create the default tabs
    def DefaultTabs(self):
        # creates a tab control
        control = self.CreateTabControl()
        # create and add a tab page 1
        tab1 = forms.TabPage()
        tab1.Text = "Tab 1"
        tab1.Content = self.TabOne()
        control.Pages.Add(tab1)
        # create and add a tab page 2
        tab2 = forms.TabPage()
        tab2.Text = "Tab 2"
        tab2.Content = self.TabTwo()
        control.Pages.Add(tab2)
        # return
        return control
    
    # AddTab button click handler
    def AddTabClick(self, sender, e):
        tab = forms.TabPage()
        tab.Text = "Tab" + str(self.TabControl.Pages.Count + 1)
        if (self.TabControl.Pages.Count % 2 == 0):
            tab.Content = self.TabOne()
        else:
            tab.Content = self.TabTwo()
        self.TabControl.Pages.Add(tab)
    
    # RemoveTab button click handler
    def RemoveTabClick(self, sender, e):
        if (self.TabControl.SelectedIndex >= 0 and self.TabControl.Pages.Count > 0):
            self.TabControl.Pages.RemoveAt(self.TabControl.SelectedIndex)
    
    # SelectTab button click handler
    def SelectTabClick(self, sender, e):
        if (self.TabControl.Pages.Count > 0):
            self.TabControl.SelectedIndex = self.Rnd.Next(self.TabControl.Pages.Count)
    
    # Creates an add tab button
    def AddTab(self):
        button = forms.Button()
        button.Text = "Add Tab"
        button.Click += self.AddTabClick
        return button
    
    # Creates a remove tab button
    def RemoveTab(self):
        button = forms.Button()
        button.Text = "Remove Tab"
        button.Click += self.RemoveTabClick
        return button
    
    # Creates a tab selection button
    def SelectTab(self):
        button = forms.Button()
        button.Text = "Select Tab"
        button.Click += self.SelectTabClick
        return button
    
    # Delegate function to tab position control
    def DockPositionDelegate(self, position):
        self.TabControl = position
    
    # Creates the one and only tab control
    def CreateTabControl(self):
        tab = forms.TabControl()
        # Orient the tabs at the top
        tab.TabPosition = forms.DockPosition.Top
        return tab
    
    # Creates a tab page's content
    def TabOne(self):
        control = forms.Panel()
        control.Content = LabelPanel()
        return control
    
    # Creates a tab page's content
    def TabTwo(self):
        control = forms.Panel()
        control.Content = TextAreaPanel()
        return control

################################################################################
# TestSampleEtoTabbedDialog function
################################################################################
def TestSampleEtoTabbedDialog():
    dialog = SampleEtoTabbedDialog()
    dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)

################################################################################
# Check to see if this file is being executed as the 'main' python
# script instead of being used as a module by some other python script
# This allows us to use the module which ever way we want.
################################################################################
if __name__ == "__main__":
    TestSampleEtoTabbedDialog()

Maybe. I’m assuming you’re running this older scripts with the new ScriptEditor in V8?

If you use the older EditPythonScript/RunPythonScript commands, the sample will run.

Eto is open source.

For C++, you might consider a different GUI framework. Rhino still uses MFC, fwiw.

@eirannejad - can you have a look at this?

1 Like

@Henry_Wede

The script example shared above runs with no changes in an IronPython 2 script in the new editor.

I modified the script so it runs in Python 3 as well in the new editor:

  • Added super().__init__() calls in the __init__ method of classes subclassing dotnet types. The editor also complains if they are missing. See Problems tray at the bottom:

  • Explicitly creating StackLayoutItem and TableRow instances as the automatic casting does not yet work in Python 3. I have a ticket to improve this ( RH-82477)
#! python 3

################################################################################
# SampleEtoTabbedDialog.py
# Copyright (c) 2018 Robert McNeel & Associates.
# See License.md in the root of this repository for details.
################################################################################

# Imports
import System
import Rhino.UI
import Eto.Drawing as drawing
import Eto.Forms as forms

################################################################################
# Creates a panel that displays a text label
################################################################################
class LabelPanel(forms.Panel):
    # Initializer
    def __init__(self):
        # create a control
        super().__init__()
        label = forms.Label()
        label.Text = "Text Label"
        # create a layout
        layout = forms.DynamicLayout()
        layout.DefaultSpacing = drawing.Size(5, 5)
        layout.Padding = drawing.Padding(10)
        # add the control to the layout
        layout.Add(label)
        # set the panel content
        self.Content = layout

################################################################################
# Creates a panel that displays a text area control
################################################################################
class TextAreaPanel(forms.Scrollable):
    # Initializer
    def __init__(self):
        super().__init__()
        # create a control
        text = forms.TextArea()
        text.Text = "Every Good Boy Deserves Fudge."
        # create a layout
        layout = forms.TableLayout()
        layout.Padding = drawing.Padding(10)
        layout.Spacing = drawing.Size(5, 5)
        cell = forms.TableCell()
        cell.Control = text
        row = forms.TableRow()
        row.Cells.Add(cell)
        layout.Rows.Add(row)
        # set the panel content
        self.Content = layout

################################################################################
# SampleEtoTabbedDialog dialog class
################################################################################
class SampleEtoTabbedDialog(forms.Dialog):
    # Initializer
    def __init__(self):
        super().__init__()
        self.Rnd = System.Random()
        self.Title = "Sample Eto Tabbed Dialog"
        self.Padding = drawing.Padding(10)
        self.Resizable = True
        self.Content = self.Create()
    
    # Create the dialog content
    def Create(self):
        # create default tabs
        self.TabControl = self.DefaultTabs()
        # create stack layout item for tabs
        tab_items = forms.StackLayoutItem(self.TabControl, True)
        # create layout for buttons
        button_layout = forms.StackLayout()
        button_layout.Orientation = forms.Orientation.Horizontal
        button_layout.Items.Add(None)
        button_layout.Items.Add(self.AddTab())
        button_layout.Items.Add(self.RemoveTab())
        button_layout.Items.Add(self.SelectTab())
        button_layout.Items.Add(None)
        # create stack layout for content
        layout = forms.StackLayout()
        layout.Spacing = 5
        layout.HorizontalContentAlignment = forms.HorizontalAlignment.Stretch
        # add the stuff above to this layout
        button_layout_item = forms.StackLayoutItem(button_layout)
        layout.Items.Add(button_layout_item)
        layout.Items.Add(tab_items)
        return layout
    
    # Create the default tabs
    def DefaultTabs(self):
        # creates a tab control
        control = self.CreateTabControl()
        # create and add a tab page 1
        tab1 = forms.TabPage()
        tab1.Text = "Tab 1"
        tab1.Content = self.TabOne()
        control.Pages.Add(tab1)
        # create and add a tab page 2
        tab2 = forms.TabPage()
        tab2.Text = "Tab 2"
        tab2.Content = self.TabTwo()
        control.Pages.Add(tab2)
        # return
        return control
    
    # AddTab button click handler
    def AddTabClick(self, sender, e):
        tab = forms.TabPage()
        tab.Text = "Tab" + str(self.TabControl.Pages.Count + 1)
        if (self.TabControl.Pages.Count % 2 == 0):
            tab.Content = self.TabOne()
        else:
            tab.Content = self.TabTwo()
        self.TabControl.Pages.Add(tab)
    
    # RemoveTab button click handler
    def RemoveTabClick(self, sender, e):
        if (self.TabControl.SelectedIndex >= 0 and self.TabControl.Pages.Count > 0):
            self.TabControl.Pages.RemoveAt(self.TabControl.SelectedIndex)
    
    # SelectTab button click handler
    def SelectTabClick(self, sender, e):
        if (self.TabControl.Pages.Count > 0):
            self.TabControl.SelectedIndex = self.Rnd.Next(self.TabControl.Pages.Count)
    
    # Creates an add tab button
    def AddTab(self):
        button = forms.Button()
        button.Text = "Add Tab"
        button.Click += self.AddTabClick
        return forms.StackLayoutItem(button)
    
    # Creates a remove tab button
    def RemoveTab(self):
        button = forms.Button()
        button.Text = "Remove Tab"
        button.Click += self.RemoveTabClick
        return forms.StackLayoutItem(button)
    
    # Creates a tab selection button
    def SelectTab(self):
        button = forms.Button()
        button.Text = "Select Tab"
        button.Click += self.SelectTabClick
        return forms.StackLayoutItem(button)
    
    # Delegate function to tab position control
    def DockPositionDelegate(self, position):
        self.TabControl = position
    
    # Creates the one and only tab control
    def CreateTabControl(self):
        tab = forms.TabControl()
        # Orient the tabs at the top
        tab.TabPosition = forms.DockPosition.Top
        return tab
    
    # Creates a tab page's content
    def TabOne(self):
        control = forms.Panel()
        control.Content = LabelPanel()
        return control
    
    # Creates a tab page's content
    def TabTwo(self):
        control = forms.Panel()
        control.Content = TextAreaPanel()
        return control

################################################################################
# TestSampleEtoTabbedDialog function
################################################################################
def TestSampleEtoTabbedDialog():
    dialog = SampleEtoTabbedDialog()
    dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)

################################################################################
# Check to see if this file is being executed as the 'main' python
# script instead of being used as a module by some other python script
# This allows us to use the module which ever way we want.
################################################################################
if __name__ == "__main__":
    TestSampleEtoTabbedDialog()
1 Like

Where can I find examples that will help me build a GUI today - with the current version of Rhino 8 - using Python 3?

Is using tkinter an option instead of ETO? If so, where are examples of this?

Thanks for your help on getting started.

Tkinter works on Windows but not on macOS at the moment (RH-67505)

My suggestion would be to use Eto. We are in the process of creating a larger example library so Eto examples will be included in that. Most of previous examples shoud work with minimal changes (e.g. adding super().__init__() calls in Python 3 and they should work with no changes in Python 2 in the new editor

1 Like

Thank you Ehsan.

Unfortunately, Rhino is consistently crashing when using the demo script. There is some message about an invalid object and then it goes away (too quick to read). There is a dialog for sending McNeel a “error report” or something similar. With an adorable deflated beach ball. If I get a solution then I will revisit it.

Until then I am going to see if I can get tkinter to work. This is very frustrating…

Do you actually get to see the window? Does it crash while using the newly created dialog?

Awesome to hear you all are working on more Eto examples/docs!!

Definitely put this in BIG BOLD letters at the top. :wink:
This was a big pain point until someone pointed it out. We had no idea haha

Just tried it again - the window shows and I can change tabs. When I close the ETO window, Rhino hangs for a few seconds and then disappears.

I have no idea what is going on. Maybe it is specific to my computer? If there are any log files that would help please let me know.

That’s probably something internal in python code that is happening. There is a little bit of challenge with event handlers. Let me run this code a bunch more and test