作者:“咕咕咕?下一篇马上就写好了”
通过上一篇【基础10】的文章,大家已经了解到一个GH电池在画布上的样式是由其背后的 GH_Attribute
类实例来决定的,而大部分的GH电池都使用了它的一个派生类 GH_ComponentAttribute
来配置电池的外观。今天我们就继续上一篇的内容,通过它来给我们的电池配置一个按钮,当我们的按钮被按下去的时候,可以切换我们今天例子中电池的工作模式。
首先我们来介绍一下今天的例子,它将会是一个简单的求 某个数的平方根 的电池:
public class SqrtRootPosNeg : GH_Component
{
public SqrtRootPosNeg()
: base("SqrtRootPosNeg", "SRPN",
"Description",
"Params", "DigitalCrab") { }
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddNumberParameter("A", "A", "", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddNumberParameter("A", "A", "", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA)
{
double a = 0.0;
if (!DA.GetData(0, ref a)) return;
DA.SetData(0, Math.Sqrt(a));
}
protected override System.Drawing.Bitmap Icon => null;
public override Guid ComponentGuid => Guid.Parse("请替换成自己生成的GUID");
}
从代码上也能看出来这是个十分简单的电池,仅有一个输入和一个输出,SolveInstance
的过程也只是将输入的double
变量直接求算术平方根。
但是,一个数的平方根有两个,一个是正数,一个负数,Math.Sqrt()
函数给出的只是那个正的平方根,我们接下来就做一个电池,让这个电池上面有一个按钮,点一下之后,这个电池就能输出负的平方根,再点一下就切换回来输出正的平方根。简而言之,就是使用一个在电池上的按钮来切换电池的工作形态。
定义电池的工作形态
要让电池实现两种不同的工作模式,那必然是要在电池类上添加一个表达电池当前 状态 的某种“变量”,然后我们通过按钮来改变这个“变量”的值,就可以实现电池的工作状态切换了。
最简单的方式当然是给电池一个bool
属性,true
的时候输出正平方根,false
的时候输出负平方根。但是嘛,bool
值毕竟不太直观,直接上个enum
,增强可阅读性。
那么,电池的非UI部分的主要逻辑(关于数据处理的逻辑)就完成了:
public class SqrtRootPosNeg : GH_Component
{
public enum SqrtMode { Positive, Negative } /* 定义一个enum类型 */
public SqrtMode CompWorkMode { get; set; } = SqrtMode.Positive; /* 使用这个enum类型来定义一个代表电池工作状态的变量 */
/* 修改电池的SolveInstance逻辑,引入对电池工作状态的判断 */
protected override void SolveInstance(IGH_DataAccess DA)
{
double a = 0.0;
if (!DA.GetData(0, ref a)) return;
if (CompWorkMode == SqrtMode.Positive)
DA.SetData(0, Math.Sqrt(a));
else
DA.SetData(0, -Math.Sqrt(a));
}
/* ... 略 ... */
}
接下来的部分就是要实现电池上增加一个按钮的功能。
创建一个自定义Attribute
我们可以直接在同一个.cs
文件内,在电池的class
的外面再定义一个新的class
,作为我们电池的Attribute
。一般来说我个人建议是每个类都单独成为一个文件,这样的话,管理起来比较方便,也更直观。但是在本例中我们就直接放在一个.cs
文件内吧,这样做也是可以被编译器所接受的。
public class SqrtRootPosNeg : GH_Component
{
public override void CreateAttributes() /* 重写CreateAttribute方法以启用自定义电池外观 */
{
Attributes = new SqrtAttribute(this);
}
/* ... 略 ... */
}
public class SqrtAttribute : GH_ComponentAttributes
{
public SqrtAttribute(SqrtRootPosNeg component) : base(component) { }
protected override void Layout()
{
base.Layout();
}
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
base.Render(canvas, graphics, channel);
}
}
如上面代码所示,我们需要创建一个类,继承自GH_ComponentAttribute
,并给出一个继承的构造方法。下面就是重点,需要重写类的两个方法以实现我们增加按钮的目的。
重写 Layout()
方法
在【基础10】中我们提到了,电池在画布上具体是如何渲染是决定于Render()
方法,但是为什么我们在这里要重写Layout()
方法?这是因为,Layout()
方法会在Render()
方法之前被执行,用来决定这个电池的具体在画布上占用多大的空间,这个是由Bounds
属性决定的。而想要改变这个属性,在Layout()
属性中更改是最合适不过了。
我们常用的按钮大概高度可以在 16px (16像素)左右,考虑到按钮周围应该留出 1~2px 的空间(让电池看起来不是贴在电池边),我们这里设置将 Bounds
加高 20px。
public class SqrtAttribute : GH_ComponentAttributes
{
public SqrtAttribute(SqrtRootPosNeg component) : base(component) { }
protected override void Layout()
{
base.Layout();
/* 先执行base.Layout(),可以按GH电池默认方式计算电池的出/入口需要的高度,我们在下面基于这个高度进行更改 */
Bounds = new RectangleF(Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height + 20.0f);
}
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
base.Render(canvas, graphics, channel);
}
}
此时,我们的电池已经在常规电池大小的下方多出来一块地方来放置我们的按钮了。
重写 Render()
方法
在准备好渲染按钮的空间之后,接下来就可以真正地开始绘制和渲染电池了:
- 使用
RectangleF
预先确定要绘制按钮的区域 - 使用
GH_Capsule
创建并绘制一个按钮
这里的 GH_Capsule
是一个继承自 IDisposible
接口的对象,代表着它可以手动释放资源,为了能够正确释放资源,我们可以使用 using
关键词来自动处理。
public class SqrtAttribute : GH_ComponentAttributes
{
public SqrtAttribute(SqrtRootPosNeg component) : base(component) { }
protected override void Layout()
{
base.Layout();
Bounds = new RectangleF(Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height + 20.0f);
}
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
base.Render(canvas, graphics, channel); /* 执行基本的电池渲染 */
/* 额外的电池渲染,仅在“Objects”这个渲染轨道绘制 */
if (channel == GH_CanvasChannel.Objects)
{
RectangleF buttonRect = /* 按钮的位置 */ new RectangleF(Bounds.X, Bounds.Bottom - 20, Bounds.Width, 20.0f);
/* 在X、Y方向分别留出2px的空隙,以免button贴住电池边 */
buttonRect.Inflate(-2.0f, -2.0f);
using (GH_Capsule capsule = GH_Capsule.CreateCapsule(buttonRect, GH_Palette.Black))
{
/* 按照该电池的“是否被选中”、“是否被锁定”、“是否隐藏”三个属性来决定渲染的按钮样式 */
/* 这样可以使得我们的按钮更加贴合GH原生的样式 */
/* 也可以自己换用其他的capsule.Render()重载,渲染不同样式电池 */
capsule.Render(graphics, Selected, Owner.Locked, Owner.Hidden);
}
}
}
}
好了,看看电池现在的状态把,我们的按钮应该就会被渲染出来了:
嗯…… 看起来好像还少了行字,让我们渲染个字上去吧。按钮上的字可以用来表示当前电池的工作状态,这样用户一看就知道目前电池是什么状态了。
public class SqrtAttribute : GH_ComponentAttributes
{
/* ... 略 ... */
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
/* ... 略 ... */
if (channel == GH_CanvasChannel.Objects)
{
/* ... 略 ... */
graphics.DrawString(
((SqrtRootPosNeg)Owner).CompWorkMode.ToString(),
new Font(GH_FontServer.ConsoleSmall, FontStyle.Bold),
Brushes.White,
buttonRect,
new StringFormat()
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center
});
}
}
}
这回看起来好多了。
好了剩下的就是如何响应鼠标的点击了。
重写 RespondToMouseDown()
函数
当GH画布检测到鼠标事件之后,会检测画布当前位置是否存在电池(还记得【基础10】里的Bounds
和InPickRegion
吗),如果存在,则会触发该电池的RespondToMouseDown()
函数。当然,如果你没有重写这个函数的话,在Grasshopper的默认实现里是什么额外操作都不会做的,要实现在鼠标点击我们的按钮区域,切换电池工作状态的话,就需要重写这个逻辑,做自己的实现。
public class SqrtAttribute : GH_ComponentAttributes
{
/* ... 略 ... */
public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
{
return base.RespondToMouseDown(sender, e);
}
}
整个鼠标事件大概是这样:我们先检测鼠标事件,是否是鼠标左键点击且在按钮的区域内,如果满足这俩条件,则改变电池工作状态并通知画布ExpireSolution
,若这两条件有一个不满足,则无事发生。
public class SqrtAttribute : GH_ComponentAttributes
{
/* ... 略 ... */
public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
{
RectangleF buttonRect = /* -重新计算按钮的区域大小- */ new RectangleF(Bounds.X, Bounds.Bottom - 20, Bounds.Width, 20.0f);
if (e.Button == MouseButtons.Left && buttonRect.Contains(e.CanvasLocation))
{
SqrtRootPosNeg comp = (SqrtRootPosNeg)Owner; /* 通过Owner属性来获得电池本身 */
/* 依照电池当前工作状态来改变电池 */
if (comp.CompWorkMode == SqrtRootPosNeg.SqrtMode.Negative)
comp.CompWorkMode = SqrtRootPosNeg.SqrtMode.Positive;
else
comp.CompWorkMode = SqrtRootPosNeg.SqrtMode.Negative;
/* 改变完电池后,重启计算 */
comp.ExpireSolution(true);
/* 结束鼠标事件处理,通知GH已经处理完毕 */
return GH_ObjectResponse.Handled;
}
/* 若上述条件未满足,则直接返回“未处理” */
return GH_ObjectResponse.Ignore;
}
}
下面就是这个电池的整个工作状态了:
同样的方式,只需要计算好电池需要增加电池的区域,预先在Layout()
中规划Bound
属性,然后再在Render()
方法中制作对应的GH_Capsule
即可在任何地方添加按钮了(添加到电池外面也是可以的,任意位置……只要你想)。如果还想在电池中绘制奇奇怪怪的东西,可以参照微软对Graphcis
类的教程,链接如下,里面详细说了如何绘制线、矩形、圆形、如何构建笔刷等等的步骤:
老规矩,完整cs源码在最后,下次再继续。
🦀
using System;
using System.Drawing;
using System.Windows.Forms;
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Attributes;
namespace DigitalCrab.Grasshopper
{
public class SqrtRootPosNeg : GH_Component
{
public enum SqrtMode { Positive, Negative }
public SqrtMode CompWorkMode { get; set; } = SqrtMode.Positive;
public SqrtRootPosNeg()
: base("SqrtRootPosNeg", "SRPN",
"Description",
"Params", "DigitalCrab") { }
public override void CreateAttributes()
{
Attributes = new SqrtAttribute(this);
}
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddNumberParameter("A", "A", "", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddNumberParameter("A", "A", "", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA)
{
double a = 0.0;
if (!DA.GetData(0, ref a)) return;
if (CompWorkMode == SqrtMode.Positive)
DA.SetData(0, Math.Sqrt(a));
else
DA.SetData(0, -Math.Sqrt(a));
}
protected override System.Drawing.Bitmap Icon => null;
public override Guid ComponentGuid => Guid.Parse("请替换成自己生成的GUID");
}
public class SqrtAttribute : GH_ComponentAttributes
{
public SqrtAttribute(SqrtRootPosNeg component) : base(component) { }
protected override void Layout()
{
base.Layout();
Bounds = new RectangleF(Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height + 20.0f);
}
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
base.Render(canvas, graphics, channel);
if (channel == GH_CanvasChannel.Objects)
{
RectangleF buttonRect = new RectangleF(Bounds.X, Bounds.Bottom - 20, Bounds.Width, 20.0f);
buttonRect.Inflate(-2.0f, -2.0f);
using (GH_Capsule capsule = GH_Capsule.CreateCapsule(buttonRect, GH_Palette.Black))
{
capsule.Render(graphics, Selected, Owner.Locked, Owner.Hidden);
}
graphics.DrawString(
((SqrtRootPosNeg)Owner).CompWorkMode.ToString(),
new Font(GH_FontServer.ConsoleSmall, FontStyle.Bold),
Brushes.White,
buttonRect,
new StringFormat()
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center
});
}
}
public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
{
RectangleF buttonRect = new RectangleF(Bounds.X, Bounds.Bottom - 20, Bounds.Width, 20.0f);
if (e.Button == MouseButtons.Left && buttonRect.Contains(e.CanvasLocation))
{
SqrtRootPosNeg comp = (SqrtRootPosNeg)Owner;
if (comp.CompWorkMode == SqrtRootPosNeg.SqrtMode.Negative)
comp.CompWorkMode = SqrtRootPosNeg.SqrtMode.Positive;
else
comp.CompWorkMode = SqrtRootPosNeg.SqrtMode.Negative;
comp.ExpireSolution(true);
return GH_ObjectResponse.Handled;
}
return GH_ObjectResponse.Ignore;
}
}
}