Overview
Objects on the Grasshopper canvas consist of two parts. The most important piece is the class that implements the IGH_DocumentObject interface. This interface provides the basic plumbing needed to make objects work within a Grasshopper node network. The interface part of objects however is handled separately. Every IGH_DocumentObject
carries around an instance of a class that implements the IGH_Attributes interface (indeed, every IGH_DocumentObject
knows how to create its own stand-alone attributes) and it is this class that takes care of display, mouse interactions, popup menus, tooltips and so forth.
In this guide we’ll see how you can create your own attributes object. Since it’s not possible to have an IGH_Attributes
instance work on its own, we need an IGH_DocumentObject
to tie it to. For this guide we’ll assume we have a custom simple parameter (i.e. without persistent data) that holds integers.
Attributes Object
public class MySimpleIntegerParameter : GH_Param<Types.GH_Integer>
{
public MySimpleIntegerParameter() :
base(new GH_InstanceDescription("Integer with stats", "Int(stats)",
"Integer with basic statistics",
"Params", "Primitive")) { }
public override System.Guid ComponentGuid
{
get { return new Guid("{33D07726-8298-4104-9EBC-5398D8AD5421}"); }
}
}
Public Class MySimpleIntegerParameter
Inherits GH_Param(Of Types.GH_Integer)
Public Sub New()
MyBase.New(New GH_InstanceDescription("Integer with stats", "Int(stats)", "Integer with basic statistics", "Params", "Primitive"))
End Sub
Public Overrides ReadOnly Property ComponentGuid() As System.Guid
Get
Return New Guid("{33D07726-8298-4104-9EBC-5398D8AD5420}")
End Get
End Property
End Class
What we’ll do is create a special attributes object for this parameter which also displays the median and mean values of the collection of all integers. We want to put this information below the parameter name, but inside the parameter box. The first step here is to override the CreateAttributes()
on MySimpleIntegerParameter
and assign a new instance of our (yet to be written) attributes class:
public override void CreateAttributes()
{
m_attributes = new MySimpleIntegerParameterAttributes(this);
}
Public Overrides Sub CreateAttributes()
m_attributes = New MySimpleIntegerParameterAttributes(Me)
End Sub
That’s it, no more code is required inside the MySimpleIntegerParameter
class. This part at least is simple. If you don’t override the CreateAttributes()
method, then an instance of GH_FloatingParamAttributes will be created instead. If your parameter is to be attached to a component as an input or output, then the component will assign an instance of GH_LinkedParamAttributes to the parameter and CreateAttributes()
will never be called.
Grasshopper.Kernel.GH_Attributes
Although the IGH_Attributes interface is required for custom attributes, it is usually a good idea to derive from one of the abstract attribute classes already available. GH_Attributes(T) is the most basic and obvious choice and it implements a large amount of methods with default behaviour, saving you a lot of time and effort:
public class MySimpleIntegerParameterAttributes : GH_Attributes<MySimpleIntegerParameter>
{
public MySimpleIntegerParameterAttributes(MySimpleIntegerParameter owner) : base(owner) { }
}
Public Class MySimpleIntegerParameterAttributes
Inherits GH_Attributes(Of MySimpleIntegerParameter)
Public Sub New(ByVal owner As MySimpleIntegerParameter)
MyBase.New(owner)
End Sub
End Class
This is enough so far to make it work, even though all the logic is still standard. We need to start overriding methods in MySimpleIntegerParameterAttributes
to suit our needs. But first some basic information regarding the default behaviour.
GH_Attributes<T>
assumes that the object that owns it is rectangular. This is true for most objects in Grasshopper, but there are some notable exceptions such as Pie-Graphs, Sketches and Scribbles. But this assumption (which holds true in our case) allows GH_Attributes<T>
to supply basic functionality for a wide variety of methods.
All attributes have a property that defines the size of the object called Bounds. Basically everything that happens outside of the Bounds
goes by unnoticed. Also, if the Bounds rectangle is not visible within the canvas area, Grasshopper might decide to not even bother calling any painting methods.
Because our parameter will be rectangular, we don’t have to override any of the picking logic, as the default implementation of IsPickRegion, IsMenuRegion and IsTooltipRegion will already work.
Layout
We do however need to supply custom Layout logic. The width of our attributes depends on both the length of the NickName
of the MySimpleIntegerParameter
that owns these attributes and on the length of the statistics information we want to include. The height of the parameter however is fixed, though larger than the standard height for parameters in Grasshopper.
In order to supply custom layout logic, we need to override the Layout method. In this case I measure the width of the NickName
of the Owner object, and make sure the parameter is never narrower than 80 pixels:
protected override void Layout()
{
// Compute the width of the NickName of the owner (plus some extra padding),
// then make sure we have at least 80 pixels.
int width = GH_FontServer.StringWidth(Owner.NickName, GH_FontServer.Standard);
width = Math.Max(width + 10, 80);
// The height of our object is always 60 pixels
int height = 60;
// Assign the width and height to the Bounds property.
// Also, make sure the Bounds are anchored to the Pivot
Bounds = new RectangleF(Pivot, new SizeF(width, height));
}
Protected Overrides Sub Layout()
'Compute the width of the NickName of the owner (plus some extra padding),
'then make sure we have at least 80 pixels.
Dim width As Int32 = GH_FontServer.StringWidth(Owner.NickName, GH_FontServer.Standard)
width = Math.Max(width + 10, 80)
'The height of our object is always 60 pixels
Dim height As Int32 = 60
'Assign the width and height to the Bounds property.
'Also, make sure the Bounds are anchored to the Pivot
Bounds = New RectangleF(Pivot, New SizeF(width, height))
End Sub
The Pivot
is a PointF
structure that is changed when the object is dragged. It is therefore important that you always “anchor” the layout of some attributes to the Pivot
. If you fail to do so, your attributes will become undraggable.
There is a method you can override that will be called prior to the call to Layout
which can be used to destroy any cached data you might have that’s to do with display. But note that if you override ExpireLayout you must place a call to the base class method as well:
publicoverride void ExpireLayout()
{
base.ExpireLayout();
// Destroy any data you have that becomes
// invalid when the layout expires.
}
Public Overrides Sub ExpireLayout()
MyBase.ExpireLayout()
'Destroy any data you have that becomes
'invalid when the layout expires.
End Sub
Render
Now that we have handled the Layout, we need to override the display of the parameter. There’s two parts to doing so. You always have to override the Render method, as this is where the drawing takes place. Render is called a number of times as there are several “layers” or “channels” to a single Grasshopper canvas. At first, the background of the canvas is drawn. During this process attributes are not yet involved. Then there will be four channels where IGH_Attributes
will be allowed to draw various shapes.
First the groups are drawn (as they are behind all other objects), but every GH_Attributes.Render()
method will be called once for the Groups channel. Typically you should not draw anything in the Groups channel.
Next up is the Wires channel where all parameter connector wires are drawn. If your object has input parameters or is a parameter, it is your responsibility to draw all wires coming into your object. Wires going out the right side will be drawn by the recipient objects.
Next the actual Components and Parameters themselves are drawn inside the Objects channel. This is typically the most work, though there are lots of classes that take care of common tasks. The default visual style of Components and parameter objects is the shiny, rounded rectangle. You can use the GH_Capsule type to draw these shapes with a minimum of fuss.
Ultimately there’s an Overlay channel which is rarely used but it allows you to draw shapes that need to be on top of all other components and parameters. After this, there are still more channels to do with canvas widgets, but IGH_Attributes
are not involved here.
Inside our implementation of the Render()
method, we need to draw the wires coming into the MySimpleIntegerParameter
, then the parameter capsule, while taking care to assign the correct colours (grey for normal, green for selected, dark for disabled, orange for warnings and red for errors). Finally we have to draw three lines of text on top of the capsule; the name of the owner, the median integer and the mean integer. The important types involved here are:
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
// Render all the wires that connect the Owner to all its Sources.
if (channel == GH_CanvasChannel.Wires)
{
RenderIncomingWires(canvas.Painter, Owner.Sources, Owner.WireDisplay);
return;
}
// Render the parameter capsule and any additional text on top of it.
if (channel == GH_CanvasChannel.Objects)
{
// Define the default palette.
GH_Palette palette = GH_Palette.Normal;
// Adjust palette based on the Owner's worst case messaging level.
switch (Owner.RuntimeMessageLevel)
{
case GH_RuntimeMessageLevel.Warning:
palette = GH_Palette.Warning;
break;
case GH_RuntimeMessageLevel.Error:
palette = GH_Palette.Error;
break;
}
// Create a new Capsule without text or icon.
GH_Capsule capsule = GH_Capsule.CreateCapsule(Bounds, palette);
// Render the capsule using the current Selection, Locked and Hidden states.
// Integer parameters are always hidden since they cannot be drawn in the viewport.
capsule.Render(graphics, Selected, Owner.Locked, true);
// Always dispose of a GH_Capsule when you're done with it.
capsule.Dispose();
capsule = null;
// Now it's time to draw the text on top of the capsule.
// First we'll draw the Owner NickName using a standard font and a black brush.
// We'll also align the NickName in the center of the Bounds.
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
format.Trimming = StringTrimming.EllipsisCharacter;
// Our entire capsule is 60 pixels high, and we'll draw
// three lines of text, each 20 pixels high.
RectangleF textRectangle = Bounds;
textRectangle.Height = 20;
// Draw the NickName in a Standard Grasshopper font.
graphics.DrawString(Owner.NickName, GH_FontServer.Standard, Brushes.Black, textRectangle, format);
// Now we need to draw the median and mean information.
// Adjust the formatting and the layout rectangle.
format.Alignment = StringAlignment.Near;
textRectangle.Inflate(-5, 0);
textRectangle.Y += 20;
graphics.DrawString(String.Format("Median: {0}", Owner.MedianValue), _
GH_FontServer.StandardItalic, Brushes.Black, _
textRectangle, format);
textRectangle.Y += 20;
graphics.DrawString(String.Format("Mean: {0:0.00}", Owner.MeanValue), _
GH_FontServer.StandardItalic, Brushes.Black, _
textRectangle, format);
// Always dispose of any GDI+ object that implement IDisposable.
format.Dispose();
}
}
Protected Overrides Sub Render(ByVal canvas As GH_Canvas, ByVal graphics As Graphics, ByVal channel As GH_CanvasChannel)
'Render all the wires that connect the Owner to all its Sources.
If (channel = GH_CanvasChannel.Wires) Then
RenderIncomingWires(canvas.Painter, Owner.Sources, Owner.WireDisplay)
Return
End If
'Render the parameter capsule and any additional text on top of it.
If (channel = GH_CanvasChannel.Objects) Then
'Define the default palette.
Dim palette As GH_Palette = GH_Palette.Normal
'Adjust palette based on the Owner's worst case messaging level.
Select Case Owner.RuntimeMessageLevel
Case GH_RuntimeMessageLevel.Warning
palette = GH_Palette.Warning
Case GH_RuntimeMessageLevel.Error
palette = GH_Palette.Error
End Select
'Create a new Capsule without text or icon.
Dim capsule As GH_Capsule = GH_Capsule.CreateCapsule(Bounds, palette)
'Render the capsule using the current Selection, Locked and Hidden states.
'Integer parameters are always hidden since they cannot be drawn in the viewport.
capsule.Render(graphics, Selected, Owner.Locked, True)
'Always dispose of a GH_Capsule when you're done with it.
capsule.Dispose()
capsule = Nothing
'Now it's time to draw the text on top of the capsule.
'First we'll draw the Owner NickName using a standard font and a black brush.
'We'll also align the NickName in the center of the Bounds.
Dim format As New StringFormat()
format.Alignment = StringAlignment.Center
format.LineAlignment = StringAlignment.Center
format.Trimming = StringTrimming.EllipsisCharacter
'Our entire capsule is 60 pixels high, and we'll draw
'three lines of text, each 20 pixels high.
Dim textRectangle As RectangleF = Bounds
textRectangle.Height = 20
'Draw the NickName in a Standard Grasshopper font.
graphics.DrawString(Owner.NickName, GH_FontServer.Standard, Brushes.Black, textRectangle, format)
'Now we need to draw the median and mean information.
'Adjust the formatting and the layout rectangle.
format.Alignment = StringAlignment.Near
textRectangle.Inflate(-5, 0)
textRectangle.Y += 20
graphics.DrawString(String.Format("Median: {0}", Owner.MedianValue), _
GH_FontServer.StandardItalic, Brushes.Black, _
textRectangle, format)
textRectangle.Y += 20
graphics.DrawString(String.Format("Mean: {0:0.00}", Owner.MeanValue), _
GH_FontServer.StandardItalic, Brushes.Black, _
textRectangle, format)
'Always dispose of any GDI+ object that implement IDisposable.
format.Dispose()
End If
End Sub
Note that in this case I assume that MySimpleIntegerParameter
has two ReadOnly properties called MedianValue
and MeanValue
. I haven’t written those, as they are not within the scope of this guide.
If you have cached display objects (for whatever reason I don’t want to hear), a good place to ensure they are PrepareForRender method. It is called once (and only once) just before any calls to Render()
. You do not need to call the overridden method as it is empty by default.