← 返回索引

范围:框架依赖接入、首个可用 Screen 的搭建,以及组件、布局、事件与覆盖层的使用。

前置:快速开始 的依赖坐标与最小可运行 Screen 全码。


1. 框架依赖接入

FluentUI 发布在 GitHub Packages,版本列表见发布页。GitHub Packages 的 Maven 仓库读取时需要凭据:先申请一个勾选了 read:packages 的 personal access token,再在 build.gradle 中添加仓库与依赖:

repositories {
    maven {
        url 'https://maven.pkg.github.com/xianfish33/FluentUI-MC'
        credentials {
            username = "你的 GitHub 用户名"
            password = "你的 personal access token"
        }
    }
}

dependencies {
    modImplementation "xianfish.fluentui:fluentui:${project.fluentui_version}"
}

fluentui_version 填发布页上的版本号,当前是 26A.14.0。

Note

版本号中,第一个版本号代表兼容性分支,例如“26A“代表26版本中兼容一定游戏版本的分支。 第二个版本号代表同步改动版本,即为所有就兼容性分支同步的改动基线,不分大小,依次递增。 最后一个版本号代表兼容性分支内的改动,未同步到同步改动版本,不分大小,依次递增。

完成配置后,所有 UI 类都在 xianfish.fluentui.client.ui.* 包下,可以直接 import。


2. 首个 Screen 的创建

框架约定:每个界面由一个 FluentScreen 子类承载。FluentScreen 基类本身不保存元素树和交互对象,这三个字段是子类的惯例字段,在 init() 中创建并初始化:

字体赋值:在 init() 中创建 root 之后,对显示文字的组件调用 .font(font),即传入 Screen 自带的字体。缺少该调用时不崩溃也不报错,因为文本组件在 extractRenderState 开头检测到 font == null 即跳过整个绘制,界面文字静默消失。

字段职责
root界面的根容器(元素树),所有组件作为其子节点
overlays覆盖层管理器,tooltip / 对话框经它推入
interactionManager交互管理器,把屏幕输入路由进元素树

最小完整示例如下:

public class MyScreen extends FluentScreen {
    private LayoutElement<?> root;
    private OverlayManager overlays;
    private InteractionManager interactionManager;
    private final UiTickDriver uiTickDriver = new UiTickDriver();

    public MyScreen() {
        super(Component.literal("My Screen"));
    }

    @Override protected void init() {
        // 1. 建根容器并摆到屏幕中央
        StackPanel root = new StackPanel(Orientation.VERTICAL)
            .size(400, 300)
            .topLeft(width / 2 - 200, height / 2 - 150);
        this.root = root;

        // 2. 挂上第一个组件(.font(font) 必须给——否则按钮文字静默不画)
        root.add(new Button("Click Me", 200, 24)
            .font(font)
            .action(() -> System.out.println("Clicked!")));

        // 3. 创建覆盖层管理器与交互管理器,把前两者交给它
        overlays = new OverlayManager();
        interactionManager = new InteractionManager(root, overlays);
        InteractionManager.active = interactionManager;
    }

    @Override public void extractRenderState(GuiGraphicsExtractor e, int mx, int my, float d) {
        Tooltip.cursor(mx, my, width, height);
        uiTickDriver.onRenderFrame(interactionManager);
        root.extractRenderState(e, mx, my, d);
        overlays.renderOverlay(e, mx, my, d);
    }
}

8 条输入转发方法(mouseClicked/mouseReleased/mouseDragged/mouseScrolled/keyPressed/keyReleased/charTyped/preeditUpdated)缺少时元素收不到任何输入,完整代码见 快速开始,原样复制到 Screen 子类。

打开屏幕:

Minecraft.getInstance().setScreen(new MyScreen());

组件通过链式调用配置:new Button("Save", 120, 24).action(...).setEnabled(isValid).margin(...) 连续调用,每个 setter 返回组件本身。两条规则:始终调用 setter(.width(100)),不直接写 protected 字段(width = 100);直写会跳过 dirty 标记并破坏惰性布局。链式 API 与元素层次的完整规则见 核心概念。


3. 常用组件速览

以下为常用组件;每个组件的全部构造重载与方法以 UI 组件 为准。

按钮:action 接收点击回调,setEnabled(false) 禁用(变灰且不响应点击):

new Button("Save", 120, 24)
    .action(() -> saveConfig())
    .setEnabled(isValid);

文本框:onChanged 接收一个参数(新值)的 Consumer<String>;focus() 使组件进入屏幕时即获得焦点:

new TextBox(200, 20)
    .text("")
    .placeholder(Component.translatable("ui.name"))
    .maxLength(100)
    .onChanged(v -> updatePreview(v))
    .onEnter(() -> submit())
    .focus();

开关:Toggle 尺寸固定(30×14),onChange 在状态翻转时触发;回调中通过 isOn() 读取当前状态:

Toggle toggle = new Toggle(0, 0)
    .on(true)
    .onChange(() -> applyDarkMode(toggle.isOn()));

滑块:steps 设置离散步数,onChanged 接收当前浮点值:

new Slider(200)
    .value(0.5f)
    .steps(10)
    .nonlinear(true)
    .onChanged(v -> setVolume(v));

复选框:SelectBox 是单选框样式的布尔开关,onClick 接收切换回调:

new SelectBox()
    .onClick(() -> toggleFeature());

4. 容器与布局

组件不会自行排列,需要由容器组织。三个内置容器覆盖绝大多数界面形状,选型细节与全部容器 API 见 布局容器;容器内部的测量与排列公式归 布局系统。

StackPanel:将子元素排成一行或一列,spacing 控制间距,OverflowMode.SCROLL 开启滚动(超出部分可用滚轮滚动):

// 垂直布局
StackPanel panel = new StackPanel(Orientation.VERTICAL)
    .size(400, 300)
    .spacing(8)
    .overflow(OverflowMode.SCROLL)
    .scrollbar(true);

panel.add(new Button("Button 1", 200, 24));
panel.add(new Button("Button 2", 200, 24));

// 水平布局
StackPanel row = new StackPanel(Orientation.HORIZONTAL)
    .spacing(8)
    .centered(true);

row.add(new Button("Left", 100, 24));
row.add(new Button("Right", 100, 24));

Canvas:不使用自动排列,按坐标或锚点自由摆放:

Canvas canvas = new Canvas(400, 300);
canvas.add(child, 10, 20);                     // 指定坐标
canvas.add(child, Anchor.CENTER);              // 锚点定位
canvas.add(child, Anchor.BOTTOM_RIGHT, -8, -8); // 锚点 + 偏移

EntryRow:设置行的标准形状为左标签、右控件。left()/right() 按调用顺序分左右两列,行高自动取两列较高者(至少 20px),右列过宽会被压缩到默认上限 260px,始终贴右对齐:

new EntryRow(380, 120)
    .left(new Text("Volume", 120).font(font))   // Text 实现见 developer.CoreConcepts.md
    .right(new Slider(240).value(0.8f).onChanged(v -> saveVolume(v)));

5. 事件回调

框架把输入收敛成三类回调,在构造组件时注册即可。

点击回调:按钮最直接的响应入口:

new Button("Click Me", 120, 24)
    .action(() -> {
        // 点击时执行
        System.out.println("Button clicked!");
    });

值变化回调:文本与数值控件在值改变时触发回调;TextBox.onChanged 与 Slider.onChanged 都是单参数 lambda,参数即新值:

new TextBox(200, 20)
    .onChanged(v -> validateForm());   // v = 最新文本

new Slider(200)
    .onChanged(value -> setVolume(value));

键盘输入:键盘事件只发送给当前聚焦的元素,即 FocusableElement 子类(如 TextBox)。没有聚焦元素时,按键不会进入元素树:

TextBox input = new TextBox(200, 20)
    .focus()
    .onEnter(() -> submit());   // 按 Enter 时执行

点击事件的路由优先级(先分发给谁、由谁消费)属于交互引擎内部,见 交互引擎;点击失灵的排查按该文档的分发顺序执行。


6. 工具提示与覆盖层

覆盖层是位于根界面之上的浮层,tooltip、下拉、对话框都使用同一套系统。外部关闭语义的权威说明在 覆盖层。

静态工具提示:tooltip(String) 属于 TooltippingElement 一族,Button、TextBox、Slider、Toggle、SelectBox 等都有:

new Button("保存", 80, 20).tooltip("立即写入磁盘");
new TextBox(200, 20).tooltip("最多 100 个字符");

动态工具提示:tooltip(Supplier<Component>) 在每次展示前求值。构建表达式中不得引用尚未赋值的变量,应先创建实例再挂载工具提示:

Slider slider = new Slider(200);
slider.tooltip(() -> Component.literal("Volume: " + (int)(slider.value() * 100) + "%"));

模态对话框:对话框是带背景的容器,经 InteractionManager.active.overlays() 推入覆盖层树。双参形态为 push(el, dismissOnOutsideClick);示例使用双参指定「点外部不自动关闭」,由对话框自身的按钮负责关闭:

StackPanel dialog = new StackPanel(Orientation.VERTICAL)
    .size(300, 150)
    .bgColor(0xFF1E1E1E);

dialog.add(new Text("Delete this item?", 280).font(font));   // Text 实现见 developer.CoreConcepts.md
dialog.add(new StackPanel(Orientation.HORIZONTAL)
    .spacing(8)
    .add(new Button("Yes", 100, 24).action(() -> { deleteItem(); closeDialog(dialog); }))
    .add(new Button("No", 100, 24).action(() -> closeDialog(dialog))));

InteractionManager.active.overlays().push(dialog, false);   // 点外部不关

// 关闭即从覆盖层移除:
// InteractionManager.active.overlays().remove(dialog);

单参 overlays().push(el) 默认「点外部自动关闭」,适合 tooltip 式轻浮层。组合方式与「点击落点如何在覆盖层间分配」的完整语义见 覆盖层。


7. 不变量与禁止写法

  • 只调用 setter,不直写字段:width = 100 这类直写会跳过 markDirty(),界面停在旧布局上。链式 API 的完整规则以 核心概念 为准。
  • 8 条输入转发方法均不可缺:缺少任何一个,对应输入无法进入元素树(「点了没反应」多为此原因)。清单与写法以 快速开始 为准。
  • init() 必须完成三个字段的初始化:root、overlays、interactionManager 都要在 init() 中创建,并将前两者传给 InteractionManager;缺少 overlays 时 tooltip/对话框无法推入,缺少 interactionManager 时全界面无交互。这三个字段的所有权解释见 根树与层级系统。
  • .tooltip(...) 只定义在 TooltippingElement 一族:无法调用时先到 UI 组件 查该组件的继承关系,不强行编码。
  • 调用 .tooltip(...) 之前先调用 .font(...):提示的宽度、折行、高度均由字体度量决定;字体为 null 时度量高度为 0,提示不显示。

连带影响

  • 框架升级时,输入转发方法签名、overlays().push 入口、容器 API 任何一处变更都会导致 Screen 子类编译失败;对应说明分别见 快速开始、覆盖层、布局容器,按报错定位到相应文档核对。
  • 示例引用的组件行为(onChanged 单参签名、EntryRow 左右列规则、tooltip 归属等)以 UI 组件 与 布局容器 为准。

8. 相关文档

  • 核心概念:链式 API、元素层次、字体与脏标记
  • 快速开始:最小 Screen 全码与 8 条输入转发方法
  • 布局容器:全部容器的选型与 API
  • UI 组件:每个组件的构造重载与方法一览
  • 覆盖层:overlays().push 入口与关闭语义
  • 布局系统:测量/排列公式与惰性布局机制(贡献者视角)