====== 添加屏幕 ====== **屏幕**是指的图形用户界面,其类继承了 ''Screen'',允许用户进行交互并实现一些功能。屏幕一个例子是你的模组的配置屏幕。屏幕仅在客户端存在,因此可将相应的类注解为 ''@Environment(EnvType.CLIENT)''。 你可以使用 mixin 以在现有的屏幕中,加入你的屏幕的链接。但是很多情况下,我们可以实现 Mod Menu 模组中的 ''ModMenuApi'',这样就能够在模组菜单屏幕中,通过配置按钮来进入屏幕。本文章不会讲述如何实现 ''ModMenuApi''。 ===== 添加部件 ===== 一个屏幕应当有多个“部件”,也就是屏幕中的元素。我们在 ''init'' 方法中添加部件。 @Environment(EnvType.CLIENT) public class TutorialScreen extends Screen { protected TutorialScreen() { // 此参数为屏幕的标题,进入屏幕中,复述功能会复述。 super(Text.literal("我的教程屏幕")); } public ButtonWidget button1; public ButtonWidget button2; @Override protected void init() { button1 = ButtonWidget.builder(Text.literal("按钮 1"), button -> { System.out.println("你点击了按钮 1!"); }) .dimensions(width / 2 - 205, 20, 200, 20) .tooltip(Tooltip.of(Text.literal("按钮 1 的提示"))) .build(); button2 = ButtonWidget.builder(Text.literal("按钮 2"), button -> { System.out.println("你点击了按钮 2!"); }) .dimensions(width / 2 + 5, 20, 200, 20) .tooltip(Tooltip.of(Text.literal("按钮 2 的提示"))) .build(); addDrawableChild(button1); addDrawableChild(button2); } } ==== “init”方法 ==== ''init'' 方法在会以下时候下调用: * 屏幕创建时。 * 屏幕尺寸调整时。在调用 ''init'' 之前,所有已存在的元素都会被移除。 你可以使用 ''addDrawable''、''addSelectableChild''、''addDrawableChild'' 来给屏幕添加元素,区别在于: * ''addDrawable'':元素会被渲染,但是你不可以通过鼠标指针或键盘选择它。 * ''addSelectableChild'':你可以选择并与之交互,但它不会被渲染。 * ''addDrawableChild'':元素会被渲染,也可交互,这是最常见的情况。 在 ''ButtonWidget.builder(...).builder()'' 中,你可以用过 ''size'' 和 ''position'' 指定按钮的大小和位置,也可以直接使用 ''dimensions''。''tooltip'' 指定按钮的提示,这个提示会在鼠标指针移到或者使用 Tab 选择到元素时渲染和复述。其中,''Tooltip.of'' 接收两个参数,第一个用于显示,第二个(可选)用于复述。 在 1.19.3 之前的版本,没有 ''ButtonWidget.builder(...)'',这种情况下你可以直接使用 ''ButtonWidget'' 的构造方法。 ==== 可以在构造方法中实例化部件吗 ==== 有些用户可能会在构造方法中或类的初始化中实例化部件,例如,代码可能会像这样: public ButtonWidget button1 = ButtonWidget.builder(...).build(); public ButtonWidget button2 = ButtonWidget.builder(...).build(); @Override protected void init() { addDrawableChild(button1); addDrawableChild(button2); } 这也是可以了,其优点就是,如果部件有多个“状态”(例如 ''CyclingWidget'' 的当前选择,或者在 ''TextFieldWidget'' 中已输入的文本),这些状态在重新调整屏幕大小之后,不会被重置。但是,调整尺寸时,屏幕的 ''width'' 和 ''height'' 会改变,但元素的位置和大小没有改变,因此在这种情况下,你需要在 ''init'' 方法中,更新元素的大小和位置。 @Override protected void init() { button1.setPosition(width / 2 - 205, 20); button2.setPosition(width / 2 + 5, 20); addDrawableChild(button1); addDrawableChild(button2) } ==== 注意顺序 ==== 添加大量元素之后,所有这些元素都可以渲染和选择。有些人可能不关心元素添加的顺序,因为所有这些部件都是同时渲染的。但是,如果你可能按 Tab 键来选择元素,你可能发现这些元素的选中的顺序是乱的。因此,请确保元素是以正确的顺序添加的,Tab 键的行为会依赖这一顺序。 ===== 上级屏幕 ===== 我通过另一个屏幕(如 Mod Menu 屏幕)访问了这个屏幕,但是我按下 Esc 键返回的时候,直接跳回到了主屏幕,而不是上一级屏幕,为什么? 这是因为你没有指定上一级屏幕,''close'' 方法直接跳回了主屏幕。 添加一个 ''parent'' 参数和字段,并在 ''close'' 方法中使用它。 private final Screen parent; protected TutorialScreen(Screen parent) { super(Text.literal("我的教程屏幕")); this.parent = parent; } @Override public void close() { client.setScreen(parent); } ===== 添加复述 ===== 默认情况下,在复述功能启用时,屏幕的标题以及你鼠标悬浮或选中的元素的信息会被复述。如果屏幕需要额外的复述(例如渲染了一些文本但没有添加为部件),你可以覆盖 ''addScreenNattaions'' 或 ''addElementNarrations''。这些方法接收一个 ''NarrationBuilder'',在这里你可以使用其 ''add'' 方法以添加复述消息。复述的消息分为以下部分(''NarrationPart''): * **Title**:屏幕的标题,会在构造函数中指定。当你进入屏幕时,这个标题会被自动复述。 * **Position**:告诉你当前正在选中的部件的位置。在原版中,是“//屏幕控件,第%s个,共%s个//”。此外,如果是在列表中,还会复述以下内容:“//已选中列表的第%s行,共%s行//”。 * **Hint**:这个是指的当前选中或悬浮的元素的提示。例如,你可能会记得在上面的代码中创建 ''ButtonWidget'' 时有个 ''tooltip'' 方法,这个 tooltip 就是在这一部分复述的。 * **Usage**:在原版中,用法为“//使用鼠标指针或Tab键选择屏幕控件//”。 除了对屏幕的复述之外,你还可以自定义元素的复述,也就是覆盖元素的类的 ''appendNarrations'' 方法,屏幕复述完成后,就会复述元素。 在添加复述的方法中,使用 ''narrationBuilder.nextMessage()'' 可以在当前复述消息之后添加复述,而不是替换复述中的已存在的部分。 在有些类中,你可能需要重复的复述,而不是只复述一次。例如,在加载世界时,加载的百分比会重复复述,告诉用户当前的加载状态。你可以在 ''render'' 或 ''tick'' 方法中,调用 ''narrateScreenIfNarrationEnabled''。对于更多细节,请参考 ''LevelLoadingScreen'' 的源代码。 ===== 添加文本 ===== 在 ''render'' 方法中,你可以调用像 ''textRenderer.draw''、''drawTextWithShadow'' 或 ''drawCenteredTextWithShadow'' 这样的方法,以在屏幕中渲染文本。 // 对于 1.20 以下版本 @Override public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { super.render(matrices, mouseX, mouseY, delta); drawCenteredTextWithShadow(matrices, textRenderer, Text.literal("你必须看到我"), width / 2, height / 2, 0xffffff); } // 对于 1.20 及以上的版本 @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { super.render(context, mouseX, mouseY, delta); context.drawCenteredTextWithShadow(textRenderer, Text.literal("你必须看到我"), width / 2, height / 2, 0xffffff); } 如果你担心文本太长超出了屏幕的边界。你可以使用 ''MultilineText'' 这样文本可以恰当地换行。 final MultilineText multilineText = MultilineText.create(textRenderer, Text.literal("这个文本很长 ".repeat(20)), width - 20); // 对于 1.20 以下版本 multilineText.drawWithShadow(matrices, 10, height / 2, 16, 0xffffff); // 对于 1.20 及以下的版本 multilineText.drawWithShadow(context, 10, height / 2, 16, 0xffffff); 另一个选择是使用 ''TextWidget'' 或 ''MultilineTextWidget''。这些部件默认不会被选中也不会被复述,因为其 ''active'' 字段为 ''false''。 ===== 滚动 ===== 屏幕不支持滚动,但是你可以添加支持滚动的部件。'''' 用于包含多个项并支持滚动的部件。不过,一般不是直接继承它,而是继承 '''' 或 '''' 更加合适,这些类实现了导航和复述。二者的区别在于: * '''' 指你可以选择一行的部件,在继承了此类的部件中,你通常会选择列表中的一个项。例如,在原版中的自选世界(单一生物群系)世界选项中的生物建筑系选择列表,以及语言选择列表。 * '''' 指每行包含多个子元素的部件。在继承了此类的部件中,你可以选择并交到各行中的元素。就行 ''Screen'' 一样,''.'' 应该有零个、一个或多个子元素。 ===== 完成之前需要检查的事情 ===== 在完成你的屏幕之后,为避免潜在的问题,请检查: * 按下 Esc 时,屏幕是否能够返回上一级屏幕 * 这些类是否仅存在于客户端(也就是说在专用服务器上不会加载) * 按下 Tab 选择元素时,这些元素被选中的顺序是否正确 * 重新调整屏幕尺寸时的行为是否正确 * 启用了复述功能时,按下 Tab 或使用鼠标指针选择元素时的复述是否正确