【Grasshopper基础9】添加右键菜单

其实经过【基础2】~【基础6】、以及【基础8】的内容,几乎所有插件的后台数据处理流程都可以实现了(往往也是最关键的业务核心内容):

  • RegisterInputParamRegisterOutputParam中注册数据的入口和出口
  • SolveInstance中处理及传递数据
  • ReadWrite中进行数据的序列化与反序列化

这也是为什么很多时候,目前大部分的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;
}

此时编译运行,这个电池就能弹出我们刚刚制作的菜单了:

WeChat Screenshot_20210331153624

但是此时点击它并没有什么反应。要实现点击能够执行某些功能,需要用到“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)的话,可以通过代码提示找到答案:

WeChat Screenshot_20210331160049

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,而+=就是向这个列表中添加函数,也就是告诉代理者“兄弟,这活儿接好了”。

所以,这里的menuItemClick其实是一个函数的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;
}

编译执行结果为:

Record_2021_03_31_16_47_14_449

在这个基础上,后端数据处理判断一下这个 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;
}

Record_2021_03_31_17_23_44_913

更多的应用在这里就不展开了。

添加额外菜单项至默认菜单

很多时候我们还是希望保留原来的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);
}

WeChat Screenshot_20210331173337

除了自己创建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
}

Record_2021_03_31_17_39_42_68

另外,如果对 Windows Form 熟悉的话,还能做一个自己的Control类,然后扔到右键菜单里去:(这里MyControl类就不把代码贴过来了,自己在VS里用编辑器创建一个自定义Control类就行)

protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
    var mctl = new MyControl(); // 实例化自己的Control类
    Menu_AppendCustomItem(menu, mctl);  // 加到菜单里去
}

Record_2021_03_31_17_49_48_333

啊这???这都行!???可算知道那些蛇皮右键菜单是怎么做出来的了!

是的,这都行。赶紧尝试玩出花来吧!感觉这次更完又可以歇一段时间了,嘿嘿嘿嘿……

最近有小伙伴提到如何做二级菜单的问题,其实二级菜单就是在一级菜单的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类,同样也可换成其他的类。

有问题欢迎多多交流鸭。

🦀

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值