Gandi 3.0文档
官网 打开编辑器
文档/自制积木与数据

可扩展参数:积木上那对 ➕➖

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

连接、算术、与或三块积木都长出了第三格

哪些积木有#

原版里几块最常用的核心积木已经支持:

积木 加出来的是 范围
「连接 ( ) 和 ( )」 再连一段字符串 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>。

相关#