# Remote Control Panel Using Eto/GhPython

**URL:** <https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140>\
**Category:** Grasshopper Developer\
**Tags:** windows, ghpython, eto, rcp, remote-control-panel\
**Created:** [September 30, 2026, 9:20am UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140 "2026-09-30T09:20:22Z")\
**Posts on this page:** 12\
**Page:** 1

<div class="post-metadata">

**Author:** ![AndersDeleuran](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/andersdeleuran/32/476683_2.png) [@AndersDeleuran](https://discourse.mcneel.com/u/AndersDeleuran)\
**Post date:** [September 30, 2026, 9:20am UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/1 "2026-09-30T09:20:22Z")

</div>

Hi all (or at least @ShynnSup),

After years of being restricted by [lack of features](https://discourse.mcneel.com/t/remote-control-panel-rcp-wishes-and-feedback/51545 "https://discourse.mcneel.com/t/remote-control-panel-rcp-wishes-and-feedback/51545?u=andersdeleuran") in the native _Remote Control Panel (RCP)_, and given the [likely long wait](https://discourse.mcneel.com/t/remote-control-panel-rcp-wishes-and-feedback/51545/48 "https://discourse.mcneel.com/t/remote-control-panel-rcp-wishes-and-feedback/51545/48?u=andersdeleuran") till a new one shows up in Grasshopper 2, I’ve been dabbling with Eto in GhPython as a possible replacement. Obviously there are many plugins that fill the same gap, but [I prefer to keep things native, dependency free](https://discourse.mcneel.com/t/remote-control-panel-rcp-wishes-and-feedback/51545/52), and scriptable whenever possible. So here’s here’s a first stab at writing a basic RCP that supports e.g. drop downs, checkbox lists and editable text panels. Give it a go if you’re already using the existing Grasshopper RCP, or if you want/need to make a simple GUI/front-end for your Grasshopper definitions. Here’s a quickie presentation video (where you get to hear me ramble!), the code and GH file:

```python
"""
First stab at coding a basic RCP with Eto/GhPython (e.g. no custom drawables).
    Inputs:
        Elements: {list,str}
        Width: {item,float}
        Spacing: {item,float}
        FontSize: {item,float}
        FontName: {item,str}
        WindowStyle: {item,int}
        Background: {item,color}
        RhinoStyle: {item,bool}
        ShowRCP: {item,bool}
    Outputs:
    Remarks:
        Author: Anders Holden Deleuran (BIG CPH)
        Rhino: 8.35.26251.13001
        Version: 260930
"""

import System
import Rhino as rc
import Eto.Forms as ef
import Eto.Drawing as ed
import Grasshopper as gh
import GhPython.Assemblies.ExecutingComponent as component

def setGhSlider(etoSlider,e):
    
    """ Event for setting Grasshopper slider using Eto slider """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_NumberSlider:
            if etoSlider.ToolTip.startswith(obj.NickName):
                if type(etoSlider) is ef.Slider:
                    obj.TickValue = int(etoSlider.Value)
                elif type(etoSlider) is rc.UI.Controls.Slider:
                    obj.Slider.Value = etoSlider.Value1
                obj.ExpireSolution(True)

def setGhBooleanToggle(etoCheckbox,e):
    
    """ Event for setting Grasshopper boolean toggle using Eto checkbox """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_BooleanToggle:
            if etoCheckbox.ToolTip.startswith(obj.NickName):
                obj.Value = not etoCheckbox.Checked
                obj.ExpireSolution(True)

def setGhDropDown(etoDropdown,e):
    
    """ Event for setting Grasshopper drop down (i.e. valuelist) using Eto dropdown """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ValueList:
            if etoDropdown.ToolTip.startswith(obj.NickName):
                if etoDropdown.SelectedKey:
                    obj.SelectItem(etoDropdown.DataStore.index(etoDropdown.SelectedKey))
                    obj.ExpireSolution(True)

def setGhCheckList(etoCheckBoxList,e):
    
    """ Event for setting Grasshopper check list (i.e. valuelist) using Eto check box list """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ValueList:
            if etoCheckBoxList.ToolTip.startswith(obj.NickName):
                sids = [etoCheckBoxList.DataStore.index(k) for k in etoCheckBoxList.SelectedKeys]
                for i,v in enumerate(obj.ListItems):
                    if i in sids:
                        if not v.Selected:
                            obj.ToggleItem(i)
                    else:
                        if v.Selected:
                            obj.ToggleItem(i)
                obj.ExpireSolution(True)

def setGhColorSwatch(etoColorpicker,e):
    
    """ Event for setting Grasshopper valuelist using Eto dropdown """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ColourSwatch:
            if etoColorpicker.ToolTip.startswith(obj.NickName):
                obj.SwatchColour = rc.UI.EtoExtensions.ToSystemDrawing(etoColorpicker.Value)
                obj.ExpireSolution(True)

def setGhPanelText(etoTextArea,e):
    
    """ Event for setting Grasshopper panel text using Eto text area """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_Panel and len(obj.NickName):
            if etoTextArea.ToolTip.startswith(obj.NickName):
                if obj.SourceCount == 0:
                    obj.UserText = etoTextArea.Text
                    obj.ExpireSolution(True)

def setGhButtonDown(etoButton,e):
    
    """ Event for setting Grasshopper button state to down using Eto button """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ButtonObject:
            if etoButton.ToolTip.startswith(obj.NickName):
                obj.ButtonDown = True
                obj.ExpireSolution(True)

def setGhButtonUp(etoButton,e):
    
    """ Event for setting Grasshopper button state to up using Eto button """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ButtonObject:
            if etoButton.ToolTip.startswith(obj.NickName):
                obj.ButtonDown = False
                obj.ExpireSolution(True)

def structureGhElements(elements):
    
    """ Structure Grasshopper elements into type/name and group by expanders """
    
    elGroups = []
    for el in elements:
        if el:
            
            # Split comma separated values and assign
            el = el.split(",")
            if len(el) == 2:
                elType,elName = el
                elTip = None
            elif len(el) == 3:
                elType,elName,elTip = el
                
            # Remove surrounding white space and format tool tip
            elType = elType.strip()
            elName = elName.strip()
            if elTip:
                elTip = elTip.strip()
                elTips = elTip.split("\\n")
                elTip = elName + ":"
                for s in elTips:
                    elTip += "\n" + s.strip()
            else:
                elTip = elName
                
            # Add group list
            if elType == "Group":
                elGroups.append([])
                
            # Add element data list to last group
            elGroups[-1].append((elType,elName,elTip))
            
    return elGroups

def makeRCP(windowStyle,rhinoStyle,location):
    
    """ Make remote control panel Eto form (TODO: Make class instance?) """
    
    rcp = ef.Form()
    rcp.Location = location
    rcp.AutoSize = True
    rcp.MovableByWindowBackground = True
    rcp.WindowStyle = ef.WindowStyle(int(windowStyle))
    if windowStyle == 1:
        rcp.BackgroundColor = ed.Colors.Transparent
    else:
        rcp.BackgroundColor = ed.Colors.White
    rcp.Owner = gh.Instances.EtoDocumentEditor
    if rhinoStyle and rc.RhinoApp.Version.Major > 7:
        rc.UI.EtoExtensions.UseRhinoStyle(rcp)
        
    return rcp

def addElementsToRCP(rcp,elements,dynamicSlider):
    
    """ Add elements to RCP by grouped expanders and canvas objects """
    
    # Make outer layout and add stuff
    elNames = []
    layoutA = ef.StackLayout()
    for elGrp in elements:
        
        # Make outer container
        oc = ef.Panel()
        #oc = ef.GroupBox()
        #oc = ef.Scrollable()
        
        # Make expander
        xp = ef.Expander()
        xp.Header = ef.Label()
        xp.Header.Text = elGrp[0][1]
        xp.Expanded = True
        
        # Make inner layout and add Grasshopper objects from elements
        layoutB = ef.StackLayout()
        for el in elGrp[1:]:
            elType,elName,elTip = el
            for obj in gh.Instances.ActiveCanvas.Document.Objects:
                
                # Track element names and find match
                if obj.NickName not in elNames and elName == obj.NickName:
                    
                    # Make/add check box from boolean toggle
                    if elType == "CheckBox" and type(obj) is gh.Kernel.Special.GH_BooleanToggle:
                        cb = ef.CheckBox(ToolTip=elTip)
                        cb.MouseUp += setGhBooleanToggle
                        elNames.append(elName)
                        layoutB.Items.Add(cb)
                        
                    # Make/add button from button
                    elif elType == "Button" and type(obj) is gh.Kernel.Special.GH_ButtonObject:
                        bt = ef.Button(ToolTip=elTip)
                        bt.Text = " {} ".format(elName)
                        bt.MinimumSize = ed.Size.Empty
                        bt.Click += setGhButtonDown
                        bt.Click += setGhButtonUp
                        elNames.append(elName)
                        layoutB.Items.Add(bt)
                        
                    # Make/add drop down from value list
                    elif elType == "DropDown" and type(obj) is gh.Kernel.Special.GH_ValueList:
                        dd = ef.DropDown(ToolTip=elTip)
                        dd.DataStore = [v.Name for v in obj.ListItems]
                        dd.SelectedKeyChanged += setGhDropDown
                        elNames.append(elName)
                        layoutB.Items.Add(dd)
                        
                    # Make/add check box list from value list
                    elif elType == "CheckBoxList" and type(obj) is gh.Kernel.Special.GH_ValueList:
                        cbl = ef.CheckBoxList(ToolTip=elTip)
                        cbl.DataStore = [v.Name for v in obj.ListItems]
                        cbl.SelectedKeysChanged += setGhCheckList
                        elNames.append(elName)
                        layoutB.Items.Add(cbl)
                        
                    # Make/add color picker from colour swatch
                    elif elType == "ColorPicker" and type(obj) is gh.Kernel.Special.GH_ColourSwatch:
                        cp = ef.ColorPicker(ToolTip=elTip)
                        cp.ValueChanged += setGhColorSwatch
                        elNames.append(elName)
                        layoutB.Items.Add(cp)
                        
                    # Make/add text area from panel
                    elif elType == "TextArea" and type(obj) is gh.Kernel.Special.GH_Panel:
                        tb = ef.TextArea(ToolTip=elTip)
                        tb.TextChanged += setGhPanelText
                        elNames.append(elName)
                        layoutB.Items.Add(tb)
                        
                    # Make/add label from panel
                    elif elType == "Label" and type(obj) is gh.Kernel.Special.GH_Panel:
                        lb = ef.Label(ToolTip=elTip)
                        elNames.append(elName)
                        layoutB.Items.Add(lb)
                        
                    # Make/add slider from slider
                    elif elType == "Slider" and type(obj) is gh.Kernel.Special.GH_NumberSlider:
                        s = ef.Slider(ToolTip=elTip)
                        s.TickFrequency = 0
                        if dynamicSlider:
                            s.ValueChanged += setGhSlider
                        else:
                            s.MouseUp += setGhSlider 
                        layoutB.Items.Add(s)
                        elNames.append(elName)
                        
                    # Make/add Rhino slider from slider
                    elif elType == "RhinoSlider" and type(obj) is gh.Kernel.Special.GH_NumberSlider:
                        s = rc.UI.Controls.Slider(ef.Slider(),False)
                        s.ToolTip = elTip
                        s.MouseMove += setGhSlider
                        layoutB.Items.Add(s)
                        elNames.append(elName)
                        
        # Add expander/layout to outer layout
        xp.Content = layoutB
        oc.Content = xp
        layoutA.Items.Add(oc)
        
        # Add layouts to remote control panel
        rcp.Content = layoutA

def updateElementsInRCP(rcp,elements):
    
    """ Update RCP element values based on current Grasshopper values state """
    
    # Iterate RCP controls and Grasshopper objects
    for oc in rcp.Content.Controls:
        for el in oc.Content.Content.Items:
            for obj in gh.Instances.ActiveCanvas.Document.Objects:
                
                # Find match and get element control
                if obj.NickName and el.Control.ToolTip.startswith(obj.NickName):
                    ct = el.Control
                    
                    # Update check box from boolean toggle
                    if type(ct) is ef.CheckBox and type(obj) is gh.Kernel.Special.GH_BooleanToggle:
                        ct.Checked = obj.Value
                        
                    # Update drop down from value list
                    elif type(ct) is ef.DropDown and type(obj) is gh.Kernel.Special.GH_ValueList:
                        ct.SelectedIndex = ct.DataStore.index(obj.SelectedItems[0].Name)
                        
                    # Update check box list from value list
                    elif type(ct) is ef.CheckBoxList and type(obj) is gh.Kernel.Special.GH_ValueList:
                        ct.SelectedKeys = [v.Name for v in obj.SelectedItems]
                        
                    # Update color picker from colour swatch
                    elif type(ct) is ef.ColorPicker and type(obj) is gh.Kernel.Special.GH_ColourSwatch:
                        ct.Value = rc.UI.EtoExtensions.ToEto(obj.SwatchColour)
                        
                    # Update text area from panel
                    elif type(ct) is ef.TextArea and type(obj) is gh.Kernel.Special.GH_Panel:
                        ct.Text = obj.UserText
                        
                    # Update label from panel
                    elif type(ct) is ef.Label and type(obj) is gh.Kernel.Special.GH_Panel:
                        ct.Text = ""
                        for i,vd in enumerate(obj.VolatileData):
                            ct.Text += str(vd)
                            if i < obj.VolatileDataCount-1:
                                ct.Text += "\n"
                                
                    # Update slider from slider
                    elif type(ct) is ef.Slider and type(obj) is gh.Kernel.Special.GH_NumberSlider:
                        ct.MinValue = 0
                        ct.MaxValue = obj.TickCount
                        ct.Value = obj.TickValue
                        
                    # Update Rhino slider from slider
                    elif type(ct) is rc.UI.Controls.Slider and type(obj) is gh.Kernel.Special.GH_NumberSlider:
                        ct.SetMinMax(obj.Slider.Minimum,obj.Slider.Maximum)
                        ct.Decimals = obj.Slider.DecimalPlaces
                        ct.Value1 = obj.Slider.Value
                        
def styleRCP(rcp,spacing,color,width,fontSize,fontName):
    
    """ Dynamically set styling properties of the RCP elements """
    
    # Make font and colors
    font = ed.Font(fontName,fontSize)
    color = ed.Color.FromArgb(color.R,color.G,color.B,color.A)
    
    # Set outer layout properties
    rcp.Content.Spacing = spacing
    rcp.Content.Padding = spacing
    
    # Set outer container properties
    for oc in rcp.Content.Controls:
        oc.Padding = spacing
        oc.BackgroundColor = color
        if type(oc) is ef.Scrollable:
            oc.ExpandContentHeight = False
            oc.ExpandContentWidth = False
            
        # Set expander properties
        xp = oc.Content
        xp.Padding = int(spacing/2)
        xp.Header.Font = font
        if width:
            xp.Width = width
            
        # Set innner layout properties
        xp.Content.Spacing = spacing
        for el in xp.Content.Items:
            
            # Set slider properties
            if type(el.Control) is ef.Slider:
                el.Control.Size = ed.Size(0,18)
                el.HorizontalAlignment = ef.HorizontalAlignment.Stretch
                
            # Set Rhino slider properties
            elif type(el.Control) is rc.UI.Controls.Slider:
                el.Control.Height = 20
                #el.Control.Size = ed.Size(0,19)
                el.HorizontalAlignment = ef.HorizontalAlignment.Stretch
                
            # Set drop down properties
            elif type(el.Control) is ef.DropDown:
                el.Control.Height = 21
                el.HorizontalAlignment = ef.HorizontalAlignment.Stretch
                
            # Set color picker properties
            elif type(el.Control) is ef.ColorPicker:
                el.Control.Width = 50
                el.Control.Height = 16
                
            # Set check box list properties
            elif type(el.Control) is ef.CheckBoxList:
                el.Control.Orientation = ef.Orientation.Vertical
                
            # Set button properties
            elif type(el.Control) is ef.Button:
                pass
                
            # Set text area properties
            elif type(el.Control) is ef.TextArea:
                el.Control.Height = 40
                el.HorizontalAlignment = ef.HorizontalAlignment.Stretch

# Set component names/status
ghenv.Component.Name = "EtoRemoteControlPanel"
ghenv.Component.NickName = "ERCP"
if rc.RhinoApp.Version.Major > 7:
    ghenv.Component.ToggleObsolete(False)

class EtoRemoteControlPanel(component):
    
    # Make persistent variables
    rcp,loc,elements = None,None,None
    
    def RunScript(self,Elements,Width,Spacing,FontSize,FontName,WindowStyle,Background,RhinoStyle,ShowRCP):
        
        # Check/cast inputs
        if Elements and FontSize and FontName:
            if Width:
                width = int(Width)
            else:
                width = 0
                
        # Persistent screen location tracking
        self.trackLocation()
        
        # Reset state
        if not self.rcp:
            self.elements = structureGhElements(Elements)
            self.rcp = makeRCP(int(WindowStyle),RhinoStyle,self.loc)
            addElementsToRCP(self.rcp,self.elements,True)
            
        # Show and close states
        if ShowRCP:
            updateElementsInRCP(self.rcp,self.elements)
            styleRCP(self.rcp,int(Spacing),Background,width,int(FontSize),FontName)
            self.rcp.Show()
        else:
            self.closeRCP()
            
    def trackLocation(self):
        
        """ Track location between sessions """
        
        self.ghvt = gh.Instances.ActiveCanvas.Document.ValueTable
        self.locKey = "loc-" + str(ghenv.Component.InstanceGuid)
        if not self.ghvt.GetValue(self.locKey,""):
            wa = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea
            self.loc = ed.Point(wa.Width/3,wa.Height/3)
            self.ghvt.SetValue(self.locKey,str(self.loc))
        else:
            x,y = self.ghvt.GetValue(self.locKey,"").split(",")
            self.loc = ed.Point(int(float(x)),int(float(y)))
            
    def closeRCP(self):
        
        """ Record location and close/delete RCP """
        
        self.ghvt.SetValue(self.locKey,str(self.rcp.Location))
        self.rcp.Close()
        self.rcp = None
        
    def updateComponent(self,e):
        
        """ Recompute this GhPython component """
        
        self.ExpireSolution(False)
        
    def closeRCPEvent(self,sender,e):
        self.closeRCP()
        
    def updateComponentEvent(self,sender,e):
        self.OnPingDocument().ScheduleSolution(1,self.updateComponent)
        
    def __enter__ (self):
        gac = gh.Instances.ActiveCanvas
        gac.MouseUp += self.updateComponentEvent
        if gac.Document:
            gac.Document.UndoStateChanged += self.updateComponentEvent
            gac.Document.ContextChanged += self.closeRCPEvent
            
    def __exit__ (self):
        gac = gh.Instances.ActiveCanvas
        gac.MouseUp -= self.updateComponentEvent
        if gac.Document:
            gac.Document.UndoStateChanged -= self.updateComponentEvent
            gac.Document.ContextChanged -= self.closeRCPEvent

```

[260930\_RemoteControlPanel\_EtoGhPython\_00.gh](https://discourse.mcneel.com/uploads/short-url/o6ona8dCvAt9weOCb5KG4CU9BIT.gh) (28.0 KB)

I’m sure there are bugs and cases I’ve missed. Feel free to reply with issues. Or if you add new stuff 🙂

Best,

Anders

---

<div class="post-metadata">

**Author:** ![AndersDeleuran](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/andersdeleuran/32/476683_2.png) [@AndersDeleuran](https://discourse.mcneel.com/u/AndersDeleuran)\
**Post date:** [September 30, 2026, 12:55pm UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/2 "2026-09-30T12:55:50Z")

</div>

Was accidentally targeting non-named Grasshopper objects. And also not making an integer slider when using the Rhino slider, quickie update:

[260930\_RemoteControlPanel\_EtoGhPython\_01.gh](https://discourse.mcneel.com/uploads/short-url/kFr8FRirCOvH3KF1Bq9UL22XvIs.gh) (29.4 KB)

---

<div class="post-metadata">

**Author:** ![ShynnSup](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/shynnsup/32/767220_2.png) [@ShynnSup](https://discourse.mcneel.com/u/ShynnSup)\
**Post date:** [September 30, 2026, 1:23pm UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/3 "2026-09-30T13:23:30Z")

</div>

Hello Anders, this is awesome! Thank you so much for openly sharing this work!

I tried it right away and it seems to work great!

* * *

What I wanted to ask you, Value List is the CheckBox no? This becomes an issue when the Value List has a lot of options (We usually do this because a slider recomputes the entire definition everytime and some definitions are heavy, so instead of allowing the user to do the mistake of dragging the slider, we just use a big value list as we don’t need every decimal.

It looks like this:

 ![image](https://global.discourse-cdn.com/mcneel/uploads/default/original/4X/1/b/8/1b893f36a3ad4def3101782a4c6975ec1cb29245.png)

Ideally one would have that big list of numbers deploy if you click the value list, not display it all the time.

Also, I noticed in that case a sidebar to scroll becomes necessary.

 ![image](https://global.discourse-cdn.com/mcneel/uploads/default/original/4X/8/2/9/8291f3140ff95bc8a8525b3c7af3cd79751671ee.png)

* * *

When you close the window while True, script becomes red with error and says:

1. Solution exception:Cannot set Visibility or call Show, ShowDialog, or WindowInteropHelper.EnsureHandle after a Window has closed.

But re opening it with the toggle fixes it.

I also experienced some freeze after capturing and double clicking the python to open the editor. It seemed like the component was constantly scanning the canvas for changes and lagging… I’ll see if I can replicate.

---

<div class="post-metadata">

**Author:** ![ShynnSup](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/shynnsup/32/767220_2.png) [@ShynnSup](https://discourse.mcneel.com/u/ShynnSup)\
**Post date:** [September 30, 2026, 1:34pm UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/4 "2026-09-30T13:34:55Z")

</div>

Oh actually the correct type for Value List is DropDown…

Looks nice and already has a scrollbar! ✨

 ![image](https://global.discourse-cdn.com/mcneel/uploads/default/original/4X/4/0/d/40d2ef1d641caa159923554a3f1b256cb57fa062.png)

* * *

Should it be possible to specify names to the controls and not have them appear only when hovering?

 ![image](https://global.discourse-cdn.com/mcneel/uploads/default/original/4X/a/a/3/aa3018f26543c43dbed1a4bfb21601eb13b086b8.png)

Like RCP does.

 ![image](https://global.discourse-cdn.com/mcneel/uploads/default/original/4X/c/e/e/cee9aba74731cd8edd28299b79894aba9c1fac99.png)

---

<div class="post-metadata">

**Author:** ![AndersDeleuran](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/andersdeleuran/32/476683_2.png) [@AndersDeleuran](https://discourse.mcneel.com/u/AndersDeleuran)\
**Post date:** [September 30, 2026, 2:35pm UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/5 "2026-09-30T14:35:02Z")

</div>

> [@ShynnSup](#):
>
> Should it be possible to specify names to the controls and not have them appear only when hovering?

For some Eto controls it’s as simple as setting the `Text` input:

 ![2026-09-30 16_27_21-Grasshopper - 260930_RemoteControlPanel_EtoGhPython_01_](https://global.discourse-cdn.com/mcneel/uploads/default/original/4X/f/6/8/f68364a24c72c979cd5d601966e961f51581db33.png)

But as far as I can tell, most of them require adding a `Label` yourself. See an example here:

> [@Is there a way to make a number slider in rhino?](https://discourse.mcneel.com/t/is-there-a-way-to-make-a-number-slider-in-rhino/200736/2):
>
> There is the Number Stepper and the Slider in Eto, what would you change about these to get your desired control? slight_smile using Eto.Drawing; using Eto.Forms; var stepperLabel = new Label() { Text = "0" }; var sliderLabel = new Label() { Text = "0" }; var stepper = new NumericStepper(); var slider = new Slider() { Value = 0, MinValue = -100, MaxValue = 100}; stepper.ValueChanged += (s,e) =\> stepperLabel.Text = stepper.Value.ToString(); slider.ValueChanged += (s,e) =\> sliderLabel.Text…

And with `Eto.Forms.Drawable` it sounds like anything is possible 🤯

---

<div class="post-metadata">

**Author:** ![AndersDeleuran](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/andersdeleuran/32/476683_2.png) [@AndersDeleuran](https://discourse.mcneel.com/u/AndersDeleuran)\
**Post date:** [September 30, 2026, 2:41pm UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/6 "2026-09-30T14:41:47Z")

</div>

> [@ShynnSup](#):
>
> When you close the window while True, script becomes red with error and says:
> 
> 1. Solution exception:Cannot set Visibility or call Show, ShowDialog, or WindowInteropHelper.EnsureHandle after a Window has closed.
> 
> But re opening it with the toggle fixes it.
> 
> I also experienced some freeze after capturing and double clicking the python to open the editor. It seemed like the component was constantly scanning the canvas for changes and lagging… I’ll see if I can replicate.

Yeah I’m sure I haven’t caught all the cases here! Also the method I’m using to match Eto controls with Grasshopper object names currently needs to be more robust (i.e. than using `strip()` and `startswith()`). I’m only subscribing to the Grasshopper canvas `MouseUp` and the Grasshopper document `UndoStateChanged` events for recomputing the component, but that might also still cause some issues I’ve not caught yet. Let me know if you can reproduce things and I’ll try to fix them 🧑‍🔧

---

<div class="post-metadata">

**Author:** ![AndersDeleuran](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/andersdeleuran/32/476683_2.png) [@AndersDeleuran](https://discourse.mcneel.com/u/AndersDeleuran)\
**Post date:** [October 1, 2026, 7:22am UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/8 "2026-10-01T07:22:07Z")

</div>

> [@AndersDeleuran](#):
>
> … the method I’m using to match Eto controls with Grasshopper object names currently needs to be more robust (i.e. than using `strip()` and `startswith()`).

Made this more explicit by using the Eto controls `Tag` property when finding matching Grasshopper objects (i.e. names have to be exact match, including white space, and an Eto control can drive multiple Grasshopper canvas objects):

```python
"""
First stab at coding a basic RCP with Eto/GhPython (e.g. no custom drawables).
    Inputs:
        Elements: {list,str}
        Width: {item,float}
        Spacing: {item,float}
        FontSize: {item,float}
        FontName: {item,str}
        WindowStyle: {item,int}
        Background: {item,color}
        RhinoStyle: {item,bool}
        ShowRCP: {item,bool}
    Outputs:
    Remarks:
        Author: Anders Holden Deleuran (BIG CPH)
        Rhino: 8.35.26251.13001
        Version: 261001
"""

import System
import Rhino as rc
import Eto.Forms as ef
import Eto.Drawing as ed
import Grasshopper as gh
import GhPython.Assemblies.ExecutingComponent as component

def setGhSlider(etoSlider,e):
    
    """ Event for setting Grasshopper slider using Eto slider """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_NumberSlider:
            if etoSlider.Tag == obj.NickName:
                if type(etoSlider) is ef.Slider:
                    obj.TickValue = int(etoSlider.Value)
                elif type(etoSlider) is rc.UI.Controls.Slider:
                    obj.Slider.Value = etoSlider.Value1
                obj.ExpireSolution(True)

def setGhBooleanToggle(etoCheckbox,e):
    
    """ Event for setting Grasshopper boolean toggle using Eto checkbox """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_BooleanToggle:
            if etoCheckbox.Tag == obj.NickName:
                obj.Value = not etoCheckbox.Checked
                obj.ExpireSolution(True)

def setGhDropDown(etoDropdown,e):
    
    """ Event for setting Grasshopper drop down (i.e. valuelist) using Eto dropdown """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ValueList:
            if etoDropdown.Tag == obj.NickName:
                if etoDropdown.SelectedKey:
                    obj.SelectItem(etoDropdown.DataStore.index(etoDropdown.SelectedKey))
                    obj.ExpireSolution(True)

def setGhCheckList(etoCheckBoxList,e):
    
    """ Event for setting Grasshopper check list (i.e. valuelist) using Eto check box list """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ValueList:
            if etoCheckBoxList.Tag == obj.NickName:
                sids = [etoCheckBoxList.DataStore.index(k) for k in etoCheckBoxList.SelectedKeys]
                for i,v in enumerate(obj.ListItems):
                    if i in sids:
                        if not v.Selected:
                            obj.ToggleItem(i)
                    else:
                        if v.Selected:
                            obj.ToggleItem(i)
                obj.ExpireSolution(True)

def setGhColorSwatch(etoColorpicker,e):
    
    """ Event for setting Grasshopper valuelist using Eto dropdown """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ColourSwatch:
            if etoColorpicker.Tag == obj.NickName:
                obj.SwatchColour = rc.UI.EtoExtensions.ToSystemDrawing(etoColorpicker.Value)
                obj.ExpireSolution(True)

def setGhPanelText(etoTextArea,e):
    
    """ Event for setting Grasshopper panel text using Eto text area """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_Panel and len(obj.NickName):
            if etoTextArea.Tag == obj.NickName:
                if obj.SourceCount == 0:
                    obj.UserText = etoTextArea.Text
                    obj.ExpireSolution(True)

def setGhButtonDown(etoButton,e):
    
    """ Event for setting Grasshopper button state to down using Eto button """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ButtonObject:
            if etoButton.Tag == obj.NickName:
                obj.ButtonDown = True
                obj.ExpireSolution(True)

def setGhButtonUp(etoButton,e):
    
    """ Event for setting Grasshopper button state to up using Eto button """
    
    for obj in gh.Instances.ActiveCanvas.Document.Objects:
        if type(obj) is gh.Kernel.Special.GH_ButtonObject:
            if etoButton.Tag == obj.NickName:
                obj.ButtonDown = False
                obj.ExpireSolution(True)

def structureGhElements(elements):
    
    """ Structure Grasshopper elements into type/name and group by expanders """
    
    elGroups = []
    for el in elements:
        if el:
            
            # Split comma separated values and assign
            el = el.split(",")
            if len(el) == 2:
                elType,elName = el
                elTip = None
            elif len(el) == 3:
                elType,elName,elTip = el
                
            # Remove surrounding white space and format tool tip
            elType = elType.strip()
            elName = elName.strip()
            if elTip:
                elTip = elTip.strip()
                elTips = elTip.split("\\n")
                elTip = elName + ":"
                for s in elTips:
                    elTip += "\n" + s.strip()
            else:
                elTip = elName
                
            # Add group list
            if elType == "Group":
                elGroups.append([])
                
            # Add element data list to last group
            elGroups[-1].append((elType,elName,elTip))
            
    return elGroups

def makeRCP(windowStyle,rhinoStyle,location):
    
    """ Make remote control panel Eto form (TODO: Make class instance?) """
    
    rcp = ef.Form()
    rcp.Location = location
    rcp.AutoSize = True
    rcp.MovableByWindowBackground = True
    rcp.WindowStyle = ef.WindowStyle(int(windowStyle))
    if windowStyle == 1:
        rcp.BackgroundColor = ed.Colors.Transparent
    else:
        rcp.BackgroundColor = ed.Colors.White
    rcp.Owner = gh.Instances.EtoDocumentEditor
    if rhinoStyle and rc.RhinoApp.Version.Major > 7:
        rc.UI.EtoExtensions.UseRhinoStyle(rcp)
        
    return rcp

def addElementsToRCP(rcp,elements,dynamicSlider):
    
    """ Add elements to RCP by grouped expanders and canvas objects """
    
    # Make outer layout and add stuff
    elNames = []
    layoutA = ef.StackLayout()
    for elGrp in elements:
        
        # Make outer container
        oc = ef.Panel()
        #oc = ef.GroupBox()
        #oc = ef.Scrollable()
        
        # Make expander
        xp = ef.Expander()
        xp.Header = ef.Label()
        xp.Header.Text = elGrp[0][1]
        xp.Expanded = True
        
        # Make inner layout and add Grasshopper objects from elements
        layoutB = ef.StackLayout()
        for el in elGrp[1:]:
            elType,elName,elTip = el
            for obj in gh.Instances.ActiveCanvas.Document.Objects:
                
                # Track element names and find match
                if obj.NickName not in elNames and elName == obj.NickName:
                    
                    # Make/add check box from boolean toggle
                    if elType == "CheckBox" and type(obj) is gh.Kernel.Special.GH_BooleanToggle:
                        cb = ef.CheckBox(Tag=elName,ToolTip=elTip,Text=elName)
                        cb.MouseUp += setGhBooleanToggle
                        elNames.append(elName)
                        layoutB.Items.Add(cb)
                        
                    # Make/add button from button
                    elif elType == "Button" and type(obj) is gh.Kernel.Special.GH_ButtonObject:
                        bt = ef.Button(Tag=elName,ToolTip=elTip)
                        bt.Text = " {} ".format(elName)
                        bt.MinimumSize = ed.Size.Empty
                        bt.Click += setGhButtonDown
                        bt.Click += setGhButtonUp
                        elNames.append(elName)
                        layoutB.Items.Add(bt)
                        
                    # Make/add drop down from value list
                    elif elType == "DropDown" and type(obj) is gh.Kernel.Special.GH_ValueList:
                        dd = ef.DropDown(Tag=elName,ToolTip=elTip)
                        dd.DataStore = [v.Name for v in obj.ListItems]
                        dd.SelectedKeyChanged += setGhDropDown
                        elNames.append(elName)
                        layoutB.Items.Add(dd)
                        
                    # Make/add check box list from value list
                    elif elType == "CheckBoxList" and type(obj) is gh.Kernel.Special.GH_ValueList:
                        cbl = ef.CheckBoxList(Tag=elName,ToolTip=elTip)
                        cbl.DataStore = [v.Name for v in obj.ListItems]
                        cbl.SelectedKeysChanged += setGhCheckList
                        elNames.append(elName)
                        layoutB.Items.Add(cbl)
                        
                    # Make/add color picker from colour swatch
                    elif elType == "ColorPicker" and type(obj) is gh.Kernel.Special.GH_ColourSwatch:
                        cp = ef.ColorPicker(Tag=elName,ToolTip=elTip)
                        cp.ValueChanged += setGhColorSwatch
                        elNames.append(elName)
                        layoutB.Items.Add(cp)
                        
                    # Make/add text area from panel
                    elif elType == "TextArea" and type(obj) is gh.Kernel.Special.GH_Panel:
                        tb = ef.TextArea(Tag=elName,ToolTip=elTip)
                        tb.TextChanged += setGhPanelText
                        elNames.append(elName)
                        layoutB.Items.Add(tb)
                        
                    # Make/add label from panel
                    elif elType == "Label" and type(obj) is gh.Kernel.Special.GH_Panel:
                        lb = ef.Label(Tag=elName,ToolTip=elTip)
                        elNames.append(elName)
                        layoutB.Items.Add(lb)
                        
                    # Make/add slider from slider
                    elif elType == "Slider" and type(obj) is gh.Kernel.Special.GH_NumberSlider:
                        s = ef.Slider(Tag=elName,ToolTip=elTip)
                        s.TickFrequency = 0
                        if dynamicSlider:
                            s.ValueChanged += setGhSlider
                        else:
                            s.MouseUp += setGhSlider 
                        layoutB.Items.Add(s)
                        elNames.append(elName)
                        
                    # Make/add Rhino slider from slider
                    elif elType == "RhinoSlider" and type(obj) is gh.Kernel.Special.GH_NumberSlider:
                        s = rc.UI.Controls.Slider(ef.Slider(),False)
                        s.Tag = elName
                        s.ToolTip = elTip
                        s.MouseMove += setGhSlider
                        layoutB.Items.Add(s)
                        elNames.append(elName)
                        
        # Add expander/layout to outer layout
        xp.Content = layoutB
        oc.Content = xp
        layoutA.Items.Add(oc)
        
        # Add layouts to remote control panel
        rcp.Content = layoutA

def updateElementsInRCP(rcp,elements):
    
    """ Update RCP element values based on current Grasshopper values state """
    
    # Iterate RCP controls and Grasshopper objects
    for oc in rcp.Content.Controls:
        for el in oc.Content.Content.Items:
            for obj in gh.Instances.ActiveCanvas.Document.Objects:
                
                # Find match and get element control
                if obj.NickName and el.Control.Tag == obj.NickName:
                    ct = el.Control
                    
                    # Update check box from boolean toggle
                    if type(ct) is ef.CheckBox and type(obj) is gh.Kernel.Special.GH_BooleanToggle:
                        ct.Checked = obj.Value
                        
                    # Update drop down from value list
                    elif type(ct) is ef.DropDown and type(obj) is gh.Kernel.Special.GH_ValueList:
                        ct.SelectedIndex = ct.DataStore.index(obj.SelectedItems[0].Name)
                        
                    # Update check box list from value list
                    elif type(ct) is ef.CheckBoxList and type(obj) is gh.Kernel.Special.GH_ValueList:
                        ct.SelectedKeys = [v.Name for v in obj.SelectedItems]
                        
                    # Update color picker from colour swatch
                    elif type(ct) is ef.ColorPicker and type(obj) is gh.Kernel.Special.GH_ColourSwatch:
                        ct.Value = rc.UI.EtoExtensions.ToEto(obj.SwatchColour)
                        
                    # Update text area from panel
                    elif type(ct) is ef.TextArea and type(obj) is gh.Kernel.Special.GH_Panel:
                        ct.Text = obj.UserText
                        
                    # Update label from panel
                    elif type(ct) is ef.Label and type(obj) is gh.Kernel.Special.GH_Panel:
                        ct.Text = ""
                        for i,vd in enumerate(obj.VolatileData):
                            ct.Text += str(vd)
                            if i < obj.VolatileDataCount-1:
                                ct.Text += "\n"
                                
                    # Update slider from slider
                    elif type(ct) is ef.Slider and type(obj) is gh.Kernel.Special.GH_NumberSlider:
                        ct.MinValue = 0
                        ct.MaxValue = obj.TickCount
                        ct.Value = obj.TickValue
                        
                    # Update Rhino slider from slider
                    elif type(ct) is rc.UI.Controls.Slider and type(obj) is gh.Kernel.Special.GH_NumberSlider:
                        ct.SetMinMax(obj.Slider.Minimum,obj.Slider.Maximum)
                        if obj.Slider.Type == gh.GUI.Base.GH_SliderAccuracy.Integer:
                            ct.Decimals = 0
                        else:
                            ct.Decimals = obj.Slider.DecimalPlaces
                        ct.Value1 = obj.Slider.Value

def styleRCP(rcp,spacing,color,width,fontSize,fontName):
    
    """ Dynamically set styling properties of the RCP elements """
    
    # Make font and colors
    fontA = ed.Font(fontName,fontSize)
    fontB = ed.Font(fontName,fontSize)
    color = ed.Color.FromArgb(color.R,color.G,color.B,color.A)
    
    # Set outer layout properties
    rcp.Content.Spacing = spacing
    rcp.Content.Padding = spacing
    
    # Set outer container properties
    for oc in rcp.Content.Controls:
        oc.Padding = spacing
        oc.BackgroundColor = color
        if type(oc) is ef.Scrollable:
            oc.ExpandContentHeight = False
            oc.ExpandContentWidth = False
            
        # Set expander properties
        xp = oc.Content
        xp.Padding = int(spacing/2)
        xp.Header.Font = fontA
        if width:
            xp.Width = width
            
        # Set innner layout properties
        xp.Content.Spacing = spacing
        for el in xp.Content.Items:
            
            # Set slider properties
            if type(el.Control) is ef.Slider:
                el.Control.Size = ed.Size(0,18)
                el.HorizontalAlignment = ef.HorizontalAlignment.Stretch
                
            # Set Rhino slider properties
            elif type(el.Control) is rc.UI.Controls.Slider:
                el.Control.Height = 20
                #el.Control.Size = ed.Size(0,19)
                el.HorizontalAlignment = ef.HorizontalAlignment.Stretch
                
            # Set drop down properties
            elif type(el.Control) is ef.DropDown:
                el.Control.Height = 21
                el.Control.Font = fontB
                el.HorizontalAlignment = ef.HorizontalAlignment.Stretch
                
            # Set color picker properties
            elif type(el.Control) is ef.ColorPicker:
                el.Control.Width = 50
                el.Control.Height = 16
                el.HorizontalAlignment = ef.HorizontalAlignment.Right
                
            # Set check box list properties
            elif type(el.Control) is ef.CheckBoxList:
                el.Control.Orientation = ef.Orientation.Vertical
                
                
                
            # Set button properties
            elif type(el.Control) is ef.Button:
                el.Control.Font = fontB
                
            # Set check box properties
            elif type(el.Control) is ef.CheckBox:
                el.Control.Font = fontB
                
            # Set text area properties
            elif type(el.Control) is ef.TextArea:
                el.Control.Height = 40
                el.HorizontalAlignment = ef.HorizontalAlignment.Stretch

# Set component names/status
ghenv.Component.Name = "EtoRemoteControlPanel"
ghenv.Component.NickName = "ERCP"
if rc.RhinoApp.Version.Major > 7:
    ghenv.Component.ToggleObsolete(False)

class EtoRemoteControlPanel(component):
    
    # Make persistent variables
    rcp,loc,elements = None,None,None
    
    def RunScript(self,Elements,Width,Spacing,FontSize,FontName,WindowStyle,Background,RhinoStyle,ShowRCP):
        
        # Check/cast inputs
        if Elements and FontSize and FontName:
            if Width:
                width = int(Width)
            else:
                width = 0
                
        # Persistent screen location tracking
        self.trackLocation()
        
        # Reset state
        if not self.rcp:
            self.elements = structureGhElements(Elements)
            self.rcp = makeRCP(int(WindowStyle),RhinoStyle,self.loc)
            addElementsToRCP(self.rcp,self.elements,True)
            
        # Show and close states
        if ShowRCP:
            updateElementsInRCP(self.rcp,self.elements)
            styleRCP(self.rcp,int(Spacing),Background,width,int(FontSize),FontName)
            self.rcp.Show()
        else:
            self.closeRCP()
            
    def trackLocation(self):
        
        """ Track location between sessions """
        
        self.ghvt = gh.Instances.ActiveCanvas.Document.ValueTable
        self.locKey = "loc-" + str(ghenv.Component.InstanceGuid)
        if not self.ghvt.GetValue(self.locKey,""):
            wa = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea
            self.loc = ed.Point(wa.Width/3,wa.Height/3)
            self.ghvt.SetValue(self.locKey,str(self.loc))
        else:
            x,y = self.ghvt.GetValue(self.locKey,"").split(",")
            self.loc = ed.Point(int(float(x)),int(float(y)))
            
    def closeRCP(self):
        
        """ Record location and close/delete RCP """
        
        self.ghvt.SetValue(self.locKey,str(self.rcp.Location))
        self.rcp.Close()
        self.rcp = None
        
    def updateComponent(self,e):
        
        """ Recompute this GhPython component """
        
        self.ExpireSolution(False)
        
    def closeRCPEvent(self,sender,e):
        self.closeRCP()
        
    def updateComponentEvent(self,sender,e):
        self.OnPingDocument().ScheduleSolution(1,self.updateComponent)
        
    def __enter__ (self):
        gac = gh.Instances.ActiveCanvas
        gac.MouseUp += self.updateComponentEvent
        if gac.Document:
            gac.Document.UndoStateChanged += self.updateComponentEvent
            gac.Document.ContextChanged += self.closeRCPEvent
            
    def __exit__ (self):
        gac = gh.Instances.ActiveCanvas
        gac.MouseUp -= self.updateComponentEvent
        if gac.Document:
            gac.Document.UndoStateChanged -= self.updateComponentEvent
            gac.Document.ContextChanged -= self.closeRCPEvent

```

[261001\_RemoteControlPanel\_EtoGhPython\_00.gh](https://discourse.mcneel.com/uploads/short-url/8bBAitFyBCiD5kiQ05fvHJhRGEw.gh) (27.4 KB)

---

<div class="post-metadata">

**Author:** ![Cumberland](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/cumberland/32/770886_2.png) [@Cumberland](https://discourse.mcneel.com/u/Cumberland)\
**Post date:** [October 1, 2026, 11:30am UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/9 "2026-10-01T11:30:28Z")

</div>

Nice but how to close the RCP panel because I do closed the definition and now I can’t close the RCP window.

P.S: Crashed Rhino7.

---

<div class="post-metadata">

**Author:** ![AndersDeleuran](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/andersdeleuran/32/476683_2.png) [@AndersDeleuran](https://discourse.mcneel.com/u/AndersDeleuran)\
**Post date:** [October 1, 2026, 12:00pm UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/10 "2026-10-01T12:00:16Z")

</div>

> [@Cumberland](#):
>
> Nice but how to close the RCP panel because I do closed the definition and now I can’t close the RCP window.

It should close itself when you close the Grasshopper definition. If not, you can Alt + Tab and close it in the pop up list like a normal window.

> [@Cumberland](#):
>
> P.S: Crashed Rhino7.

I have not been developing in under Rhino 7. That said, I have tested it a bit and did not get any crashes. Let me know if you can repeat it.

---

<div class="post-metadata">

**Author:** ![AndersDeleuran](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/andersdeleuran/32/476683_2.png) [@AndersDeleuran](https://discourse.mcneel.com/u/AndersDeleuran)\
**Post date:** [October 12, 2026, 7:58am UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/11 "2026-10-12T07:58:22Z")

</div>

I’ve added text to some Eto controls and have mostly been able to also set their font (same as input font, but one size smaller in the attached code). But I can’t figure out how to do so for the [`CheckBoxList`](https://pages.picoe.ca/docs/api/html/T_Eto_Forms_CheckBoxList.htm):

 ![2026-10-12 09_41_11-Rhino 8 Commercial - Perspective](https://global.discourse-cdn.com/mcneel/uploads/default/original/4X/9/f/5/9f569e45920248daa12bbf312b44f63b43733ee8.png)

I assume one has to iterate over the (assuming) underlying `CheckBox` items somehow and set their font. But even using the `CheckBoxList.Items.Add` method instead does not appear to return those when iterating `Items` afterwards:

 ![2026-10-12 09_45_52-Rhino 8 Commercial - Perspective](https://global.discourse-cdn.com/mcneel/uploads/default/original/4X/9/d/c/9dc60139880c7165ed94895725aceaaade175f5d.png)

[261012\_RemoteControlPanel\_EtoGhPython\_00.gh](https://discourse.mcneel.com/uploads/short-url/pdqOSboiGTzJs4BhNvt1SzPIEL.gh) (33.4 KB)

Maybe @curtisw, @CallumSykes, @Gijs or @dale might know? **Edit:** Nevermind, solved [below](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/13).

---

<div class="post-metadata">

**Author:** ![ShynnSup](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/shynnsup/32/767220_2.png) [@ShynnSup](https://discourse.mcneel.com/u/ShynnSup)\
**Post date:** [October 12, 2026, 12:37pm UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/12 "2026-10-12T12:37:37Z")

</div>

> [@AndersDeleuran](#):
>
> assume one has to iterate over the (assuming) underlying `CheckBox` items somehow and set their font. But even using the `CheckBoxList.Items.Add` method instead does not appear to return those when iterating `Items` afterwards:

So AI has told me this, maybe it helps 🤷‍♂️

I think I figured it out. `CheckBoxList.Items` returns `ListItem` objects, not the actual underlying `CheckBox` controls, which is why there’s no `Font` property.

Eto does create real `CheckBox` controls internally, but they only seem to be available through the control hierarchy after loading. So this works:

```auto
def styleCheckBoxList(cbl, font):
    def applyFont(sender=None, e=None):
        if cbl.Content:
            for control in cbl.Content.Controls:
                if type(control) is ef.CheckBox:
                    control.Font = font

    if cbl.Loaded:
        applyFont()
    else:
        cbl.LoadComplete += applyFont

```

Then

```auto
elif type(el.Control) is ef.CheckBoxList:
    el.Control.Orientation = ef.Orientation.Vertical
    styleCheckBoxList(el.Control, fontB)

```

Interestingly CheckBoxList exposes TextColor, but not Font, so this seems to be the workaround unless you build the checkboxes manually.

---

<div class="post-metadata">

**Author:** ![AndersDeleuran](https://sea3.discourse-cdn.com/mcneel/user_avatar/discourse.mcneel.com/andersdeleuran/32/476683_2.png) [@AndersDeleuran](https://discourse.mcneel.com/u/AndersDeleuran)\
**Post date:** [October 12, 2026, 1:08pm UTC](https://discourse.mcneel.com/t/remote-control-panel-using-eto-ghpython/223140/13 "2026-10-12T13:08:16Z")

</div>

> [@ShynnSup](#):
>
> `cbl.Content.Controls`

It did indeed help, thanks for that! I had gone down both `cbl.Content` and `cbl.Controls`, but stopped before getting to `cbl.Content.Controls` 😵‍💫

 ![2026-10-12 15_05_13-Greenshot](https://global.discourse-cdn.com/mcneel/uploads/default/original/4X/c/c/b/ccb1aa979f861c5d996939df6a154cef34a0019a.jpeg)  
[261012\_RemoteControlPanel\_EtoGhPython\_01.gh](https://discourse.mcneel.com/uploads/short-url/tMbPmqPCAm9wsjlgp4k9xw2JkVo.gh) (29.8 KB)
