【Grasshopper基础15】“右键菜单似乎不太对劲”
距离上一篇文章已经过去了挺久的,很长时间没有写GH基础部分的内容了,原因其一是本职工作太忙了,进度也有些落后,白天工作累成马,回家只想躺着;其二则是感觉GH基础系列基本上也介绍得差不多了,电池二次开发的一些基本操作(功能/外观)都介绍得差不多了,再加上前几期写的数据类型,这基本上就囊括了所有二次开发需要用到的内容。
不过,理论知识和实践总归是有一些差距的,在CSDN上还是会偶尔收到私信问一些细节问题的二开爱好者们。这些问题确实是做电池二次开发的时候遇到的,但它们本身可能与电池的二次开发没有关系:其中有一部分是C#代码本身的编程逻辑问题,还有一部分是有关于Rhino的SDK的问题,另外还有一些关于Windows Form、WPF等前端框架的问题。有些问题会被反复地问到,所以笔者决定还是多多将大家遇到的有共性的问题也做一系列解答,方便读者在还没有遇到这些类似的问题的时候,能够有那么一点点印象,当真正碰到这些问题的时候,能够找对解决问题的方向,少走一些弯路。
这篇文章要讲的问题是有关于右键菜单的菜单项的回调函数的问题,这个问题的根源是来自
C#代码编程本身,也是十分具有迷惑性,相信没有完整看过C#基础知识直接上手二开的爱好者们在第一次遇到这个问题的时候肯定十分地困惑。下面就来看具体问题吧。
近期经常收到一个问题 —— “为什么我添加的右键菜单项有Bug?” “我用了一个for循环去添加菜单项,想一次性添加x个菜单项,并在菜单被点击的时候执行 xxxx,但是结果总是不变,而且不对,这是不是GH出Bug了?”
相信有不少二开的小伙伴会做这样的一个需求:需要一个电池,这个电池需要依照情况输出若干个确定的值,具体输出哪个值需要用右键菜单来指定。类似于 ValueList 电池那样可以通过选择来输出若干个指定值其中的一个。


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

甚至,在广为人知的另一门编程语言 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类型作为循环控制变量
这样一来,这个电池的工作就正常了:

为什么会是这样的
细心的读者已经发现了,在上面的例子中,我们都使用了 匿名函数。没错,问题就是出在 匿名函数 中。
匿名函数写起来十分方便,但其实在它简单的语法背后,编译器为我们做了许多额外的事情。其中之一就是对其中的变量做 “变量捕获 (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。

- 匿名函数是:
(int input) => input * x - 匿名函数的输入变量是
input - 匿名函数体是
input * x
匿名函数体中包含了两个变量,input和x。因为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");}
}
下次再见 🦀
相关文章:
【Grasshopper基础15】“右键菜单似乎不太对劲”
距离上一篇文章已经过去了挺久的,很长时间没有写GH基础部分的内容了,原因其一是本职工作太忙了,进度也有些落后,白天工作累成马,回家只想躺着;其二则是感觉GH基础系列基本上也介绍得差不多了,电…...
华为Mate60低调发布,你所不知道的高调真相?
华为Mate60 pro 这两天的劲爆新闻想必各位早已知晓,那就是华为Mate60真的来了!!!并且此款手机搭载了最新国产麒麟9000s芯片,该芯片重新定义了手机性能的巅峰。不仅在Geekbench测试中表现出色,还在实际应用…...
C++(18):命名空间
多个库将名字放置在全局命名空间中将引发命名空间污染。 命名空间可以用来防止名字冲突,它分割了全局命名空间,其中每个命名空间是一个作用域。通过在某个命名空间中定义库的名字,库的作者(以及用户)可以避免全局名字…...
K8S最新版本集群部署(v1.28) + 容器引擎Docker部署(上)
温故知新 📚第一章 前言📗背景📗目的📗总体方向 📚第二章 基本环境信息📗机器信息📗软件信息📗部署用户kubernetes 📚第三章 Kubernetes各组件部署📗安装kube…...
生产环境部署与协同开发 Git
目录 一、前言——Git概述 1.1 Git是什么 1.2 为什么要使用Git 什么是版本控制系统 1.3 Git和SVN对比 SVN集中式 Git分布式 1.4 Git工作流程 四个工作区域 工作流程 1.5 Git下载安装 1.6 环境配置 设置用户信息 查看配置信息 二、git基础 2.1 本地初始化仓库 编辑…...
Qt/C++编写视频监控系统80-远程回放视频流
一、前言 远程回放NVR或者服务器上的视频文件,一般有三种方式,第一种是调用厂家的SDK,这个功能最全,但是缺点明显就是每个厂家的设备都有自己的SDK,只兼容自家的设备,如果你的软件需要接入多个厂家的&…...
用于设计和分析具有恒定近心点半径的低推力螺旋轨迹研究(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
MongoDB - 构造复杂查询条件执行查询
文章目录 1. 构造 keyword 的查询条件2. 构造 threatSubType 的查询条件3. 相应的实体类 /*** 查询白名单详情** param offset 第几页开始* param limit 每页显示的最大值* param keyword 模糊搜索值* param order 排序方式(升序/降序…...
如何从ChatGPT中获得最佳聊天对话效果
从了解ChatGPT工作原理开始,然后从互动中学习,这是一位AI研究员的建议。 人们利用ChatGPT来撰写文章、论文、生成文案和计算机代码,或者仅仅作为学习或研究工具。然而,大多数人不了解它的工作原理或它能做什么,所以他…...
深入浅出:手把手教你实现单链表
一、什么是链表 链表是一种链状数据结构。简单来说,要存储的数据在内存中分别独立存放,它们之间通过某种方式相互关联。 如果我们使用C语言来实现链表,需要声明一个结构体作为链表的结点,结点之间使用指针关联。 二、单向链表的结…...
vite 打包项目后访问显示空白页的问题,开发环境正常,生产环境无报错。
有没有可能, 你跟我遇到同样的问题 白屏的写法 const routes [{path: /,component: import(../views/index.vue),} ]正确的写法 const routes [{path: /,component: () > import(../views/index.vue),} ]有时候方向很重要,当在错误的方向上无脑冲…...
打造成功的砍价营销大解析,销量飙升
砍价活动是吸引顾客的一种有效方式,可以帮助提高销量和提升品牌知名度。在乔拓云平台上,我们提供了一套简单易用的工具,让您能够轻松地制作一个成功的砍价活动。下面,我将详细介绍具体步骤,让您能够轻松上手。 第一步&…...
【Flink进阶】- Flink kubernetes operator 常用的命令
目录 1、应用程序管理 (1)提交 Flink 应用程序 (2)查看 Flink 应用程序列表...
ASP.NET Core 的日志系统
ASP.NET Core 提供了丰富日志系统。 可以通过多种途径输出日志,以满足不同的场景,内置的几个日志系统包括: Console,输出到控制台,用于调试,在产品环境可能会影响性能。Debug,输出到 System.Di…...
android13(T) 以太网设置工具类
13 版本的以太网设置和以前版本有所变动,在 AS 中就能直接调用对应 API 将 build.gradle 版本修改 compileSdkVersion 31, 即可直接调用 EthernetManager 相关, 设置静态等方法可以通过反射调用设置。 以下是核心设置静态和动态参数工具类,…...
电脑报错提示xinput1_3.dll缺失怎么办?xinput1_3.dll丢失的简单恢复方案
今天,我将为大家分享一个与我们日常工作息息相关的话题——xinput1_3.dll丢失的4种解决方法。在我们的日常工作和生活中,电脑出现问题是常有的事,而xinput1_3.dll丢失则是其中较为常见的一种问题。那么,什么是xinput1_3.dll?它为…...
unity 之参数类型之引用类型
文章目录 引用类型引用类型与值类型的差异 引用类型 在Unity中,引用类型是指那些在内存中存储对象引用的数据类型。以下是在Unity中常见的引用类型的介绍: 节点(GameObject): 在Unity中,游戏对象ÿ…...
SpringBoot自定义工具类—基于定时器完成文件清理功能
直接复制粘贴既可!! import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.io.File; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOff…...
安卓设置混淆后,gson报错解决方法
一,设置开启混淆release {minifyEnabled truezipAlignEnabled trueshrinkResources trueproguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } 二,混淆的文件中,对gson相关类不进行混淆,否…...
WPF实战项目十四(API篇):登录注册接口
1、新建UserDto.cs public class UserDto : BaseDto{private string userName;/// <summary>/// 用户名/// </summary>public string UserName{get { return userName; }set { userName value;OnPropertyChanged(); }}private string account;/// <summary>…...
跨平台项目实战:完整UI组件库与状态管理方案
一、项目实战概述随着移动端、Web端、桌面端多终端统一开发的需求日益普及,跨平台开发已成为企业级项目的主流选型。传统分端开发模式存在代码冗余、迭代效率低、UI风格不统一、状态逻辑复用困难等痛点。本项目以一套代码多端适配、UI标准化、状态统一管控为核心目标…...
Windows使用Powershell自动安装SqlServer2025服务器与SSMS管理工具
下载地址: https://www.microsoft.com/zh-cn/evalcenter/evaluate-sql-server-2025 安装结果: 安装前准备: 1.下载mssql server 2025安装器 2.下载iso镜像 3.下载好SSMS安装程序,并放到iso同目录下...
Python爬虫实战:从零编写一个健壮的静态页面抓取器!
㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~ ㊙️本期爬虫难度指数:⭐⭐⭐ (进阶) 🉐福利: 一次订阅后,专栏内的所有文…...
抖音无水印批量下载终极指南:douyin-downloader免费神器
抖音无水印批量下载终极指南:douyin-downloader免费神器 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback sup…...
3分钟掌握PlantUML Editor:用代码思维绘制专业UML图表的终极指南
3分钟掌握PlantUML Editor:用代码思维绘制专业UML图表的终极指南 【免费下载链接】plantuml-editor PlantUML online demo client 项目地址: https://gitcode.com/gh_mirrors/pl/plantuml-editor 还在为复杂的UML图表绘制而烦恼吗?传统的拖拽式绘…...
Closures实战指南:简化UITableView和UICollectionView数据绑定的终极教程 [特殊字符]
Closures实战指南:简化UITableView和UICollectionView数据绑定的终极教程 🚀 【免费下载链接】Closures Swifty closures for UIKit and Foundation 项目地址: https://gitcode.com/gh_mirrors/cl/Closures Closures是一个强大的iOS框架ÿ…...
链游3.0时代:GameFi+NFT+SocialFi如何引爆万亿级“数字乌托邦“?
——区块链游戏开发的全栈解密与商业落地指南引言:当游戏世界开始"造富" 当Axie Infinity的玩家在菲律宾靠打怪月入过万,当Decentraland的虚拟土地拍出243万美元天价,当StepN的运动鞋NFT创造45天回本神话——链游已不再是加密圈的小…...
论文AI率90%熬夜怎么办?2026年5招实测,一次过知网维普AIGC
2025 年 12 月 25 日知网 AIGC 检测系统升级,2026 年 4 月 27 日维普 AI 率检测平台升级…2026 毕业季,各大主流 AIGC 检测软件陆续升级系统,识别 AI 痕迹更加精准。 临近毕业,同学们看者飘红的 AIGC 检测报告、纷繁复杂的降 AI 系…...
static-php-cli跨平台构建实战:Linux、macOS、Windows全攻略
static-php-cli跨平台构建实战:Linux、macOS、Windows全攻略 【免费下载链接】static-php-cli Build standalone portable PHP binaries on Linux, macOS, Windows, with PHP project together, with popular extensions included. 项目地址: https://gitcode.com…...
机器学习之逻辑回归算法
一、逻辑回归简介 1. 定义 逻辑回归(Logistic Regression)是一种有监督学习算法,主要用于解决二分类问题的统计学习方法。尽管名字中带有“回归”,但它实际上是一种分类算法。 大白话解释 逻辑回归就是一种“做判断题”的算法&…...

