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

news2025/5/17 6:33:27

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

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

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

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

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

Snipaste_2023-08-29_15-18-27

Snipaste_2023-08-29_15-30-28

要实现这个功能,最简单直观的就是在电池中加入一个属性叫 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。

Rhino_6mowQGoDy5

?????

“这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();
    }
}

Snipaste_2023-08-29_16-09-17

甚至,在广为人知的另一门编程语言 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 类型作为循环控制变量

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

Rhino_pbpZsxoWW4

为什么会是这样的

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

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

Snipaste_2023-08-29_16-38-22

  • 匿名函数是: (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");
    }
}

下次再见 🦀

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/944907.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

保护网站安全:学习蓝莲花的安装和使用,复现跨站脚本攻击漏洞及XSS接收平台

这篇文章旨在用于网络安全学习&#xff0c;请勿进行任何非法行为&#xff0c;否则后果自负。 环境准备 一、XSS基础 1、反射型XSS 攻击介绍 原理 攻击者通过向目标网站提交包含恶意脚本的请求&#xff0c;然后将该恶意脚本注入到响应页面中&#xff0c;使其他用户在查看…

如何建设数据资产目录?

数据资产目录其实一个记录和管理组织内各种数据资产的清单或目录。它提供了对数据资产的基本信息和属性的可视化和查询能力&#xff0c;让组织能够更好的了解和利用自己拥有的数据资产。数据资产目录通常包括数据集的名称、描述、来源、格式、存储位置、更新频率等信息&#xf…

fatal: ServicePointManager 不支持具有 socks5 方案的代理。

报错 解决前 git config --global --list 查看git的设置 解决后 // 代理更改为http (7890是我的代理软件clash的port默认的&#xff0c;有些博客使用的是1080&#xff0c;依个人情况而定) git config --global http.proxy http://127.0.0.1:7890 git config --global https…

40、Thymeleaf的自动配置和基本语法、springboot 整合 Thymeleaf

★ Spring Boot支持如下模板技术&#xff1a; FreeMarkerGroovyThymeleafMustache官方推荐使用 ThymeleafJSP不再被推荐。★ Thymeleaf的优势 Thymeleaf标准方言中的大多数处理器都是属性处理器。这种页面模版即使在未被处理之前&#xff0c;浏览器也可正确地显示HTML模板文件…

听力总结易错点+口语准则

目录 听力总结易错点 where are you come from&#xff1f;其实是错的 杯子的大小表达 口语准则 一些常见蔬菜的英文名称&#xff1a; To get a lot out of 英语复述句子题 听力总结易错点 1,在section 1 &#xff0c;很容易把a读成n 2. silver colored cloth 这个clo…

Spring PropertyEditor 使用案例

1. 继承 PropertyEditorSupport 重写 setAsText 方法 import java.beans.PropertyEditorSupport;public class StringToArrayEditor extends PropertyEditorSupport {Overridepublic void setAsText(String text) throws IllegalArgumentException {//接收Text的值并处理String…

Centos7本地安装Docker-compose

考虑github时常出现问题。以下内容是基于本地安装 安装包地址 文章参考链接 1、下载安装包上传包到/usr/local/bin 2、执行命令 # 修改权限 chmod x /usr/local/bin/docker-compose ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose3、验证是否安装成功 docker-…

内网环境搭建-前篇

通常所说的内网渗透测试&#xff0c;很大程度上就是域渗透测试。搭建域渗透测试环境&#xff0c;在Windows的活动目录环境下进行一系列操作&#xff0c;掌握其操作方法和运行机制&#xff0c;对内网的安全维护有很大的帮助。常见的域环境是使用Windows Server2012 R2、Windows7…

基于SpringBoot+Vue的大学生家教系统设计和实现

前言 &#x1f497;全网粉丝10W、全栈领域优质创作者、掘金、阿里云等社区博客专家、专注于全栈领域和毕业项目实战&#x1f497; 文章最下面有微信&#xff0c;获取完整源码和数据库&#xff0c;没有套路&#xff0c;没有套路&#xff0c;没有套路&#xff01;&#xff01;&am…

Java JDK各版本特性

一、Java 8 之前版本重要的特性 Java 8 之前版本中有一些重要特性和改进。以下是其中一些主要特性以及它们发布版本&#xff1a; Java SE 7&#xff08;发布于2011年&#xff09;&#xff1a; Switch 字符串&#xff1a;允许在 switch 语句中使用字符串。泛型的类型推断&…

如何提高视频清晰度?视频调整清晰度操作方法

现在很多小伙伴通过制作短视频发布到一些短视频平台上记录生活&#xff0c;分享趣事。但制作的视频有些比较模糊&#xff0c;做视频的小伙伴应该都知道&#xff0c;视频画质模糊不清&#xff0c;会严重影响观众的观看体验。 通过研究&#xff0c;总结了以下几点严重影响的点 …

Opencv-C++笔记 (18) : 轮廓和凸包

文章目录 一、轮廓findContours发现轮廓drawContours绘制轮廓代码 二.几何及特性概括——凸包(Convex Hull)凸包概念凸包扫描算法介绍——Graham扫描算法 相关API介绍程序示例轮廓集合及特性性概括——轮廓周围绘制矩形框和圆形相关理论介绍轮廓周围绘制矩形 -API绘制步骤程序实…

如何使用ADX指标呢?10秒教会你

这是使用ADX大佬的收益结果&#xff0c;这是没有使用ADX技术指标的新手表情&#xff0c;事实证明只要会使用ADX指标&#xff0c;交易的结果就是令人可喜的&#xff0c;那么如何使用ADX指标呢?anzo capital昂首资本10秒教会你。 从评估价格方向、模式和水平开始技术分析。使用…

BingChat与ChatGPT比较,哪个聊天机器人能让你获益更多?

人工智能领域的最新进展为普通人创造新的收入来源提供了更多机会。今年早些时候&#xff0c;微软对OpenAI进行了大量投资。此后&#xff0c;微软在Microsoft Edge浏览器中推出了自家的聊天机器人Bing Chat。 在论坛和社交媒体上&#xff0c;你可以发现这两个AI工具都吸引了很…

C++之ifstream成员函数get、tellg、eof实例(一百八十五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

js查找结构不同的两个数组中相同的元素并删除元素

准确的是循环中删除数组元素会遇到的bug及解决办法。 删除后需要注意元素的索引值 &#xff0c;比如以下案例&#xff0c;删除2之后索引值继续 1,但原数组索引已经变化了&#xff0c;所以会出现遍历漏掉元素和索引值对不上的情况 然后就把forEach循环改成了for循环&#xff0…

龙蜥白皮书精选:SysAK—大规模复杂场景的系统运维利器

文/系统运维 SIG 01 概述 SysAK&#xff08;System Analyse Kit&#xff09;是龙蜥社区系统运维 SIG&#xff0c;通过对过往百万服务器运维经验进行抽象总结&#xff0c;而提供的一个全方位的系统运维工具集&#xff0c;可以覆盖系统的日常监控、线上问题诊断和系统故障修复…

Java 中数据结构LinkedList的用法

LinkList 链表&#xff08;Linked list&#xff09;是一种常见的基础数据结构&#xff0c;是一种线性表&#xff0c;但是并不会按线性的顺序存储数据&#xff0c;而是在每一个节点里存到下一个节点的地址。 链表可分为单向链表和双向链表。 一个单向链表包含两个值: 当前节点…

MySQL 日期格式 DATETIME 和 TIMESTAMP

MySQL日期格式介绍 存储日期的方式mysql中存储日期的格式datetimetimestampDatetime和Timestamp的比较相同点&#xff1a;不同点&#xff1a; 数值型时间戳&#xff08;INT&#xff09;DATETIME vs TIMESTAMP vs INT&#xff0c;怎么选&#xff1f; 存储日期的方式 字符串Date…