Hi all (or at least @ShynnSup),
After years of being restricted by lack of features in the native Remote Control Panel (RCP), and given the likely long wait 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, 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:
"""
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 (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








