← 返回索引

范围:元素与组件的继承层次、链式 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 的回调清单若增删,「输入回调与调用方」的清单与 快速开始 的转发清单需一起对齐。

相关文档