距离上次的【基础9】已经过去了又有差不多1个月了,工作上的事情越来越多,能抽出空来(主要是要找到“有空并且有舒适的心情来写”的时间挺难的…)的时间也变少了。笔者最近在做的工作跟Grasshopper本身也没有特别多的联系,但是对于Grasshopper的兴趣还是依然存在,这也是笔者能够一直坚持这个系列创作的最重要的原因吧,希望平日大家在设计工作之余,也能做出更多好玩有意思的东西来。
好了,闲话少说,【基础1~9】基本上可以算是把GH_Component
这个类能做的所有事情都囊括了进去,这其中包括了数据的类型、数据的传递和处理、电池输入输出变量的动态变更,以及自定义鼠标右键菜单的构造等。更复杂的东西,GH_Component
就无法胜任了,需要借助到其他的类了。
在整个Grasshopper电池的制作过程当中,大家一定会有一个疑问,电池在画布上为什么会长成一个圆角长方形的样子,上面为什么又可以根据电池的名字出现相应的文字,如果我想要让自己的电池长成另外的样子行不行?这一切的问题答案都源自于一个类:
GH_Attribute<T>
它是决定电池长相以及交互的类。一切有关于电池的前端都由它来负责。由于它所要处理的事务特别多、也特别琐碎,不可能用一篇文章介绍完毕,接下来的若干篇文章都会围绕它来展开。
今天作为【基础】的第10篇,正好凑个整,就先来总体介绍一下GH_Attribute<T>
这个类包含哪些可以为我们所用的函数(常用的),每个函数又大致是什么样的功能吧。不过在具体介绍之前,我们还是先来总结一下GH_Attribute<T>
与其他我们已经介绍过的类是什么样一种关系。
TL, DR :
实现自定义电池外观最快的办法:
- 继承
GH_ComponentAttribute
- 重写
Render
函数。
详见最后代码
GH_Attribute<T>
与 GH_Component
首先明确一点,GH_Attribute<T>
服务对象不仅仅限于GH_Component
。
我们已经知道GH_Component
是GH_DocumentObject
的子类,所有在Grasshopper上出现的东西(包括电池、各种奇奇怪怪的按钮等等)都是派生自GH_DocumentObject
的。而每一个GH_DocumentObject
类中会有一个属性是GH_Attribute<T>
,也就是说,
- 每一个
GH_DocumentObject
对应一个GH_Attribute<T>
GH_DocumentObject
负责处理业务逻辑,其包含如何运作数据等GH_Attribute<T>
负责处理前端逻辑,其包含电池应该长什么样,怎么相应鼠标事件等
这俩都是抽象类,不能直接创作实例,因为它们俩本身都不是真正能够给用户提供功能的类。
就像“我们平时用的是GH_Component
而不是GH_DocumentObject
”一样,GH_Attribute<T>
不是能够直接使用的。
与GH_Component
对应的是GH_ComponentAttributes
类,它是GH_Attribute<T>
的子类,其中泛型T
对应的类型这里是IGH_Component
。也就是这个类是针对于GH_Component
所特有的。
下面是重点:
我们如果想要自定义电池的外观,最简单、直观的方法是创作一个类,继承自
GH_ComponentAttributes
类。
既然GH_ComponentAttributes
是继承自GH_Attribute<T>
的类,我们通过详细了解GH_Attribute<T>
这个类,自然也了解了如何实现自定义电池外观了。
GH_Attribute<T>
所实现的接口类
整个Grasshopper的架构几乎都是基于接口构建的,所以GH_Attribute<T>
本质上也只是一个实现了IGH_Attribute
的接口类,了解了接口需要实现什么样的函数,也就了解了GH_Attribute<T>
的本质了。
IGH_Attribute
接口类所需要实现的内容有:
Pivot
属性PointF
类型- 用于定义电池在Grasshopper上的挂载点,也就是“位于Grasshopper画布上具体哪个坐标点”
- 改变
Pivot
坐标等价于移动电池在画布上的位置
Bounds
属性RectangleF
类型- 用于定义电池在Grasshopper上的绘制范围,也就是电池所在的长方形大小的具体长、宽信息
- 这个类型里附带
X
和Y
属性,但是GH并不会读取它们,有用的是Width
和Height
属性
DocObject
属性IGH_DocumentObject
类型- 用于获取与之“一对一”对应的
GH_DocumentObject
实例
Selected
属性bool
类型- 顾名思义,是用来保存该电池是否被选中的一个状态量(选中的电池呈绿色)
IsPickRegion
方法- 主要是用来确定给定点是否在电池的
Bounds
范围内 - 用于触发鼠标事件等
- 主要是用来确定给定点是否在电池的
PerformLayout
方法- 用来根据其他数据(比如电池的输入/输出端参数个数)来修正
Bounds
或者其他与电池外观相关的参数 - 该方法会在最终绘制之前运行,用来“准备电池外观”
- 用来根据其他数据(比如电池的输入/输出端参数个数)来修正
RenderToCanvas
方法- 重点
- 由该方法最终决定电池在画布上的外观
所以,任何一个GH电池的长相大致的逻辑在最终由Grasshopper底层调用时是:
- 由
PerformLayout
生成需要绘制的电池是多大、有哪些文字、位置在哪里 - 由
RenderToCanvas
最终将电池绘制在GH的画布上:- 依据
Pivot
属性确定电池位置 - 依据
Bounds
属性确定电池框大小 - 依据
Selected
属性确定绘制的颜色 - 依照其他属性再绘制相应的元素/颜色
- ……
- 依据
但是,由于RenderToCanvas
需要绘制的内容有许多,还包括额外的画布上的Widget内容,所以,GH作者在用GH_Attribute<T>
来最终实现接口时,把RenderToCanvas
方法封装了起来,在继承时无法获得,转而暴露出来可以供子类重写的方法是Render
。
这个Render
在我们自定义电池样式的时候就是 重中之重 了。
光说不练假把式,直接上代码。
重写Render
改变电池样式小例子
1. 创建一个继承自GH_ComponentAttributes
的子类
首先,在我们已经有自定义电池(笔者这里的电池类的名字叫做CustomAttribute01
),新建一个C#类,继承自GH_ComponentAttributes
(别忘了引用Grasshopper.Kernel.Attributes
命名空间)。
这个类随便叫什么,但是它的构造函数需要直接使用基类的构造函数,并且构造函数的这个参数类型需要与你想要实现的自定义电池保持一致。
using Grasshopper.Kernel.Attributes;
///....
namespace DigitalCrab.Grasshopper
{
// 自定义一个类,继承自 GH_ComponentAttributes
class MyCustomizedAttr : GH_ComponentAttributes
{
// 构造函数使用基类构造函数,函数参数类型为自定义电池类型
public MyCustomizedAttr(CustomAttribute01 owner) : base(owner) { }
}
// 常规的自定义电池类,继承于我们熟悉的 GH_Component
public class CustomAttribute01 : GH_Component
{
public CustomAttribute01()
: base("CustomAttribute01", "Nickname",
"Description", "Params", "DigitalCrab") { }
// .... 省略常规电池代码
}
}
2. 重写Render
接下来我们需要在这个刚刚添加的自定义类里添加一个对于Render
的重写函数
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
base.Render(canvas, graphics, channel);
}
由于我们不需要用到它自己的电池原来的样子,可以直接删除这行对于基类函数的调用。接下来,我们就需要用到代码来实现自己的电池的外观了。
Grasshopper的电池都是同过这个Graphics
类绘制在画布上的,并且,由于GH上画布元素比较复杂,所以Render
函数一共会被调用4次,每次调用时,传入的参数GH_CanvasChannel
都不一样。电池的绘制是发生在GH_CanvasChannel.Objects
情况下的,所以我们有如下代码来绘制一个圆作为我们的电池的外观:
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
switch (channel)
{
case GH_CanvasChannel.First:
break;
case GH_CanvasChannel.Wires:
break;
case GH_CanvasChannel.Objects:
graphics.DrawEllipse(new Pen(Color.CadetBlue), new RectangleF(new PointF(Pivot.X - 25, Pivot.Y - 25), new SizeF(50, 50)));
break;
case GH_CanvasChannel.Overlay:
break;
default:
break;
}
}
大功告成!!
但是,要让它能够真正展示在画布上,我们需要让我们的自定义电池能够“挂载”上我们自定义的这个外观。
3. 在GH_Component
上挂载自定义外观
挂载外观需要重写GH_Component
的一个函数:CreateAttribute()
。
public class CustomAttribute01 : GH_Component
{
public CustomAttribute01()
: base("CustomAttribute01", "Nickname",
"Description",
"Params", "DigitalCrab") { }
// 重写CreateAttributes方法,来实现对刚刚自定义外观的挂载
public override void CreateAttributes()
{
Attributes = new MyCustomizedAttr(this);
}
// 继续省略后续一万行代码 …
}
下面就是见证奇迹的时刻
4. 自定义外观GIF
这,被选中了感觉没啥反应啊?不知道这个电池有没有被选中,用户体验好似有点差,那没关系,加个选择逻辑:
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
switch (channel)
{
case GH_CanvasChannel.First:
break;
case GH_CanvasChannel.Wires:
break;
case GH_CanvasChannel.Objects:
if (Selected)
graphics.DrawEllipse(new Pen(Color.Red), new RectangleF(new PointF(Pivot.X - 25, Pivot.Y - 25), new SizeF(50, 50)));
else
graphics.DrawEllipse(new Pen(Color.CadetBlue), new RectangleF(new PointF(Pivot.X - 25, Pivot.Y - 25), new SizeF(50, 50)));
break;
case GH_CanvasChannel.Overlay:
break;
default:
break;
}
}
妙啊!
那是不是我们要重头到位手工绘制所有的复杂外观呢?画一个真正好看的电池岂不是累死了…… 当然不是了!Grasshopper内部有许多帮忙绘制长得还可以的电池的类,我们可以用它们来制作稍微有那么一丢丢好看的电池(比如这里是用了GH_Capsule
,用来画一个长得跟Grasshopper原来差不多的电池)
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
switch (channel)
{
case GH_CanvasChannel.First:
break;
case GH_CanvasChannel.Wires:
break;
case GH_CanvasChannel.Objects:
// 创建一个GH样式的电池外形
var cap = GH_Capsule.CreateCapsule(
new RectangleF(Pivot, new SizeF(180, 48)),
GH_Palette.Normal, 10, 2);
// 依照选中状态,以不同颜色染色
if (Selected)
cap.Render(graphics, Color.LawnGreen);
else
cap.Render(graphics, Color.Transparent);
// 也可以用GH_Capsule自带的方法来自动决定着色,下面一行代码更实用一些
// cap.Render(graphics, Selected, Owner.Locked, Owner.Hidden);
break;
case GH_CanvasChannel.Overlay:
break;
default:
break;
}
}
感觉交互有点怪怪的… 有时候明明鼠标已经是在电池范围内,但是无法实现点击拖动,有时候明明没在电池范围里,但又偏偏能拖动,这是为什么呢?
5. IsPickRegion
方法
正如方法名所描述的一样,这个函数会决定鼠标位置是否可以拖动电池。由于这个函数的默认实现是依照函数内部的Bounds
和Pivot
属性来确定是否能够拖动电池,而函数默认属性Bounds
我们没有直接改变它,仅仅改变的是电池在画布上的绘制逻辑,所以,在画布上看起来能够拖动电池的鼠标位置,在IsPickRegion
函数看起来是在电池的Bounds
之外,不能被拖动的。
所以,直接对这个方法进行一个重写,让它的判断区域与电池的绘制区域重合即可
public override bool IsPickRegion(PointF point)
{
// 判断电池真正绘制的区域是否包含鼠标(给定的PointF结构体所代表的坐标点)位置即可
return new RectangleF(Pivot, new SizeF(180, 48)).Contains(point);
}
这回就正常多了:
但是事实上……
相信大家尝试过几次之后就会发现,但凡稍微复杂一点的电池,上面介绍的方法都将会遇到几个很大的问题:
- 输入/输出端的变量不会出现
- 电池输入端变量的连线不会出现
- Widget渲染位置不对
最可怕的是: 这些都不正确的情况下,电池还能跑起来……
比如,我往这个电池的输入端添加了3个Curve变量,添加了一个输出端的Curve变量,电池就会变成这样:
这咋玩?
所以,许多时候我们想要实现一个“自定义外观”并非是要重头开始写一个外观,而是某种程度上的“改造”,比如加一个小按钮来方便切换电池的某种行为、添加一个指示器来表明电池数据的某种状态,等等这类。
我们不希望破坏原有的Grasshopper电池外观的总体逻辑(同时这也符合用户体验的统一性原则,除非电池作用逻辑与Grasshopper本身数据流的逻辑有较大冲突),希望在保留原有外观的前提下,进行一些改进。
嗯,下一期就分别以“在普通电池上增加一个按钮”和“在普通电池上增加一个指示器”为例子,介绍一下如何基于原有电池来魔改。今天就先介绍到这里了。
有问题欢迎留言
🦀
老规矩,最后的代码:
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Attributes;
using Rhino.Geometry;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq.Expressions;
namespace DigitalCrab.Grasshopper
{
class MyCustomizedAttr : GH_ComponentAttributes
{
public MyCustomizedAttr(CustomAttribute01 owner) : base(owner) { }
public override bool IsPickRegion(PointF point)
{
return new RectangleF(Pivot, new SizeF(180, 48)).Contains(point);
}
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
switch (channel)
{
case GH_CanvasChannel.First:
break;
case GH_CanvasChannel.Wires:
break;
case GH_CanvasChannel.Objects:
/ 简单圆
//if (Selected)
// graphics.DrawEllipse(new Pen(Color.Red), new RectangleF(new PointF(Pivot.X - 25, Pivot.Y - 25), new SizeF(50, 50)));
//else
// graphics.DrawEllipse(new Pen(Color.CadetBlue), new RectangleF(new PointF(Pivot.X - 25, Pivot.Y - 25), new SizeF(50, 50)));
var cap = GH_Capsule.CreateCapsule(new RectangleF(Pivot, new SizeF(180, 48)), GH_Palette.Normal, 10, 2);
if (Selected)
cap.Render(graphics, Color.LawnGreen);
else
cap.Render(graphics, Color.Transparent);
break;
case GH_CanvasChannel.Overlay:
break;
default:
break;
}
}
}
public class CustomAttribute01 : GH_Component
{
public CustomAttribute01()
: base("CustomAttribute01", "Nickname",
"Description",
"Params", "DigitalCrab")
{
}
public override void CreateAttributes()
{
Attributes = new MyCustomizedAttr(this);
}
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddCurveParameter("a", "b", "c", GH_ParamAccess.item);
pManager.AddCurveParameter("a", "b", "c", GH_ParamAccess.item);
pManager.AddCurveParameter("a", "b", "c", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddCurveParameter("a", "b", "c", GH_ParamAccess.item);
pManager.AddCurveParameter("a", "b", "c", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA)
{
}Bitmap Icon
{
get
{
return null;
}
}
public override Guid ComponentGuid
{
get => throw new Exception("请自行生成GUID放在该返回值中");
}
}
}