【Grasshopper基础15】“右键菜单似乎不太对劲”

这篇具有很好参考价值的文章主要介绍了【Grasshopper基础15】“右键菜单似乎不太对劲”。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

距离上一篇文章已经过去了挺久的,很长时间没有写GH基础部分的内容了,原因其一是本职工作太忙了,进度也有些落后,白天工作累成马,回家只想躺着;其二则是感觉GH基础系列基本上也介绍得差不多了,电池二次开发的一些基本操作(功能/外观)都介绍得差不多了,再加上前几期写的数据类型,这基本上就囊括了所有二次开发需要用到的内容。

不过,理论知识和实践总归是有一些差距的,在CSDN上还是会偶尔收到私信问一些细节问题的二开爱好者们。这些问题确实是做电池二次开发的时候遇到的,但它们本身可能与电池的二次开发没有关系:其中有一部分是C#代码本身的编程逻辑问题,还有一部分是有关于Rhino的SDK的问题,另外还有一些关于Windows Form、WPF等前端框架的问题。有些问题会被反复地问到,所以笔者决定还是多多将大家遇到的有共性的问题也做一系列解答,方便读者在还没有遇到这些类似的问题的时候,能够有那么一点点印象,当真正碰到这些问题的时候,能够找对解决问题的方向,少走一些弯路。

这篇文章要讲的问题是有关于右键菜单的菜单项的回调函数的问题,这个问题的根源是来自
C#代码编程本身,也是十分具有迷惑性,相信没有完整看过C#基础知识直接上手二开的爱好者们在第一次遇到这个问题的时候肯定十分地困惑。下面就来看具体问题吧。

近期经常收到一个问题 —— “为什么我添加的右键菜单项有Bug?” “我用了一个for循环去添加菜单项,想一次性添加x个菜单项,并在菜单被点击的时候执行 xxxx,但是结果总是不变,而且不对,这是不是GH出Bug了?”

相信有不少二开的小伙伴会做这样的一个需求:需要一个电池,这个电池需要依照情况输出若干个确定的值,具体输出哪个值需要用右键菜单来指定。类似于 ValueList 电池那样可以通过选择来输出若干个指定值其中的一个。

【Grasshopper基础15】“右键菜单似乎不太对劲”,Grasshopper开发基础,C#,c#,Grasshopper,右键菜单,匿名函数,延时绑定

【Grasshopper基础15】“右键菜单似乎不太对劲”,Grasshopper开发基础,C#,c#,Grasshopper,右键菜单,匿名函数,延时绑定

要实现这个功能,最简单直观的就是在电池中加入一个属性叫 ComponentPropertyValue,然后在右键菜单中改变它,并调用 ExpireSolution,同时,SolveInstance 函数中依照这个属性来赋值:

private int ComponentPropertyValue { get; set; }

protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
    menu.Items.Add(new ToolStripMenuItem("1", null, 
        (o, e) => { ComponentPropertyValue = 1; this.ExpireSolution(true); }));

    menu.Items.Add(new ToolStripMenuItem("2", null, 
        (o, e) => { ComponentPropertyValue = 2; this.ExpireSolution(true); }));

    menu.Items.Add(new ToolStripMenuItem("3", null, 
        (o, e) => { ComponentPropertyValue = 3; this.ExpireSolution(true); }));

    menu.Items.Add(new ToolStripMenuItem("4", null, 
        (o, e) => { ComponentPropertyValue = 4; this.ExpireSolution(true); }));

    menu.Items.Add(new ToolStripMenuItem("5", null, 
        (o, e) => { ComponentPropertyValue = 5; this.ExpireSolution(true); }));
}

protected override void SolveInstance(IGH_DataAccess DA)
{
    // 这里为了举例方便设置为该数值的平方
    // 实际可能会有较为复杂的运算逻辑
    DA.SetData(0, ComponentPropertyValue * ComponentPropertyValue);
}

显然,作为一个写过一段时间代码的正常人,应该能想到使用一个 for 循环来改写函数 AppendAdditionalComponentMenuItems 中的代码:

protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
    for (var i = 1; i < 6; i++)
    {
        // 将对应的列表项的文字和赋值语句换成 i 即可
        menu.Items.Add(new ToolStripMenuItem($"{i}", null, 
            (o, e) => { ComponentPropertyValue = i; this.ExpireSolution(true); }));
    }
}

但是这个时候运行代码就会出现一个现象,无论选哪个,最后出来的结果都会是36。

【Grasshopper基础15】“右键菜单似乎不太对劲”,Grasshopper开发基础,C#,c#,Grasshopper,右键菜单,匿名函数,延时绑定

?????

“这GH是出Bug了!”

其实不然,即便是一个控制台应用程序,下面这段代码也会只输出一个值:

static void Main() 
{
    var list = new List<Action>();
    for (var x = 0; x < 10; x++)
    {
        list.Add(() => Console.WriteLine(x));
    }
    foreach (var action in list)
    {
        action();
    }
}

【Grasshopper基础15】“右键菜单似乎不太对劲”,Grasshopper开发基础,C#,c#,Grasshopper,右键菜单,匿名函数,延时绑定

甚至,在广为人知的另一门编程语言 Python 中,以及其他许多编程语言中,都会有这种情况。(在 Python 中,这种现象称之为“闭包延时绑定”,可自行搜索Python延时绑定关键词来查询相关底层知识)

我们先说怎么解决这个问题,再来谈这个问题是什么原因导致的。


如何解决

解决的方法很简单,只需要额外增加一个局部变量即可:

protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
    for (var i = 1; i < 6; i++)
    {
        var j = i; // 增加一个额外的变量j,令其值等于i,然后在lambda函数中使用j即可
        menu.Items.Add(new ToolStripMenuItem($"{j}", null,
            (o, e) => { ComponentPropertyValue = j; this.ExpireSolution(true); }));
    }
}

简而言之,就是在 for 循环内部作用域,创建一个额外的临时变量(上例中的j),令其等于循环控制变量(上例中的i),然后在循环内部作用域使用这个额外的临时变量即可。

笔者提示:此外,如果循环控制变量(上例中的i)是引用类型(不是int/double/long等值类型),这个循环内部的额外临时变量则需要使用复制构造来创建新实例 —— 虽然很少出现使用非 int 类型作为循环控制变量

这样一来,这个电池的工作就正常了:

【Grasshopper基础15】“右键菜单似乎不太对劲”,Grasshopper开发基础,C#,c#,Grasshopper,右键菜单,匿名函数,延时绑定

为什么会是这样的

细心的读者已经发现了,在上面的例子中,我们都使用了 匿名函数。没错,问题就是出在 匿名函数 中。

匿名函数写起来十分方便,但其实在它简单的语法背后,编译器为我们做了许多额外的事情。其中之一就是对其中的变量做 “变量捕获 (Captures)”。

变量捕获描述的是这样一个过程:

对于匿名函数的函数体中使用到的不存在于函数输入参数的变量,匿名函数会捕获该变量的引用。在随后匿名函数被调用时,被捕获的变量的值将会是函数调用这一瞬间的值,而非匿名函数构造时的值。

上面两句话阐述了两个问题:

  • 什么样的变量会被捕获
  • 被捕获变量的行为是什么

下面看一个例子:

var x = 10;
Func<int, int> lambda = (int input) => input * x;
x += 10;
var result = lambda(5);
Console.WriteLine(result);

我们使用 Visual Studio 中的 C# Interactive 来执行上面的代码,可以看到,lambda(5) 的结果是100,而不是50。

【Grasshopper基础15】“右键菜单似乎不太对劲”,Grasshopper开发基础,C#,c#,Grasshopper,右键菜单,匿名函数,延时绑定

  • 匿名函数是: (int input) => input * x
  • 匿名函数的输入变量是 input
  • 匿名函数体是 input * x

匿名函数体中包含了两个变量,inputx。因为input是匿名函数的输入变量,所以它不是被捕获的变量。x不是匿名函数的输入变量,所以它将会被匿名函数捕获。

在我们使用lambda(5)调用匿名函数时,被捕获变量x的值是匿名函数函数调用时的值(20,因为在调用前我们使用x += 10改变了x),而非匿名函数被定义的时候的值(10)。因此,最后的结果是 5 * 20 = 100

通过这个例子,我们可以看出:

匿名函数中的被捕获的变量的值会是匿名函数被调用时的值,而非匿名函数构造时的值。

因此,在的Grasshopper电池菜单项的问题上,我们构造菜单项时,是嵌套在 for 循环中,构造匿名函数时,由于循环变量i并不是匿名函数的输入参数,所以它将会被捕获!我们通过 for 循环构造了5个菜单项,但他们的回调函数捕获的是同一个循环变量 i

protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
{
    for (var i = 1; i < 6; i++)
    {
        menu.Items.Add(new ToolStripMenuItem($"{i}", null, 
            (o, e) => { ComponentPropertyValue = i; this.ExpireSolution(true); }));
    }
}

进一步的,在菜单被点击的时候,回调函数被触发,此时匿名函数内的i的值会是匿名函数被调用时候的值(此时,构造菜单项的 for 循环早已完成,因此循环变量停留在了最后一次 for循环的值6)。这也是为什么我们在之前出现,任何一个菜单项点击都是6的结果的原因。

老规矩,上代码

using System;
using System.Windows.Forms;

using Grasshopper.Kernel;

namespace GrasshopperPluginExample01
{
    public class ProvideValues : GH_Component
    {
        public ProvideValues() : base("ProvideValues", "Val",
              "ProvideValues",
              "Params", "DigitalCrab")
        {
        }
        private int ComponentPropertyValue;
        protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager) { }
        protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
        {
            pManager.AddIntegerParameter("Out", "O", "output value", GH_ParamAccess.item);
        }
        protected override void SolveInstance(IGH_DataAccess DA)
        {
            DA.SetData(0, ComponentPropertyValue * ComponentPropertyValue);
        }
        protected override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu)
        {
            //menu.Items.Add(new ToolStripMenuItem("1", null, (o, e) => { ComponentPropertyValue = 1; this.ExpireSolution(true); }));
            //menu.Items.Add(new ToolStripMenuItem("2", null, (o, e) => { ComponentPropertyValue = 2; this.ExpireSolution(true); }));
            //menu.Items.Add(new ToolStripMenuItem("3", null, (o, e) => { ComponentPropertyValue = 3; this.ExpireSolution(true); }));
            //menu.Items.Add(new ToolStripMenuItem("4", null, (o, e) => { ComponentPropertyValue = 4; this.ExpireSolution(true); }));
            //menu.Items.Add(new ToolStripMenuItem("5", null, (o, e) => { ComponentPropertyValue = 5; this.ExpireSolution(true); }));

            for (var i = 1; i < 6; ++i)
            {
                var j = i;
                menu.Items.Add(new ToolStripMenuItem($"{j}", null, (o, e) => { ComponentPropertyValue = j; this.ExpireSolution(true); }));
            }
        }
        protected override System.Drawing.Bitmap Icon => null;
        public override Guid ComponentGuid => new("7805627F-6422-457D-969D-C5E19B124D87");
    }
}

下次再见 🦀文章来源地址https://www.toymoban.com/news/detail-682093.html

到了这里,关于【Grasshopper基础15】“右键菜单似乎不太对劲”的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Qt创建右键菜单的两种通用方法(QTableView实现右键菜单,含源码+注释)

    下图为两种右键菜单实现的示例图,源码在本文第三节(源码含详细注释)。 提示:不会使用Qt设计师设计界面的小伙伴点击这里。 该方法是触发contextMenuEvent事件来实现右键菜单,只需在该事件函数中写入对应的右键菜单代码即可。 该方法是通过控件发出的customContextMenuR

    2024年02月15日
    浏览(50)
  • 2023年完美解决:windows 11/win 11使用经典右键菜单(win10版右键菜单)

    下载安装会吧:https://www.autohotkey.com 1. 桌面新建一个txt,就是文本文档。然后把以下代码复制到里面去。 解释一下: #IfWinActive ahk_exe explorer.exe 如果资源管理器(explorer.exe)处于激活状态 RButton:: Send {LShift down}{RButton}{LShift up} 点击鼠标右键时,发送快捷键:shift + 右键 2、改名

    2024年02月06日
    浏览(89)
  • 【Windows】Win11右键恢复完整右键菜单

    在 Windows 11 中,微软针对右键菜单进行了精简,只显示了一些常用操作,而将一些不常用的选项隐藏了起来,这使得有些用户可能会感到不方便。新的右键快捷菜单,就不少朋友表示接受不了。 想象一下一个有十个人工作的工作场所,每个电脑用户每天点击桌面100次,每次点

    2024年02月06日
    浏览(72)
  • QTreewidget右键菜单功能实现

    QTreewidget有一个信号继承自QWidget的信号void QWidget::customContextMenuRequested(const QPoint pos);我们来看看官方介绍: 简单翻译一下:当widget的 contextMenuPolicy即上下文菜单属性是 Qt::CustomContextMenu,并且用户已request widget上的上下文菜单时(也就是点了右键),会发出此信号。位置 pos 是wi

    2024年02月16日
    浏览(34)
  • Windows 右键菜单扩展容器 [开源]

    今天给大家分享一个我做的小工具,可以自定义扩展右键菜单的功能来提高工作效率,效果图如下: 如上图,右键菜单多了几个我自定义的菜单: 复制文件路径 复制文件夹路径 我的工具箱 走配置文件动态创建子菜单,下面会讲 我上图是在 win10 操作系统下演示的,在 win1

    2024年02月02日
    浏览(97)
  • win11右键新建菜单添加选项

    需要操作 2 处注册表, 以下以在右键新建菜单中添加 .html 为例 在主键 HKEY_CLASSES_ROOT 中,搜索 .html 找到后 ,右键点击它,选 新建 - 项 , 在这里插入图片描述 项目名字是: ShellNew 新建后,点击它,并在其右侧窗格中,新建 字符串值 ,名字是 FileName 2. HKEY_CURRENT_USERSoftwar

    2024年02月05日
    浏览(73)
  • C# textBox 右键菜单 contextMenuStrip

    需求: 想在上图空白处可以右键弹出菜单,该怎么做呢? 1.首先,拖出一个 ContextMenuStrip。  随便放哪里都行,如下:  2.在textBox里关联这个“右键控件”即可,如下:  最终效果如下: 以上 参考:C# 右键菜单 contextMenuStrip_camellias_的博客-CSDN博客 3.添加响应代码: 如图,双击

    2024年02月11日
    浏览(35)
  • 离线 notepad++ 添加到右键菜单

    复制下面代码,修改文件后缀名为: reg

    2024年02月07日
    浏览(37)
  • git bash右键菜单失效解决方法

    git bash右键菜单失效解决方法 这几天重新更新了git,直接安装新版本后,右键菜单失效找不到了。找了好几个博客,发现都不全面,最后总结一下解决方法: (1)按win+r,输入regedit打开注册表,然后分别修改4个位置路径,修改为最后安装的软件的路径。 (2).修改如下四处

    2024年01月24日
    浏览(43)
  • windows 删除+增加右键新建菜单选项

    目录 一、删除右键新建菜单选项 1. win + R 打开注册表 2. 查看现有的右键新建菜单选项 3. 删除现有的右键新建菜单选项 二、增加右键新建菜单选项   1. win + R 打开注册表 键盘 win + R,输入 regedit,从而打开注册表。 2. 查看现有的右键新建菜单选项 如果你不清楚你的右键新建

    2024年02月06日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包