范围:框架依赖接入、首个可用 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。
版本号中,第一个版本号代表兼容性分支,例如“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 组件 与 布局容器 为准。