附加包教程:60.SAPI:表单
前言
从这期开始,我们就开始介绍一些基本的 SAPI 知识。我们首先来看目前最新的表单实现方法:数据驱动 UI(DDUI)。
数据驱动 UI 是 26.10.21 加入的新内容,在这之前,我们通常使用 ModalFormData 来创建比较复杂的自定义表单。但是这种旧表单限制较多,功能较少。因此,出现了新的表单,即 DDUI 式表单。
https://klpbbs.com/static/image/hrline/line5.png
准备
由于数据驱动 UI 使用 @minecraft/server-ui 模块,我们首先需要在 manifest.json 中导入这个模块的最新版本。
{
"module_name": "@minecraft/server-ui",
"version": "2.1.0-beta"
}
之后,我们在脚本文件中导入 DDUI 式表单需要用到的类,CustomForm 和 Observable。
import { CustomForm, Observable } from "@minecraft/server-ui";
CustomForm 是一个用于创建最基础的表单的类,而 Observable 是 DDUI 独有的新功能:可观察对象,它属于一种包装器。
那么可观察对象是什么?其实就是一个代表着 UI 中的值的对象,比如要在表单里添加“目前人数:2”的文本,那么这个文本就是一个可观察对象。我们需要用可观察对象,而不是直接指定值,这样,我们可以修改可观察对象的值,然后 UI 就会自动更新。
总之,如果要在 UI 里定义不变的量,比如固定文本的提示,或者滑块的最大最小值,就直接用对应的字面量;但是如果要显示“滑块的值为:40”“选择的时间是 22:17”“附近有 12 个实体”这样的文本,并且动态更新其中的值,或者要提供表单的默认值(实际上,也是获取对于这个值的引用)那么应该使用可观察对象。
我们首先来做一个最基本的表单。什么都没有,只有一个标题。
CustomForm.create(player, "我是标题").show()
这段代码就创建了一个最基本的表单,其中,player 是表单要显示给的玩家,后面的字符串自然是标题。现在它看起来是这样的。
https://pic1.imgdb.cn/item/69833e83b38a7ceb9305cbee.jpg
目前为止,相当简单!
然后……也许给它添加一条分割线?
CustomForm
.create(player, "我是标题")
.divider()
.show()
这段代码与上面那段相比,只是增加了 .divider(),这就是添加分割线的关键。
现在它是这样的!
https://pic1.imgdb.cn/item/69833f04b38a7ceb9305cc0a.jpg
在添加更多控件之前,我们首先来看看上面的代码到底有什么含义。首先,既然我们要创建自定义表单,那么当然要写出 CustomForm,它代表自定义表单这个类。这个类下面有一些方法(函数),既然要“创建”表单,那么肯定要调用 create 方法。这个方法接受两个参数,第一个是显示的玩家,第二个就是表单的标题。所以说,对 CustomForm 调用 create 方法,并传入两个参数——
CustomForm
.create(player, "我是标题")
之后,怎么添加更多控件?其实 CustomForm 的 create 方法有返回值。也就是说,调用了一个方法之后,它总是会返回点什么东西,有时候是 undefined,而对于这个 create 方法,它返回 CustomForm。所以说,create 之后,我们会得到一个 CustomForm 对象。这又属于 CustomForm 类,这样,我们就可以在返回的对象上调用其他属于 CustomForm 的方法了。
大多数给表单添加控件的方法都会返回修改后的 CustomForm,返回了 CustomForm,就可以继续添加控件。divider 方法也是如此,它也返回了 CustomForm,这样我们就可以继续添加控件。不过我们没有继续添加,而是让表单结束,调用 show 方法,把表单显示给玩家。show 方法不会返回 CustomForm,毕竟表单都已经显示给玩家了。
既然了解了代码的原理,我们就可以加入更多控件了。让我们调用 label 和 spacer,给表单添加一点文本和空行……
CustomForm
.create(e.sourceEntity, '我是标题')
.divider()
.label('我是测试文本!')
.spacer()
.label('§a丰§b富§c多§d彩§e的§g文§u本')
.show()
https://pic1.imgdb.cn/item/69834238b38a7ceb9305ccfc.jpg
是的!DDUI 的 label 是支持 § 格式化代码的,这一点与 Ore UI(蜂鸟 UI)不同。
对了,有一件事需要注意,那就是两个 label 文本之间一定要加 spacer!否则……https://pic1.imgdb.cn/item/698343ee02c0a8411ebd8b65.jpg文字都挤到一起了……
好了,我们已经学会了最基本的表单控件。接下来,我们即将创建一个按钮。我们暂时还不会遇到可观察对象,不过很快就会见到它的!
那么,按钮怎么定义?其实相当简单。
.button('一个神秘的按钮', _ => {})
这段代码的后边怎么那么多奇怪符号?别急,它们其实只有一个作用。不过我们先看 button 方法的第一个参数,它很简单,就是指定按钮上的文本。这里,文本是“一个神秘的按钮”。
而接下来,既然它是一个按钮,那么按下之后肯定要有点反应。如何实现这个“反应”?当然是调用函数。这就是代码里 _ => {} 的作用,定义一个函数!
当然,这只是一个什么内容都没有的函数,按钮也暂时还是没有任何作用。不过既然有了函数,向里面添加东西就简单多了。
如果你不认识这种函数定义,打开这个折叠块:在 JavaScript 中,我们可以通过 function 关键字定义一个带名称的函数。不过,也可以使用箭头表达式,定义一个匿名函数,也就是没有名称的函数。
箭头表达式的核心是 =>,一个箭头符号。它的前面是函数的参数,它的后面则是函数的代码。函数的参数可以有一个,有很多,或者一个也没有。函数的代码,如果直接写在箭头后面,那就相当于把后面的语句返回的值作为函数返回值;如果写一个 {},那么可以在里面写更复杂的语句,并使用 return 关键字指定返回值。
一般来说,箭头表达式是 () => {} 形式的。不过如果没有参数,我们可以使用 _ 代替 ()。另外,如果只有一个参数,比如 (entity) => entity.remove(),我们可以省略参数的括号,写成 entity => entity.remove()。有多个参数时,不能省略括号。
而对于返回值的问题,可以参考下面的例子:
const isEqual = (a, b) => a == b
代码定义了 isEqual 这个函数,接受两个参数,比较它们是否相等,返回布尔值。也就是说,上面的写法与
const isEqual = (a, b) => {
return a == b;
}
基本等价。
我们经常能看到一些代码,明明没有给箭头表达式传递参数,却能在箭头表达式的右侧使用外部的参数,这是因为我们创建了闭包。不过现在还不涉及这些,等到时候我们再详细解释。
以上就是按钮的基本特性了。我们还可以给按钮加一个提示,就像这样:
.button('一个神秘的按钮', _ => {}, {
tooltip: '但是目前没什么用 ^_^'
})
这里,我们传入了第三个参数,它是一个对象,其中有 tooltip 属性,表示它的提示文本。鼠标悬停在按钮上的时候,就可以看到提示文本。对于触屏,暂时未知如何看到此文本。
好了,隆重向你介绍,可观察对象!
Observable
我们来创建一个可观察对象吧。
const bool = Observable.create(true);
相当简单。Observable 有一个静态方法 create,可以用它创建可观察对象。可观察对象可以是布尔值,true 和 false;可以是数值,0 和 70 之类;也可以是字符串。
创建了可观察对象之后,我们就可以用它代表一个开关的值,创建那个开关了。
.toggle('扳弄一下我!', bool)
开关要用 toggle 方法创建。它接受两个参数,第一个是开关的名称,第二个就是刚才说的可观察对象,表示开关的值。因为刚才我们定义可观察对象时,传入的是 true,所以开关默认是开的。
类似地,我们可以定义更多可观察对象,把文本框、下拉菜单和滑块全都整出来。
const inputText = Observable.create('我好饿!');
const dropdownGuest = Observable.create(0);
const percentageValue = Observable.create(70);
.textField('请用文本投喂我', inputText, {
description: '文本是什么?好好吃的样子'
})
.spacer()
.dropdown('来都来了拉一下我嘛 *_*\n\n你猜我最喜欢哪个数字?', dropdownGuest, [{
label: '11 看起来不错',
value: 0
},
{
label: '17 也很好',
value: 1
}
])
.spacer()
.slider('诶?我是百分比嘛?', percentageValue, 0, 100, {
description: '我的分度值是 10,好像不是诶……',
step: 10
})
这段代码有点长,其实只是因为我们一口气定义了三个控件,而且它们接受的参数又很多。我们一个一个看,首先是 textField 方法,它定义了文本框。
textField 的第一个参数是文本框的名称,会显示在框上面。第二个参数就是可观察对象,表示文本框里的内容。第三个参数是个对象(其实应该说是接口),里面的 description 属性表示这个文本框的解释性文字,会以灰色小字的形式出现在框下方。
dropdown 的第一个参数是下拉菜单的名称,显示在下拉菜单上面。第二个参数是可观察对象,表示选中了下拉菜单的哪个选项。第三个参数是个包含着对象的数组,表示下拉菜单的选项。每个选项都由 label 和 value 定义,label 是选项名称,value 是用来标记选项的内部值。以后可以通过代码判断 value 值,来判断到底选择了哪个选项。可观察对象的值,也是这个 value 值。
slider 的第一个参数是滑块的名称,显示在滑块上面。与名称同一行的最右侧,还会显示滑块目前的值。接下来,第二个参数是可观察对象,指定了滑块的值;第三个和第四个参数分别是滑块的最小值与最大值。最后是一个对象,其中的 description 与 textField 的 description 相同,都是解释性文字。不过 slider 这边还多出了一个 step 属性,表示这个滑块的分度值,或者说步进值。
接下来,我们把所有东西合并到一起(省略了可观察对象的声明):
CustomForm
.create(e.sourceEntity, '我是标题')
.divider()
.label('我是测试文本!')
.spacer()
.label('§a丰§b富§c多§d彩§e的§g文§u本')
.spacer()
.button('一个神秘的按钮', _ => {}, {
tooltip: '但是目前没什么用 ^_^'
})
.spacer()
.textField('请用文本投喂我', inputText, {
description: '文本是什么?好好吃的样子'
})
.spacer()
.dropdown('来都来了拉一下我嘛 *_*\n\n你猜我最喜欢哪个数字?', dropdownGuest, [{
label: '11 看起来不错',
value: 0
},
{
label: '17 也很好',
value: 1
},
{
label: '37?又是它?',
value: 2
},
{
label: '40 嘛……很整了',
value: 3
}
])
.spacer()
.toggle('扳弄一下我!', toggleEagerForUse)
.spacer()
.slider('诶?我是百分比嘛?', percentageValue, 0, 100, {
description: '我的分度值是 10,好像不是诶……',
step: 10
})
.spacer()
.show()
效果就是这样的:https://pic1.imgdb.cn/item/6983d0c702c0a8411ebeaaa2.jpghttps://pic1.imgdb.cn/item/6983d0c802c0a8411ebeaaa4.jpghttps://pic1.imgdb.cn/item/6983d0c802c0a8411ebeaaa3.jpg
接下来,我们即将发挥可观察对象的优势,创建一个不一样的表单。我们会创建一个带有“高级设置”的表单,其中有一个叫“显示高级设置”的开关,它打开时,可以在表单下方看到高级设置;关闭时,则看不到。还有一个叫“启用高级设置”的开关,它打开时,高级设置会启用;关闭时,高级设置会禁用,无法交互。
那么,我们先把这两个开关,和一个代表高级设置的按钮创建出来吧。
const isAdvancedMode = Observable.create(false);
const showAdvancedOptions = Observable.create(false);
.toggle('启用高级设置', isAdvancedMode)
.toggle('显示高级设置', showAdvancedOptions)
.button('哇,好高端啊', () => {})
这时候,showAdvancedOptions 可观察对象的值需要根据“显示高级设置”的变化而变化。而这就涉及到了客户端修改可观察对象的值的问题,我们需要允许客户端的修改。允许的方法很简单,在创建的时候,再传入一个对象,把其中的 clientWritable 字段设为 true 即可。
const isAdvancedMode = Observable.create(false, {
clientWritable: true
});
const showAdvancedOptions = Observable.create(false, {
clientWritable: true
});
接下来,我们想办法让这些可观察对象影响按钮这个控件。
.button('哇,好高端啊', () => {}, {
visible: showAdvancedOptions,
disabled: isNotAdvancedMode
})
没错,方法就是在传入的第三个参数中,设置 visible 和 disabled 字段,它们控制一个控件的可见性与可用性。
但是这时,我们发现了一个问题:如果把 disabled 设为 true,那就是禁用这个按钮,而这时候 isAdvancedMode 的值应该是 false。所以说,这里需要的可观察对象的值与 isAdvancedMode 的值是反着的,我们需要新的 isNotAdvancedMode 可观察对象,把 isAdvancedMode 的值反过来。
那么这时候为什么不用 ! 来反转值?因为我们操作的不是布尔值,而是可观察对象。
定义 isNotAdvancedMode:
const isNotAdvancedMode = Observable.create(true, {
clientWritable: true
});
之后,我们就要根据 isAdvancedMode 设置 isNotAdvancedMode 的值了。这时候,我们就迎来了可观察对象的一个非常重要的方法,subscribe。
isAdvancedMode.subscribe(newValue => isNotAdvancedMode.setData(!newValue));
可观察对象的 subscribe 方法可以让我们订阅 这个可观察对象的值的变化。它的值变化的时候,就会执行后面的回调函数。这里我们也使用了箭头表达式来定义这个函数。
subscribe 方法为我们提供了相关的参数,我们只需要关心,它提供的第一个参数是可观察对象改变后的新的值。这里,我们使用 newValue 来命名这个参数。而可观察对象还有 setData 方法用来设置它的值,因此,isNotAdvancedMode.setData(!newValue) 的意思就是把 isNotAdvancedMode 可观察对象的值设为 newValue 的相反值。setData 方法返回了什么不重要,重要的是它设置了值,这就够了。
这样,我们就完成了全部代码。把它们放到一起:
const isAdvancedMode = Observable.create(false, {
clientWritable: true
});
const isNotAdvancedMode = Observable.create(true, {
clientWritable: true
});
const showAdvancedOptions = Observable.create(false, {
clientWritable: true
});
isAdvancedMode.subscribe(newValue => isNotAdvancedMode.setData(!newValue));
function ddui(e) {
CustomForm
.create(e.sourceEntity, '我是标题')
.divider()
.toggle('启用高级设置', isAdvancedMode)
.toggle('显示高级设置', showAdvancedOptions)
.button('哇,好高端啊', () => {}, {
visible: showAdvancedOptions,
disabled: isNotAdvancedMode
})
.divider()
.show()
}
效果就是这样的:https://pic1.imgdb.cn/item/6983daf702c0a8411ebeaac9.jpghttps://pic1.imgdb.cn/item/6983dafb02c0a8411ebeaaca.jpghttps://pic1.imgdb.cn/item/6983dafb02c0a8411ebeaacb.jpg
最后,表单的 show 方法返回的是一个 Promise,会在玩家关闭表单时变为 Resolved 状态。这时候我们可以用 then 方法执行一些代码,还可以继续用 catch 方法捕获错误。也可以用 close 方法关闭表单。
const playerName = Observable.create('默认名称', {
clientWritable: true
});
function ddui(e) {
CustomForm.create(e.sourceEntity, '设置')
.spacer()
.divider()
.spacer()
.textField('玩家名称', playerName, {
description: '您在游戏中的显示名称'
})
.show()
.then(() => {
console.log(`填写的玩家名称是 ${playerName.getData()}`);
})
.catch(e => {
console.error(e);
});
}
效果是这样的:https://pic1.imgdb.cn/item/6983e35602c0a8411ebec118.jpghttps://pic1.imgdb.cn/item/6983e35802c0a8411ebec119.jpghttps://pic1.imgdb.cn/item/6983e35802c0a8411ebec11a.jpg
以上就是 CustomForm 的大部分内容了。还有一个 MessageBox,它用于创建简单的双按钮对话框。它也有 create 方法,不过这次它只接受要显示给的玩家,而标题通过 title 方法指定。
import { MessageBox } from "@minecraft/server-ui";
MessageBox.create(e.sourceEntity)
.title('确认操作')
.body('确定要删除此物品吗?此操作无法撤消。')
.button1('删除')
.button2('取消', '保留物品并关闭对话框')
.show()
之后,可以通过 body 方法,指定一段显示在标题下方的说明性文字。之后就是 button1 和 button2 方法了,定义两个按钮上面的文字。它们还接受另一个参数,用于显示提示。鼠标悬停在按钮上的时候,就可以看到提示。最后,可以通过 show 方法显示表单,然后用 then 和 catch 处理结果。还可以用 close 方法关闭表单。
https://klpbbs.com/static/image/hrline/line1.png
总结
这一期,我们讲解了最新的 DDUI 技术。
第五十九期 第六十期 第六十一期 本帖最后由 牢冥王 于 2026-2-22 20:04 编辑
以后服务器表单也变成Ore UI了[贴吧_狂汗]
触屏是不是可以长按控件(即选中而不触发)就可以看到tooltip了[抖音_494]
本帖最后由 grieogfhroi 于 2026-2-26 15:15 编辑
怎么用菜单和可观察对象来显示实体的数量?
写的太棒了
页: [1]