写点什么

30 天拿下 Rust 之图形编程

作者:希望睿智
  • 2024-06-12
    安徽
  • 本文字数:4746 字

    阅读完需:约 16 分钟

30天拿下Rust之图形编程

💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。

概述

Rust 语言以其卓越的安全性、性能和可靠性赢得了广大开发者的青睐,逐渐在系统编程、网络服务、游戏开发等领域崭露头角。随着 Rust 生态的日益繁荣,图形编程领域也涌现出一批优秀的框架和库,使得用 Rust 进行高效、安全的图形应用开发成为可能。

图形库对比

在 Rust 中,有多个图形库可供选择,其中一些最流行的包括:GTK-rs、Iced、Egui 等。这些库提供了与 GPU 进行交互的接口,封装了底层的图形 API,使得开发者能够更轻松地构建图形应用。下面,我们将分别介绍这些图形库。

GTK-rs:作为 GTK 的 Rust 绑定,GTK-rs 借助 GTK 本身的广泛使用和成熟度,提供了跨平台的 GUI 开发能力。GTK 本身在 Linux 社区尤其流行,因此对于需要在 Linux 上开发桌面应用的 Rust 开发者而言,GTK-rs 是一个自然的选择。其活跃的社区、良好的文档和稳定的更新,使其保持较高的流行度。

Iced:Iced 以其简洁的 API 设计、现代的外观和对跨平台的支持受到欢迎。它强调快速、可靠和易于定制,适合开发轻量级到中等复杂度的桌面应用。Iced 有着活跃的开发进度、详尽的文档和教程,以及相对较大的用户群体,使其在 Rust GUI 库中占据显著位置。

Druid:Druid 提供了一个完整的桌面应用程序框架,结合了 Egui 图形库,强调高性能和自定义能力。虽然可能不如 GTK-rs 或 Iced 那么广为人知,但其新颖的设计理念和特定领域的优秀表现吸引了部分开发者关注。

Egui:Egui 是一个即时模式 GUI 库,以其小巧、易用和跨平台特性受到关注,特别适用于嵌入到游戏和其他应用程序中。Egui 的更新频繁,社区活跃度较高,且由于其轻量化和灵活性,可能在某些特定场景下被开发者广泛采用。

Fltk-rs:Fltk-rs 作为 FLTK C++ GUI 库的 Rust 绑定,以其小巧的尺寸、高效的性能和易用性著称,特别适合开发轻量级工具软件。其简单快速的开发流程和较小的运行时资源占用,可能会吸引一部分追求简洁和高效的开发者。

GTK-rs 库的使用

GTK-rs 是 Rust 的一个 GTK 绑定,它使得 Rust 开发者能够使用 GTK 库来创建跨平台的图形用户界面。GTK 库本身是一个非常流行且功能强大的 GUI 库,提供了丰富的控件和布局方式。通过使用 GTK-rs,Rust 开发者可以享受到 GTK 的便利和强大功能,同时保留 Rust 语言的类型安全和内存安全特性。

要使用 GTK-rs 库,首先,需要在 Rust 项目中添加 GTK-rs 的依赖。打开 Cargo.toml 文件,并添加如下内容。

[dependencies]gtk = "0.15"
复制代码

其次,需要先初始化 GTK 环境,这通常是在 main 函数的开始处使用 gtk::init()方法完成的。

接下来,我们可以通过 gtk::ApplicationWindow::new()方法创建一个窗口,并设置其标题和默认尺寸。

GTK-rs 库提供了丰富的控件供我们选择,比如:按钮、文本框等。在下面的示例代码中,我们首先创建了一个带有标签的按钮,连接了一个点击事件处理器。当按钮被点击时,它会打印一条消息到控制台。然后,我们将按钮添加到了窗口的内容区域。接下来,我们连接了一个删除事件处理器到窗口。当用户关闭窗口时,这个处理器会被调用,并调用 gtk::main_quit 来退出事件循环。最后,我们调用 gtk::main 来启动事件循环。

extern crate gtk;
use gtk::prelude::*;
fn main() { if gtk::init().is_err() { println!("Unable to initialize GTK."); return; } let window = gtk::ApplicationWindow::new(None::<&gtk::Application>); window.set_title("Demo"); window.set_default_size(600, 500);
let btn = gtk::Button::new_with_label("Click me"); btn.connect_clicked(move |_| { println!("Button clicked"); }); let content = window.get_content_area(); content.add(&btn); window.connect_delete_event(|_, _| { gtk::main_quit(); Inhibit(true) });
window.show_all(); gtk::main();}
复制代码

Iced 库的使用

Iced 是一个用于构建跨平台、高性能用户界面的 Rust 图形库。它采用了 Elm 架构和反应式编程模型,使得编写声明式、易于推理的 UI 代码成为可能。Iced 提供了一套丰富的 widget 库,可用于构建复杂的界面,并且支持原生的窗口系统集成,确保应用程序在 Windows、macOS、Linux 等平台上具有良好的用户体验。

在 Iced 库中,引入了一些图像编程相关的基本概念,主要包括:Widgets、Views、Events & Messages、Application Loop。

Widgets

Iced 中的 UI 是由一系列可重用的组件(称为 widgets)构建起来的。这些组件包括:按钮、文本、输入框、滑块、列表、表格等各种常见 UI 元素。每个 widget 都有自己的状态和样式属性,可以响应用户交互并触发事件。

Views

在 Iced 中,View 是一个函数,它定义了如何根据应用程序的状态生成特定的 UI 结构。当状态改变时,View 函数会被重新调用,生成新的 widget 树,这种机制确保了 UI 总是反映最新的应用程序状态。

Events & Messages

用户与 UI 的交互会产生 events,这些事件被传递给应用程序,通常触发状态更新。状态更新通过发送 messages 来完成,消息是应用程序内部定义的数据结构,用于描述状态变化请求。消息通过 update 函数处理,并最终导致状态变更和视图重新渲染。

Application Loop

Iced 应用程序遵循一个典型的工作循环,主要包括以下三个步骤。

1、Update: 处理传入的消息,更新应用程序状态。

2、Layout: 根据新的状态计算 widget 树的布局信息。

3、Paint: 使用布局信息和样式渲染 widget 到屏幕。

要使用 Iced 库,需要在 Rust 项目中添加 Iced 的依赖。打开 Cargo.toml 文件,并添加如下内容。

[dependencies]iced = { version = "0.12", features = ["canvas", "tokio", "debug"] }time = { version = "0.3", features = ["local-offset"] }
复制代码

在下面的示例代码中,我们实现了一个图形化的模拟时钟应用程序。该应用具备以下三个核心特性。

1、实时更新:应用程序每隔 500 毫秒通过订阅系统触发一次 Message::Tick 消息,更新当前显示的时间。

2、自定义外观:在画布(canvas)上绘制模拟时钟,包括:背景色、指针(时针、分针、秒针)及指针宽度。指针长度和旋转角度,与实际时间同步。

3、响应式布局:时钟容器采用响应式设计,填充其父容器的可用空间,并带有内边距。


use iced::executor;use iced::mouse;use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke};use iced::widget::{canvas, container};use iced::{ Application, Color, Command, Element, Length, Point, Rectangle, Renderer, Settings, Subscription, Theme, Vector,};
pub fn main() -> iced::Result { Clock::run(Settings { antialiasing: true, ..Settings::default() })}
struct Clock { now: time::OffsetDateTime, clock: Cache,}
#[derive(Debug, Clone, Copy)]enum Message { Tick(time::OffsetDateTime),}
impl Application for Clock { type Executor = executor::Default; type Message = Message; type Theme = Theme; type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) { ( Clock { now: time::OffsetDateTime::now_local() .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), clock: Cache::default(), }, Command::none(), ) }
fn title(&self) -> String { String::from("Clock") }
fn update(&mut self, message: Message) -> Command<Message> { match message { Message::Tick(local_time) => { let now = local_time;
if now != self.now { self.now = now; self.clock.clear(); } } }
Command::none() }
fn view(&self) -> Element<Message> { let canvas = canvas(self as &Self) .width(Length::Fill) .height(Length::Fill);
container(canvas) .width(Length::Fill) .height(Length::Fill) .padding(20) .into() }
fn subscription(&self) -> Subscription<Message> { iced::time::every(std::time::Duration::from_millis(500)).map(|_| { Message::Tick( time::OffsetDateTime::now_local() .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), ) }) }}
impl<Message> canvas::Program<Message> for Clock { type State = ();
fn draw( &self, _state: &Self::State, renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec<Geometry> { let clock = self.clock.draw(renderer, bounds.size(), |frame| { let center = frame.center(); let radius = frame.width().min(frame.height()) / 2.0;
let background = Path::circle(center, radius); frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8));
let short_hand = Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius));
let long_hand = Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius));
let width = radius / 100.0;
let thin_stroke = || -> Stroke { Stroke { width, style: stroke::Style::Solid(Color::WHITE), line_cap: LineCap::Round, ..Stroke::default() } };
let wide_stroke = || -> Stroke { Stroke { width: width * 3.0, style: stroke::Style::Solid(Color::WHITE), line_cap: LineCap::Round, ..Stroke::default() } };
frame.translate(Vector::new(center.x, center.y));
frame.with_save(|frame| { frame.rotate(hand_rotation(self.now.hour(), 12)); frame.stroke(&short_hand, wide_stroke()); });
frame.with_save(|frame| { frame.rotate(hand_rotation(self.now.minute(), 60)); frame.stroke(&long_hand, wide_stroke()); });
frame.with_save(|frame| { frame.rotate(hand_rotation(self.now.second(), 60)); frame.stroke(&long_hand, thin_stroke()); }); });
vec![clock] }}
fn hand_rotation(n: u8, total: u8) -> f32 { let turns = n as f32 / total as f32;
2.0 * std::f32::consts::PI * turns}
复制代码

执行该程序后,其运行效果大致如下。


总结

Rust 凭借其安全、高性能的特点,以及逐渐成熟的图形库和生态系统,已成为图形编程领域的一股重要力量,适用于从轻量级 GUI 应用到高性能游戏引擎的各种图形开发场景。随着社区的发展和新技术的融合,Rust 在图形编程领域的影响力有望进一步增强。


发布于: 刚刚阅读数: 5
用户头像

希望睿智

关注

一起学习,一起成长,一起进步! 2024-05-21 加入

中国科学技术大学毕业,在客户端、运营级平台、Web开发、嵌入式开发、深度学习、人工智能、音视频编解码、图像处理、流媒体等多个领域具备实战开发经验和技术积累,共发表发明专利十余项,软件著作权几十项。

评论

发布
暂无评论
30天拿下Rust之图形编程_rust_希望睿智_InfoQ写作社区