ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Rust egui窗口配置全攻略:从NativeOptions到ViewportBuilder实战

Rust egui窗口配置全攻略:从NativeOptions到ViewportBuilder实战 我在Rust桌面应用里用egui写小工具也有段时间了每次新建项目都要重新配一遍窗口大小、标题、全屏、图标、渲染器、垂直同步……这些参数统一由eframe::NativeOptions管理。今天这篇就直接给一份“窗口配置小抄”把常见窗口形态的配置方式都整理出来读完可以照着抄。适合刚接触egui的Rust玩家也适合写过几个demo但想系统过一遍配置项的朋友。需要先说明这篇主要基于 egui 0.29 / 0.30 这一代版本API以实际使用的版本为准。好在eframe的窗口配置从0.24之后结构基本稳定下来了核心思路和大字段都没怎么变老项目升级和新项目上手都能参考。1. 先把NativeOptions的“家底”摸清楚1.1 NativeOptions到底配置了什么对egui不熟的朋友我简单梳理一下egui本身是个即时模式GUI库只管画界面和响应交互不负责创建窗口、处理系统事件。真正把egui和操作系统窗口绑定到一起的是eframe这个官方应用框架。eframe::NativeOptions就是“告诉eframe怎么创建原生窗口”的配置结构体。它里面塞了一大堆字段我习惯把它们分成几类来记窗口外观类大小、位置、标题、图标、装饰有没有边框和标题栏、是否透明、是否可调整大小。窗口行为类最小化、最大化、全屏、置顶、焦点、是否记忆上次的窗口位置大小。渲染类用Glow还是Wgpu、垂直同步开关、MSAA采样数、深度缓冲和模板缓冲位数、硬件加速策略。应用级类是否居中、是否允许系统拖放文件、主题跟随系统还是固定某个主题。最初我刚用egui的时候这些字段散落在NativeOptions各个地方记一下就忘。后来发现一个规律和老版本相比现在凡是“初始状态下窗口长什么样”这种需求绝大多数都收进了viewport字段类型是egui::ViewportBuilder。NativeOptions本体的字段基本只负责渲染、生命周期和全局行为。所以你现在看到一个常见的初始化代码长这样let native_options eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([960.0, 600.0]) .with_min_inner_size([400.0, 300.0]) .with_title(我的应用), ..Default::default() };我个人的理解是NativeOptions是“应用启动的大管家”ViewportBuilder是“窗口的初始快照”。搞清楚这个分工后面查文档都会快很多。1.2 版本差异别拿老代码直接编译如果你翻到一篇2023年的egui博客很可能会看到这种写法let native_options eframe::NativeOptions { initial_window_size: Some(egui::vec2(960.0, 600.0)), min_window_size: Some(egui::vec2(400.0, 300.0)), ..Default::default() };在egui 0.23之后initial_window_size、min_window_size、max_window_size、resizable、decorated、transparent、always_on_top等字段都被陆续移进了ViewportBuilder。有些是老字段直接废弃有些是保留但不再推荐。所以如果你从旧项目复制配置过来经常遇到“这个字段不存在”的编译错误。我踩过几次坑之后现在的做法很固定写任何新项目都从egui::ViewportBuilder::default()开始然后用链式方法配置窗口的初始状态不再去碰NativeOptions里那些遗留字段。另外还有一处版本差异要注意。0.29之前主题相关字段是follow_system_theme: true, default_theme: eframe::Theme::Dark,0.29之后合并成了theme字段类型是eframe::ThemePreferencetheme: eframe::ThemePreference::Dark,三种取值分别是System、Dark、Light会根据系统偏好自动切换也可以直接指定。老项目升级的时候这两个字段的迁移经常被忽略编译会直接报错。2. 搭一个能反复试的窗口实验室2.1 最小可运行工程“小抄”这种东西光列配置项很难记住最好自己动手把每种配置跑一遍。我建议先建一个最小的egui工程然后只改NativeOptions每改一处就运行看效果。这样比干看文档有效十倍。先建工程cargo new egui-window-lab cd egui-window-lab然后在Cargo.toml里加上[dependencies] eframe 0.29 egui 0.29接着写一个最简单的入口use eframe::egui; fn main() - eframe::Result { let native_options eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([960.0, 600.0]) .with_min_inner_size([400.0, 300.0]) .with_title(窗口实验室), ..Default::default() }; eframe::run_native( window-lab, native_options, Box::new(|_cc| Ok(Box::new(WindowLab::default()))), ) } #[derive(Default)] struct WindowLab; impl eframe::App for WindowLab { fn update(mut self, ctx: egui::Context, _frame: mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { ui.heading(窗口配置小抄); ui.label(在这里随意修改 NativeOptions 观察窗口变化); }); } }这里注意Box::new(|_cc| Ok(Box::new(...)))是0.24之后的写法app构造闭包需要返回Result。如果看到老代码里是Box::new(|_cc| Box::new(...))说明那是在新版本之前的老API直接照抄会报错。2.2 快速试验不同配置的方法只有上面这个模板还不够每改一次配置都要重新编译运行太慢。我后来在这个实验工程里加了一个小技巧在界面上放几个按钮运行时直接通过Context::send_viewport_cmd切换窗口状态。这样不用重新编译就能直观看到“全屏和非全屏”“置顶和非置顶”之间的差别。fn update(mut self, ctx: egui::Context, _frame: mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { ui.heading(窗口控制实验); let mut fullscreen false; if ui.button(切换全屏).clicked() { fullscreen !fullscreen; ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(fullscreen)); } if ui.button(窗口置顶).clicked() { ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel( egui::WindowLevel::AlwaysOnTop, )); } if ui.button(取消置顶).clicked() { ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel( egui::WindowLevel::Normal, )); } if ui.button(最小化).clicked() { ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(true)); } }); }ViewportCommand是运行时控制窗口的官方入口灵活度比NativeOptions高很多。我习惯把NativeOptions理解成“出生设置”ViewportCommand理解成“游戏中的技能”一个是启动时生效一个是运行中动态调用。后面第5章会单独出一份命令小抄。3. 窗口的“脸面”外观与形态配置3.1 大小、位置与缩放从内尺寸到最小尺寸窗口大小是最常调的。注意ViewportBuilder里的尺寸默认指内容区inner size不含标题栏和边框。这个概念很重要尤其在macOS上同样设置[960.0, 600.0]实际外面包一圈边框之后视觉尺寸会比Windows上大一点。常用方法viewport: egui::ViewportBuilder::default() .with_inner_size([960.0, 600.0]) // 初始大小 .with_min_inner_size([400.0, 300.0]) // 最小尺寸 .with_max_inner_size([1920.0, 1080.0]) // 最大尺寸 .with_position([100.0, 100.0]) // 窗口左上角位置逻辑坐标 .with_resizable(true) // 是否允许用户拖拽改变大小位置和尺寸的单位是“逻辑像素”不是物理像素。在高DPI屏幕上操作系统会把逻辑坐标换算成物理坐标。如果你在Windows上发现窗口位置偏了多半是没考虑缩放比例。此时直接使用centered: true让eframe帮你居中比手动算坐标靠谱。let native_options eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([960.0, 600.0]), centered: true, ..Default::default() };我实际项目里的习惯是工具类小窗口只设with_inner_size加with_min_inner_size再配centered: true。编辑器类大窗口则交给persist_window去记忆用户上次调整好的状态这个后面会讲。3.2 标题、图标与无边框窗口标题有两个地方可以设容易搞混。一个在run_native的第一个参数eframe::run_native( window-lab, // 这个同时是 app_id native_options, Box::new(|_cc| Ok(Box::new(WindowLab::default()))), )另一个在ViewportBuilder::with_title(窗口实验室)。我的经验是run_native的第一个参数主要作为应用标识app_id在很多平台不能包含中文和空格最好用英文kebab-case。with_title才是用户能看到的窗口标题可以随便写中文。图标加载推荐用eframe::icon_data::from_png_bytes直接内嵌PNGviewport: egui::ViewportBuilder::default() .with_icon( eframe::icon_data::from_png_bytes(include_bytes!(assets/icon.png)) .expect(图标读取失败), )from_png_bytes内部会解析PNG并生成IconData省去手写解码逻辑。没有合适的PNG时也可以手动构造egui::IconData直接上RGBA数据。无边框窗口是很多人想玩的效果也是坑比较多的配置viewport: egui::ViewportBuilder::default() .with_decorations(false)设置后标题栏、关闭按钮、最小化按钮全没了。如果你还给用户保留了“拖动窗口”这个需求就要自己实现拖动逻辑。最省事的方案是拦截鼠标按下事件发送ViewportCommand::StartDragif ui.interact(ui.max_rect(), egui::Id::new(drag_bar), egui::Sense::click()) .drag_started() { ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag); }不过要提醒的是StartDrag只能在鼠标左键按下时调用。更稳妥的做法是做一个自定义标题栏区域在标题栏内按下时触发拖动其他地方不处理。3.3 全屏、最大化与置顶全屏和最大化容易混我一开始也搞不清。简单说Maximized是铺满工作区但还保留任务栏和标题栏Fullscreen是真正意义上的全屏连标题栏都隐藏。初始状态配置viewport: egui::ViewportBuilder::default() .with_maximized(true) // 启动即最大化 .with_fullscreen(true) // 启动即全屏注意with_fullscreen和with_maximized同时为true时全屏优先级更高。实际开发里很少在启动时直接全屏更多的是用户按快捷键或按钮进入全屏。这时用ViewportCommandctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true)); ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));全屏后有个常见问题退出不方便。尤其在macOS上全屏是进入独立Space空间的系统自带的退出手势需要用户学习。我一般会在应用里监听Esc键按下时退出全屏if ui.input(|i| i.key_pressed(egui::Key::Escape)) { ui.ctx().send_viewport_cmd(egui::ViewportCommand::Fullscreen(false)); }置顶的需求也很常见比如做悬浮工具条、画中画面板。我记得N久之前翻egui仓库置顶还没直接方法现在简单多了viewport: egui::ViewportBuilder::default() .with_always_on_top(true)运行时切换用ViewportCommand::WindowLevel效果更细粒度比如置底也可以做到。4. 渲染与平台层面的重要配置4.1 渲染器选型Glow和Wgpu怎么选NativeOptions里有个renderer字段就两个值eframe::Renderer::Glow和eframe::Renderer::Wgpu。我做过一个对比直接在项目里调这块感受挺明显维度GlowWgpu底层后端OpenGLVulkan / Metal / DirectX 12 / WebGPU兼容性老机器、虚拟机、远程桌面更稳现代图形特性更全但依赖驱动编译体积小较大适用场景小工具、教学Demo、快速原型需要与wgpu生态结合或做复杂渲染我现在的选择标准很简单默认Glow除非明确需要Wgpu。原因是Glow在Windows和Linux的兼容性覆盖面明显更广跑在配置不明的用户机器上踩坑概率小。如果你只是画几个窗口、按钮、表格Glow完全够用。let native_options eframe::NativeOptions { renderer: eframe::Renderer::Glow, ..Default::default() };如果选Wgpu在虚拟机或老显卡环境可能直接panic提示找不到适配器。这种场景下切回Glow是最快的解决方案。4.2 vsync、MSAA、深度与模板缓冲vsync: bool控制垂直同步。开着能避免画面撕裂但会限制帧率到显示器刷新率通常60Hz或120Hz。对界面应用来说我建议保持默认true。只有在做基准测试或追求极低输入延迟时才关。multisampling是MSAA采样数可以设0、2、4、8等。数值越大图形边缘越平滑性能开销也越大。egui本身在Shader层面已经做了内置抗锯齿对普通UI来说MSAA的提升不算特别明显。我一般设4兼顾效果和开销如果目标机器性能弱直接设0也没问题。depth_buffer和stencil_buffer这两个字段做纯UI基本用不到我就没见过谁的egui界面需要深度测试。设0即可。只有在自定义shader或与3D内容混合渲染时才需要关心。4.3 硬件加速与透明窗口hardware_acceleration有三个值Preferred、Required、Off。默认Preferred表示有可用GPU就加速没有就软件渲染兜底。Required在无GPU环境会直接报错Off则强制软件渲染。我在远程桌面场景遇到GPU不可用时的经验是主动设成Off或Preferred比让程序当场panic体验好得多。透明窗口是大家问得很多的功能。初始配置本身不复杂let native_options eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_transparent(true) .with_decorations(false), ..Default::default() };但光配这个还不够还要告诉egui“清屏颜色为透明”。默认清屏是灰色不覆盖就是一片灰底。impl eframe::App for WindowLab { fn clear_color(self, _visuals: egui::Visuals) - [f32; 4] { [0.0, 0.0, 0.0, 0.0] // RGBA全零完全透明 } }同时界面面板的背景也要设成透明否则面板的底色会挡住透明效果egui::CentralPanel::default() .frame(egui::Frame::none().fill(egui::Color32::TRANSPARENT)) .show(ctx, |ui| { ui.label(透明窗口); });要注意平台支持差异。macOS对透明窗口支持很好配合无边框可以做出很漂亮的悬浮球。Windows上开启透明后窗口阴影和圆角表现会有差异。Linux这边坑最多X11下很多合成器根本不理透明请求这就是为什么我建议在Linux测试透明窗口时先确认桌面环境是不是Wayland且开启了合成器。4.4 跟随系统和默认主题主题这件事在新版本里用一个字段就能搞定let native_options eframe::NativeOptions { theme: eframe::ThemePreference::System, ..Default::default() };System让窗口跟随系统深浅色系统切深色模式应用立刻跟着变体感很顺滑。Dark和Light则是固定主题。老版本写法是follow_system_theme和default_theme两个字段如果你是从低版本升级上来的记得合并成一个theme。我在0.28升级到0.29时被这个字段卡了好几分钟编译错误非常典型。另外程序运行中想动态切主题不一定要重启窗口。egui的Context::set_visuals可以在运行时切换风格ctx.set_visuals(egui::Visuals::dark());如果你的需求是界面里放一个“深色/浅色/跟随系统”的切换按钮用set_visuals配合手动保存一个主题状态变量是更自然的做法不需要动NativeOptions。5. 实战经验碰过的坑与速查表5.1 常见问题与排查实录做窗口配置这块有些坑是“版本初期必踩”我整理了一个速查表格后面再逐个解释症状可能原因解决建议启动报错找不到Wgpu适配器虚拟机、老显卡或驱动不兼容渲染器换成Glow或硬件加速设为Off窗口不居中、位置有偏移高DPI缩放导致坐标换算问题直接用centered: true别手动算坐标无边框窗口拖不动decorations(false)移除了系统拖拽区自己处理鼠标事件并发送StartDragLinux上透明窗口显示黑底平台或合成器不支持透明检查Wayland/compositor或放弃该平台透明窗口位置大小每次启动都“记仇”persist_window开启且本地配置已存在保留这功能即可它本来就是记忆窗口状态全屏后想退出但没入口系统没有自动加退出按钮监听Esc或做自己的退出按钮发送关闭/全屏命令升级版本后一堆字段编译不过老字段被移入ViewportBuilder或重命名看报错信息按新API迁移到ViewportBuilder拿“启动报错找不到Wgpu适配器”具体说。这个报错我遇到过好几次基本都发生在远程桌面环境或低配虚拟机。因为Wgpu要选后端Vulkan/DX12在这些环境经常没有直接就panic了。解决办法很简单把renderer改成Glow走OpenGL路径绝大多数环境都有软实现或老驱动能跑起来。另一个容易被忽略的是persist_window。这个字段在Windows和macOS上默认时false需要显式设置let native_options eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_app_id(com.example.myapp) // Windows下需要应用ID .with_title(我的应用), persist_window: true, ..Default::default() };打开之后窗口退出时会记住位置和大小下次启动自动恢复。对工具类应用是很加分的体验。有个细节Windows上要正常生效最好同时设置with_app_id。没有app_id时eframe会想办法从运行参数生成一个但多显示器场景下位置恢复偶尔不准确。我踩过之后现在一律显式写app_id。5.2 NativeOptions速查表把常用字段汇总成一张表适合贴在手边随时查字段值示例作用说明viewportViewportBuilder窗口初始状态统一入口占窗口配置80%的需求rendererGlow/Wgpu渲染后端选择默认Glowvsynctrue/false垂直同步开关默认truemultisampling0/4/8MSAA采样数UI场景4够用depth_buffer0深度缓冲位数纯UI设0stencil_buffer0模板缓冲位数纯UI设0hardware_accelerationPreferred/Required/OffGPU加速策略兼容性优先就Preferredcenteredtrue/false启动窗口是否居中比手动算坐标省心persist_windowtrue/false是否记忆并恢复窗口位置大小需配app_idthemeSystem/Dark/Light主题策略0.29用这个字段ViewportBuilder里的方法也要单独记一份方法作用with_inner_size/with_min_inner_size/with_max_inner_size设置内容区尺寸和限制with_position设置窗口位置with_title设置显示标题with_app_id设置应用标识持久化相关with_icon设置窗口图标传ArcIconDatawith_decorations是否显示标题栏和边框with_transparent是否支持透明背景with_resizable是否允许用户调整窗口大小with_maximized启动即最大化with_fullscreen启动即全屏with_always_on_top启动即置顶with_drag_and_drop是否接受系统拖放文件5.3 动态窗口控制ViewportCommand小抄前面提过NativeOptions是出生设置运行期要改状态用ViewportCommand。我整理了最常用的一批// 全屏 / 退出全屏 ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true)); // 最大化 / 还原 ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true)); // 最小化 ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(true)); // 关闭窗口 ctx.send_viewport_cmd(egui::ViewportCommand::Close); // 设置置顶或普通层级 ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel( egui::WindowLevel::AlwaysOnTop, )); // 运行时修改标题 ctx.send_viewport_cmd(egui::ViewportCommand::Title(新标题.to_string())); // 运行时修改窗口尺寸 ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize([800.0, 600.0])); // 无边框窗口的拖动 ctx.send_viewport_cmd(egui::ViewportCommand::StartDrag); // 强制窗口获得焦点 ctx.send_viewport_cmd(egui::ViewportCommand::Focus);这套命令最常用的两个场景一是快捷键全屏和退出全屏二是自定义标题栏。自定义标题栏这块值得多说一句。关掉系统装饰with_decorations(false)之后关闭按钮、最小化按钮都要自己画然后发送对应命令。我做过一个很简单的自定义标题栏三个按钮分别调Minimized(true)、Maximized(true)、Close实测体验和系统标题栏差距已经很小了。唯一要注意的是双击标题栏最大化这个系统行为在无边框模式原生不支持需要自己监听双击事件判断是否最大化。最后说一个搭配方案我最近做小工具都这么配置Glow渲染器、centered: true、persist_window: true加固定app_id、主题走ThemePreference::System、大小设[960.0, 600.0]最小[400.0, 300.0]。这套组合的兼容性最稳用户体感也最自然。窗口状态用户调一次就记住了下次打开还是原来的位置和大小这是非常提升好感度的小细节。
返回列表