范围:元素与组件的继承层次、链式 setter 的必需性、静态文本实现(框架未内置文本组件)、元素可接收的输入回调及其调用方。
前置:快速开始 的最小闭环。
元素与组件层次
框架中每一个可见对象都是 Element 的实例,提供几何(位置、宽高)、外边距、可见性、字体与输入回调。需要容纳子元素的容器分出 LayoutElement 一支;需要悬停提示的控件继承 TooltippingElement,其中需要接收键盘输入的控件再继承 FocusableElement;采用“弹出后点外关闭“生命周期的浮层直接继承 Element 并自行管理。现成组件归属于对应能力的基类,自定义组件按同样的规则选择父类:
Element<T> 几何、命中、输入回调
├── LayoutElement<T> 持有 children、滚动
│ ├── StackPanel 一行/一列线性排列
│ ├── Canvas 自由定位 + 9 锚点
│ └── EntryRow 左标签、右控件的两列行
├── TooltippingElement<T> 悬停弹出 tooltip
│ ├── Toggle / Slider / SelectBox 开关、滑块、复选
│ ├── Button 按钮
│ ├── ListView<T> ── ItemList<T> 可滚动列表 → 可增删列表
│ ├── ItemView Minecraft 物品格
│ ├── SelectDropdown 下拉选项
│ └── FocusableElement<T> 焦点 + 键盘/IME 输入
│ ├── TextBox 带输入法的文本框
│ ├── Stepper 数字步进器
│ └── AutoCompleteBox 自动补全输入框
├── ContextMenu / MessageFlyout 浮层组件(直接继承 Element)
├── Tooltip / ProgressBar 直接继承 Element 的独立组件
自定义组件按能力选父类:LayoutElement 持有 children,TooltippingElement 提供悬停提示,FocusableElement 承接键盘输入,缺哪项能力就选哪个基类。Tooltip 与 ProgressBar 直接继承 Element,Button、SelectDropdown、Toggle、Slider、SelectBox、ListView、ItemView 继承 TooltippingElement,TextBox、Stepper、AutoCompleteBox 继承 FocusableElement,ItemList 继承 ListView,ContextMenu 与 MessageFlyout 直接继承 Element。
链式 API
搭界面使用链式 API:Element 上修改状态的方法都返回自身(自界定泛型将其转为具体子类型),因此可以连续调用。按钮的文案、动作、外边距、层级可一次配置完成,禁用则需单独调用:
Button save = new Button("保存", 120, 24)
.action(() -> doSomething())
.margin(Margin.symmetric(4, 8));
save.setEnabled(false); // setEnabled 返回 void,是单独调用的状态开关,接不进链上
链上每个方法都返回 save 实例,因此可以连续书写;setEnabled 只改状态、不返回值,必须单独调用。需要一行静态文字时使用 new Text("音量", 120)(实现见「静态文本配方」),它同样能接 .margin(...) 这类 Element 链式方法。
改尺寸、边距、可见性只调用 setter,不直接写字段;完整规则见「setter 不变量」,链式写法本身即 setter 调用。
静态文本配方
框架没有内置任何静态文本组件。框架未独立定义文本绘制,统一使用 Minecraft 自带方法。画一行静态文字需编写一个继承 Element 的小类,用元素的 font 字段调用绘制接口。全库统一的配方即以下实现:其他文档中的 new Text(...) 都指它,文档内部同样直接使用该实现;修改构造签名时,所有引用处都需同步修改。
画文字使用 Minecraft 自带 GuiGraphicsExtractor 内的 e.text(font, 文本, x, y, color) 即可:先由 font.width(...) 量出文字宽度做水平居中,再调 e.text(...);ListView 画行文字同样调 e.text(...),传入的是 String。Text 按这条真实路径实现:
public class Text extends Element<Text> {
private String content;
private int color = 0xFFFFFFFF;
public Text(String content, int width) {
super(width, 9); // 高度先按默认行高给,measure() 在有字体时校正
this.content = content;
}
public Text color(int c) { color = c; return this; }
@Override protected void measure() {
if (font == null) font = net.minecraft.client.Minecraft.getInstance().font;
height = font.lineHeight; // 行高以实际字体为准
}
@Override public void extractRenderState(GuiGraphicsExtractor e, int mx, int my, float d) {
if (!visible || font == null) return;
layout();
e.text(font, content, getAbsoluteX(), getAbsoluteY(), color);
}
}
measure() 在字体缺失时回退到游戏默认字体,再按 font.lineHeight 定高;该回退与 MessageFlyout 的做法一致,因此 new Text("Resolution", 120) 不额外 .font(font) 也能绘制。extractRenderState 顶部的守卫沿用开头的模式;measure() 本身可空,在构造器内固定宽高时留一个空实现即可。用法与普通组件相同:root.add(new Text("标题", 200))。
绘制文本由原版自带方法负责;实现细节见 Minecraft 源码。
setter 不变量
规则:改宽高、尺寸、外边距、可见性时,一律调用链式 setter:.width(...)、.height(...)、.size(...)、.margin(...)、.visible(...),不直接给字段赋值(width = 100 这种写法)。这些 setter 在改值的同时会将元素标脏(markDirty)并沿父链冒泡,下一帧布局才重新测量排列;直接写字段跳过标脏。
违反后果:字段值确实改变,但界面保持不变,直到其他操作恰好将该子树标脏,尺寸才更新为新值。出现“改了宽高没反应“时,先检查是否绕过 setter。
布局引擎内部如何冒泡、何时重排、坐标怎么换算,机制细节见 布局系统。
字体、可见性与外边距
大多数组件在 font == null 时整个 extractRenderState 直接返回,连背景带文字一起不画(Button.extractRenderState 开头就是这个守卫)。构建时用 .font(font) 把 Screen 的字体传入需要显示文字的元素;MessageFlyout 与 Text 这类组件会在内部回退到游戏默认字体。自定义组件的 extractRenderState 以 if (!visible || font == null) return; 开头。换字体不改几何,因为 font() 不触发重排,与 setter 不变量不冲突(见 元素系统)。
可见性与层级各管一件事:.visible(false) 隐藏整个子树(不渲染、不参与命中);同一父元素内的子元素按 children() 列表顺序绘制与命中,后添加的绘制在上、命中优先,跨父元素置顶需改用浮层(见 覆盖层与浮动元素)。
外边距参与父容器的测量与排列(StackPanel 会把子元素的 margin 计入间距):Margin.all(8) 四边 8px,Margin.symmetric(4, 8) 上下 4 左右 8,new Margin(top, right, bottom, left) 各边独立。
输入回调与调用方
每个 Element 都可以覆写以下输入回调:
- 按下 / 抬起 / 拖拽 / 滚轮:
mousePressed、mouseReleased、mouseDragged、mouseScrolled,均返回boolean,返回true表示消费本次输入。 - 悬停进出与移动:
mouseEntered、mouseMoved、mouseExited。 - 浮层外部交互:
onOutsideInteraction,在元素挂载到浮层树上、且一次明确交互(按下/抬起/拖拽/滚轮)落在它命中区之外时被调用,“点外面关闭“由它负责;纯悬停不触发。
调用方:Screen 子类先把 Minecraft 的输入转发给 InteractionManager,8 条转发方法、各自漏转的症状、基类已处理的边界见 快速开始;InteractionManager 再按命中链与焦点把上述回调派给树上的元素,分发顺序与命中链见 交互引擎。
不变量
- 静态文本没有内置组件:编写框架中不存在的文本标签类会直接编译失败;使用「静态文本配方」中的
Text实现,或在自身工程内维护同名类。 - 尺寸/边距/可见性绕过 setter 直写字段 → 字段变了、布局不动,直到其他操作标脏后才更新。
- 文本元素忘
.font(font)(且组件自身无回退)→ 组件隐形:font == null守卫直接跳过整个绘制。 - 跨父元素比较子元素顺序 → 不生效;同父内才按
children()顺序,跨层置顶改用浮层。 - 把
setEnabled这类返回void的方法接进链 → 编译错误;它单独调用。
连带影响
Text配方是全库静态文本的单一权威:修改它的构造签名或绘制方式,会波及所有写new Text(...)的文档与示例(含 布局容器 的 EntryRow 示例)。- 继承层次图与源码类声明一一对应;给新组件换基类时,本图与 UI 组件 的组件表需同步,否则“选父类“的指导会指向错误能力。
- setter 不变量依赖 布局系统 的
markDirty机制;标脏时机变更时,使用侧表述需同步。 Element的回调清单若增删,「输入回调与调用方」的清单与 快速开始 的转发清单需一起对齐。