其实经过【基础2】~【基础6】、以及【基础8】的内容,几乎所有插件的后台数据处理流程都可以实现了(往往也是最关键的业务核心内容):
- 从
RegisterInputParam
和RegisterOutputParam
中注册数据的入口和出口 - 在
SolveInstance
中处理及传递数据 - 在
Read
和Write
中进行数据的序列化与反序列化
这也是为什么很多时候,目前大部分的GH教程也是大致到这个节点步骤就结束了。因为依靠这些内容,一套完整的业务逻辑可以完成,无非就是电池样子长得平平无奇一点嘛。
不过,很多时候我们也不仅仅满足于“实现流程”,还要 实现花里胡哨的功能 增强用户体验,此时就需要在前端交互这个部分内容上下很大的功夫了。
既然目前【基础】系列的后端数据流程已经基本收尾完成。接下来所有的【基础】类教程大部分时间都会聚焦于如何处理和应对 GH特有的前端逻辑框架 —— 将自己想实现的功能在这个框架下搭建完成。
前段时间实在是太忙了,断更了好几个礼拜,接下来的一段时间工作上仍然会有很多事情,要详细地将GH的前端框架从细节开始讲起可能真得写到猴年马月才能写完整了,于是还是按照老方法,以需求为导向,一点一点地接近框架的底层。
今天这篇就先划个水,讲讲怎么做右键菜单的内容,逐步向着底层前进吧。
右键菜单的本质
GH本质上是一个 Windows Form 框架下的窗体应用程序,其右键菜单的原型是一个ToolStripDropDown
。之前有过 Windows Form 前端开发经验的话,对于GH的框架逻辑理解肯定可以更加地迅速。
不过这个ToolStripDropDown
并非是独立与每一个GH_Component
绑定的,而是统一绑定构造方法于GH画布,在右键点击的一瞬间为GH_Comoponent
构造的。至于为什么要这么设计,它与GH的前端框架实现有关,总而言之,要实现鼠标右键点击我们自定义的GH_Component
来弹出自己想要的菜单,还是得借助 override GH_Component
相关的函数才可以 —— 这些函数是负责构造ToolStripDropDown
的,这相当于重写ToolStripDropDown
的构造函数,于是就可以创作特定的菜单项。
制作右键菜单时,可以用到的重载项有下面几个:
AppendMenuItems
AppendAdditionalMenuItems
AppendAdditionalComponentMenuItems
其中,最短的那个是用来从头创建一个啥也没有的右键菜单;最长的那个是用来给GH_Component
类派生的类的默认右键菜单添加额外菜单项目的(原始的内容仍保留,比如Bake、Preview之类的右键菜单项);不长不短的是给其他非GH_Component
的子类的右键菜单添加菜单项时使用。
从头开始创建一个菜单项
如果不想要GH前端框架对于右键菜单的默认实现,第一步要做的就是override AppendMenuItems
这个函数,并且删除这个默认实现:
return base.AppendMenuItems(menu);
然后可以把自己想要的菜单项构建进去 —— 创建一个MenuItem
类,并添加到这个函数的传入的ToolStripDropDown
中去。当然,此时这个传入的ToolStripDropDown
还是一个新创建的没有任何内容的一个菜单。
添加如下代码
public override bool AppendMenuItems(ToolStripDropDown menu)
{
ToolStripMenuItem menuItem = new ToolStripMenuItem();
menuItem.Text = "Greetings, New Menu!";
menu.Items.Add(menuItem);
return true;
}
此时编译运行,这个电池就能弹出我们刚刚制作的菜单了:
但是此时点击它并没有什么反应。要实现点击能够执行某些功能,需要用到“Callback Function”,也就是“回调函数”的概念。
回调函数 (Callback Function)
回调函数牵扯到代理函数的概念,初理解起来挺难的,但是希望初学者可以反复琢磨下面的话,它们应该可以帮助从另一个角度和方面理解回调函数、代理、函数指针、事件等等等内容。
回调函数的执行与否并非由 写代码的人(我们) 来决定,而是在运行的时候由用户决定什么时候运行这个函数。
换而言之,回调函数在写好之后,我们不知道它什么时候运行,我们只管它怎么被执行。
从某种程度上来说,回调函数的调用与我们,也就是写代码的人,无关。
于是,“回调函数的执行者不是我们”。
回调函数由“代理者”执行。
回调函数是一个代理类型(delegate),为什么要有代理类型?
我们注意到,但凡是个函数,它必须得有输入/输出类型(尽管可能是void,但必须得有)。代理者最后仍然是要执行这个函数,也就是他必须负责提供输入和处理输出,于是问题就出现了:代理者不可能负责执行所有的函数,因为他没法处理所有的输入和输出。于是代理者想了个招:限定他能“帮忙执行”的函数的输入类型、输入参数个数、以及返回类型。
代理类型就是这三个定义的组合:(下面是一个代理类型定义的例子)
public delegate void MeowMeowMeow(int arg1, double arg2);
上述就是一个“代理类型”,它规定了这个函数必须要有:
- 两个输入,第一个输入是
int
,第二个输入是double
- 输出类型为
void
,也就是没有返回值
这个类型本身叫做MeowMeowMeow
让我们再次强调:代理类型的作用是什么?
因为“作为一个代理者,我没法处理所有的输入和输出组合,我必须限定我能代为处理的函数类型”
ToolStripMenuItem中的回调函数类型
当我们刚刚新鲜出炉制作的ToolStripMenuItem
—— 也就是“Greetings, New Menu”这个菜单项 —— 被用户点击的时候,这个ToolStripMenuItem
对象实例会“帮我们执行”我们写好的函数。
那么问题来了,这个ToolStripMenuItem
对象实例能帮我们执行啥样的函数呢?如果大家使用了一个有代码提示功能的编辑器(比如Visual Studio)的话,可以通过代码提示找到答案:
是EventHandler
类型。
但是这EventHandler
能接受几个参数,参数类型分别是什么,又是啥返回值?
在微软官方开发文档中可以找到答案:
https://docs.microsoft.com/en-us/dotnet/api/system.eventhandler?view=netframework-4.8
public delegate void EventHandler(object sender, EventArgs e);
原来如此,是一个object
、一个EventArgs
,返回为void
。
那么我们只需要写一个函数,第一个参数是object
,第二个参数是EventArgs
,返回为void
,这个函数就能够在用户被点击的时候执行了。
EazzzZZZZZZZ…Y。
public override bool AppendMenuItems(ToolStripDropDown menu)
{
ToolStripMenuItem menuItem = new ToolStripMenuItem();
menuItem.Text = "Greetings, New Menu!";
menuItem.Click += TheNameOfTheFunctionToBeDelegatedIsReallyNotImportant;
menu.Items.Add(menuItem);
return true;
}
void TheNameOfTheFunctionToBeDelegatedIsReallyNotImportant
(object argumentNameIsNotImportentEither, EventArgs butTheirOrderMatters)
{
RhinoApp.WriteLine("MenuItem was clicked.");
}
编译,运行,再点击我们刚刚的菜单项,Rhino主窗口就出现了下面函数中的那行字了。也就是这个函数被执行了。
“等一下!!!为什么menuItem.Click
右边是+=
?这啥?这个Click
属性是个event,event(事件)又是啥??”
event
/delegate
这对关键词是在做后台数据处理时几乎很少用到的,所以很多时候会让人摸不着头脑,那么,还是按刚刚代理者的逻辑:
- 代理者是要负责执行我们的函数的
- 代理者手头肯定不止一个函数需要执行
- 代理者一寻思,这我不整个
List
来存一存手头要干的活(要执行的函数)?
event
可以看做是一个存放代理函数的List
,而+=
就是向这个列表中添加函数,也就是告诉代理者“兄弟,这活儿接好了”。
所以,这里的menuItem
的Click
其实是一个函数的List
,用于存储它要负责执行的一系列函数。比如,我还可以再往这个列表里添加一个函数:
public override bool AppendMenuItems(ToolStripDropDown menu)
{
ToolStripMenuItem menuItem = new ToolStripMenuItem();
menuItem.Text = "Greetings, New Menu!";
menuItem.Click += TheNameOfTheFunctionToBeDelegatedIsReallyNotImportant;
menuItem.Click += YetAnotherFunction;
menu.Items.Add(menuItem);
return true;
}
void TheNameOfTheFunctionToBeDelegatedIsReallyNotImportant(object argumentNameIsNotImportentEither, EventArgs butTheirOrderMatters)
{
RhinoApp.WriteLine("MenuItem was clicked.");
}
void YetAnotherFunction(object aaaa, EventArgs bbbb)
{
RhinoApp.WriteLine("Clicked, clicked.");
}
编译运行,执行的时候就会出现两行打印信息,分别来自于这两个函数。
然后……匿名函数也是函数啊!匿名函数也要代理执行!!!没问题:
public override bool AppendMenuItems(ToolStripDropDown menu)
{
ToolStripMenuItem menuItem = new ToolStripMenuItem();
menuItem.Text = "Greetings, New Menu!";
menuItem.Click += TheNameOfTheFunctionToBeDelegatedIsReallyNotImportant;
menuItem.Click += YetAnotherFunction;
menuItem.Click += new EventHandler(
(o, e) =>
{
RhinoApp.WriteLine("Feel the power!!!");
});
menu.Items.Add(menuItem);
return true;
}
void TheNameOfTheFunctionToBeDelegatedIsReallyNotImportant(object argumentNameIsNotImportentEither, EventArgs butTheirOrderMatters)
{
RhinoApp.WriteLine("MenuItem was clicked.");
}
void YetAnotherFunction(object aaaa, EventArgs bbbb)
{
RhinoApp.WriteLine("Clicked, clicked.");
}
妙啊!!!
菜单应用举例
那…… 干点啥呢?做个常用的小勾勾吧,比如我们常见的菜单选项可以实现点一下勾选,点一下取消勾选的功能:
- 在电池类中做一个属性,类型为
bool
,选中为true
,未选中为false
bool menuItemChecked = false;
public override bool AppendMenuItems(ToolStripDropDown menu)
{
ToolStripMenuItem menuItem = new ToolStripMenuItem();
menuItem.Text = "On/Off Setting";
menuItem.Checked = menuItemChecked;
menuItem.Click += new EventHandler((o, e) => menuItemChecked = !menuItemChecked);
menu.Items.Add(menuItem);
return true;
}
编译执行结果为:
在这个基础上,后端数据处理判断一下这个 menuItemChecked
的状态就可以确定不同的数据逻辑流程。
选项开关?比如只有在某些状态下,菜单选项才可用:
bool menuItemChecked = false;
public override bool AppendMenuItems(ToolStripDropDown menu)
{
ToolStripMenuItem menuItem = new ToolStripMenuItem();
menuItem.Text = "On/Off Setting";
menuItem.Checked = menuItemChecked;
menuItem.Click += new EventHandler((o, e) => menuItemChecked = !menuItemChecked);
ToolStripMenuItem item2 = new ToolStripMenuItem();
item2.Text = "Only available when on";
item2.Enabled = menuItemChecked;
if (menuItemChecked)
{
item2.Click += new EventHandler(
(o, e) =>
{
var cparam = new Param_Circle
{
Name = "ABC",
NickName = "BC",
Description = "n/a",
Access = GH_ParamAccess.item
};
Params.RegisterInputParam(cparam);
Params.OnParametersChanged();
ExpireSolution(true);
});
}
menu.Items.Add(menuItem);
menu.Items.Add(item2);
return true;
}
更多的应用在这里就不展开了。
添加额外菜单项至默认菜单
很多时候我们还是希望保留原来的GH_Component
菜单的,毕竟类似于 Bake、Enable 这些功能还是十分实用的。如果重写所有菜单项可能有些麻烦,于是我们就可以采用重写AppendAdditionalComponentMenuItems
的方法来“额外添加”菜单项:
protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
ToolStripMenuItem item0 = new ToolStripMenuItem();
item0.Text = "Hi, Grasshopper";
item0.Image = Resources.dice.GetThumbnailImage(25, 25, null, IntPtr.Zero); // 自定义的图片, Bitmap类型转Image
menu.Items.Add(item0);
}
除了自己创建ToolStripMenuItem
实例以外,GH的前端框架也为我们提供了一些很方便的函数,可以做到一行代码添加菜单项:
protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
Menu_AppendItem(menu, "One-liner", // 名字
(o, e) => { RhinoApp.WriteLine("Callback!!"); }, // 回调函数
Resources.LayerIcon.GetThumbnailImage(25, 25, null, IntPtr.Zero)); // 自定义图片,Bitmap类型转Image
}
另外,如果对 Windows Form 熟悉的话,还能做一个自己的Control
类,然后扔到右键菜单里去:(这里MyControl
类就不把代码贴过来了,自己在VS里用编辑器创建一个自定义Control类就行)
protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
var mctl = new MyControl(); // 实例化自己的Control类
Menu_AppendCustomItem(menu, mctl); // 加到菜单里去
}
啊这???这都行!???可算知道那些蛇皮右键菜单是怎么做出来的了!
是的,这都行。赶紧尝试玩出花来吧!感觉这次更完又可以歇一段时间了,嘿嘿嘿嘿……
最近有小伙伴提到如何做二级菜单的问题,其实二级菜单就是在一级菜单的
Items
再添加新的ToolStripMenuItem
实例。下面就是一个二级菜单的例子:protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu) { ToolStripMenuItem first_level_menu = Menu_AppendItem(menu, "一级菜单"); first_level_menu.DropDownItems.Add(new ToolStripMenuItem("二级菜单")); first_level_menu.DropDownItems.Add( new ToolStripControlHost( new TextBox())); }
在上面的例子中,
first_level_menu
是一个一级菜单,通过往一级菜单的DropDownItems
中添加别的ToolstripItem
类实例就可以实现增加二级菜单。我们添加了两个菜单项到这个一级菜单中。三级菜单、四级菜单也是同理,直接套娃就完事儿了。
ToolStripControlHost
类可以实现在菜单中添加任意Control
,在上面的例子里,我们是添加了一个TextBox
类,同样也可换成其他的类。
有问题欢迎多多交流鸭。
🦀