作者:“咕咕咕?下一篇马上就写好了”
上一篇【基础11】向大家介绍了怎么在Grasshopper里制作自己的带有按钮的电池外观。从反馈来看,挺多读者对这个例子十分感兴趣,同时也私信联系到我问了一些更详细的需求细节。其中一位“风起云淡”网友提到了下面的问题:
“如果这个自定义属性类是单独写的一个类,想要这个自定义属性类写成通用的是否可以?”
“我在构造函数里面传入的IGH_Component
Owner接口,但是到了在GH_Attribute
里面的文字渲染(重写Render
方法)的时候,没有办法拿到传入那个枚举类型的“工作模式转文字”输出渲染了。”
这个问题挺有意思的,简单聊了几句之后,发现其实这个需求十分的实际,也是一个十分好的从实际需求出发的一个讲解C#这门语言本身的精髓的一个案例:
有10个自定义电池,每个电池都想要一个按钮,但是这每个按钮上面的文字内容、按下去之后的响应却都需要不一样。
哼哼,这还不简单,脑门一拍方案就出来了:参照上一篇 【基础11】,每个电池重写Render
方法,代码Ctrl+C、Ctrl+V之后仅需稍加修改,对于点击事件分别实现;只要给我钛金Ctrl
/C
/V
键,别说10个电池,100个电池又如何?
嗯,似乎没毛病。
但是,
一个月后,你学到了一个新的超级酷炫无敌X炸天的Graphic库,用它来渲染按钮帅的一B,现在想要把这100个电池的按钮样式都换了
原地升天。
又要Ctrl+C、Ctrl+V辛苦劳作一整天?不不不,这其实就需要我们将代码的思维再拔高一个层面,我们这次要写的不是某一个类了,我们这次来写“一类”类,也就是所谓的“框架”。
为什么写框架
写框架 = 复用。
毫无疑问的,如果某个类只会用到一次,那是没必要写框架的。就比如这按钮电池外观这个例子,如果就写这一个带按钮的电池外观类,那么框架带来额外代码工作量是不划算的。但是但凡第二次开始写与这个类差不多功能的类的时候,框架的优势就开始体现了。
计算机业内有句话:
“第一次写某个功能,请直接实现,第二次写这个功能,也请直接实现,但记住它,第三次写这个功能,写一套可复用的函数,并以后一直改进它”。
框架就是这“一系列可复用的函数”。
框架的思想
其实整个代码思想都是围绕着“复用”这个概念来的。无论是前一段时间很火的“面向对象编程”,还是“面向过程编程”,最终的思路都是少写点代码。为了少写点代码,我们就需要:
观察需求中 重复 的部分
把重复的部分具体化,包含在框架里,把不同的部分抽象化,暴露在框架外。
这里提到了两个词,“具体化”和“抽象化”。简单地理解,“具体化”就是把代码写死,固定住,这代表着框架中固定不变的部分。“抽象化”就是把代码留给框架的使用者来写,仅仅规定“要写什么”,而不规定“具体写的是什么”。具体化很好理解,抽象化却是有点抽象,我们接下来细细展开。
以我们的本文最初的核心,“打造自定义可复用电池外观模版”,为例:模版不变的部分就是“外观上需要一个按钮”,模版变化的部分就是“按钮上的文字”和“按钮按下后的触发方法”;写一个简单模版。
一个简单的可复用电池外观模版
首先分析需求:
- 我们的模版肯定是一个类,这个类还得能够被Grasshopper本身认识。
- 通过使用这个模版,我们以后不想每次都需要粘贴
Render
方法里的代码。 - 通过使用这个模版,我们需要能够对不同的电池实现不同的按钮文字和事件。
Simple and easy.
直接使用C#中的抽象类来完成这个使命:
我们定义一个抽象类,将 Render
方法“具体化”地写死,但留下一个 string
属性和一个用来触发按钮的方法作为抽象化,只有具体再继承的时候,才需要写具体的实现。假设我们的模板类名字取为“MyCompAttrTemplate
”
新建一个MyCompAttrTemplate.cs
:
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Attributes;
using System.Drawing;
using System.Windows.Forms;
namespace DigitalCrab.Grasshopper
{
public abstract class MyCompAttrTemplate : GH_ComponentAttributes
{
protected MyCompAttrTemplate(IGH_Component component) : base(component) { }
public abstract string ButtonText { get; } // 抽象化属性
public abstract void ButtonClickHandler(object sender, GH_CanvasMouseEvent e); //抽象化函数
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(
ButtonText, // 在渲染时,调用该“抽象化”的属性
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))
{
// 在做事件处理时,调用“抽象化”的函数来做真正的事件处理
ButtonClickHandler(sender, e);
return GH_ObjectResponse.Handled;
}
return GH_ObjectResponse.Ignore;
}
}
}
上面的代码中,Layout
和Render
的方法具体实现以及RespondToMenuDown
的事件处理具体实现是从【基础11】直接Copy的,但具体的文字和处理内容则是使用了“抽象化”的部分来进行处理。
最终这俩在模板中抽象化的内容,就仅仅需要在我们写最终实现的类中写就可以了,比如下面我就可以很简短地一口气写3个类,分别有着不同的按钮文字内容,和按钮事件:
public class CompAttr1 : MyCompAttrTemplate
{
public CompAttr1(IGH_Component owner) : base(owner) { }
public override string ButtonText => "Hello";
public override void ButtonClickHandler(object sender, GH_CanvasMouseEvent e)
=> MessageBox.Show($"Hello, Grasshopper"); // 直接弹窗
}
public class CompAttr2 : MyCompAttrTemplate
{
public CompAttr2(IGH_Component owner) : base(owner) { }
public override string ButtonText => "Sleep";
public override void ButtonClickHandler(object sender, GH_CanvasMouseEvent e)
=> Task.Run(() => { Thread.Sleep(1500); MessageBox.Show("Slept for 1.5 seconds"); }); // 延时弹窗
}
public class CompAttr3 : MyCompAttrTemplate
{
public CompAttr3(IGH_Component owner) : base(owner) { }
public override string ButtonText => "Desktop";
public override void ButtonClickHandler(object sender, GH_CanvasMouseEvent e)
=> System.Diagnostics.Process.Start(Environment.GetFolderPath(Environment.SpecialFolder.Desktop)); // 打开桌面文件夹
}
然后就可以分别挂载在三个不同的电池上,最终形成这样的效果:
芜湖起飞。此时我觉得按钮颜色不好看,比如我想改个蓝的,只需要改一改模版里的MyCompAttrTemplate.cs
里的Render
实现就可以了,瞬间完成。
using (GH_Capsule capsule = GH_Capsule.CreateCapsule(buttonRect, GH_Palette.Blue))
{
capsule.Render(graphics, Selected, Owner.Locked, Owner.Hidden);
}
再偷懒一点点的可复用电池模版
上面的模版虽然实现了简单的可复用,但是还是好麻烦啊,我每次稍微改个字,就得新建一个类,类累不累我不知道反正我为了写这个例子是累死了。
偷个懒,能不能把要显示的文字和点击事件处理方法在这个类的初始化函数里预先存好,然后直接调用啊?当然可以,稍微改造一下MyCompAttrTemplate.cs
,不使用抽象类了,我们直接把他作为一个具体类,去掉了两个带有abstract
关键词的内容:
public class MyCompAttrTemplate : GH_ComponentAttributes
{
protected MyCompAttrTemplate(IGH_Component component, string buttonText, Action<object, GH_CanvasMouseEvent> handler) : base(component)
{
ButtonText = buttonText;
ButtonClickHandler = handler;
}
public string ButtonText { get; set; } // 文字存储属性
public Action<object, GH_CanvasMouseEvent> ButtonClickHandler { get; set; }//点击事件处理函数存储
/* 剩下的代码不用动 */
}
然后我们在用的时候只需要:
public class MyComp1 : GH_Component
{
public MyCompTemp() : base("Comp1", "Comp1", "Comp1", "Params", "Best Digital Crab") { }
// 调用对应的初始化函数
public override void CreateAttributes()
{
Attributes = new CompAttr1(this, "Hello",
(o,e) => MessageBox.Show($"Hello, Grasshopper")); // 给定一个函数作为输入参数
}
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddGenericParameter("AA", "AA", "AAA", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
pManager.AddGenericParameter("AA", "AA", "AAA", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA) { }
}
芜湖起飞,再也不用为每个电池单独写一个Attribute
类了。
水了一期,最近工作真滴忙呀! 🦀
老规矩, 代码电池见下面。这次的代码电池本身我也写了个抽象类来做基类,问就是懒…
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Attributes;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.Threading.Tasks;
using System;
namespace DigitalCrab.Grasshopper
{
public class CompAttr1 : MyCompAttrTemplate
{
public CompAttr1(IGH_Component owner) : base(owner) { }
public override string ButtonText => "Hello";//Guid.NewGuid().ToString().Substring(0, 5);
public override void ButtonClickHandler(object sender, GH_CanvasMouseEvent e)
=> MessageBox.Show($"Hello, Grasshopper");
}
public class CompAttr2 : MyCompAttrTemplate
{
public CompAttr2(IGH_Component owner) : base(owner) { }
public override string ButtonText => "Sleep";
public override void ButtonClickHandler(object sender, GH_CanvasMouseEvent e)
=> Task.Run(() => { Thread.Sleep(1500); MessageBox.Show("Slept for 1.5 seconds"); });
}
public class CompAttr3 : MyCompAttrTemplate
{
public CompAttr3(IGH_Component owner) : base(owner) { }
public override string ButtonText => "Desktop";
public override void ButtonClickHandler(object sender, GH_CanvasMouseEvent e)
=> System.Diagnostics.Process.Start(Environment.GetFolderPath(Environment.SpecialFolder.Desktop));
}
public abstract class MyCompTemp : GH_Component
{
public MyCompTemp(string a) : base(a, a, a, "Params", "Best Digital Crab") { }
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddGenericParameter("AA", "AA", "AAA", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
pManager.AddGenericParameter("AA", "AA", "AAA", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA) { }
}
public class MyComp1 : MyCompTemp
{
public MyComp1() : base("Comp1") { }
public override void CreateAttributes() => Attributes = new CompAttr1(this);
public override Guid ComponentGuid => new Guid("{6006750D-57A5-493F-92C6-144FAFC4FA53}");
}
public class MyComp2 : MyCompTemp
{
public MyComp2() : base("Comp2") {}
public override void CreateAttributes() => Attributes = new CompAttr2(this);
public override Guid ComponentGuid => new Guid("{DB8B3431-E850-4854-B62F-F30C9401BB0F}");
}
public class MyComp3 : MyCompTemp
{
public MyComp3() : base("Comp3") { }
public override void CreateAttributes() => Attributes = new CompAttr3(this);
public override Guid ComponentGuid => new Guid("{380BBEFA-CFCE-4FCB-A9BC-1560A1B487E7}");
}
public abstract class MyCompAttrTemplate : GH_ComponentAttributes
{
protected MyCompAttrTemplate(IGH_Component component) : base(component)
{ }
public abstract string ButtonText { get; }
public abstract void ButtonClickHandler(object sender, GH_CanvasMouseEvent e);
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.Blue))
{
capsule.Render(graphics, Selected, Owner.Locked, Owner.Hidden);
}
graphics.DrawString(
ButtonText,
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))
{
ButtonClickHandler(sender, e);
return GH_ObjectResponse.Handled;
}
return GH_ObjectResponse.Ignore;
}
}
}