> ## Content Index
> Fetch the complete content index at: https://yinguobing.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# egui水平居中没那么简单
- URL: https://yinguobing.com/blog/egui-horizontal-centering/
- Published: 2026-06-08T08:18:18.000Z
- Updated: 2026-06-08T08:18:18.000Z
- Description: > 一个 Immediate Mode GUI 的经典坑，以及四种解法
- Author: 小黑
- Tags: egui, Rust, GUI

\> 一个 Immediate Mode GUI 的经典坑，以及四种解法

最近在写一个视频剪切工具（[dnclip](https://github.com/yinguobing/dnclip?ref=yinguobing.com)），用 egui 做界面。功能本身不复杂——加载视频、预览、打 I/O 点、导出——但做到底栏的时候卡住了。

需求很简单：**底栏有三组控件（时间显示、播放控制、I/O 标记），中间那一组要水平居中。**

听起来像 ui.horizontal\_centered() 一句话的事？我试了，没效果。

## 问题出在哪

先看看直观的做法：

```rust
ui.horizontal_centered(|ui| {
    ui.button("⏮");
    ui.button("◀◀");
    ui.button("▶");
    ui.button("▶▶");
    ui.button("⏭");
});
```

结果按钮全部挤在左侧。翻文档才发现——horizontal\_centered 的"居中"是指**在竖直布局的容器里，让元素水平居中**，不是你想象的那种"把这一行整个摆中间"。

egui 是 Immediate Mode GUI，它的工作方式是：**你告诉它放什么，它当场布局，布完就画。** 它没法先画一遍量好尺寸再调整位置——因为已经画完了。常规 GUI（Retained Mode）那种"先占位、再居中"的思路，在 Immediate Mode 下走不通。

这个问题在社区里反复出现：[Discussion #2916](https://github.com/emilk/egui/discussions/2916?ref=yinguobing.com)、[#1409](https://github.com/emilk/egui/discussions/1409?ref=yinguobing.com)、Rust Users Forum、Reddit、Stack Overflow……搜了一圈，基本可以确认：**这是 egui 的老大难问题之一。**

## 解法一览

### 方案 1：手动垫空间

如果控件宽度是固定的，可以直接算：

```rust
let total_width = 5 * button_size + 4 * spacing;
let space = (ui.available_width() - total_width) / 2.0;
ui.add_space(space);
// 然后放按钮
```

简单直接，但宽度一变化就不好使了。

### 方案 2：Sizing Pass（两帧法）

第一帧只算尺寸不渲染，第二帧用算出的尺寸真正画。egui 原生支持这个模式：

```rust
ui.scope_builder(
    UiBuilder::new().sizing_pass().invisible(),
    |ui| {
        // 第一帧：widget 在这里渲染但不显示
        ui.button("▶");
        // 记下尺寸
        widget_size = Some(ui.min_rect().width());
    },
);
// 第二帧：用算好的尺寸偏移
ui.add_space((ui.max_rect().width() - widget_size) / 2.0);
ui.button("▶");
```

好处是只渲染一次，坏处是需要额外状态、尺寸变化时会延迟一帧。

### 方案 3：双遍渲染（每帧画两次）

不用状态，同一帧内画第一次测得尺寸、第二次真正放：

```rust
// 第一遍：算尺寸（在不可见的 scope 里）
let measured_size = ui.scope_builder(
    UiBuilder::new().invisible(),
    |ui| { ui.button("▶"); },
).response.rect.width();

// 第二遍：按尺寸居中
ui.add_space((ui.available_width() - measured_size) / 2.0);
ui.button("▶");
```

省心但费性能——好在大部分场景下画两次的代价可以忽略。

### 方案 4：egui\_alignments crate（强烈推荐）

[\`egui\_alignments\`](https://crates.io/crates/egui%5Falignments?ref=yinguobing.com) 是个专门解决对齐问题的第三方库。它把上述逻辑封装成了开箱即用的函数：

```rust
use egui_alignments::center_horizontal;

center_horizontal(ui, |ui| {
    ui.button("⏮");
    ui.button("◀◀");
    ui.button("▶");
    ui.button("▶▶");
    ui.button("⏭");
});
```

就这一句。内部用的是 Ui::scope\_builder \+ 尺寸计算，用户完全不用操心状态管理。

## 实战：dnclip 底栏三列布局

借这个居中方案，我把整个底栏重构了一下。先看最终效果：

```rust
// 底栏：三列等宽
ui.columns(3, |cols| {
    // 左列：时间显示（左对齐）
    cols[0].vertical_centered(|ui| {
        ui.with_layout(egui::Layout::left_to_right(egui::Align::LEFT), |ui| {
            ui.label("01:23:45.000 / 01:30:00.000");
        });
    });

    // 中列：播放控制（水平居中——终于对了）
    cols[1].vertical_centered(|ui| {
        center_horizontal(ui, |ui| {
            ui.spacing_mut().item_spacing.x = 4.0;
            ui.button("⏮");
            ui.button("◀◀");
            ui.button("▶");
            ui.button("▶▶");
            ui.button("⏭");
        });
    });

    // 右列：I/O 标记（右对齐）
    cols[2].vertical_centered(|ui| {
        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
            ui.label("IN:01:00.000");
            ui.button("I");
            ui.label("OUT:01:30.000");
            ui.button("O");
        });
    });
});
```

几个关键点：

1. **\`ui.columns(3)\` 创建三等分列**，每列宽度由 egui 自动分配，不会溢出
2. **每列先用 \`vertical\_centered\` 让内容垂直居中**，保证三行控件在一条水平线上
3. **中列用 \`center\_horizontal\` 让播放按钮在列内水平居中**
4. 左右两列分别用 left\_to\_right 和 right\_to\_left 控制文本对齐方向
5. 时间轴滑块单独放在控制行上方，用 slider\_width = ui.max\_rect().width() 撑满

## 总结

| 方案               | 复杂度    | 适用场景          |
| ---------------- | ------ | ------------- |
| 手动垫空间            | 低      | 宽度固定的简单场景     |
| Sizing Pass      | 中      | 追求性能、尺寸变化不频繁  |
| 双遍渲染             | 低      | 尺寸频繁变化、不想维护状态 |
| egui\_alignments | **最低** | **通用场景、推荐首选** |

我的建议是：**先用 \`egui\_alignments\`。** 它把最脏的活干了，API 也干净，而且不会影响性能——center\_horizontal 内部用的是基于 scope 的尺寸计算，不是每帧画两遍那种暴力的方式。

如果不想加三方依赖，可以直接抄它的思路：ui.scope\_builder \+ min\_rect().width() 计算偏移量。核心代码不到 20 行，但用起来远不如 center\_horizontal(ui, |ui| { ... }) 优雅。

---

*后记：这个问题让我想起了一个更大的命题——Immediate Mode GUI 是不是个错误的方向？我的看法是：不。布局上的不便换来的是极低的状态复杂度和可预测的渲染行为。当你调试一个 Retained Mode 的布局 bug 时，你会怀念 Immediate Mode 的简单粗暴。只不过，"简单"是对框架作者而言的，我们这些使用者得承受一些代价。*