可扩展参数:积木上那对 ➕➖
有些积木右下角挂着一对小圆按钮 ➕ ➖。点 ➕,积木上多一个参数格;点 ➖,少一个。 一块积木带几个参数,由使用它的人决定,而不是写死的。

哪些积木有#
原版里几块最常用的核心积木已经支持:
| 积木 | 加出来的是 | 范围 |
|---|---|---|
| 「连接 ( ) 和 ( )」 | 再连一段字符串 | 2 – 64 |
( ) +▾ ( )(带下拉的算术) |
再加一个运算数 | 2 – 64 |
( ) >▾ ( )(带下拉的比较) |
再加一个比较项,相邻每一对都要成立 | 2 – 64 |
⟨ ⟩ 与▾ ⟨ ⟩(带下拉的与或) |
再加一个条件 | 2 – 64 |
| 「如果 ⟨ ⟩ 那么」 | 一条「否则如果」 | 最多 64 条 |
带下拉的那三块是新积木,和原版的「+」「-」「*」「/」「>」「<」「=」「与」 「或」并排放在「运算」分类里,原版那几块没有任何改动。它们中间的运算符是下拉, 一格一个、可以混用,一律从左往右算,没有优先级。
下拉里各有哪些:
| 积木 | 运算符 |
|---|---|
| 带下拉的算术 | + - * / ^ |
| 带下拉的比较 | > < = ≠ ≥ ≤ ≡ |
| 带下拉的与或 | 与、或 |
乘除用的是 * 和 /,和旁边原版的乘除积木上写的记号一致。^ 是乘方,和其它运算
一样从左往右算:2 + 3 ^ 2 是 25。
≠ 和 = 一样不区分大小写。≡ 是区分大小写的等于:两边都是数字时按数值比(1.0 ≡ 1
成立),否则逐字比较(A ≡ a 不成立)。
≠、≥、≤ 原版没有。 存进 .sb3 的时候写成「不成立 ⟨ ( ) = ( ) ⟩」「不成立 ⟨ ( ) < ( ) ⟩」
这类原版积木,所以作品在原版 Scratch 里照样打得开、照样跑;再用 Gandi 打开时又收回成一个
≥。
^ 和 ≡ 原版表达不了。 存档时那一格写成 Gandi 自己的积木(「( ) ^ ( )」「( ) ≡ ( )」),
其余部分照常写成原版积木,再用 Gandi 打开时收回。用了这两个运算符的作品,原版 Scratch
打不开。
除此之外,扩展也能给自己的积木加这对按钮(见下面「给扩展作者」)。
点击之后的行为#
- ➕ 在段的末尾接一个新格子,带着默认值,拖出来就能用。
- ➖ 去掉最后一个格子。里面塞着的积木会被拆下来留在工作区,不会跟着消失。
- 到了下限 ➖ 自动隐藏,到了上限 ➕ 自动隐藏。所以「连接」刚拖出来时只有一个 ➕,它已经在下限上。
「如果」按分支口逐个变化:第一次 ➕ 从一个口变成「如果 / 否则」两个口;再点 ➕ 才在 「否则」前面加入一条「否则如果 ⟨ ⟩ 那么」,变成三个口。➖ 按相反顺序逐口收起, 所以原来的「如果否则」也有 ➖,可以退回纯「如果」。移除分支时,其中的积木会拆下来 留在工作区,撤销能连回原位。

存出去仍然通用#
这些形状是 Gandi 的,但存进 .sb3 的时候会被拆成原版的嵌套结构:三段的
「连接」拆成两块原版「连接」套在一起,「否则如果」的链拆成一层层嵌套的
「如果否则」。语义完全一样,只是不紧凑,作品拿到原版 Scratch 里照样打得开。再用
Gandi 打开时又会自动收回紧凑形态。
扩展积木上的 ➕➖#
扩展声明之后,它的积木上也有这对按钮。示例扩展
(public/examples/gandi-showcase.js,见扩展接口)里各种
写法都有一块:

从上到下分别是:一个格子只有一个输入(「把 … 和 … 连起来」);一个格子由一对 键值组成(「记住:关卡 = 1,参数2 = 2」);段前面还有固定参数(「用 ( 和 ) 括住 …」);以及每个格子的类型可以逐个选(「打包 …」)。
声明了逐槽类型的积木,点 ➕ 会先问一句「加一个什么」:

一块积木上可以有好几段,每段自己一对 ➕➖、各加各的。段与段之间可以夹固定文 字和固定参数。
右键菜单里是同一套操作,另外多了「改类型」和「改名」(考虑到有人习惯用右键,也有人 不会注意到积木右下角那两个小圆点);有好几段时每一行前面带上这一段的名字,便于分辨 点的是哪一段:

帽子积木也能这样扩展。示例扩展的「当示例事件发生」默认是零个参数,点 ➕ 才出 现;椭圆上的名字就是参数名,可以拖出去当值用,用右键的「给第 N 个参数改名…」 修改:

给扩展作者#
在 getInfo() 的积木上写一段 expandable 就有了。不用引任何工具、不碰 Blockly,
远程扩展也一样。
{
opcode: 'switchToWithParams',
blockType: Scratch.BlockType.COMMAND,
// [EXPAND] 就是可重复段的位置
text: '切换到场景 [SCENE],参数 [EXPAND]',
arguments: {SCENE: {type: Scratch.ArgumentType.STRING, menu: 'scenes'}},
expandable: {
// 一个可重复单元由哪些东西组成:插槽和字面文字,按顺序
slot: [
{name: 'KEY', type: Scratch.ArgumentType.STRING, defaultValue: '关卡'},
{text: '='},
{name: 'VALUE', type: Scratch.ArgumentType.STRING, defaultValue: '1'}
],
between: ',', // 格子与格子之间的连接词
min: 1,
max: 12,
start: 1
}
}能写的字段:
| 字段 | 意思 |
|---|---|
slot |
一个可重复单元。数组,每项是插槽 {name, type, menu?, defaultValue?}、字面文字 {text: '='},或者一张 C 口 {branch: true}(见下面「能加口的 C 形积木」)。至少要有一个插槽。 |
between |
格子与格子之间那个词(第 2 个格子起)。 |
prefixText |
段开头那句话,可以随格子数变(0 个和 n 个说法不同)。 |
min / max |
上下限。默认 1 / 16,硬上限 64。 |
start |
起始格子数。默认 max(min, 1)。 |
types |
允许用户逐槽选类型:['string', 'number', 'boolean']。写了它,点 ➕ 会先弹菜单问「加一个什么」。 |
rename |
允许右键给某一格里的占位积木改名。 |
addTooltip / removeTooltip |
➕➖ 的悬停说明。 |
id |
这一段的名字,配合 [EXPAND:id] 用。只有一块积木上有好几段时才需要。 |
label |
这一段在右键菜单上怎么称呼(「参数:加一个参数」)。几段并存时不写就是「第 N 段」。 |
defaultValue、between、prefixText 都可以按下标给:常量(每格一样)、
数组(短了就一直用最后一个)、或者函数 i => …。下标一律从 1 起,只有
prefixText 例外:它的下标是格子数,所以从 0 起。
能加口的 C 形积木#
槽里放一张 C 口,点 ➕ 就多出一整条带口的东西,像「如果」加出「否则如果 ⟨ ⟩ 那么」:
{
opcode: 'ifChain',
blockType: Scratch.BlockType.CONDITIONAL,
branchCount: 2, // 「那么」一张口,「否则」一张口
text: ['如果 [COND] 那么', '[EXPAND] 否则'],
arguments: {COND: {type: Scratch.ArgumentType.BOOLEAN}},
expandable: {
slot: [
{text: '否则如果'},
{name: 'C', type: Scratch.ArgumentType.BOOLEAN},
{text: '那么'},
{branch: true}
],
min: 0,
start: 1
}
}
text 写成数组时,[EXPAND] 在第几行,加出来的那几条就长在第几张口后面;上面这块是
「那么」的口之后、「否则」之前。
口的编号接在积木固定的那几张后面:第 i 条的口是第 branchCount + i 号分支,进去照常
util.startBranch(号),解释器和编译模式走的是同一套编号:
ifChain (args, util) {
if (Scratch.Cast.toBoolean(args.COND)) return util.startBranch(1, false);
for (const slot of Scratch.expandedSlots(args, ['C'])) {
if (Scratch.Cast.toBoolean(slot.C)) return util.startBranch(2 + slot.index, false);
}
util.startBranch(2, false);
}slot.index 是这一格是第几条(从 1 起)。要用它而不是数组下标:空着的布尔槽不进 args,
后面的格子会顶上来,下标就错开了。
点 ➖ 收掉一条时,口里的积木留在工作区上,不会跟着消失。LOOP 也可以这样写;分支参数
(CCW_HAT_PARAMETER + startBranch 的第三个参数)在这些口里照常能用。
一块积木上好几段#
[EXPAND] 在 text 里写几次,expandable 就写成几段的数组,按出现顺序一一对应。
每一段有自己的一对 ➕➖、自己的上下限、自己的右键菜单项。
{
opcode: 'callWithParams',
text: '调用 [NAME],参数 [EXPAND:ARGS],标签 [EXPAND:TAGS]',
arguments: {NAME: {type: Scratch.ArgumentType.STRING}},
expandable: [
{id: 'ARGS', label: '参数', slot: [{name: 'ARG'}], between: ',', start: 2},
{id: 'TAGS', label: '标签', slot: [{name: 'TAG'}], between: '、', min: 1}
]
}记号上的名字([EXPAND:ARGS] 配 id: 'ARGS')不是必须的,不写就按出现顺序对
应。但段数一多,改一次文案就可能让两段互换位置,表现只是「加错了那一段」,并不报
错;所以两段起就写上。
几段之间参数名不能重复:运行时就是靠名字分段的。
运行时把参数读回来只有一句,用平台提供的辅助函数,不要自己写正则:
switchToWithParams (args) {
// [{KEY: '关卡', VALUE: '1'}, {KEY: '难度', VALUE: '3'}]
for (const slot of Scratch.expandedSlots(args, ['KEY', 'VALUE'])) {
this.set(slot.KEY, slot.VALUE);
}
}只要一列值时用 Scratch.expandedValues(args, 'SCENE') → ['id1', 'id2']。
沙箱和非沙箱两条路都提供这两个函数。expandedSlots 返回的每一组还带一个 index
(第几格,从 1 起),它不可枚举,Object.keys 和 JSON.stringify 里看不到。
一块积木上有好几段时,名字那个参数不是可选的:不传的话两段的第 1 格都是序号 1,会被并进同一组。传这一段声明的那几个名字,就只取这一段。
不要读 args.mutation。 解释器无条件把它塞进 args,编译器只在
mutation.blockInfo.isDynamic 为真时才传,而用 expandable 的积木不属于那一类:
开了编译器的作品里它是 undefined。这会造成解释器正确、编译器少算一组而且不报错的
分叉。expandedSlots 只认 args 的键名,两条执行路径在这一点上完全一致。
几条限制(触发时控制台会有一条 [expandable] 告警,积木退成没有 ➕➖ 的普通积木,
不会整块消失):
text里的[EXPAND]个数必须和expandable声明的段数一样多;写了名字的记号 找不到id对得上的那一段也不行。- C 形积木可以用。
text是数组时,所有[EXPAND]要写在同一行里。 - 带 C 口的段必须是最后一段,同一行里它后面不能再有参数;一格里最多一张口,而且这一格 至少要有一个插槽;只有 CONDITIONAL 和 LOOP 能带口。
- 段后面如果还有固定参数,那里必须至少有一个插槽;只有下拉字段挪不回原位。 段后面直接跟着下一段没有这个要求。
- 格子用
menu时,那个菜单必须acceptReporters: true,否则它在积木上是个 下拉字段而不是插槽,按 input 名增删的机制处理不了它。 start: 0(出厂一个格子都没有)只有最后一段、而且它后面没有固定参数时才 支持,别的位置会被抬到 1;min: 0(点 ➖ 减到一个不剩)哪一段都可以。- 格子名必须是标识符,不能和
arguments里的固定参数重名,几段之间也不能互相 重名。 - 空的布尔槽不占一组:空插槽本来就不进
args,后面那一组会顶上来。要按位置 取值就读每一组的index。
第 i 个格子的 input 名是 <name>i(KEY1、VALUE1、KEY2…),从 1 起;
格子数记在积木的 mutation 上,跟着作品存、跟着作品回来。好几段时 mutation 上按段
用分号分开(items="3;2"、prefix="KEY,VALUE;TAG"),只有一段时一个分号都没有,
所以老作品原样打得开。带口的段在 prefix 里多一项 SUBSTACK+<branchCount>,
第 i 条的口存成 SUBSTACK<branchCount + i>。
相关#
- 给扩展的宿主接口
Scratch.gandi - 自制积木 —— 参数个数由做积木的人定死的那一种。