每天 Shaarli

一天内的所有链接,汇聚在一个页面上。

October 13, 2025

Note: FLTK 容器选择

下面这份“小抄”按常见需求→该用哪个容器来选,外加要点/坑位提醒(Rust fltk 名称在括号里):

选哪个?

需求 首选容器 为什么 关键要点 / 易踩点
普通窗口 Fl_Window (window::Window) 基本窗口 需要防闪烁/重绘多→用 Fl_Double_Window
重绘频繁、动画、自绘画布 Fl_Double_Window (WindowType::Double 或 window::DoubleWindow) 双缓冲,减少闪烁 会多占一块离屏缓冲(内存↑)
OpenGL 内容 Fl_Gl_Window (window::GlWindow) 独立 GL 上下文 在 draw 里做 GL 绘制;注意与 UI 线程/重绘时机
内容需要滚动 Fl_Scroll (group::Scroll) 提供滚动条 通常在 Scroll 里再放一个内部 Group 承载内容;大内容要 resizable
页签切换 Fl_Tabs (group::Tabs) 多页签 每个 Tab 是一个子 Group;调整 resizable 让内容随窗缩放
向导/一步一步 Fl_Wizard (group::Wizard) 同时只显示一个页面 用 next()/prev() 切换;每页是一个子 Group
左右/上下线性自适应布局 Fl_Flex (group::Flex) 现代化线性布局(行/列),支持权重 用 flex.set_size(&child, px) 固定某子项尺寸;其余自动分配
栅格(表单/面板) Fl_Grid (group::Grid) 行×列网格,自动布局 适合复杂对齐;比 Pack 更可控;避免与其它“自伸缩容器”深度嵌套
简单“依次排队” Fl_Pack (group::Pack) 顺序排列(横/竖) 轻量但不如 Flex/Grid 灵活;不建议做复杂自适应
可拖拽分隔的“分栏” Fl_Tile (group::Tile) 平铺区块,可拖动边界 类似“分割面板”;子项自己绘制边界/把手感稍原始
只是把一批控件当作整体管理/隐藏/禁用 Fl_Group (group::Group) 基类容器,纯“打包” 用来分组、统一 deactivate()/hide()、抓事件;坐标仍靠你手摆

实战建议(精简版)

  • 新项目优先:Flex(线性)/Grid(表格) 做主布局,里面再放普通 Group+控件。
  • 滚动内容Scroll -> Group -> 内容 三级结构更稳;给内部 group 做 resizable
  • 分隔面板Tile;如果只是简单两栏,也可“Flex + 固定一侧尺寸”。
  • 重绘多:窗口设 WindowType::Double,自绘控件也尽量在双缓冲里。
  • 避免把多个“自伸缩容器”(Flex / Grid / Pack / Tile)层层套娃;层次深了容易出现尺寸争用或奇怪的重排。
  • 缩放跟随:为顶层窗口 window.resizable(&some_child) 指定“跟随缩放”的基准控件(常是主内容区)。
  • Tabs/Wizard:每页用子 Group;只让每页的主内容 resizable,避免整个页上所有控件乱拉伸。

迷你模板(Rust)

Flex(列布局)

use fltk::{prelude::*, group::Flex, enums::Align};

let mut flex = Flex::default_fill().column(); // 占满父容器,竖向排
flex.set_margin(8);
flex.set_pad(8);

// ... add children here ...
// flex.set_size(&child_fixed, 120); // 固定某子项高度(列布局时=高度)

flex.end();

Scroll with inner group

use fltk::{prelude::*, group::{Scroll, Group}};

let mut scroll = Scroll::default_fill();
let mut inner = Group::default().with_size(800, 1200); // 内容实际大小
// ... 放很多子控件 ...
inner.end();

scroll.end();
// 让滚动区域以 inner 为基准自适应
scroll.set_scrollbar_size(14);

双缓冲窗口

use fltk::{prelude::*, window::Window, enums::WindowType};

let mut win = Window::new(100, 100, 900, 600, None);
win.set_type(WindowType::Double);
// win.resizable(&main_area);
win.end();
win.show();
Note: FLTK GUI 设计

https://www.fltk.org/

https://www.fltk.org/doc-1.5/index.html
https://www.fltk.org/

FLTK

fltk = { version = "^1.5", features = ["fltk-bundled"] }

CMAKE

https://cmake.org/download/
https://github.com/Kitware/CMake/releases/download/v4.1.2/cmake-4.1.2-windows-x86_64.msi

FLTK-FLUID

cargo install fltk-fluid
cargo uninstall fltk-fluid
fluid &

FL2RUST

cargo install fl2rust
fl2rust <fl file>.fl > <output file>.rs

fltk-fluid 只负责“画界面(布局/控件)”
程序逻辑要在你自己的 Rust 代码里写fl2rust ui.fl > src/ui.rs 只把界面蓝图翻译成 Rust 结构体和一个构造函数,不要在生成的 ui.rs 里写业务逻辑(下次重新生成会被覆盖)。

下面给你一套常用做法 + 可运行示例 👇

推荐做法(总结)

  1. FLUID 画界面,给重要控件起名字(如 btn_inc, lbl_count)。
  2. fl2rust 生成 ui.rs(包含 UserInterface 等结构和 make_window())。
  3. main.rs(或你自己的模块)里
  • 创建 Appui
  • 给控件绑定回调(set_callback);
  • 更新控件状态(set_labelset_value…);
  • app::channel 做更复杂的消息/状态管理。
  1. 不要改 ui.rs,只把它当“视图层”。逻辑放 main.rs 或其他模块,必要时给 UserInterface 写扩展 impl(单独文件即可)。

最小示例:按钮计数器

1) 在 FLUID 里

  • 新建窗口类名(Class)设为:UserInterface(默认就行)。
  • 放一个 Button,名字(Name)设为:btn_inc,标签(Label)设为:点击 +1
  • 放一个 Box(或 Frame),名字设为:lbl_count,初始文本设为:0
  • 保存为 ui.fl

2) 生成 Rust 界面代码

fl2rust ui.fl > src/ui.rs

src/ui.rs 会导出一个类似:

pub struct UserInterface {
    pub window: fltk::window::Window,
    pub btn_inc: fltk::button::Button,
    pub lbl_count: fltk::frame::Frame,
    // ...
}
impl UserInterface {
    pub fn make_window() -> Self { /* ... */ }
}

3) 在 Cargo.toml 加依赖

[dependencies]
fltk = "1.5"

4) 在 src/main.rs 写逻辑

方案 A:用闭包直接改 UI(最直观)

use fltk::{prelude::*, *};

mod ui; // 就是生成的 src/ui.rs

fn main() {
    let app = app::App::default();
    let mut ui = ui::UserInterface::make_window();
    ui.window.make_resizable(true);

    // 计数状态放在闭包里;要更新标签,需要 clone 一份引用
    let mut count = 0;
    let mut lbl = ui.lbl_count.clone();
    ui.btn_inc.set_callback(move |_| {
        count += 1;
        lbl.set_label(&count.to_string());
    });

    app.run().unwrap();
}

方案 B:用 channel 做解耦(适合复杂逻辑)

use fltk::{prelude::*, *};

mod ui;

#[derive(Clone, Copy)]
enum Msg { Inc }

fn main() {
    let app = app::App::default();
    let (s, r) = app::channel::();
    let mut ui = ui::UserInterface::make_window();
    ui.window.make_resizable(true);

    // UI → 发送消息
    ui.btn_inc.set_callback(move |_| s.send(Msg::Inc));

    // 应用状态
    let mut count = 0;

    // 事件循环:接收消息并更新 UI
    while app.wait() {
        if let Some(msg) = r.recv() {
            match msg {
                Msg::Inc => {
                    count += 1;
                    ui.lbl_count.set_label(&count.to_string());
                }
            }
        }
    }
}

为什么不建议改 ui.rs?

  • 这是生成文件,你每次改图再生成都会覆盖;
  • 把逻辑放到 main.rs 或单独模块,界面变了也不影响业务逻辑;
  • 如需给 UserInterface 增加方法,可在另一个文件写扩展 impl
// src/ui_ext.rs
use crate::ui::UserInterface;
impl UserInterface {
    pub fn init_logic(&mut self) {
        // 绑回调、初始化状态…
    }
}

然后在 main.rs

mod ui;
mod ui_ext;
// ...
let mut ui = ui::UserInterface::make_window();
ui.init_logic();

小技巧

  • 在 FLUID 里给控件起 Name(不是 Label),fl2rust 才会把它们作为 pub 字段导出,便于在 Rust 里访问。
  • 业务状态尽量放在你自己的 struct 或闭包/通道里,不放生成文件里。
  • 需要跨回调共享控件就 clone()(FLTK 控件是轻量句柄,可安全克隆)。
  • 如果你不想每次手动运行 fl2rust,可以用我之前给你的 build.rs 集成proc-macro 宏方案,编译时自动生成。

如果你愿意,我可以根据你现在的 ui.fl 帮你把 main.rs 的逻辑搭好(比如输入框 + 按钮 + 校验 + 弹窗),你只要贴一下界面里控件的名字(Name)即可。

后面弹出控制台的解决办法

// main.rs 最开头插入:
// 发布版隐藏控制台,调试版保留控制台
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]

// 无论调试还是发布都隐藏控制台
// #![cfg_attr(target_os = "windows", windows_subsystem = "windows")]

eg.:

// main.rs

// 发布版隐藏控制台,调试版保留控制台
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]

// 无论调试还是发布都隐藏控制台
// #![cfg_attr(target_os = "windows", windows_subsystem = "windows")]

use fltk::{prelude::*, *};

mod ui;

fn main() {
    let app = app::App::default();

    // 现在会有 struct UserInterface 和 make_window()
    let mut ui = ui::UserInterface::make_window();

    // 给 Text_editor 绑定一个 TextBuffer(关键!)
    let buf = text::TextBuffer::default();
    ui.text_editor.set_buffer(Some(buf)); // 先把所有权交给 editor

    // 之后通过 editor 拿 buffer 来改内容,避免 clone 带来的困惑
    if let Some(mut b) = ui.text_editor.buffer() {
        b.set_text("在这里编辑…");
    }

    // 如果生成代码里没 show,这里补一下:
    ui.window.show();

    app.run().unwrap();
}
// ui.rs

// Automatically generated by fl2rust

#![allow(unused_variables)]
#![allow(unused_mut)]
#![allow(unused_imports)]
#![allow(dead_code)]
#![allow(clippy::needless_update)]

use fltk::browser::*;
use fltk::button::*;
use fltk::dialog::*;
use fltk::enums::*;
use fltk::frame::*;
use fltk::group::*;
use fltk::image::*;
use fltk::input::*;
use fltk::menu::*;
use fltk::misc::*;
use fltk::output::*;
use fltk::prelude::*;
use fltk::table::*;
use fltk::text::*;
use fltk::tree::*;
use fltk::valuator::*;
use fltk::widget::*;
use fltk::window::*;

#[derive(Debug, Clone)]
pub struct UserInterface {
    pub window: Window,
    pub text_editor: TextEditor,
}

impl UserInterface {
    pub fn make_window() -> Self {
    let mut window = Window::new(397, 259, 969, 573, None);
    window.set_label(r#"TEST"#);
    window.set_type(WindowType::Double);
    window.make_resizable(true);
    window.hide();
    let mut fl2rust_widget_0 = MenuBar::new(0, 0, 969, 22, None);
    fl2rust_widget_0.set_label(r#"mb1"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu1/item1"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu1/item2"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu1/item3"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu1/item4"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu2/item1"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu2/item2"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu2/item3"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu2/item4"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu3/item1"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu3/item2"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu3/item3"#);
    let idx = fl2rust_widget_0.add_choice(r#"submenu3/item4"#);
    fl2rust_widget_0.end();
    let mut fl2rust_widget_1 = CheckBrowser::new(10, 30, 144, 517, None);
    fl2rust_widget_1.set_label(r#"cb1"#);
    let mut text_editor = TextEditor::new(157, 48, 801, 353, None);
    text_editor.set_label(r#"TE1"#);
    window.resizable(&text_editor);
    text_editor.set_tooltip(r#"Please Input"#);
    window.end();
    Self {
        window,
        text_editor,
    }
    }
}
// cargo.toml

[package]
name = "fltk_widgets_showcase"
version = "0.1.0"
edition = "2024"

[dependencies]
fltk = { version = "^1.5", features = ["fltk-bundled"] }