使用Rust编写快速、轻简的GUI程序:fltk-rs crate

警告
本文最后更新于 2022-10-16,文中内容可能已过时。

全文字数:3198词

阅读时长:大约5分钟(不包括所有代码)

使用Rust编写快速、轻简的GUI程序:fltk-rs crate

这是一篇推荐你使用Rust的文章……

“Rust,一门予每个人构建可靠且高效软件力的语言。”

截至2021年,在Stack Overflow的年度 Overview 上,Rust 已经连续六年成为最受开发者喜欢的编程语言!

Rust作为一门具有零开销抽象的系统编程语言,具有以生命周期和所有权借用的内存安全机制,使代码只要通过编译就没有内存安全问题。再加上rust具有极其活跃友好的社区,丰富的crate库,完备且大部分免费的参考资料,这让世界上的多数开发者喜欢上这门语言!正如Rust官网的介绍所说,作为一门编程语言,其最大特点在于“赋能”!

在学习编程语言时,尤其是在学习Rust这种学习曲线 极其陡峭(可能是目前最难学的)的语言时,保持自己对学习的兴趣很重要。而有什么能比编写出能看得到界面的程序更简单、能让人激动的呢?

于是,我利用业余时间将fltk-rs book翻译为中文(翻译水平不足,请大家谅解),方便大家学习fltk-rs,并使用Rust写出一个快速漂亮的程序!

由于国外网络有时不能访问,大家在公众号消息界面回复“fltk-rs”即可获得该电子书的中文和英文pdf文件,如果可以的话,请在GitHub上点个Star哦!

FLTK for Rust?

在 Crates.io 上,Rust已经有很多跨平台的GUI框架可供使用了,比如 Iced,Frui,egui 等等。这里我们为什么要选择从FLTK开始学习呢?

最该放在前面的理由就是“简单”!

FLTK,全名 Fast Light Toolkit。而它正如它的名字一样,又小又快。FLTK库本身是使用 C++ 98开发的,fltk-rs则是使用Rust实现的,它通过FFI(Foreign Function Interface)调用一个FLTK封装器 cfltk(该库使用 C89 和 C++11编写),来达到使用rust编写fltk程序的目的。

小知识:C++之父也在使用FLTK哦!

现在说说我们使用fltk的好处:

  • 构造简单,对习惯使用面向对象GUI库的开发者及其友好
  • 文档齐全,通过查阅文档可以解决几乎所有问题
  • 又轻又快,编译出的文件很小(iced一个测试文件占用100M以上,fltk则不到1M),运行时占用内存小,且快速
  • 跨平台,无需动态链接,这让你一次程序可以在Window,Linux,macOS甚至Android上运行(Android还是写原生比较好)
  • 组件丰富,具有图像支持,开发方式丰富,开源协议宽松(MIT License)

fltk也有些许缺点,比如对于复杂界面的编写没有很好的支持组件等等,但这些无伤大雅,对于入门rust GUI编写已经绰绰有余了。

下面,就让我们开始简单地学习下fltk-rs吧!

下面的内容可能有些草率,但这是让你看一下如何使用fltk-rs编写GUI程序。更全面地学习请参见fltk-rs官方文档和fltk-rs book。

配置

首先,确保你配置了 g++,CMake,git和make(linux上),同时可能还要安装一些必须的库。关于库的安装个其他平台工具链的配置请参阅fltk-rs book的Setup配置章节。

将以下代码添加到你的 Cargo.toml 文件:

1
2
[dependencies]
fltk = { version = "^1.3", features = ["fltk-bundled"] }

这基本上就可以了,Cargo会帮你下载和导入所有的库。

在这里我曾经踩到个坑,Cargo在下载库时可能会因为网络问题导致没有完全下载好所有需要的crate。但是,它不会再重新下载了,而且不会再给出提醒,这将导致一系列的链接错误!唯一的解决办法只有清楚cargo目录下缓存的crate文件,然后重新 cargo build下载,注意提示信息并保持网络通畅!

如果你也遇到了链接错误的问题,仔细想想cargo build 时有没有出错呢。

创建一个APP结构

fltk-rs crate在app模块中提供了一个App结构。初始化App结构可以初始化所有内部样式、字体和支持的图像类型。它还初始化了程序将要运行的多线程环境。

1
2
3
4
5
6
use fltk::*;

fn main() {
    let app = app::App::default();
    app.run().unwrap();
}

run方法运行gui应用程序的事件循环(event loop)。 要对事件进行精细的控制,可以使用wait()方法:

1
2
3
4
5
6
7
8
use fltk::*;

fn main() {
    let app = app::App::default();
    while app.wait() {
        // handle events
    }
}

此外,App结构允许你使用with_scheme()初始器来设置程序的全局主题:

1
2
3
4
5
6
use fltk::*;

fn main() {
    let app = app::App::default().with_scheme(app::Scheme::Gtk);
    app.run().unwrap();
}

这将使你的程序具有Gtk程序的样子。还有其他的内置方案,Basic、Plastic和Gleam。

App结构还负责在应用程序开始时使用load_system_fonts()方法加载系统字体。

一个典型的fltk-rs应用程序,将在创建任何部件和显示主窗口之前构建App结构。

任何在run()方法调用后添加的逻辑,将在事件循环结束后执行(通常是关闭应用程序的所有窗口时,或者调用quit()方法时)。该逻辑可能包括在必要时重启程序的逻辑。

除了App结构外,App模块本身还包含与你的程序的全局状态有关的结构和自由函数。其中包括设置背景和前景颜色、默认字体和大小等视觉效果、屏幕功能、剪贴板功能、全局处理器、应用事件、通道(channels)(发送器和接收器)和超时。

做出一个窗口

FLTK会在它支持的系统平台上调用原生窗口,然后基本上通过自己的方法来绘制。它会在windows上调用HWND,在MacOS上调用NSWindow,在X11系统(linux, BSD)上调用XWindow。

使用这样的代码可以创建一个window:

1
2
3
4
5
6
7
8
9
use fltk::{prelude::*, *};

fn main() {
    let app = app::App::default();
    let mut my_window = window::Window::new(100, 100, 400, 300, "My Window");
    my_window.end();
    my_window.show();
    app.run().unwrap();
}

调用new()函数需要五个参数:

  • x 以电脑屏幕最左侧为原点的水平距离。
  • y 以电脑屏幕最左侧为原点的垂直距离。
  • width window的宽度。
  • height window的高度。
  • title window标题。

接下来注意对end()的调用。window,以及其他类型的widget,实现了GroupExt trait。实现该trait的这些部件将 持有 任何在call()和end()间创建的widget(通过new()创建串口时,隐式调用了begin()),或者作为其父widget。 下一个调用show()唤起了window,使其出现在显示屏上。

放几个组件

FLTK提供了大约80个窗口组件。这些组件都实现了WidgetBase和WidgetExt的基本trait集。 我们已经遇到了我们的第一个组件,Window。 正如我们在Window widget中所看到的,小组件也可以根据其功能实现其他trait。 在我们之前写的例子中添加一个按钮:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use fltk::{prelude::*, *};

fn main() {
    let app = app::App::default();
    let mut my_window = window::Window::new(100, 100, 400, 300, "My Window");
    let mut but = button::Button::new(160, 200, 80, 40, "Click me!");
    my_window.end();
    my_window.show();
    app.run().unwrap();
}

注意,这个按钮的父组件是my_window,因为它是在begin()和end()之间创建的。 另一种添加组件的方法是,使用实现了GroupExt trait的widget所提供的add(widget)方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use fltk::{prelude::*, *};

fn main() {
    let app = app::App::default();
    let mut my_window = window::Window::new(100, 100, 400, 300, "My Window");
    my_window.end();
    my_window.show();

    let mut but = button::Button::new(160, 200, 80, 40, "Click me!");
    my_window.add(&but);

    app.run().unwrap();
}

另一件要注意的事情是按钮的初始化,它的构造函数基本上与Window相同,这是因为它实现了WidgetBase trait。注意,虽然Window的x和y坐标是相对于屏幕的,但按钮的x和y坐标却是相对于包含按钮的窗口的。你可能已经注意到,这也适用于我们在前一页的嵌入式窗口。

下面我们来看几个其他的示例,具体编写方法和原理请参见官方文档或book!

菜单和按钮的示例

这里我们使用闭包设置回调编写这个示例,实际上,fltk还可以允许你使用函数对象来处理回调,或者直接使用sender和message来编写。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
use fltk::{enums::*, prelude::*, *};

fn menu_cb(m: &mut impl MenuExt) {
    if let Some(choice) = m.choice() {
        match choice.as_str() {
            "New\t" => println!("New"),
            "Open\t" => println!("Open"),
            "Third" => println!("Third"),
            "Quit\t" => {
                println!("Quitting");
                app::quit();
            },
            _ => println!("{}", choice),
        }
    }
}

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let mut menubar = menu::MenuBar::new(0, 0, 400, 40, "rew");
    menubar.add("File/New\t", Shortcut::None, menu::MenuFlag::Normal, menu_cb);
    menubar.add(
        "File/Open\t",
        Shortcut::None,
        menu::MenuFlag::Normal,
        menu_cb,
    );
    let idx = menubar.add(
        "File/Recent",
        Shortcut::None,
        menu::MenuFlag::Submenu,
        menu_cb,
    );
    menubar.add(
        "File/Recent/First\t",
        Shortcut::None,
        menu::MenuFlag::Normal,
        menu_cb,
    );
    menubar.add(
        "File/Recent/Second\t",
        Shortcut::None,
        menu::MenuFlag::Normal,
        menu_cb,
    );
    menubar.add(
        "File/Quit\t",
        Shortcut::None,
        menu::MenuFlag::Normal,
        menu_cb,
    );
    let mut btn1 = button::Button::new(160, 150, 80, 30, "Modify 1");
    let mut btn2 = button::Button::new(160, 200, 80, 30, "Modify 2");
    let mut clear = button::Button::new(160, 250, 80, 30, "Clear");
    win.end();
    win.show();

    btn1.set_callback({
        let menubar = menubar.clone();
        move |_| {
            if let Some(mut item) = menubar.find_item("File/Recent") {
                item.add(
                    "Recent/Third",
                    Shortcut::None,
                    menu::MenuFlag::Normal,
                    menu_cb,
                );
                item.add(
                    "Recent/Fourth",
                    Shortcut::None,
                    menu::MenuFlag::Normal,
                    menu_cb,
                );
            }
        }
    });

    btn2.set_callback({
        let mut menubar = menubar.clone();
        move |_| {
            menubar.add(
                "File/Recent/Fifth\t",
                Shortcut::None,
                menu::MenuFlag::Normal,
                menu_cb,
            );
            menubar.add(
                "File/Recent/Sixth\t",
                Shortcut::None,
                menu::MenuFlag::Normal,
                menu_cb,
            );
        }
    });

    clear.set_callback(move |_| {
        menubar.clear_submenu(idx).unwrap();
    });

    a.run().unwrap();
}

一个逐渐消失的动画

我们使用了线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use fltk::{enums::*, prelude::*, *};

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    win.set_color(Color::White);
    // our button takes the whole left side of the window
    let mut sliding_btn = button::Button::new(0, 0, 100, 300, None);
    style_btn(&mut sliding_btn);
    win.end();
    win.show();

    sliding_btn.set_callback(|btn| {
        if btn.w() > 0 && btn.w() < 100 {
            return; // we're still animating
        }
        std:🧵:spawn({
            let mut btn = btn.clone();
            move || {
                while btn.w() != 0 {
                    btn.set_size(btn.w() - 2, btn.h());
                    app::sleep(0.016);
                    app::awake(); // to awaken the ui thread
                    btn.parent().unwrap().redraw();
                }
            }
        });
    });
    a.run().unwrap();
}

fn style_btn(btn: &mut button::Button) {
    btn.set_color(Color::from_hex(0x42A5F5));
    btn.set_selection_color(Color::from_hex(0x42A5F5));
    btn.set_frame(FrameType::FlatBox);
}

一个绘画板程序

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
use fltk::{
    app,
    draw::{
        draw_line, draw_point, draw_rect_fill, set_draw_color, set_line_style, LineStyle, Offscreen,
    },
    enums::{Color, Event, FrameType},
    frame::Frame,
    prelude::*,
    window::Window,
};
use std::cell::RefCell;
use std::rc::Rc;

const WIDTH: i32 = 800;
const HEIGHT: i32 = 600;

fn main() {
    let app = app::App::default().with_scheme(app::Scheme::Gtk);

    let mut wind = Window::default()
        .with_size(WIDTH, HEIGHT)
        .with_label("RustyPainter");
    let mut frame = Frame::default()
        .with_size(WIDTH - 10, HEIGHT - 10)
        .center_of(&wind);
    frame.set_color(Color::White);
    frame.set_frame(FrameType::DownBox);

    wind.end();
    wind.show();

    // We fill our offscreen with white
    let offs = Offscreen::new(frame.width(), frame.height()).unwrap();
    #[cfg(not(target_os = "macos"))]
    {
        offs.begin();
        draw_rect_fill(0, 0, WIDTH - 10, HEIGHT - 10, Color::White);
        offs.end();
    }

    let offs = Rc::from(RefCell::from(offs));

    frame.draw({
        let offs = offs.clone();
        move |_| {
            let mut offs = offs.borrow_mut();
            if offs.is_valid() {
                offs.rescale();
                offs.copy(5, 5, WIDTH - 10, HEIGHT - 10, 0, 0);
            } else {
                offs.begin();
                draw_rect_fill(0, 0, WIDTH - 10, HEIGHT - 10, Color::White);
                offs.copy(5, 5, WIDTH - 10, HEIGHT - 10, 0, 0);
                offs.end();
            }
        }
    });

    frame.handle({
        let mut x = 0;
        let mut y = 0;
        move |f, ev| {
            // println!("{}", ev);
            // println!("coords {:?}", app::event_coords());
            // println!("get mouse {:?}", app::get_mouse());
            let offs = offs.borrow_mut();
            match ev {
                Event::Push => {
                    offs.begin();
                    set_draw_color(Color::Red);
                    set_line_style(LineStyle::Solid, 3);
                    let coords = app::event_coords();
                    x = coords.0;
                    y = coords.1;
                    draw_point(x, y);
                    offs.end();
                    f.redraw();
                    set_line_style(LineStyle::Solid, 0);
                    true
                }
                Event::Drag => {
                    offs.begin();
                    set_draw_color(Color::Red);
                    set_line_style(LineStyle::Solid, 3);
                    let coords = app::event_coords();
                    draw_line(x, y, coords.0, coords.1);
                    x = coords.0;
                    y = coords.1;
                    offs.end();
                    f.redraw();
                    set_line_style(LineStyle::Solid, 0);
                    true
                }
                _ => false,
            }
        }
    });

    app.run().unwrap();
}

一个flutter风格的计数器

FLTK在风格化应用方面提供了许多东西,这里通过WindowExt trait完成Styling:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
use fltk::{
    enums::{Align, Color, Font, FrameType},
    prelude::*,
    *,
};

const BLUE: Color = Color::from_hex(0x42A5F5);
const SEL_BLUE: Color = Color::from_hex(0x2196F3);
const GRAY: Color = Color::from_hex(0x757575);
const WIDTH: i32 = 600;
const HEIGHT: i32 = 400;

fn main() {
    let app = app::App::default();
    let mut win = window::Window::default()
        .with_size(WIDTH, HEIGHT)
        .with_label("Flutter-like!");
    let mut bar =
        frame::Frame::new(0, 0, WIDTH, 60, "  FLTK App!").with_align(Align::Left | Align::Inside);
    let mut text = frame::Frame::default()
        .with_size(100, 40)
        .center_of(&win)
        .with_label("You have pushed the button this many times:");
    let mut count = frame::Frame::default()
        .size_of(&text)
        .below_of(&text, 0)
        .with_label("0");
    let mut but = button::Button::new(WIDTH - 100, HEIGHT - 100, 60, 60, "@+6plus");
    win.end();
    win.make_resizable(true);
    win.show();

    // Theming
    app::background(255, 255, 255);
    app::set_visible_focus(false);

    bar.set_frame(FrameType::FlatBox);
    bar.set_label_size(22);
    bar.set_label_color(Color::White);
    bar.set_color(BLUE);
    bar.draw(|b| {
        draw::set_draw_rgb_color(211, 211, 211);
        draw::draw_rectf(0, b.height(), b.width(), 3);
    });

    text.set_label_size(18);
    text.set_label_font(Font::Times);

    count.set_label_size(36);
    count.set_label_color(GRAY);

    but.set_color(BLUE);
    but.set_selection_color(SEL_BLUE);
    but.set_label_color(Color::White);
    but.set_frame(FrameType::OFlatFrame);
    // End theming

    but.set_callback(move |_| {
        let label = (count.label().parse::<i32>().unwrap() + 1).to_string();
        count.set_label(&label);
    });

    app.run().unwrap();
}
Buy me a coffee~
支付宝
微信
0%