终于还是到这一步了,可视化,我的超人!
FLTK是一个跨平台的轻量级 gui 库。该库本身是用 C++98 编写的,具有很高的可移植性。fltk crate 是用 Rust 编写的,并使用 FFI 调用 FLTK 包装器cfltk,它是用 C89 和 C++11 编写的。
虽然Rust里面不太兴面向对象编程,但是不可否认的是fltk却有浓浓的面向对象风格,不过这也要学了才知道的:
配置,Helloworld:
这个应该是有教学手册且最简单配置的rust GUI库,没有之一
话不多说直接Hello world:
[dependencies]
fltk = { version = "^1.2", features = ["fltk-bundled"] }
fn main() {
let a = app::App::default();
let mut wind = window::Window::new(100, 100, 800, 300, "Hello world");
wind.end();
wind.show();
a.run().unwrap();
}
我们成功的利用fltk创建出了一个窗口
APP结构
crate在app模块提供一个App结构。初始化App结构会初始化所有内部样式,字体,支持的图像模型,运行的多线程换环境等等
App 结构允许您使用 with_scheme() 初始化程序设置应用程序的全局方案:
let c = app::App::default().with_scheme(app::Scheme::Plastic);
官网给出了四种风格:Gtk,Basic、Plastic 和 Gleam等等。App是我们整个应用的承载。
Window
窗口是我们所有组件、图片的容器,FLTK 在它支持的每个平台上调用原生窗口,然后基本上是自己绘制(看来这个轮子不多,要自己画)。这意味着它在 Windows 上调用 HWND
关于窗口的定义请允许我重新展示一边:
let mut wind = window::Window::new(x, y, width, height, name);
- x, 画出窗口后距离屏幕左侧的水平距离
- y, 画出窗口后距屏幕顶部的垂直距离
- width: 窗口的宽度
- height: 窗口的高度
- name: 窗口的名字,或者说title标题
窗口时可以嵌套的,我们嵌套窗口的方法:
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 my_window2 = window::Window::new(10, 10, 380, 280, "");
my_window2.set_color(Color::Black);
// 关于end我会在下面写注释
my_window2.end();
my_window.end();
my_window.show();
app.run().unwrap();
}
窗口本身就是一个容器,容器之间的嵌套分子级父级关系的 。向我们上面的写法,我们只需主窗口调用end前定义,即可证明父子关系,这样对子组件的修饰甚至无需考虑执行顺序!,并且只需要主窗口调用show()即可。
let mut my_window = window::Window::new(100, 100, 400, 300, "My Window");
let mut my_window2 = window::Window::new(10, 10, 380, 280, "");
my_window.show();
my_window2.set_color(Color::Black);
app.run().unwrap();
如果完完全全的分开两个窗口,不构建父子关系,那么会创建新的窗口,,
let mut my_window = window::Window::new(100, 100, 400, 300, "My Window");
my_window.show();
let mut my_window2 = window::Window::new(10, 10, 380, 280, "");
my_window2.set_color(Color::Blue);
my_window2.show();
组件
fltk 提供了80多个组件,当然你让我去学完80多个组件不可能的好嘛
按钮:
let mut but = button::Button::new(160, 200, 80, 40, "Click me!");
钮的父级是 my_window,因为它是在隐式 begin() 和 end() 调用之间创建的。添加小部件的另一种方法是使用实现 GroupExt 特征的小部件提供的 add(widget) 方法:
let mut my_window2 = window::Window::new(10, 10, 380, 280, "");
my_window2.set_color(Color::Blue);
let but = button::Button::new(10,10,50,30,"button");
my_window2.add(&but);
按钮的 x 和 y 坐标是相对于包含该按钮的窗口的
此外,组件都可以使用构建器模式构建:
let but1 = Button::default().with_pos(10, 10).with_size(80, 40).with_label("Button 1");
对于这种可点击的组件,我们肯定是要绑定事件的:
let mut but = button::Button::new(10,10,50,30,"button");
but.set_callback(move |_| {println!("Hello world");});
这样我们每点击一次按钮就会运行此函数
除了最基本的按钮,按钮还包括很多。我将展示其中几个按钮的效果(怎么有种上个世纪GUI的感觉):
-
LightButton:
let mut but2 = button::LightButton::new(100,100,100,50,"button2");
-
CheckedButton:
let mut but2 = button::CheckButton::new(100,100,100,50,"button2");
-
RoundButton:
let mut but2 = button::RoundButton::new(100,100,100,50,"button2");
需要注意的是,对于某些比如我想选的RoundButton或者说CheckedButton, 我们可以使用value查看选中的值:
println!("{} {}",but2.value(),but3.value());
false true
所以我们可以构建枚举与数据结构完成整个页面的数据收集:
// 目前只假设两个按钮的数据收集
#[derive(Debug)]
enum button_status{
clicked(String),
unclicked(Option<i32>),
}
#[derive(Debug)]
struct checked{
but1_status:button_status,
but2_status:button_status,
}
//
impl checked {
fn new(but1_value: button_status, but2_value:button_status) -> checked {
checked{but1_status:but1_value, but2_status:but2_value}
}
}
fn main(){
let app = app::App::default();
let mut my_window = window::Window::new(100, 100, 400, 300, "My Window");
let mut my_window2 = window::Window::new(10, 10, 380, 280, "");
my_window2.set_color(Color::Blue);
let mut but1 = button::RoundButton::new(100,100,100,50,"button1");
let mut but2 = button::RoundButton::new(100,200,100,50,"button2");
// 清除边框
but1.clear_visible_focus();
but2.clear_visible_focus();
let mut but = button::Button::new(300,200,50,30,"Submit");
my_window.show();
but.set_callback(move |but| {
let mut but1_ = button_status::unclicked(None);
let mut but2_ = button_status::unclicked(None);
if but1.value(){
but1_ = button_status::clicked(but1.label());
}
if but2.value(){
but2_ = button_status::clicked(but2.label());
}
let p = checked::new(but1_,but2_);
println!("Data now: {:?}", p);
});
app.run().unwrap();
}
// 输出
Data now: checked { but1_status: unclicked(None), but2_status: clicked("button2") }
标签
FLTK没有标签部件,标签属于其他组件的自带部分。
FLTK标签非常的有意思,在某种程度上甚至有markdown的感觉,参见网址:https://fltk-rs.github.io/fltk-book/Labels.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CpbsvRPe-1653141672371)(https://www.fltk.org/doc-1.4/symbols.png)]
这个真的害挺好玩的。
菜单
菜单可以分为两种类型:选择型菜单与菜单栏
use fltk::{prelude::*, *};
fn main() {
let app = app::App::default();
let mut wind = window::Window::default().with_size(400, 300);
let mut choice = menu::Choice::default().with_size(80, 30).center_of_parent().with_label("Select item");
let mut button= button::Button::new(150,200,100,70,"submit");
choice.add_choice("Choice 1");
choice.add_choice("Choice 2");
choice.add_choice("Choice 3");
// You can also simply type choice.add_choice("Choice 1|Choice 2|Choice 3");
wind.end();
wind.show();
// 这种方法是由我们来处理回调
button.set_callback(move |c| {
match choice.choice(){
Some(a) =>{println!("{}",a)},
_ =>{println!("No chooesd");},
}
});
app.run().unwrap();
}
Choice1
在其他的GUI假面中都会有信号机制,其实在fltk中也有。还记得我们学过的send 和 recv么,在这里也可以用起来啊
// 官网示例,跑过可以用
use fltk::{prelude::*, *};
#[derive(Clone)]
enum Message {
Choice1,
Choice2,
Choice3,
}
fn main() {
let a = app::App::default();
let (s, r) = app::channel();
let mut wind = window::Window::default().with_size(400, 300);
let mut choice = menu::Choice::default()
.with_size(80, 30)
.center_of_parent()
.with_label("Select item");
choice.add_emit(
"Choice 1",
enums::Shortcut::None,
menu::MenuFlag::Normal,
s.clone(),
Message::Choice1,
);
choice.add_emit(
"Choice 2",
enums::Shortcut::None,
menu::MenuFlag::Normal,
s.clone(),
Message::Choice2,
);
choice.add_emit(
"Choice 3",
enums::Shortcut::None,
menu::MenuFlag::Normal,
s,
Message::Choice3,
);
wind.end();
wind.show();
while a.wait() {
if let Some(msg) = r.recv() {
match msg {
Message::Choice1 => println!("choice 1 selected"),
Message::Choice2 => println!("choice 2 selected"),
Message::Choice3 => println!("choice 3 selected"),
}
}
}
}
这个add_emit,或者说sender机制目前只在菜单栏这里看到过,有大佬用过其他API欢迎补充。总之菜单差不多这样就OK啦
而菜单栏,就是一堆菜单对单一块的栏目就是菜单栏啦,
use fltk::{prelude::*, *};
use fltk::enums::{FrameType, Shortcut};
fn main() {
let a = app::App::default();
let mut win = window::Window::new(300, 300, 800, 600, "New window");
// 添加我们的菜单栏,这个很好理解我就不说了
let mut menu = menu::SysMenuBar::default().with_size(800, 35);
// 要使用frame布局,这是布局的一部分后面会讲到
menu.set_frame(FrameType::FlatBox);
// sender和receiver上面有
let (s, r) = app::channel();
menu.add_emit(
"&test1/test1a\t",
Shortcut::Ctrl | 'a',
menu::MenuFlag::MenuHorizontal,
s.clone(),
String::from("this is test1/test1a"),
);
menu.add_emit(
"&test1/test1b\t",
Shortcut::empty(),
menu::MenuFlag::Inactive,
s.clone(),
String::from("this is test1/test1b"),
);
menu.add_emit(
"&test2/test2a\t",
Shortcut::empty(),
menu::MenuFlag::Normal,
s.clone(),
String::from("this is test2/test2a"),
);
menu.add_emit(
"&test2/test2b\t",
Shortcut::empty(),
menu::MenuFlag::Normal,
s.clone(),
String::from("this is test2/test2b"),
);
menu.add_emit(
"&test2/test2c\t",
Shortcut::empty(),
menu::MenuFlag::Normal,
s.clone(),
String::from("this is test2/test2c"),
);
win.show();
while a.wait(){
match r.recv(){
Some(msg) =>{println!("{}",msg);},
_ =>{continue}
}
}
}
Shoetcut主要是快捷键的设定,比如说:Shortcut::Ctrl | ‘a’, 就是用ctrl + a就能够唤醒的意思。menuflag主要是我们GUI显示的样式定义,比如menu::MenuFlag::MenuHorizontal显示快捷键啥的。这个页面长这个样子哦:
Input
Input就是文本框,没啥难的(简单示例):
fn main() {
let a = app::App::default();
let mut win = window::Window::new(300, 300, 800, 600, "New window");
let mut input_area = input::Input::default().with_size(100, 30).with_label("input your name").center_of_parent();
input_area.set_value("None");
let mut but = Button::new(350,500,100,50,"submit");
win.show();
but.set_callback(move |but|{
println!("Your name is {}", input_area.value());
// input_area.set_readonly(true);
});
a.run().unwrap();
}
在input中有一些好用的方法,比如:output是一个无法编辑的文本展示框,或者使用Input。set_readonly等方法
注意,无论Input的何种其他变体,我们开发者,或者说程序读到的都是字符串类型而不是其他类型
Valuator
说实话,我并不知道这个组件怎么称呼比较合适。Valuator包含滑块,滚动条一类的可以拖拽调节的东西:
use fltk::{prelude::*, *};
use fltk::button::Button;
use fltk::enums::{FrameType, Shortcut};
fn main() {
let a = app::App::default();
let mut win = window::Window::new(300, 300, 800, 600, "New window");
let mut border = valuator::ValueSlider::new(200,200,50,300,"Slider");
// 设置滚动条的最大与最小值
border.set_maximum(100.0);
border.set_minimum(0.0);
// 滚动条每次变化以1为单位
border.set_step(1.0,1);
// 初识位置为20,下同
border.set_value(20.0);
let mut c = valuator::Counter::new(400,200,200,50,"Counter");
c.set_value(0.8);
let mut d = valuator::Dial::new(400,400,100,100,"Dial");
d.set_maximum(100.0);
d.set_minimum(0.0);
d.set_step(0.5,1);
let mut e = valuator::Adjuster::new(600,0,200,100,"adjuster");
let mut f = valuator::FillSlider::new(0,0,200,100,"FillSlider");
d.set_callback(move |d|{
println!("{}", d.value());
});
e.set_callback(move |e|{
println!("{}", e.value());
});
win.show();
a.run().unwrap();
}
讲道理看着玩玩害挺不错,这一节也没有其他特别要注意的点,自己动手试试API挺好玩的
Text
主要目的是显示和编辑文本,需要TextBuffer缓冲区缓冲文本,因为肯定会涉及到文档保存的。
use fltk::{prelude::*, *};
fn main() {
let a = app::App::default();
let mut buf = text::TextBuffer::default();
let mut win = window::Window::default().with_size(800, 700).with_label("Editor");
let mut txt = text::TextEditor::default().with_size(790, 590).center_of_parent();
let mut button = button::Button::new(350,650,100,50,"Submit");
txt.set_buffer(buf.clone());
win.end();
win.show();
buf.set_text("Hello world!\nThis is a text editor!");
button.set_callback(move |button|{
println!("Do you want to save {}", buf.text());
});
a.run().unwrap();
}
我们可以使用一些DisplayExe提供的方法为我们的文本框添加一些更绚丽的效果。我们TextBuffer的作用不仅是缓冲数据,更可以缓冲样式
不过官网的例子导师有些花里胡哨了,我简单的更改字体与颜色
txt.set_buffer(buf);
txt.set_text_font(Font::Courier);
txt.set_text_size(16);
各种设置可以参见这里:https://docs.rs/fltk/latest/fltk/prelude/trait.DisplayExt.html#tymethod.style_buffer
fn main() {
let a = app::App::default();
let mut buf = text::TextBuffer::default();
let mut sbuf = text::TextBuffer::default();
let mut win = window::Window::default().with_size(400, 300);
let mut txt = text::TextEditor::default()
.with_size(390, 290)
.center_of_parent();
// 这里用clone才能追踪到buf的变化。
txt.set_buffer(buf.clone());
txt.set_text_font(Font::Courier);
txt.set_text_size(16);
win.end();
win.show();
let mut p = 0;
while a.wait(){
// 每换一行就会改变颜色的小玩具
let h = buf.text().lines().count() % 2;
if h == 1{
txt.set_text_color(Color::Blue);
}else{
txt.set_text_color(Color::Black);
}
}
}
Browser
此浏览器非我们上网用的浏览器,更像是任务管理器之流的浏览器,最要是有点类似表格一样的使用。
//官网的代码,演示的代码确实有点潦草可以理解:
use fltk::{prelude::*, *};
fn main() {
let app = app::App::default();
let mut win = window::Window::default().with_size(900, 300);
let mut b = browser::Browser::new(10, 10, 900 - 20, 300 - 20, "");
let widths = &[50, 50, 50, 70, 70, 40, 40, 70, 70, 50];
b.set_column_widths(widths);
b.set_column_char('\t');
// we can now use the '\t' char in our add method.
b.add("USER\tPID\t%CPU\t%MEM\tVSZ\tRSS\tTTY\tSTAT\tSTART\tTIME\tCOMMAND");
b.add("root\t2888\t0.0\t0.0\t1352\t0\ttty3\tSW\tAug15\t0:00\t@b@f/sbin/mingetty tty3");
b.add("erco\t2889\t0.0\t13.0\t221352\t0\ttty3\tR\tAug15\t1:34\t@b@f/usr/local/bin/render a35 0004");
b.add("uucp\t2892\t0.0\t0.0\t1352\t0\tttyS0\tSW\tAug15\t0:00\t@b@f/sbin/agetty -h 19200 ttyS0 vt100");
b.add("root\t13115\t0.0\t0.0\t1352\t0\ttty2\tSW\tAug30\t0:00\t@b@f/sbin/mingetty tty2");
b.add(
"root\t13464\t0.0\t0.0\t1352\t0\ttty1\tSW\tAug30\t0:00\t@b@f/sbin/mingetty tty1 --noclear",
);
win.end();
win.show();
app.run().unwrap();
}
browser特效格式写法参见:https://fltk-rs.github.io/fltk-book/Browsers.html