10-gui · git:20260819.ca99162 · 2026-08-19 · sha256 48222b50c0cd8b48

10-gui git:20260819.ca99162A

Immutable. This exact content is served forever at /api/v1/blob/48222b50c0cd8b48.

# 10 — GUI / Screen 开发

> 适用版本:Fabric 1.14.4

---

## 约束

### 核心原则

- GUI 代码**仅在客户端**执行(`ClientModInitializer`)
- Screen 类继承 `Screen` 或其子类(容器屏通常再包一层 HandledScreen / ContainerScreen)
- 容器屏通过 `ScreenProviderRegistry.INSTANCE.registerFactory` 注册(loader-api 已核;与 `ContainerProviderRegistry` 共用同一个 `Identifier`)
- **禁止**在服务端引用任何 GUI 类(`Screen`、`ButtonWidget` 等)。不要抄 1.20+ `DrawContext`

---

## Decision Flow

### Decision: 选择 GUI 框架

```
IF 简单的输入界面(文本框、按钮、说明)
  → Screen + TextFieldWidget + ButtonWidget

IF 容器型 GUI(箱子、熔炉、机器)
  → Container + ContainerScreen + ScreenProviderRegistry / ContainerProviderRegistry

IF 打开时要同步额外数据(坐标、流体量等)
  → 额外数据:openContainer 的 PacketByteBuf writer

IF 给原版 Screen 加控件
  → 只能改自己的 Screen.init();本档无 ScreenEvents.AFTER_INIT
```

---

## 基本 Screen

按钮在 `init()` 里创建;**标题/说明文字在 `render()` 里画**,不要在 `init()` 里 `drawString`。
不要用 `TextWidget`(部分档没有)或 `SimpleNamedWidget`(编造)。

Yarn 1.14.4 `Screen` 构造是 `(Text title)`([yarn 1.14.4 Screen.mapping](https://github.com/FabricMC/yarn/blob/1.14.4/mappings/net/minecraft/client/gui/screen/Screen.mapping))。`ButtonWidget` 第 5 参是 `String`。`render` 无 `MatrixStack`。`renderBackground` 只有 `(int alpha)`。

```java
public class MyScreen extends Screen {
    public MyScreen(Text title) {
        super(title);
    }

    @Override
    protected void init() {
        addButton(new ButtonWidget(width / 2 - 50, height / 2 + 20, 100, 20,
            "Click Me", btn -> {
                this.minecraft.openScreen(null);
            }));
    }

    @Override
    public void render(int mouseX, int mouseY, float delta) {
        this.renderBackground(0);
        this.drawString(this.font, "Hello Fabric!", width / 2 - 50, height / 2 - 20, 0xFFFFFF);
        super.render(mouseX, mouseY, delta);
    }
}
```

## 注册 Screen

```java
public class ExampleModClient implements ClientModInitializer {
    @Override
    public void onInitializeClient() {
        ScreenProviderRegistry.INSTANCE.registerFactory(
            new Identifier("examplemod", "my_screen"),
            (ContainerScreenFactory<MyContainer>) container -> new MyScreen(new TranslatableText("gui.examplemod.my_screen"))
        );
    }
}
```

## Container(服务端数据)

```java
public class MyContainer extends Container {
    private final Inventory playerInventory;
    private final Inventory blockInventory;

    public MyContainer(int syncId, PlayerInventory playerInventory) {
        this(syncId, playerInventory, new SimpleInventory(9));
    }

    public MyContainer(int syncId, PlayerInventory playerInventory, Inventory blockInventory) {
        super(MY_CONTAINER, syncId); // Yarn:Container(ContainerType, int)
        this.playerInventory = playerInventory;
        this.blockInventory = blockInventory;
        // 给方块槽和玩家物品栏 addSlot(...)
    }

    // Yarn 1.14.4 移位是 transferSlot,不是 wiki 现页的 quickMove
    @Override
    public ItemStack transferSlot(PlayerEntity player, int invSlot) {
        return ItemStack.EMPTY; // 按槽位 insertItem,参考 wiki 现页逻辑但用本档方法名
    }

    @Override
    public boolean canUse(PlayerEntity player) {
        return blockInventory.canPlayerUse(player);
    }
}

public static final Identifier MY_SCREEN_ID = new Identifier("examplemod", "my_screen");

// 1.14.4 用 Container / ContainerType / Registry.CONTAINER,不是 ScreenHandler
public static final ContainerType<MyContainer> MY_CONTAINER = Registry.register(
    Registry.CONTAINER,
    MY_SCREEN_ID,
    new ContainerType<>(MyContainer::new)
);
```

## 打开容器(服务端)

1.14.4 用 Fabric `ContainerProviderRegistry`(loader-api:`registerFactory` + `openContainer`),不要 `player.openHandledScreen` / `NamedScreenHandlerFactory`。

服务端先按同一 `Identifier` 注册工厂,再打开:

```java
// 主入口(逻辑服务端也要有工厂)
ContainerProviderRegistry.INSTANCE.registerFactory(
    MY_SCREEN_ID,
    (syncId, identifier, player, buf) -> new MyContainer(syncId, player.inventory)
);

public void openScreen(PlayerEntity player) {
    if (!(player instanceof ServerPlayerEntity serverPlayer)) return;
    ContainerProviderRegistry.INSTANCE.openContainer(
        MY_SCREEN_ID,
        serverPlayer,
        buf -> {
            // 需要同步到客户端的额外数据写进 PacketByteBuf
        }
    );
}
```

`ContainerFactory#create(int, Identifier, PlayerEntity, PacketByteBuf)` 已核 loader-api。不要编造 1.16 `ExtendedScreenHandlerFactory`。

## 给已有 Screen 加控件

1.14.4 索引里**没有** `ScreenEvents.AFTER_INIT`。给自己的 Screen 在 `init()` 里 `addButton`。
不要编造 `SimpleWidget` / `SimpleNamedWidget`,也不要写 `net.fabricmc.fabric-api:...`。

## 常见错误

- ❌ 在 `onInitialize()`(主 entrypoint)里注册客户端 Screen — 专用服务端会崩溃,应在 `ClientModInitializer`
- ❌ 在 Screen 类中直接改服务端库存/方块实体 — 应通过 Container 的槽位/同步
- ❌ 在 `render()` 里 `new` 按钮或分配大对象 — 控件在 `init()` 创建
- ❌ 忘记给玩家物品栏 `addSlot` — 物品栏不显示(没有 `addPlayerInventory()` 这种原版方法)
- ❌ 在 Screen 中忘记 `super.render(...)` — 背景和子控件不渲染
- ❌ 使用 `TypedScreenHandlerFactory` / `HandlerScreen` / `Menu`(Yarn 容器是 Container,不是 Mojmap Menu)

## 扩展点

| 配合 Skill | 协作说明 |
|-----------|---------|
| `mc-registry` | Container / ContainerType 通过 `Registry.register` 注册 |
| `mc-item` | 物品右键走 `ContainerProviderRegistry.openContainer`(不要抄 1.16 `openHandledScreen`) |
| `mc-block` | 方块实体提供 Factory / 打开包数据 |
| `mc-networking` | 槽位不够时再用自定义 Payload 同步进度条等 |