当前位置: 首页 > news >正文

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

距离上一篇文章已经过去了挺久的,很长时间没有写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");}
}

下次再见 🦀

相关文章:

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

距离上一篇文章已经过去了挺久的&#xff0c;很长时间没有写GH基础部分的内容了&#xff0c;原因其一是本职工作太忙了&#xff0c;进度也有些落后&#xff0c;白天工作累成马&#xff0c;回家只想躺着&#xff1b;其二则是感觉GH基础系列基本上也介绍得差不多了&#xff0c;电…...

华为Mate60低调发布,你所不知道的高调真相?

华为Mate60 pro 这两天的劲爆新闻想必各位早已知晓&#xff0c;那就是华为Mate60真的来了&#xff01;&#xff01;&#xff01;并且此款手机搭载了最新国产麒麟9000s芯片&#xff0c;该芯片重新定义了手机性能的巅峰。不仅在Geekbench测试中表现出色&#xff0c;还在实际应用…...

C++(18):命名空间

多个库将名字放置在全局命名空间中将引发命名空间污染。 命名空间可以用来防止名字冲突&#xff0c;它分割了全局命名空间&#xff0c;其中每个命名空间是一个作用域。通过在某个命名空间中定义库的名字&#xff0c;库的作者&#xff08;以及用户&#xff09;可以避免全局名字…...

K8S最新版本集群部署(v1.28) + 容器引擎Docker部署(上)

温故知新 &#x1f4da;第一章 前言&#x1f4d7;背景&#x1f4d7;目的&#x1f4d7;总体方向 &#x1f4da;第二章 基本环境信息&#x1f4d7;机器信息&#x1f4d7;软件信息&#x1f4d7;部署用户kubernetes &#x1f4da;第三章 Kubernetes各组件部署&#x1f4d7;安装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或者服务器上的视频文件&#xff0c;一般有三种方式&#xff0c;第一种是调用厂家的SDK&#xff0c;这个功能最全&#xff0c;但是缺点明显就是每个厂家的设备都有自己的SDK&#xff0c;只兼容自家的设备&#xff0c;如果你的软件需要接入多个厂家的&…...

用于设计和分析具有恒定近心点半径的低推力螺旋轨迹研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

MongoDB - 构造复杂查询条件执行查询

文章目录 1. 构造 keyword 的查询条件2. 构造 threatSubType 的查询条件3. 相应的实体类 /*** 查询白名单详情** param offset 第几页开始* param limit 每页显示的最大值* param keyword 模糊搜索值* param order 排序方式&#xff08;升序/降序…...

如何从ChatGPT中获得最佳聊天对话效果

从了解ChatGPT工作原理开始&#xff0c;然后从互动中学习&#xff0c;这是一位AI研究员的建议。 人们利用ChatGPT来撰写文章、论文、生成文案和计算机代码&#xff0c;或者仅仅作为学习或研究工具。然而&#xff0c;大多数人不了解它的工作原理或它能做什么&#xff0c;所以他…...

深入浅出:手把手教你实现单链表

一、什么是链表 链表是一种链状数据结构。简单来说&#xff0c;要存储的数据在内存中分别独立存放&#xff0c;它们之间通过某种方式相互关联。 如果我们使用C语言来实现链表&#xff0c;需要声明一个结构体作为链表的结点&#xff0c;结点之间使用指针关联。 二、单向链表的结…...

vite 打包项目后访问显示空白页的问题,开发环境正常,生产环境无报错。

有没有可能&#xff0c; 你跟我遇到同样的问题 白屏的写法 const routes [{path: /,component: import(../views/index.vue),} ]正确的写法 const routes [{path: /,component: () > import(../views/index.vue),} ]有时候方向很重要&#xff0c;当在错误的方向上无脑冲…...

打造成功的砍价营销大解析,销量飙升

砍价活动是吸引顾客的一种有效方式&#xff0c;可以帮助提高销量和提升品牌知名度。在乔拓云平台上&#xff0c;我们提供了一套简单易用的工具&#xff0c;让您能够轻松地制作一个成功的砍价活动。下面&#xff0c;我将详细介绍具体步骤&#xff0c;让您能够轻松上手。 第一步&…...

【Flink进阶】- Flink kubernetes operator 常用的命令

目录 1、应用程序管理 (1)提交 Flink 应用程序 (2)查看 Flink 应用程序列表...

ASP.NET Core 的日志系统

ASP.NET Core 提供了丰富日志系统。 可以通过多种途径输出日志&#xff0c;以满足不同的场景&#xff0c;内置的几个日志系统包括&#xff1a; Console&#xff0c;输出到控制台&#xff0c;用于调试&#xff0c;在产品环境可能会影响性能。Debug&#xff0c;输出到 System.Di…...

android13(T) 以太网设置工具类

13 版本的以太网设置和以前版本有所变动&#xff0c;在 AS 中就能直接调用对应 API 将 build.gradle 版本修改 compileSdkVersion 31, 即可直接调用 EthernetManager 相关&#xff0c; 设置静态等方法可以通过反射调用设置。 以下是核心设置静态和动态参数工具类&#xff0c…...

电脑报错提示xinput1_3.dll缺失怎么办?xinput1_3.dll丢失的简单恢复方案

今天&#xff0c;我将为大家分享一个与我们日常工作息息相关的话题——xinput1_3.dll丢失的4种解决方法。在我们的日常工作和生活中&#xff0c;电脑出现问题是常有的事&#xff0c;而xinput1_3.dll丢失则是其中较为常见的一种问题。那么&#xff0c;什么是xinput1_3.dll?它为…...

unity 之参数类型之引用类型

文章目录 引用类型引用类型与值类型的差异 引用类型 在Unity中&#xff0c;引用类型是指那些在内存中存储对象引用的数据类型。以下是在Unity中常见的引用类型的介绍&#xff1a; 节点&#xff08;GameObject&#xff09;&#xff1a; 在Unity中&#xff0c;游戏对象&#xff…...

SpringBoot自定义工具类—基于定时器完成文件清理功能

直接复制粘贴既可&#xff01;&#xff01; 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报错解决方法

一&#xff0c;设置开启混淆release {minifyEnabled truezipAlignEnabled trueshrinkResources trueproguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } 二&#xff0c;混淆的文件中&#xff0c;对gson相关类不进行混淆&#xff0c;否…...

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>…...

Python爬虫实战:研究MechanicalSoup库相关技术

一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…...

生成xcframework

打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式&#xff0c;可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

UDP(Echoserver)

网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法&#xff1a;netstat [选项] 功能&#xff1a;查看网络状态 常用选项&#xff1a; n 拒绝显示别名&#…...

服务器硬防的应用场景都有哪些?

服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式&#xff0c;避免服务器受到各种恶意攻击和网络威胁&#xff0c;那么&#xff0c;服务器硬防通常都会应用在哪些场景当中呢&#xff1f; 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...

【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分

一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计&#xff0c;提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合&#xff1a;各模块职责清晰&#xff0c;便于独立开发…...

Xen Server服务器释放磁盘空间

disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...

并发编程 - go版

1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程&#xff0c;系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...

三分算法与DeepSeek辅助证明是单峰函数

前置 单峰函数有唯一的最大值&#xff0c;最大值左侧的数值严格单调递增&#xff0c;最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值&#xff0c;最小值左侧的数值严格单调递减&#xff0c;最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...

[论文阅读]TrustRAG: Enhancing Robustness and Trustworthiness in RAG

TrustRAG: Enhancing Robustness and Trustworthiness in RAG [2501.00879] TrustRAG: Enhancing Robustness and Trustworthiness in Retrieval-Augmented Generation 代码&#xff1a;HuichiZhou/TrustRAG: Code for "TrustRAG: Enhancing Robustness and Trustworthin…...