Click or drag to resize

Custom Attributes (C#)

This article contains a step-by-step walkthrough regarding custom object display. 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 article I'll explain 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 article we'll assume we have a custom simple parameter (i.e. without persistent data) that holds integers.

C#
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}"); }
  }
}

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:

C#
public override void CreateAttributes()
{
  m_attributes = new MySimpleIntegerParameterAttributes(Me);
}
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_AttributesT 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:

C#
public class MySimpleIntegerParameterAttributes : GH_Attributes<MySimpleIntegerParameter>
{
  public MySimpleIntegerParameterAttributes(MySimpleIntegerParameter owner) : base(owner) { }
}

This is enough so far to make it work, eventhough 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:

C#
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));
}
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:

C#
publicoverride void ExpireLayout()
{    
  base.ExpireLayout();

  // Destroy any data you have that becomes 
  // invalid when the layout expires.
}

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:

C#
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();
  }
}
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 topic.

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.