翻译最近更新于:2024/05/07

翻译已基本完成,受限于译者水平,内容有错误和不足,欢迎大家提交Issue 和 PullRequest 一起改进!

开始

欢迎来到 fltk-rs 教程 !

这是为 fltk crate 而写的一本说明书。 其他资源有:

FLTK 是一个跨平台的轻量级 GUI库。 该库自身是使用 C++98编写的,具有高度可移植性。 fltk crate 是使用 rust 编写的,它是通过FFI来调用一个 使用C89和C++11编写的FLTK封装器 cfltk

该库的构造极其简洁,对习惯使用面向对象GUI库的开发者比较友好。该封装本身也遵循简化文档的相同模型,因为方法的名称与C++所对应的函数是相同或类似的。这使得 FLTK C++ 的文档变得非常简单,因为这些方法基本上是相互对映的。

C++:

#include <FL/Fl_Window.H>

int main() {
    auto wind = new Fl_Window(100, 100, 400, 300, "My Window");
    wind->end();
    wind->show();
}

映射为Rust后:

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

fn main() {
    let mut wind = window::Window::new(100, 100, 400, 300, "My Window");
    wind.end();
    wind.show();
}

为什么选择 FLTK ?

  • 轻量。二进制文件简小,strip 后仅有大约1MB。 低内存占用
  • 快速。安装快、构建快、启动快、运行快。
  • 仅有一个运行文件。不需要配置DDL库。
  • 向前兼容,支持旧架构。
  • FLTK的允许性许可证,允许闭源应用静态链接。
  • 主题化 (4款默认支持的主题: Base, GTK, Plastic and Gleam),以及 fltk-theme 中的其他主题。
  • 提供了约80个可供自定义的 widget。
  • 内置图像支持。

用法

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

[dependencies]
fltk = "^1.4"

使用捆绑库(适用于 x64 windows (msvc & gnu (msys2)), x64 linux & macos):

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

该库提供了特定平台的绑定,它会自动编译,并使用静态链接的方式链接到你的二进制文件中。

现在编写我们的第一个示例,导入必要的 fltk 模块:

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

fn main() {
    let mut wind = window::Window::new(100, 100, 400, 300, "My Window");
    wind.end();
    wind.show();
}

运行这段示例,你会发现并没有什么反应。我们还需要使用一行代码运行事件循环(event loop),这相当于在C++中使用Fl::run()

use fltk::{app, prelude::*, window::Window};

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

这段代码中,我们实例化了 App 结构,它会初始化运行时(runtime)和样式(styles)。在程序的末尾,我们调用 run() 函数来让程序正常工作。

贡献本书

这本书是使用 mdbook,根据 fltk-book 仓库的内容生成的。本书的作者为 Mohammed Alyousef,由 Flatig L 翻译为中文

你可能需要执行 cargo install mdbook. 更多说明可以在fltk-book的README文件和mdbook的 用户指南 中找到。

你也可以在这里贡献中文翻译 fltk-book-zh

配置

编译依赖

请确保你的电脑上配置了 Rust (version > 1.45),CMake (version > 3.11),Git, C++11 编译工具链,并设置好了PATH,这样便可以方便地构建跨平台程序。我们还提供了特定平台上fltk的捆绑库形式,可以通过启用fltk-bundle这个feature来启用(这里会用到curl来下载库,tar来解包)。如果你安装了 ninja-build 构建工具,你可以使用 "use-ninja" feature来启用。它可能会加快构建速度。

  • Windows:

    • MSVC: Windows SDK
    • Gnu: 无依赖
  • MacOS: 无依赖

  • Linux: 需要安装 X11 and OpenGL 头文件。具有图形用户界面的Linux发行版上带有这些库。

    基于 Debian 的Linux发行版,运行:

    sudo apt-get install libx11-dev libxext-dev libxft-dev libxinerama-dev libxcursor-dev libxrender-dev libxfixes-dev libpango1.0-dev libgl1-mesa-dev libglu1-mesa-dev
    

    基于 RHEL的Linux发行版,运行:

    sudo yum groupinstall "X Software Development" && yum install pango-devel libXinerama-devel libstdc++-static
    

    基于 Arch 的Linux发行版,运行:

    sudo pacman -S libx11 libxext libxft libxinerama libxcursor libxrender libxfixes pango cairo libgl mesa --needed
    

    Alpine Linux:

    apk add pango-dev fontconfig-dev libxinerama-dev libxfixes-dev libxcursor-dev
    
  • Android: Android Studio,Android Sdk, Android Ndk。

运行时依赖

  • Windows: None
  • MacOS: None
  • Linux: 您需要 X11 库以及用于绘图的 pango 和 cairo(如果要启用启用-glwindow 功能,还需要 OpenGL):
apt-get install -qq --no-install-recommends libx11-6 libxinerama1 libxft2 libxext6 libxcursor1 libxrender1 libxfixes3 libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libpangoxft-1.0-0 libglib2.0-0 libfontconfig1 libglu1-mesa libgl1

注意,如果您安装了编译依赖项(上一个标题),它也会自动安装运行时依赖项。

另外请注意,大多数图形桌面环境已经安装了这些库。如果你想在 CI/docker 中测试已构建的软件包(该环境下没有图形用户界面),上述这个清单将很有用。

配置细节

这一部分将假设你没有安装Rust,分几个不同的环境进行讨论:

Windows (MSVC toolchain)

  • 访问rust语言官网的 开始
  • 按照 "Visual Studio C++ build tools "的链接,下载MSVC编译器和Windows sdk。
  • 使用安装器安装:

image

确保选中这些:

image

  • 你可以在其中查看有没有CMake安装选项,或者直接点这里下载 Cmake
  • 如果你还没有GIt,请点击下载 Git
  • 从 rust-lang.org 网站上,下载适合你的架构的正确的rustup安装程序。
  • 一切准备好后,就可以用cargo new创建一个Rust项目,在Cargo.toml中添加fltk依赖,然后开始编写你的应用程序。

Windows (gnu toolchain)

如果你没有msys2,点击这里安装 msys2

  • 你可以通过pacman软件包管理器安装Rust工具链,或者通过前面所说的rustup(推荐)。注意,使用pacman安装需要显示指定你要使用gnu工具链(否则会默认安装MSVC工具链)。 你应该依据你电脑的架构安装合适的工具链。例如,64位设备应该安装x86_64-pc-windows-gnu工具链。 如果你决定通过软件包管理器安装Rust,请确保你得到的是mingw的变体,并且有正确的MINGW_PACKAGE_PREFIX(对于64位机器,这个环境变量相当于mingw-w64-x86_64)。
  • 假设你通过pacman安装了东西,打开mingw shell(注意,这里不是msys2 shell,它可以在msys2安装目录下找到,或者通过source shell mingw64)并运行以下内容:
    pacman -S curl tar git $MINGW_PACKAGE_PREFIX-rust $MINGW_PACKAGE_PREFIX-gcc $MINGW_PACKAGE_PREFIX-cmake $MINGW_PACKAGE_PREFIX-make --needed
    
    如果你打算使用ninja,可以用$MINGW_PACKAGE_PREFIX-ninja替换$MINGW_PACKAGE_PREFIX-make
  • 一切准备好后,就可以用cargo new创建一个Rust项目,在Cargo.toml中添加fltk依赖,然后开始编写你的应用程序。

MacOS

  • 运行下列代码安装Xcode命令行工具(它带有C++编译器):

     xcode-select --install
    

    按照说明执行步骤。或者可以不用XCode,直接用Homebrew安装clang或gcc:

  • 可以点击这里下载CMake。 或者,也可以跟上面一样使用Homebrew:

    brew install cmake
    
  • 安装Rust Toolchain:

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    

    按照默认设置进行即可。

  • 一切准备好后,就可以用cargo new创建一个Rust项目,在Cargo.toml中添加fltk依赖,然后开始编写你的应用程序。

Linux

  • 使用你的软件包管理器安装一个C++编译器,以及CMake,make,git。 以Debian/Ubuntu 为例:
    sudo apt-get install g++ cmake git make
    
  • 要使用FLTK的开发依赖项(dependencies-dev),你还可以使用软件包管理器。 对基于Debian的GUI发行版,运行下列代码:
    sudo apt-get install libx11-dev libxext-dev libxft-dev libxinerama-dev libxcursor-dev libxrender-dev libxfixes-dev libpango1.0-dev libgl1-mesa-dev libglu1-mesa-dev
    
    对于基于RHEL的GUI发行版,运行下列代码:
    sudo yum groupinstall "X Software Development" && yum install pango-devel libXinerama-devel
    
    对于基于Arch Linux的GUI发行版,运行下列代码:
    sudo pacman -S libx11 libxext libxft libxinerama libxcursor libxrender libxfixes pango cairo libgl mesa --needed
    
    对于Alpine linux:
    apk add pango-dev fontconfig-dev libxinerama-dev libxfixes-dev libxcursor-dev
    
  • 安装Rust Toolchain:
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
    一切按默认即可。
  • 一切准备好后,就可以用cargo new创建一个Rust项目,在Cargo.toml中添加fltk依赖,然后开始编写你的应用程序。

交叉编译

使用预编译包

如果你要为以下平台编译fltk程序的话,很幸运,它们已经有预编译包了:

  • x86_64-pc-windows-gnu
  • x86_64-pc-windows-msvc
  • x86_64-apple-darwin
  • aarch64-apple-darwin
  • x86_64-unknown-linux-gnu
  • aarch64-unknown-linux-gnu

通过rustup设置目标平台(target),然后调用进行编译:

rustup target add <your target> # 使用上列目标平台替换target
cargo build --target=<your target> --features=fltk-bundled

对于arch64-unknonw-linux-gnu,你可能需要指定链接器:

CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc cargo build --target=aarch64-unknown-linux-gnu --features=fltk-bundled

你可以在 .cargo/config.toml (HOME下全局配置或在项目根目录下局部配置)中指定好链接器,这样你就不需要在命令中使用了:

# .cargo/config.toml
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

之后便可以直接编译了:

cargo build --target=aarch64-unknown-linux-gnu --features=fltk-bundled

使用cross

如果你安装了docker,可以试试用 cross

cargo install cross
cross build --target=<your target>  # 使用你的target替换,cross build时Docker守护进程必须正在运行,不需要通过rustup添加target

如果你的target需要外部依赖项(比如在Linux上),你必须创建自定义Docker镜像,并经过如下步骤来进行交叉编译:

  1. 设置Cross.toml文件。

    例如,对一个有如下结构的项目来说:

    myapp
         |_src
         |    |_main.rs    
         |
         |_Cargo.toml
         |
         |_Cross.toml
         |
         |_arm64-dockerfile
    

    arm64-dockerfile则是自定义的Docker镜像文件(名称并不重要,只要确保Cross.toml指向该文件)的内容:

    FROM ghcr.io/cross-rs/aarch64-unknown-linux-gnu:edge
    
    ENV DEBIAN_FRONTEND=noninteractive
    
    RUN dpkg --add-architecture arm64 && \
        apt-get update && \
        apt-get install --assume-yes --no-install-recommends \
        libx11-dev:arm64 libxext-dev:arm64 libxft-dev:arm64 \
        libxinerama-dev:arm64 libxcursor-dev:arm64 \
        libxrender-dev:arm64  libxfixes-dev:arm64  libgl1-mesa-dev:arm64 \
        libglu1-mesa-dev:arm64 libasound2-dev:arm64 libpango1.0-dev:arm64
    

    注意库包名称后面的架构,如:libx11-dev:arm64。

    Cross.toml的内容:

    [target.aarch64-unknown-linux-gnu]
    dockerfile = "./arm64-dockerfile"
    
  2. 配置Cargo.toml文件:

    [target.aarch64-unknown-linux-gnu]
    pre-build = [""" \
    dpkg --add-architecture arm64 && \
    apt-get update && \
    apt-get install --assume-yes --no-install-recommends \
    libx11-dev:arm64 libxext-dev:arm64 libxft-dev:arm64 \
    libxinerama-dev:arm64 libxcursor-dev:arm64 \
    libxrender-dev:arm64  libxfixes-dev:arm64  libgl1-mesa-dev:arm64 \
    libglu1-mesa-dev:arm64 libasound2-dev:arm64 libpango1.0-dev:arm64 \
    """]
    
  3. 运行cross:

    cross build --target=aarch64-unknown-linux-gnu
    

    第一次运行可能会花较长时间

使用交叉编译 C/C++ toolchain

你需要有一个C/C++交叉编译器,还有设置好前面的方案中提到过的target,(通过rustup target add安装)。

对于Windows和MacOS,系统编译器已经可以向特定的target编译程序了。比如在MacOS上,如果你已经可以使用编译器编译fltk应用程序,你可以这样为其他平台编译(假设你有一个intel x86_64 mac):

rustup target add aarch64-apple-darwin
cargo build --target=arch64-apple-darwin

Linux 编译 64位Windows

在你能够为自己的设备正确编译之后,如果你想在Linux上为64位Windows交叉编译应用程序:

  • 你需要使用下列命令添加Rust target:
    rustup target add x86_64-pc-windows-gnu # 此时在arch上编译
    
  • 安装一个C/C++ 交叉编译器,比如Mingw toolchain。在基于Debian的发行部上,你可以运行:
    apt-get install mingw-w64 # 或者 gcc-mingw-w64-x86-64
    
    在基于RHEL的发行部上:
    dnf install mingw64-gcc
    
    在Arch上:
    pacman -S mingw-w64-gcc
    
    在Alpine上:
    apk add mingw-w64-gcc
    
  • 在项目根目录添加.cargo/config.toml (如果你想全局设置的话,也可以修改HOME目录下的相应文件),并指定链接器和打包工具:
    # .cargo/config.toml
    [target.x86_64-pc-windows-gnu]
    linker = "x86_64-w64-mingw32-gcc"
    ar = "x86_64-w64-mingw32-gcc-ar"
    
  • 运行build:
    cargo build --target=x86_64-pc-windows-gnu
    

x64 linux-gnu 编译 aarch64 linux-gnu

另一个例子是,在基于x86_64 debian的发行版上为基于arm64 debian的发行版进行编译: 假设你已经安装了cmake:

  • 使用下列命令添加 rust target:
    rustup target add aarch64-unknown-linux-gnu
    
  • 安装一个C/C++ 交叉编译器,比如Mingw toolchain。在基于Debian的发行版上,你可以运行:
    apt-get install g++-aarch64-linux-gnu
    
  • 为你的系统添加需要的架构:
    sudo dpkg --add-architecture arm64
    
  • 你可能需要将下列镜像添加到/etc/apt/sources.list以便下载:
    sudo sed -i "s/deb http/deb [arch=amd64] http/" /etc/apt/sources.list
    echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s) main multiverse universe" | sudo tee -a /etc/apt/sources.list
    echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-security main multiverse universe" | sudo tee -a /etc/apt/sources.list
    echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-backports main multiverse universe" | sudo tee -a /etc/apt/sources.list
    echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-updates main multiverse universe" | sudo tee -a /etc/apt/sources.list
    
    第一条命令改变当前镜像为该系统的 amd64 架构的镜像。其他命令则将 arm64 部分添加到 /etc/apt/sources.list 文件中。
  • 更新程序清单:
    sudo apt-get update
    
  • 为目标平台安装需要的依赖:
    sudo apt-get install libx11-dev:arm64 libxext-dev:arm64 libxft-dev:arm64 libxinerama-dev:arm64 libxcursor-dev:arm64 libxrender-dev:arm64 libxfixes-dev:arm64 libpango1.0-dev:arm64 libgl1-mesa-dev:arm64 libglu1-mesa-dev:arm64 libasound2-dev:arm64
    
    注意,软件包名称中的:arm64后缀。
  • 运行build:
    CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc cargo build --target=aarch64-unknown-linux-gnu
    
    你可以在 .cargo/config.toml (HOME下全局配置或在项目根目录下局部配置)中指定好链接器,这样你就不需要在命令中使用了:
    # .cargo/config.toml
    [target.aarch64-unknown-linux-gnu]
    linker = "aarch64-linux-gnu-gcc"
    
    之后便可以运行:
    cargo build --target=aarch64-unknown-linux-gnu
    

使用docker

直接使用目标平台的docker镜像可以让你免去使用cross交叉编译到不同linux target的麻烦。 你需要一个Dockerfile,来拉取你需要的target,并安装Rust和C++工具链以及所需的依赖。 例如,为allpine linux构建:

FROM alpine:latest AS alpine_build
RUN apk add rust cargo git cmake make g++ pango-dev fontconfig-dev libxinerama-dev libxfixes-dev libxcursor-dev
COPY . .
RUN cargo build --release

FROM scratch AS export-stage
COPY --from=alpine_build target/release/<your binary name> .

然后运行:

DOCKER_BUILDKIT=1 docker build --file Dockerfile --output out .

你的二进制文件将生成在./out目录中。 注意在alpine上,如果你通过rustup安装Rust,你可能需要在你的dockerfile中让musl-gcc和musl-g++指向相应的工具链(运行cargo build之前)。

RUN ln -s /usr/bin/x86_64-alpine-linux-musl-gcc /usr/bin/musl-gcc
RUN ln -s /usr/bin/x86_64-alpine-linux-musl-g++ /usr/bin/musl-g++

由于Rust工具链的这个问题Issue-61328,你可能还在编译时需要添加-C target-feature=-crt-static这个环境变量。

另一个例子是在 amd64 linux-gnu 编译 arm64 linux-gnu 程序:

FROM ubuntu:20.04 AS ubuntu_build

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qq
RUN	apt-get install -y --no-install-recommends lsb-release g++-aarch64-linux-gnu g++ cmake curl tar git make
RUN apt-get install -y ca-certificates && update-ca-certificates --fresh && export SSL_CERT_DIR=/etc/ssl/certs
RUN	dpkg --add-architecture arm64 
RUN sed -i "s/deb http/deb [arch=amd64] http/" /etc/apt/sources.list
RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s) main multiverse universe" | tee -a /etc/apt/sources.list 
RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-security main multiverse universe" | tee -a /etc/apt/sources.list 
RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-backports main multiverse universe" | tee -a /etc/apt/sources.list 
RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-updates main multiverse universe" | tee -a /etc/apt/sources.list 
RUN	apt-get update -qq && apt-get install -y --no-install-recommends -o APT::Immediate-Configure=0 libx11-dev:arm64 libxext-dev:arm64 libxft-dev:arm64 libxinerama-dev:arm64 libxcursor-dev:arm64 libxrender-dev:arm64 libxfixes-dev:arm64 libpango1.0-dev:arm64 libgl1-mesa-dev:arm64 libglu1-mesa-dev:arm64 libasound2-dev:arm64
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable --profile minimal -y

ENV PATH="/root/.cargo/bin:$PATH" \
	CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ \
	CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
    CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \
    CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ \
    PKG_CONFIG_PATH="/usr/lib/aarch64-linux-gnu/pkgconfig/:${PKG_CONFIG_PATH}"

RUN rustup target add aarch64-unknown-linux-gnu

COPY . .

RUN  cargo build --release --target=aarch64-unknown-linux-gnu

FROM scratch AS export-stage
COPY --from=ubuntu_build target/aarch64-unknown-linux-gnu/release/<your binary name> .

使用CMake文件

文件的路径可以传递给 CFLTK_TOOLCHAIN 环境变量:

CFLTK_TOOLCHAIN=$(pwd)/toolchain.cmake cargo build --target=<your target>

在较新版本的 CMake(3.20 以上)中,可以直接设置 CMAKE_TOOLCHAIN_FILE 环境变量。

CMake 文件的内容通常是,设置 CMAKE_SYSTEM_NAME 以及交叉编译器。 在 Linux/BSD 上还需要设置 PKG_CONFIG_EXECUTABLE 和 PKG_CONFIG_PATH。一个示例:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

set(triplet aarch64-linux-gnu)
set(CMAKE_C_COMPILER /usr/bin/${triplet}-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/${triplet}-g++)
set(ENV{PKG_CONFIG_EXECUTABLE} /usr/bin/${triplet}-pkg-config)
set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:/usr/lib/${triplet}/pkgconfig")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

注意 CMAKE_SYSTEM_PROCESSOR 通常是目标平台上 uname -m 的值,其他可能的值参见Possible Values。 我们将此示例中的triplet变量设置为 aarch64-linux-gnu,这是用于 gcc/g++ 编译器以及 pkg-config 的前缀。 这个triplet也等同于 Rust triplet aarch64-unknown-linux-gnu。 PKG_CONFIG_PATH 设置为包含我们target的 .pc 文件的目录,这些是 Linux/BSD 上的 cairo 和 pango 依赖项所必需的。 最后 4 个选项可以防止CMake混淆host/taregt(当前机器和交叉编译的目标机器)的 include/library 路径。

一个以Windows为target 的示例(使用 mingw 工具链):

set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR AMD64)
set(triplet x86_64-w64-mingw32)
set(CMAKE_C_COMPILER /usr/bin/${triplet}-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/${triplet}-g++)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

使用 cargo-xwin

如果要以 Windows 和 MSVC编译器/ABI 为 target,你可以安装 cargo-xwin

cargo install cargo-xwin

然后就可以通过下面的命令编译你的项目:

cargo xwin build --release --target x86_64-pc-windows-msvc

使用 fltk-config feature

fltk 提供了一个叫做 fltk-config 的脚本,它有点像 pkg-config。它会跟踪已安装的 FLTK 库路径以及必要的 cflagsldflags。由于 fltk-rs 需要 FLTK 1.4 版本,而在撰写本文时,大多数发行版还没有提供libfltk1.4,因此你必须从源码中为你所需目标手动构建。不过,一旦发行版开始提供 FLTK 1.4,使用起来就会变得非常简单(针对 arm64 gnu linux):

dpkg --add-architecture arm64
apt-get install libfltk1.4-dev:arm64
cargo build --target=aarch64-unknown-linux-gnu --features=fltk-config

如果您需要为不同的架构构建 FLTK,则需要使用 CMake 工具链(使用之前的文件):

git clone https://github.com/fltk/fltk --depth=1
cd fltk
cmake -B bin -G Ninja -DFLTK_BUILD_TEST=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=/full/path/to/toolchain/file.cmake
cmake --build bin
cmake --instal bin # 在托管环境下可能需要 sudo
# 对你的项目
cargo build --target=aarch64-unknown-linux-gnu --features=fltk-config

Fluid

FLTK提供了一个名为FLUID的,所见即所得的快速GUI程序开发工具,它可以方便地编写GUI程序。 目前在Youtube上有一个教你基于Rust使用它的视频教程: Use FLUID (RAD tool) with Rust

fl2rust crate将Fluid生成的.fl文件转换成Rust代码,并编译进你的程序中。 要获取更多详细信息,请查看它的官方仓库

你可以使用cargo install 安装 fltk-fluid 和 fl2rust crate 来使用FLUID。

cargo install fltk-fluid
cargo install fl2rust

然后运行:

fluid &

你也可以通过使用包管理器获取Fluid,这样的话它将作为一个单独的程序或者是fltk程序的一部分安装到你的系统中。

目前,fl2rust并不能确保生成的Rust代码的正确性。它的使用也只限于构造方法。

用法

为了演示用法,我们使用cargo new app创建一个新的Rust项目。 fl2rust将作为 build-dependdencies 添加到你的项目中:

# Cargo.toml
[dependencies]
fltk = "1"

[build-dependencies]
fl2rust = "0.4"

然后在编译文件build.rs(该文件会在编译Rust程序时运行)中调用它提供的方法来将.fl文件转换成Rust代码。

// build.rs
fn main() {
    use std::path::PathBuf;
    use std::env;
    println!("cargo:rerun-if-changed=src/myuifile.fl");
    let g = fl2rust::Generator::default();
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    g.in_out("src/myuifile.fl", out_path.join("myuifile.rs").to_str().unwrap()).expect("Failed to generate rust from fl file!");
}

我们在src目录下创建一个fluid文件 myuifile.fl。我们通过println!中的指令告诉Cargo,如果文件发生改变就重新运行脚本。这里的文件名和目录是为了演示而设置的,你可以自己选择目录,以及输入和输出的文件名。这里我们将myuifile.fl文件转换成myuifile.rs,它生成在OUT_DIR中,因此我们不会在src目录下看到它。 为了可以使用转换后的文件,你需要在src目录创建一个与上述的输出文件同名的文件:

touch src/myuifile.rs

然后使用 include 宏,载入生成的文件中的内容。

#![allow(unused)]
fn main() {
// src/myuifile.rs
#![allow(unused_variables)]
#![allow(unused_mut)]
#![allow(unused_imports)]
#![allow(clippy::needless_update)]

include!(concat!(env!("OUT_DIR"), "/myuifile.rs"));
}

最后我们就可以在main.rs中使用这些内容了:

// src/main.rs
use fltk::{prelude::*, *};
mod myuifile;

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

现在到了gui的编写部分,打开fluid:

fltk-fluid & # 如果从包管理器安装,只需要运行 fluid

&使终端将它作为一个独立的进程打开,所以我们仍然可以使用终端来使用cargo编译我们的代码,或者你可以打开另一个终端。

image

我们看到的是一个空窗口和一个菜单栏。编写程序的第一步是创建一个类:

image

现在会弹出一个对话框,我们直接点击 "OK "让它使用默认的名称(UserInterface)。现在你会看到我们刚才创建的类出现在下面的列表中:

image

接下来,再次点击new,为这个类添加一个构造函数:

image

同样使用它的默认名称,即make_window()

接下来我们添加一个窗口:

image

现在出现了一个新的窗口,我们可以拖动边框放大它:

image

双击窗口,这会弹出一个对话框,可以用来设置窗口的gui属性(在GUI标签下)、风格(在Style标签下)和类属性(在C++标签下)。

image

我们在GUI标签中给这个窗口设置My Window标签 ,然后在Style标签中把颜色改为白色:

image

在C++标签下,我们为这个窗口变量起个名字,my_win

image

现在,可以通过myuifile::UserInterface::my_win访问窗口了。

之后,用鼠标左键点击窗口,然后添加一个Button(按钮):

image

这次我们选择Button。在C++标签下,我们为这个按钮变量起一个名字,btn。在style下,改变按钮的颜色和标签的颜色。然后在GUI下,把它的标签(按钮显示的文字) "click me"。

image

可以拖动边框来调整大小,拖动按钮来改变它的位置。Fluid有一个Layout菜单,可以用它修改一组小部件(例如有很多按钮的情况),使其具有相同的布局/大小...等。

image

现在点击File/Save As...将文件保存在src目录下,命名为myuifile.fl

可以运行cargo run来看看能不能编译通过,但我们还没有调用make_window()方法,所以暂时还不会看到任何东西。 现在修改 src/main.rs 来让窗口可以显示出来,并为我们的按钮添加一个点击回调事件(callback)。

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

fn main() {
    let app = app::App::default();
    let mut ui = myuifile::UserInterface::make_window();
    let mut win = ui.my_win.clone();
    ui.btn.set_callback(move |b| {
        b.set_label("clicked");
        win.set_label("Button clicked");
        println!("Works!");
    });
    app.run().unwrap();
}

App 结构

Fltk crate的app模块中有一个App结构体。初始化App结构体,将会初始化所有内部样式、字体和支持的图像类型。它还会初始化程序将要在其中运行的多线程环境。

use fltk::*;

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

run方法将会启动gui程序的事件循环(event loop)。 如果要对事件进行细粒度控制,可以使用wait()方法:

use fltk::*;

fn main() {
    let app = app::App::default();
    while app.wait() {
        // 处理事件
    }
}

此外,可以在App的实例上使用with_scheme()来设置程序的全局主题:

use fltk::*;

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

这将你的程序的主题设置为GTK。还有其他的内置主题方案,Basic、Plastic和Gleam(有一个fltk-theme crate提供了更多主题,你也可以自定义主题让它看起来更好看(^ο^))。

还可以在App的实例上调用load_system_fonts()方法,让程序在启动时加载系统字体。

一个典型的fltk-rs程序将在创建任何组件并显示窗口之前创建App结构体。

任何写在调用run()方法后的代码,将在事件循环结束后执行(通常是关闭程序的所有窗口时,或者调用quit()方法时)。这包括必要时重启程序的指令等。

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

其中一些将在本书的其他部分讨论。

原文名词对照:
自由函数 - free functions
屏幕,剪切板功能 - screen functions, clipboard functions
全局事件处理器 - global handler
程序事件 - app events
通道,发送器和接收器 - channels, sender and receiver

窗口 Windows

FLTK在它支持的系统平台上调用原生窗口,然后基本上是自己绘制图形界面的。它会在windows上调用HWND,在MacOS上调用NSWindow,在X11系统(linux, BSD)上调用XWindow

Window 与FLTK的其他组件具有相同的接口,WidgetExt trait。这将在下一节讨论。

让我们用到目前为止学到的东西来创建一个Window。

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();
}

img1

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

  • x 从电脑屏幕最左侧开始计算的水平距离。
  • y 从电脑屏幕最上侧开始计算的垂直距离。
  • width Window的宽度。
  • height Window的高度。
  • title Window的标题。

这里还调用了 end() 方法。GroupExt Trait定义了begin()方法和end()方法,Window以及其他实现了该Trait的组件,将持有任何在 begin()end()方法间创建的组件(通过new()创建Window时,隐式调用了begin()),或者成为这些组件的父组件。 调用 show() 会让Window出现在屏幕上。

嵌入窗口

Window可以被嵌入到其他Window内:

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, None);
    my_window2.set_color(enum::Color::Black);
    my_window2.end();
    my_window.end();
    my_window.show();
    app.run().unwrap();
}

embed

在这里创建了第二个窗口my_window2,它会被嵌入到第一个窗口my_window里面。我们把它的颜色设为黑色,这样我们才能注意到它。注意,它的父组件是第一个Window。在父窗口外创建第2个窗口才会创建出两个独立的窗口,需要注意每个窗口都需要调用show()方法才会显示:

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

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

可以使用my_window.set_border(false)方法取消my_window的边框,实现无边框窗口:

image

set_border(bool) 方法也定义在WindowExt trait中,除了直线了WidgetExt TraitGroupExt Trait的组件外(实现WindowExt需要实现GroupExt,实现GroupExt需要实现WidgetExt),FLTK中的所有窗口类型都实现了该Trait。 所有的Trait可以在fltk crate的fltk::prelude模块中找到:

FLTK Trait文档

全屏

如果你想使用 fltk-rs 开发沉浸式的应用程序,为了充分使用屏幕空间,你可以在需要全屏显示的窗口上应用 fullscreen(bool) 方法,将其设为 true

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.fullscreen(true);
    my_window.end();
    my_window.show();
    app.run().unwrap();
}

GlutWindow

fltk-rs 通过 GlutWindow 提供了 OpenGL Glut 窗口的支持。 下面列举出了使用该组件需要添加的依赖以及其所有关联函数。

开发依赖

你的电脑需要配置有 GitCMake 才可以使用 GlutWindow

  1. 安装CMake和Git: 确保它们已经被正确安装,且已经配置在你的系统的PATH环境变量中. 可以点击这里从官网下载 CMakeGit

  2. 验证PATH的配置: 在安装上述软件后,测试是否可以通过命令行执行它们。 打开你的终端或命令提示符,输入cmake --version and git --version 来验证。

  3. 验证库路径: 若编译失败,提示无法找到 fltk_gl 库,你可能需要使用 -L 验证库路径是否正确。确定 fltk_gl 库在你系统中的位置,并在构建命令中添加适当的标记,例如:

    cargo build -L /path/to/fltk_gl/library
    

    /path/to/fltk_gl/library 替换为你的系统中 fltk_gl 的实际地址。

  4. 验证程序依赖: 仔细检查 Cargo.toml 文件中配置的依赖项是否正确,确保你添加了正确版本的 fltkfltk-sys 的依赖项:

    [dependencies]
    fltk = { version = "1.4.4", features = ["enable-glwindow"] }
    
  5. 清除缓存并重编译:如果上述步骤没有解决你的问题,尝试清除构建文件并重新进行编译。使用以下命令清理构建文件:

    cargo clean
    

    清理后重新运行编译:

    cargo build
    

    完成上述步骤后,你应该能够成功构建你的项目了。

Methods

此部分翻译内容较不准确,,请参见原文:[GlutWindow](https://fltk-rs.github.io/fltk-book/GlutWindow.html)
  • default(): 创建一个默认的初始化的窗口
  • get_proc_address(&self, s: &str): 获取一个OpenGL程序的地址
  • flush(&mut self): 强制窗口绘制并调用 draw() 方法
  • valid(&self): 返回表示OpenGL上下文是否存在的布尔值
  • set_valid(&mut self, v: bool): 标记OpenGL上下文为有效
  • context_valid(&self): 返回表示创建时上下文是否有效的布尔值
  • set_context_valid(&mut self, v: bool): 标记创建时上下文为有效
  • context(&self): 返回 GlContext.
  • set_context(&mut self, ctx: GlContext, destroy_flag: bool): 设置 GlContext.
  • swap_buffers(&mut self): 清除前后端缓冲区
  • ortho(&mut self): Sets the projection so 0,0 is in the lower left of the window and each pixel is 1 unit wide/tall.
  • can_do_overlay(&self): 返回表示 GlutWindow 是否可以覆盖的布尔值
  • redraw_overlay(&mut self): 重绘并覆盖
  • hide_overlay(&mut self): 隐藏覆盖层
  • make_overlay_current(&mut self): 使覆盖层即时
  • pixels_per_unit(&self): 返回每 unit/point 的像素值
  • pixel_w(&self): 返回窗口宽度的像素值
  • pixel_h(&self): 返回窗口高度的像素值
  • mode(&self): 返回 GlutWindow 的模式
  • set_mode(&mut self, mode: Mode): 设置 GlutWindow 的模式

可以查阅官方文档获取 GlWindow 的更多信息 官方文档.

示例

绘制一个三角形 OpenGL Triangle

use fltk::{
    prelude::*,
    *,
    image::IcoImage
};
use glow::*;
fn main() {
    let app = app::App::default();
    let mut win = window::GlWindow::default().with_size(800, 600);
    let icon: IcoImage = IcoImage::load(&std::path::Path::new("src/fltk.ico")).unwrap();
    win.make_resizable(true);
    win.set_icon(Some(icon));
    win.set_mode(enums::Mode::Opengl3);
    win.end();
    win.show();
    unsafe {
        let gl = glow::Context::from_loader_function(|s| {
            win.get_proc_address(s) as *const _
        });
        let vertex_array = gl
            .create_vertex_array()
            .expect("Cannot create vertex array");
        gl.bind_vertex_array(Some(vertex_array));
        let program = gl.create_program().expect("Cannot create program");
        let (vertex_shader_source, fragment_shader_source) = (
            r#"const vec2 verts[3] = vec2[3](
                vec2(0.5f, 1.0f),
                vec2(0.0f, 0.0f),
                vec2(1.0f, 0.0f)
            );
            out vec2 vert;
            void main() {
                vert = verts[gl_VertexID];
                gl_Position = vec4(vert - 0.5, 0.0, 1.0);
            }"#,
            r#"precision mediump float;
            in vec2 vert;
            out vec4 color;
            void main() {
                color = vec4(vert, 0.5, 1.0);
            }"#,
        );
        let shader_sources = [
            (glow::VERTEX_SHADER, vertex_shader_source),
            (glow::FRAGMENT_SHADER, fragment_shader_source),
        ];
        let mut shaders = Vec::with_capacity(shader_sources.len());
        for (shader_type, shader_source) in shader_sources.iter() {
            let shader = gl
                .create_shader(*shader_type)
                .expect("Cannot create shader");
            gl.shader_source(shader, &format!("#version 410\n{}", shader_source));
            gl.compile_shader(shader);
            if !gl.get_shader_compile_status(shader) {
                panic!("{}", gl.get_shader_info_log(shader));
            }
            gl.attach_shader(program, shader);
            shaders.push(shader);
        }
        gl.link_program(program);
        if !gl.get_program_link_status(program) {
            panic!("{}", gl.get_program_info_log(program));
        }
        for shader in shaders {
            gl.detach_shader(program, shader);
            gl.delete_shader(shader);
        }
        gl.use_program(Some(program));
        gl.clear_color(0.1, 0.2, 0.3, 1.0);
        win.draw(move |w| {
            gl.clear(glow::COLOR_BUFFER_BIT);
            gl.draw_arrays(glow::TRIANGLES, 0, 3);
            w.swap_buffers();
        });
    }
    app.run().unwrap();
}

gl-img

拖动旋转 Rotate

这个示例使用了 GlWindow 创建了一个 OpenGL 窗口,并在窗口中绘制了一个三角形,你可以通过拖动鼠标来使它旋转 示例地址.

rotate

组件 Widgets

FLTK提供了80多个组件。这些组件都实现了WidgetBaseWidgetExt组成的基本集合。 我们已经见过了我们的第一个组件,Window组件。 正如我们在Window组件中了解的,基于功能的不同,不同的组件还会各自实现其他Trait。 在我们之前写的例子中添加一个按钮:

uuse 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();
}
![image](https://user-images.githubusercontent.com/37966791/100937814-adfb4900-3504-11eb-8a6b-f42a4fb4e470.png)

注意,这个按钮的父组件是my_window,因为它是在隐式调用的begin()end()之间创建的。 在程序中添加组件的另一种方法是,在实现了GroupExt Trait的Widget上调用该Trait提供的add(widget)方法。

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相同,因为new()方法是定义在WidgetBase trait中的,而大部分组件都实现了这个Trait。注意,虽然Window的x和y坐标是相对于屏幕而言的,但按钮的x和y坐标却是相对于作为按钮父组件的的Window而言的。你可能已经注意到了这一点,这也适用于我们在上一节中提到的嵌入在另一个窗口中的窗口。

Button组件也实现了ButtonExt trait,它定义了一些有用的方法,比如设置快捷键Shortcut来通过其他方法触发我们的按钮。

也可以使用构建器模式来创建一个组件:

#![allow(unused)]
fn main() {
let but1 = Button::new(10, 10, 80, 40, "Button 1");
// 下面为构建器模式
let but1 = Button::default()
    .with_pos(10, 10)
    .with_size(80, 40)
    .with_label("Button 1");
}

它们的效果基本是相同的。

目前位置,我们的程序会显示一个窗口和其中的按钮。这个按钮可以点击,但什么用都没有!但别担心,在下一节中,我们将学习为它添加一些行为(Actions)。

按钮 Buttons

Button组件的用处很多,它有多种形式:

这些组件可以在 Button mod 中找到。 其中最简单的就是Button组件,它能在发生点击事件时执行一些行为。当然所有的按钮都可以这样:

use fltk::{app, button::Button, frame::Frame, prelude::*, window::Window};

fn main() {
    let app = app::App::default();
    let mut wind = Window::default().with_size(400, 300);
    let mut frame = Frame::default().with_size(200, 100).center_of(&wind);
    let mut but = Button::new(160, 210, 80, 40, "Click me!");
    wind.end();
    wind.show();

    but.set_callback(move |_| frame.set_label("Hello world"));

    app.run().unwrap();

}

其他一些按钮可以带有表示自己某些属性的其他值: 例如CheckButton, ToggleButton, LightButton 带有表示它们当前状态(比如,是否被选中)的信息。

单选按钮(RadioRoundButtonRadioLightButtonRadioButton)也带有它们的一些值,但在父组件(任何实现GroupExt的组件都可以作为父组件)中只有一个可以被选中。因此说,这些组件是可以访问到同一个组合中其他相应组件的值的:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let flex = group::Flex::default().with_size(100, 200).column().center_of_parent();
    // 用户同一时间只能选中一个按钮,选中后,另一个会被取消选中
    let btn1 = button::RadioRoundButton::default().with_label("Option 1");
    let btn2 = button::RadioRoundButton::default().with_label("Option 2"); 
    flex.end();
    win.end();
    win.show();
    a.run().unwrap();
}

可以用clear_visible_focus()方法来取消焦点(btn1.clear_visible_focus()

![image](https://user-images.githubusercontent.com/37966791/145727291-8be40de6-8ec6-4e57-bb29-fa0f0ac3b251.png)

其他可选择的按钮没有这个属性。

ButtonExt::value()方法会返回一个布尔值,表示一个按钮是否被选中:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let flex = group::Flex::default().with_size(100, 200).column().center_of_parent();
    let btn1 = button::CheckButton::default().with_label("Option 1");
    let btn2 = button::CheckButton::default().with_label("Option 2");
    let mut btn3 = button::Button::default().with_label("Submit");
    flex.end();
    win.end();
    win.show();

    btn3.set_callback(move |btn3| {
        if btn1.value() {
            println!("btn1 is checked");
        }
        if btn2.value() {
            println!("btn2 is checked");
        }
    });

    a.run().unwrap();
}

CheckButton还提供了一个方便的方法is_checked(),一系列RadioButton提供了is_toggled()用来判断:

![image](https://user-images.githubusercontent.com/37966791/145727325-7e5bb45f-674e-4bb2-81c8-27d0ee391d34.png)

默认情况下,可选择的按钮在创建时都是没有选中的,但这可以用set_value(),(CheckButton可以使用的)set_checked()和(RadioButton可以使用的)set_toggled()等方法来默认选中一个按钮:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let flex = group::Flex::default().with_size(100, 200).column().center_of_parent();
    let mut btn1 = button::CheckButton::default().with_label("Option 1");
    btn1.set_value(true);
    // 同样可以使用 btn1.set_checked(true)
    let btn2 = button::CheckButton::default().with_label("Option 2");
    let mut btn3 = button::Button::default().with_label("Submit");
    flex.end();
    win.end();
    win.show();

    btn3.set_callback(move |btn3| {
        if btn1.value() {
            println!("btn1 is checked");
        }
        if btn2.value() {
            println!("btn2 is checked");
        }
    });

    a.run().unwrap();
}
![image](https://user-images.githubusercontent.com/37966791/145727352-bf6dba5c-1a0c-4da4-8296-093e10470f0c.png)

组件效果预览

标签 Labels

FLTK没有可以用来显示文字的Label组件,但很多组件都具有Label属性。如果你想显示文本的话,你可以使用一个Frame组件,然后为它添加Label属性。

所有组件都可以使用::new()构造函数来创建并设置Label,或者也可以用with_label()set_label()来设置。

#![allow(unused)]
fn main() {
let btn = button::Button::new(160, 200, 80, 30, "Click");
}

这个按钮上带有 "Click" 文字。

我们也可以使用set_label()with_label()

#![allow(unused)]
fn main() {
let btn = button::Button::default().with_label("Click");
// 或者
let mut btn = button::Button::default();
btn.set_label("Click");
}

然而,使用::new()方法来添加Label属性,需要的字符串是类型&'static str,所以下面的代码不能正确运行:

#![allow(unused)]
fn main() {
let label = String::from("Click");  // label变量不是 &'static str
let mut btn = button::Button::new(160, 200, 80, 30, &label);
}

在这种情况下,你应该使用btn.set_label(&label);。原因是FLTK本身要求传入的字符串是类型是const char * ,对应于Rust中则是&'static str。这些字符串保存在程序的二进制代码中。如果你反汇编一个程序,是可以看到这些字符串的。这些字符串具有静态生命周期,因此FLTK在创建组件Label时默认不会保存下这些字符串。而当使用set_label()with_label()时,FLTK将调用Fl_Widget::copy_label(),并将字符串进行存储。

Label不限于文字,FLTK预定义了一些符号,在Label中可以转换成图像。

![symbols](https://www.fltk.org/doc-1.4/symbols.png)

@符号除了可以用来使用这些符号图像外,还可以加上下面这些格式化字符,其顺序和规则如下:

  • '#'表示强制进行规则的缩放,因此可以避免组件的形状被扭曲。
  • +[1-9]或-[1-9]可以改变缩放比例。
  • '$'是水平翻转符号,'%'是垂直翻转符号。
  • [0-9] - 旋转45度的倍数。是'5'和'6'时不会发生旋转,而其他数字则会使其指向数字键盘上那个键的方向。
  • '0xxxx',0后有四个数字表示角度,会使其按该度数旋转。

因此,如果要显示一个非常大的指向下方的箭头,你可以使用标签字符串"@+92->"。

符号和文本可以结合在一个Label中,但是符号必须放在文本的开头或结尾处。如果有多行文本,那么符号将被按比例放大以匹配所有行的高度:

![ex](https://www.fltk.org/doc-1.4/symbol-examples.png)

组控件 Group widgets

这里介绍的是fltk中的容器控件,包括窗口类型和在Group mod中的其他组件,如:Group,Scroll,Pack,Tile,Flex ...等等。 它们都实现了GroupExt Trait,该Trait中定义了end()方法,这些控件必须调用::end()方法来表示其包含的范围结束:

use fltk::{
    app,
    button::Button,
    prelude::{GroupExt, WidgetBase, WidgetExt},
    window::Window,
};

fn main() {
    let a = app::App::default();
    let mut win = Window::default().with_size(400, 300);
    let _btn = Button::new(160, 200, 80, 30, "Click");
    win.end();
    win.show();
    a.run().unwrap();
}

在上面的例子中,按钮 "_btn" 的父组件是Window。 在组控件调用end()后创建的其他组件将不被包含在该控件中,即会创建在这个组控件的外面。 但这些组件仍然可以使用::add(&other_widget)::insert添加进组控件中。

use fltk::{
    app,
    button::Button,
    prelude::{GroupExt, WidgetBase, WidgetExt},
    window::Window,
};

fn main() {
    let a = app::App::default();
    let mut win = Window::default().with_size(400, 300);
    win.end();
    win.show();

    let btn = Button::new(160, 200, 80, 30, "Click");
    win.add(&btn);
    
    a.run().unwrap();
}

另一个选择是重新调用组控件的begin()方法:

use fltk::{
    app,
    button::Button,
    prelude::{GroupExt, WidgetBase, WidgetExt},
    window::Window,
};

fn main() {
    let a = app::App::default();
    let mut win = Window::default().with_size(400, 300);
    win.end();
    win.show();

    win.begin();
    let _btn = Button::new(160, 200, 80, 30, "Click");
    win.end();

    a.run().unwrap();
}

多数实现GroupExt的组控件需要手动布局,但还有几个控件可以自动布局。比如Flex组件,它会在 布局 layout 中介绍。Pack需要设置子组件的高度(height)或宽度(width)进行布局,这取决于Pack是垂直的还是水平的。

Pack默认是垂直的(Vertical),我们只需要设置其中子组件的高度:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(200, 300);
    let mut pack = group::Pack::default_fill();
    pack.set_spacing(5);
    for i in 0..2 {
        frame::Frame::default().with_size(0, 40).with_label(&format!("field {}", i));
        input::Input::default().with_size(0, 40);
    }
    frame::Frame::default().with_size(0, 40); // 占位
    button::Button::default().with_size(0, 40).with_label("Submit");
    pack.end();
    wind.end();
    wind.show();

    app.run().unwrap();
}

image

要设置水平(horizontal)的Pack,我们需要手动设置with_type(),然后只需要设置其中子组件的宽度:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(300, 100);
    let mut pack = group::Pack::default_fill().with_type(group::PackType::Horizontal);
    pack.set_spacing(5);
    for i in 0..2 {
        frame::Frame::default().with_size(40, 0).with_label(&format!("field {}", i));
        input::Input::default().with_size(40, 0);
    }
    frame::Frame::default().with_size(40, 0); // 占位
    button::Button::default().with_size(40, 0).with_label("Submit");
    pack.end();
    wind.end();
    wind.show();

    app.run().unwrap();
}

菜单 Menus

FLTK提供了菜单组件,它们均实现了 MenuExt trait。Menu组件有下面这几种:

Menu主要有两方面的作用:

  1. 使用 add_choice() 方法添加菜单选项,然后在设置回调处理不同选项执行的操作:

    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");
        choice.add_choice("Choice 1");
        choice.add_choice("Choice 2");
        choice.add_choice("Choice 3");
        // 也可以直接输入 choice.add_choice("Choice 1|Choice 2|Choice 3");
        wind.end();
        wind.show();
    
        choice.set_callback(|c| {
            match c.value() {
                0 => println!("choice 1 selected"),
                1 => println!("choice 2 selected"),
                2 => println!("choice 3 selected"),
                _ => unreachable!(),
            }
        });
    
        app.run().unwrap();
    }
![image](https://user-images.githubusercontent.com/37966791/145727397-dd713782-9f8e-474b-b009-f2ebeb5170ea.png)

另外,你也可以解构出菜单选项的字符串来进行匹配:

```rust
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");
    choice.add_choice("Choice 1|Choice 2|Choice 3");
    wind.end();
    wind.show();

    choice.set_callback(|c| {
        if let Some(choice) = c.choice() {
            match choice.as_str() {
                "Choice 1" => println!("choice 1 selected"),
                "Choice 2" => println!("choice 2 selected"),
                "Choice 3" => println!("choice 3 selected"),
                _ => unreachable!(),
            }
        }
    });

    app.run().unwrap();
}
```
  1. 通过 add() 方法添加菜单选项,你需要在其中设置好每个选项的回调:

    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()
    
        choice.add(
            "Choice 1",
            enums::Shortcut::None,
            menu::MenuFlag::Normal,
            |_| println!("choice 1 selected"),
            );
        choice.add(
            "Choice 2",
            enums::Shortcut::None,
            menu::MenuFlag::Normal,
            |_| println!("choice 2 selected"),
            );
        choice.add(
            "Choice 3",
            enums::Shortcut::None,
            menu::MenuFlag::Normal,
            |_| println!("choice 3 selected"),
            );
    
        wind.end();
        wind.show();
    
        app.run().unwrap();
    }

    另外在 事件 Events 还会提到,你可以直接传递一个函数而不适用闭包:

    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();
    }

    此外,你还可以使用add_emit()方法来传递一个sender和一个message,这样就不用直接使用回调,而是可以集中在App::wait()中处理:

    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"),
                }
            }
        }
    }

你可能会问,为什么不直接用第一个例子那样简单的代码,还要使用其他更复杂的方式完成回调方法。其实每种方法都有它的用途。 对于简单的下拉菜单,用第一种方法会更加方便。对于程序的菜单栏,用第二种方法会更好,它可以让你为菜单选项设置快捷键Shortcuts和选项的类型MenuFlags(例如下拉菜单,选择选项,用于分隔的占位菜单等),另外你不用再像第一个例子一样,在菜单的回调中处理所有事件。使用add_emit()方法处理子菜单一样很容易,就像在编辑器示例中那样:

#![allow(unused)]
fn main() {
    let mut menu = menu::SysMenuBar::default().with_size(800, 35);
    menu.set_frame(FrameType::FlatBox);
    menu.add_emit(
        "&File/New...\t",
        Shortcut::Ctrl | 'n',
        menu::MenuFlag::Normal,
        *s,
        Message::New,
    );

    menu.add_emit(
        "&File/Open...\t",
        Shortcut::Ctrl | 'o',
        menu::MenuFlag::Normal,
        *s,
        Message::Open,
    );

    menu.add_emit(
        "&File/Save\t",
        Shortcut::Ctrl | 's',
        menu::MenuFlag::Normal,
        *s,
        Message::Save,
    );

    menu.add_emit(
        "&File/Save as...\t",
        Shortcut::Ctrl | 'w',
        menu::MenuFlag::Normal,
        *s,
        Message::SaveAs,
    );

    menu.add_emit(
        "&File/Print...\t",
        Shortcut::Ctrl | 'p',
        menu::MenuFlag::MenuDivider,
        *s,
        Message::Print,
    );

    menu.add_emit(
        "&File/Quit\t",
        Shortcut::Ctrl | 'q',
        menu::MenuFlag::Normal,
        *s,
        Message::Quit,
    );

    menu.add_emit(
        "&Edit/Cut\t",
        Shortcut::Ctrl | 'x',
        menu::MenuFlag::Normal,
        *s,
        Message::Cut,
    );

    menu.add_emit(
        "&Edit/Copy\t",
        Shortcut::Ctrl | 'c',
        menu::MenuFlag::Normal,
        *s,
        Message::Copy,
    );

    menu.add_emit(
        "&Edit/Paste\t",
        Shortcut::Ctrl | 'v',
        menu::MenuFlag::Normal,
        *s,
        Message::Paste,
    );

    menu.add_emit(
        "&Help/About\t",
        Shortcut::None,
        menu::MenuFlag::Normal,
        *s,
        Message::About,
    );

    if let Some(mut item) = menu.find_item("&File/Quit\t") {
        item.set_label_color(Color::Red);
    }
}

注意最后一个调用,它使用find_item()方法,在菜单中匹配到符合的选项,然后把它的Label颜色设为红色:

![image](https://user-images.githubusercontent.com/37966791/145727434-d66c6d55-018d-4341-9570-7c2864b5bf29.png)

系统菜单栏

在MacOS上,你可能更喜欢使用系统提供的菜单栏,它通常出现在屏幕的顶部。为此可以使用 SysMenuBar 组件。它与所有实现MenuExt Trait的组件具有相同的API,当程序为MacOS以外的其他目标平台编译时,该组件将变为一个普通的MenuBar

输入输出 Input & Output

FLTK提供的输入/输出组件均实现了InputExt trait。在InputOutput mod中可以找到这些组件:

  • Input
  • IntInput
  • FloatInput
  • MultilineInput
  • SecretInput
  • FileInput
  • Output
  • MultilineOutput

实现了InputExt trait的组件都会携带一个文本值,对应用户输入的文本,可以用value()方法获得文本值,用set_value()方法修改文本值:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let flex = group::Flex::default().with_size(100, 100).column().center_of_parent();
    let label = frame::Frame::default().with_label("Enter age");
    let input = input::IntInput::default();
    let mut btn = button::Button::default().with_label("Submit");
    flex.end();
    win.end();
    win.show();

    btn.set_callback(move |btn| {
        println!("your age is {}", input.value());
    });

    a.run().unwrap();
}

image

需要注意的是,我们使用了IntInput来限制用户只能输入整数值。虽然用户不能再输入字符了,但就开发者而言,value()获取到的文本值仍然是一个String

Output组件的值不能被用户修改,可以视作一个无法编辑的输入框:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let flex = group::Flex::default().with_size(200, 50).column().center_of_parent();
    let label = frame::Frame::default().with_label("Check this text:");
    let mut output = output::Output::default();
    output.set_value("You can't edit this!");
    flex.end();
    win.end();
    win.show();
    a.run().unwrap();
}

image

也可以使用InputExt::set_readonly(bool)方法将Input组件设置为只读:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let flex = group::Flex::default().with_size(100, 100).column().center_of_parent();
    let label = frame::Frame::default().with_label("Enter age");
    let mut input = input::IntInput::default();
    let mut btn = button::Button::default().with_label("Submit");
    flex.end();
    win.end();
    win.show();

    btn.set_callback(move |btn| {
        println!("your age is {}", input.value());
        input.set_readonly(true);
    });

    a.run().unwrap();
}

这会在用户输入内容并按下按钮后让文本框不可修改。

估值器 Valuators

FLTK提供的Valuator组件均实现了ValuatorExt trait。这些组件会在内部跟踪步长step,范围range和边界bound这些数据,你会在图形界面上看到它们的具体作用。 你可能在别的地方使用过ScrollbarSliders这些组件。可以在Valuator mod中找到这些组件:

  • Slider
  • NiceSlider
  • ValueSlider
  • Dial
  • LineDial
  • Counter
  • Scrollbar
  • Roller
  • Adjuster
  • ValueInput
  • ValueOutput
  • FillSlider
  • FillDial
  • HorSlider (Horizontal slider)
  • HorFillSlider
  • HorNiceSlider
  • HorValueSlider

在图形界面通过拖动等方式改变Valuator的值会触发其回调。Valuator的当前值可以通过value()方法来获取,可以用set_value()来设置其值。根据使用情况,你也可以获取和改变rangestep的值:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let mut slider = valuator::HorNiceSlider::default().with_size(400, 20).center_of_parent();
    slider.set_minimum(0.);
    slider.set_maximum(100.);
    slider.set_step(1., 1); // 设置步长为10
    slider.set_value(50.); // 设置开始
    win.end();
    win.show();

    slider.set_callback(|s| {
        println!("slider at {}", s.value());
    });
    a.run().unwrap();
}

image

下面列举了使用不同的 Valuator 组件实现这个例子的效果:

展开查看示例

Adjuster widget

Adjuster

Counter widget

Counter

Dial widget

Dial

FillDial widget

FillDial

FillSlider widget

FillSlider

HorFillSlider widget

HorFillSlider

HorNiceSlider widget

HorNiceSlider

HorSlider widget

HorSlider

HorValueSlider widget

HorValueSlider

LineDial widget

LineDial

NiceSlider widget

NiceSlider

Roller widget

Roller

Scrollbar widget

Scrollbar

Slider widget

Slider

ValueInput widget

ValueInput

ValueOutput widget

ValueOutput

ValueSlider widget

ValueSlider


Valuator 枚举

fltk::valuator枚举为Valuator组件提供了一些可用的特性。如果你要使用这些效果,请添加下面的代码:

#![allow(unused)]
fn main() {
let mut valuator_object = valuator::Counter::default().with_size(200, 50).center_of_parent();
// 为你的组件添加下面这行代码
valuator_object.clone().with_type(fltk::valuator::CounterType::Simple);
}

下面演示了 Counter, Dial, ScrollbarSlider 组件使用这些特性的示例:

点击查看枚举示例

CounterType::Normal

CounterTypeNormal

CounterType::Simple

CounterTypeSimple


DialType::Normal

DialTypeNormal

DialType::Line

DialTypeLine

DialType::Fill

DialTypeFill


ScrollbarType::Vertical

ScrollbarTypeVertical

ScrollbarType::Horizontal

ScrollbarTypeHorizontal

ScrollbarType::VerticalFill

ScrollbarTypeVerticalFill

ScrollbarType::HorizontalFill

ScrollbarTypeHorizontalFill

ScrollbarType::VerticalNice

ScrollbarTypeVerticalNice

ScrollbarType::HorizontalNice

ScrollbarTypeHorizontalNice


SliderType::Vertical

SliderTypeVertical

SliderType::VerticalFill

SliderTypeVerticalFill

SliderType::HorizontalFill

SliderTypeHorizontalFill

SliderType::VerticalNice

SliderTypeVerticalNice

SliderType::HorizontalNice

SliderTypeHorizontalNice


文字 Text

Text组件实现了DisplayExt trait。FLTK提供了3个文字组件,可以在text mod中找到:

  • TextDisplay
  • TextEditor
  • SimpleTerminal

文本组件的主要作用是显示或编辑文本。前两个部件需要一个TextBufferSimpleTerminal内部有一个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(400, 300);
    let mut txt = text::TextEditor::default().with_size(390, 290).center_of_parent();
    txt.set_buffer(buf.clone());
    win.end();
    win.show();

    buf.set_text("Hello world!");
    buf.append("\n");
    buf.append("This is a text editor!");

    a.run().unwrap();
}

image

在文本组件上,对内容的操作多数是使用TextBuffer完成的。可以用append()来添加文本,也可以用set_text()来设置Buffer的内容。 你可以使用DisplayExt::buffer()方法得到Buffer的Clone(TextBuffer内部存储了一个对实际Buffer的可变指针引用),继而可以通过它来操作Buffer:

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

fn main() {
    let a = app::App::default();
    let buf = 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();
    txt.set_buffer(buf);
    win.end();
    win.show();

    let mut my_buf = txt.buffer().unwrap();

    my_buf.set_text("Hello world!");
    my_buf.append("\n");
    my_buf.append("This is a text editor!");

    a.run().unwrap();
}

DisplayExt定义了很多管理文本属性的方法,例如可以设置何时换行,光标位置,字体,颜色,大小等。

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

fn main() {
    let a = app::App::default();
    let mut buf = text::TextBuffer::default();
    buf.set_text("Hello world!");
    buf.append("\n");
    buf.append("This is a text editor!");

    let mut win = window::Window::default().with_size(400, 300);
    let mut txt = text::TextDisplay::default().with_size(390, 290).center_of_parent();
    txt.set_buffer(buf);
    // 设置换行模式
    // 不同于 AtPixel 和 AtColumn, AtBounds不需要第二个参数
    // AtBounds 会设置文本到达输入框边界便会自动换行,对于大小可变的窗口很好用。
    txt.wrap_mode(text::WrapMode::AtBounds, 0);
    txt.set_text_color(Color::Red);
    win.end();
    win.show();

    a.run().unwrap();
}

image

TextBuffer还有第二个用途,它可以作为样式缓冲区(Style Buffer)。Style Buffer是你的Text Buffer的一个镜像,它使用样式表(包含字体、颜色和大小的配置)来为你的文本细粒度地设置样式,样式表中的样式本身是有索引的,具体说是使用相应的顺序字母作为索引:

use fltk::{
    enums::{Color, Font},
    prelude::*,
    *,
};

const STYLES: &[text::StyleTableEntry] = &[
    text::StyleTableEntry {
        color: Color::Green,
        font: Font::Courier,
        size: 16,
    },
    text::StyleTableEntry {
        color: Color::Red,
        font: Font::Courier,
        size: 16,
    },
    text::StyleTableEntry {
        color: Color::from_u32(0x8000ff),
        font: Font::Courier,
        size: 16,
    },
];

fn main() {
    let a = app::App::default();
    let mut buf = text::TextBuffer::default();
    let mut sbuf = text::TextBuffer::default();
    buf.set_text("Hello world!");
    // A是样式表中的第一个元素的索引,这里为“Hello world!”的每个字母应用A代表的样式
    sbuf.set_text(&"A".repeat("Hello world!".len())); 
    buf.append("\n"); 
    // 虽然针对换行的样式可能并没有显示出来,但是这里还需要将其写上,以免弄乱之后的文字样式
    sbuf.append("B"); 
    buf.append("This is a text editor!");
    sbuf.append(&"C".repeat("This is a text editor!".len()));

    let mut win = window::Window::default().with_size(400, 300);
    let mut txt = text::TextDisplay::default()
        .with_size(390, 290)
        .center_of_parent();
    txt.set_buffer(buf);
    txt.set_highlight_data(sbuf, STYLES.to_vec());
    win.end();
    win.show();

    a.run().unwrap();
}

image

Terminal的例子使用了SimpleTerminal和一个有样式的TextBuffer,点击这里查看这个例子 Terminal

阅览器 Browsers

Browser组件实现了BrowserExt trait,可以在Browser mod中找到以下类型的Browser:

  • Browser
  • SelectBrowser
  • HoldBrowser
  • MultiBrowser
  • FileBrowser
  • CheckBrowser

为了实例化一个阅览器,我们需要设置好每一项item的宽度,在add()方法中还需要使用分隔符来将每一个item分开:

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');
    // 现在在我们的`add()`方法中可以使用'\t'来制表符
    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();
}

image

可以使用@并在后面跟上格式化符来实现其他丰富的格式化效果:

  • '@.' 打印其余行,且让剩余的'@'符号无效
  • '@@' 打印其余以'@'开头的行
  • '@l' 使用大号字体(24 point)
  • '@m' 使用中号字体(18 point)
  • '@s' 使用小号字体(11 point)
  • '@b' 使用宽字体(adds FL_BOLD to font)
  • '@i' 使用斜体(adds FL_ITALIC to font)
  • '@f' 或 '@t' 使用等距字体 (sets font to FL_COURIER)
  • '@c' 水平居中
  • '@r' 向右对齐文本
  • '@B0', '@B1', ... '@B255' 使用fl_color(n)填充背景
  • '@C0', '@C1', ... '@C255' 使用fl_color(n)渲染文本
  • '@F0', '@F1', ... 使用 fl_font(n) 渲染文本
  • '@S1', '@S2', ... 使用相应的尺寸来渲染文本
  • '@u' or '@_' 字体添加下划线
  • '@-' 字体中间添加修改线

在下面的例子中,我们在%CPU前面加上@C88,将其渲染成红色:

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');
    b.add("USER\tPID\t@C88%CPU\t%MEM\tVSZ\tRSS\tTTY\tSTAT\tSTART\tTIME\tCOMMAND");
    win.end();
    win.show();
    app.run().unwrap();
}

图像 image

这些颜色表示遵循FLTK的色彩映射规则,可以从0到255进行索引。

色彩映射 colormap

树 Trees

Tree组件可以实现元素按照的。你可能会想,它是不是定义在TreeExt trait中,哈哈哈,然而这里并没有。所有方法都是来自Tree结构体类型。可以使用add()方法为Tree添加元素:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let mut tree = tree::Tree::default().with_size(390, 290).center_of_parent();
    tree.add("Item 1");
    tree.add("Item 2");
    tree.add("Item 3");
    win.end();
    win.show();

    a.run().unwrap();
}

image

Item下还可以子Item,可以使用正斜线分隔符/来添加:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let mut tree = tree::Tree::default().with_size(390, 290).center_of_parent();
    tree.add("Item 1");
    tree.add("Item 2");
    tree.add("Item 3");
    tree.add("Item 3/Subitem 1");
    tree.add("Item 3/Subitem 2");
    tree.add("Item 3/Subitem 3");
    win.end();
    win.show();

    a.run().unwrap();
}

image

看过上面代码的例子,你会发现树的根标签总是 "ROOT "。可以通过set_root_label()方法来设置根标签:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let mut tree = tree::Tree::default().with_size(390, 290).center_of_parent();
    tree.set_root_label("My Tree");
    tree.add("Item 1");
    tree.add("Item 2");
    tree.add("Item 3");
    tree.add("Item 3/Subitem 1");
    tree.add("Item 3/Subitem 2");
    tree.add("Item 3/Subitem 3");
    win.end();
    win.show();

    a.run().unwrap();
}

image

还能调用set_show_root(false)方法来隐藏根标签。

可以使用first_selected_item()方法来获取被点击到的元素:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let mut tree = tree::Tree::default().with_size(390, 290).center_of_parent();
    tree.set_show_root(false);
    tree.add("Item 1");
    tree.add("Item 2");
    tree.add("Item 3");
    tree.add("Item 3/Subitem 1");
    tree.add("Item 3/Subitem 2");
    tree.add("Item 3/Subitem 3");
    win.end();
    win.show();

    
    tree.set_callback(|t| {
        if let Some(item) = t.first_selected_item() {
            println!("{} selected", item.label().unwrap());
        }
    });

    a.run().unwrap();
}

image

现在我们的Tree中的元素只能单选,让我们把它改成允许多选的树(这里我们也改变了元素之间连线的样式):

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let mut tree = tree::Tree::default().with_size(390, 290).center_of_parent();
    // 设置Tree的选择模式为多选
    tree.set_select_mode(tree::TreeSelect::Multi);
    tree.set_connector_style(tree::TreeConnectorStyle::Solid);
    tree.set_connector_color(enums::Color::Red.inactive());
    tree.set_show_root(false);
    tree.add("Item 1");
    tree.add("Item 2");
    tree.add("Item 3");
    tree.add("Item 3/Subitem 1");
    tree.add("Item 3/Subitem 2");
    tree.add("Item 3/Subitem 3");
    win.end();
    win.show();

    
    tree.set_callback(|t| {
        if let Some(item) = t.first_selected_item() {
            println!("{} selected", item.label().unwrap());
        }
    });

    a.run().unwrap();
}

现在的问题是,我们需要获取选择到的所有选项,而不只是第一个被选中的项目,这里我们使用get_selected_items()方法,该方法返回一个Option<Vec>。这里我们使用item_pathname()来获取Item在树中的路径。

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let mut tree = tree::Tree::default().with_size(390, 290).center_of_parent();
    tree.set_select_mode(tree::TreeSelect::Multi);
    tree.set_connector_style(tree::TreeConnectorStyle::Solid);
    tree.set_connector_color(enums::Color::Red.inactive());
    tree.set_show_root(false);
    tree.add("Item 1");
    tree.add("Item 2");
    tree.add("Item 3");
    tree.add("Item 3/Subitem 1");
    tree.add("Item 3/Subitem 2");
    tree.add("Item 3/Subitem 3");
    win.end();
    win.show();

    
    tree.set_callback(|t| {
        if let Some(items) = t.get_selected_items() {
            for i in items {
                println!("{} selected", t.item_pathname(&i).unwrap());
            }
        }
    });

    a.run().unwrap();
}

image

表格 Tables

FLTK还提供了Table组件,Table的使用方法可以参见GitHub库中的例子示例。有个好处是,我们提供了fltk-table crate,使用它的话我们可以写更少的样板代码,写出的界面也会更简单、直观。

extern crate fltk_table;

use fltk::{
    app, enums,
    prelude::{GroupExt, WidgetExt},
    window,
};
use fltk_table::{SmartTable, TableOpts};

fn main() {
    let app = app::App::default().with_scheme(app::Scheme::Gtk);
    let mut wind = window::Window::default().with_size(800, 600);

    // 通过 TableOpts 结构体设置行和列
    let mut table = SmartTable::default()
    .with_size(790, 590)
    .center_of_parent()
    .with_opts(TableOpts {
        rows: 30,
        cols: 15,
        editable: true,
        ..Default::default()
    });
    
    wind.end();
    wind.show();

    // 用一些值填充表格
    for i in 0..30 {
        for j in 0..15 {
            table.set_cell_value(i, j, &(i + j).to_string());
        }
    }

    // 把 第4行第5列 的表格设置为"another", 需要注意索引是从0开始的
    table.set_cell_value(3, 4, "another");

    assert_eq!(table.cell_value(3, 4), "another");

    // 防止按 Esc键 时关闭窗口
    wind.set_callback(move |_| {
        if app::event() == enums::Event::Close {
            app.quit();
        }
    });

    app.run().unwrap();
}

fltk-table

自定义组件 Custom widgets

fltk-rs允许你创建自定义组件。我们需要定义一个Struct来作为自定义组件的类型,我们需要用一个已经存在的Widgetwidget type来扩展它。最基本的Widget typewidget::Widget

  1. 定义你的Struct,以及它需要维护的内部数据:

    #![allow(unused)]
    fn main() {
    use fltk::{prelude::*, *};
    use std::cell::RefCell;
    use std::rc::Rc;
    
    struct MyCustomButton {
        inner: widget::Widget,
        num_clicks: Rc<RefCell<i32>>,
    }
    }

你会注意到两件事,我们正在使用一个Rc<RefCell<T>>来存储我们需要用到的数据。在一般情况下这是没有必要的。但是,在它的回调方法被调用时它的所有权会被移动,为了在执行完一次回调之后仍能使用它,我们将把它包装在一个Rc<RefCell<>>中。这段代码中我们已经导入了必要的模块。

  1. 为组件实现方法。最重要的是要有构造函数,因为我们要通过它来初始化组件和内部数据:

    #![allow(unused)]
    fn main() {
    impl MyCustomButton {
    
        pub fn new(radius: i32, label: &str) -> Self {
            let mut inner = widget::Widget::default()
                .with_size(radius * 2, radius * 2)
                .with_label(label)
                .center_of_parent();
            inner.set_frame(enums::FrameType::OFlatBox);
            let num_clicks = 0;
            let num_clicks = Rc::from(RefCell::from(num_clicks));
            let clicks = num_clicks.clone();
            inner.draw(|i| { 
                // 我们需要实现绘制方法
                draw::draw_box(i.frame(), i.x(), i.y(), i.w(), i.h(), i.color());
                draw::set_draw_color(enums::Color::Black);
                // 设置文字的颜色
                draw::set_font(enums::Font::Helvetica, app::font_size());
                draw::draw_text2(&i.label(), i.x(), i.y(), i.w(), i.h(), i.align());
            });
            inner.handle(move |i, ev| match ev {
                enums::Event::Push => {
                    *clicks.borrow_mut() += 1; 
                    // 使 num_clicks 在点击时递增
                    i.do_callback(); 
                    // 执行我们使用 set_callback() 设置的回调方法
                    true
                }
                _ => false,
            });
            Self {
                inner,
                num_clicks,
            }
        }
    
        // 获得我们的按钮被点击的次数
        pub fn num_clicks(&self) -> i32 {
            *self.num_clicks.borrow()
        }
    }
    }
  2. 在我们的自定义组件上应用widget_extends!宏,该宏需要传入我们的小组件,它扩展的基本类型,以及结构体中表示该基本类型的成员。这是通过实现Deref TraitDerefMut Trait实现的。该宏还会自动为我们的自定义组件添加了其他函数和固定的方法(anchoring methods):

#![allow(unused)]
fn main() {
// 通过宏扩展widget::Widget
widget_extends!(MyCustomButton, widget::Widget, inner);
}

现在来试一试我们的自定义组件:

fn main() {
    let app = app::App::default().with_scheme(app::Scheme::Gleam);
    app::background(255, 255, 255); // 设置白色背景
    let mut wind = window::Window::new(100, 100, 400, 300, "Hello from rust");
    
    let mut btn = MyCustomButton::new(50, "Click");
    // 注意,set_color和set_callback是宏自动为我们实现了
    btn.set_color(enums::Color::Cyan);
    btn.set_callback(|_| println!("Clicked"));
    
    wind.end();
    wind.show();

    app.run().unwrap();
    
    // 打印我们的按钮被点击的数字,退出
    println!("Our button was clicked {} times", btn.num_clicks());
}

全部代码:

use fltk::{prelude::*, *};
use std::cell::RefCell;
use std::rc::Rc;

struct MyCustomButton {
    inner: widget::Widget,
    num_clicks: Rc<RefCell<i32>>,
}

impl MyCustomButton {

    pub fn new(radius: i32, label: &str) -> Self {
        let mut inner = widget::Widget::default()
            .with_size(radius * 2, radius * 2)
            .with_label(label)
            .center_of_parent();
        inner.set_frame(enums::FrameType::OFlatBox);
        let num_clicks = 0;
        let num_clicks = Rc::from(RefCell::from(num_clicks));
        let clicks = num_clicks.clone();
        inner.draw(|i| { 
            // 我们需要一个绘制的方法
            draw::draw_box(i.frame(), i.x(), i.y(), i.w(), i.h(), i.color());
            draw::set_draw_color(enums::Color::Black); // 设置文字颜色
            draw::set_font(enums::Font::Helvetica, app::font_size());
            draw::draw_text2(&i.label(), i.x(), i.y(), i.w(), i.h(), i.align());
        });
        inner.handle(move |i, ev| match ev {
            enums::Event::Push => {
                *clicks.borrow_mut() += 1;
                // 使 num_clicks 在点击时递增
                i.do_callback(); 
                // 执行我们使用 set_callback() 设置的回调方法
                true
            }
            _ => false,
        });
        Self {
            inner,
            num_clicks,
        }
    }

    // 获得我们的按钮被点击的次数
    pub fn num_clicks(&self) -> i32 {
        *self.num_clicks.borrow()
    }
}

// 通过宏扩展widget::Widget
widget_extends!(MyCustomButton, widget::Widget, inner);

fn main() {
    let app = app::App::default().with_scheme(app::Scheme::Gleam);
    // 设置背景为白色
    app::background(255, 255, 255);
    let mut wind = window::Window::new(100, 100, 400, 300, "Hello from rust");
    let mut btn = MyCustomButton::new(50, "Click");
    btn.set_color(enums::Color::Cyan);
    btn.set_callback(|_| println!("Clicked"));
    wind.end();
    wind.show();

    app.run().unwrap();
    
    // 打印我们的按钮被点击的数字,退出
    println!("Our button was clicked {} times", btn.num_clicks());
}

image

对话框 Dialogs

FLTK提供了文件对话框等一系列对话框类型

文件对话框 File dialogs

有2种文件对话框类型,系统原生的文件对话框和FLTK自己的文件对话框。原生文件对话框即进行文件选择时会弹出系统的文件选择窗口。对于Windows,弹出的便是Win32对话框,对于MacOS,弹出的是Cocoa对话框,对于其他Posix系统来说,它取决于你的桌面环境。在GNOME和其他基于GTK的桌面上,弹出的是GTK对话框,在KDE上弹出的是kdialog。

原生对话框 Native dialogs

这样可以调出一个原生对话框:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(400, 300);

    let mut btn = button::Button::default()
        .with_size(80, 30)
        .with_label("Select file")
        .center_of_parent();

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

    btn.set_callback(|_| {
        let mut dialog = dialog::NativeFileChooser::new(dialog::NativeFileChooserType::BrowseFile);
        dialog.show();
        println!("{:?}", dialog.filename());
    });

    app.run().unwrap();
}

image

运行这段代码会弹出一个系统原生的文件对话框,选择一个文件后将在终端打印出文件名。你需要为new()提供一个NativeFileChooserType类型的参数,它表明你该对话框要选择的文件类型,是多个文件还是文件夹之类的。这段代码我们选择了BrowseFile,你可以用BrowseDir替换掉来试试,也可试试多文件/目录的类型选项。如果你选择了多个文件,你可以使用filenames()方法得到一个包含多个文件名的Vector:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(400, 300);

    let mut btn = button::Button::default()
        .with_size(100, 30)
        .with_label("Select files")
        .center_of_parent();

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

    btn.set_callback(|_| {
        let mut dialog = dialog::NativeFileChooser::new(dialog::NativeFileChooserType::BrowseMultiFile);
        dialog.show();
        println!("{:?}", dialog.filenames());
    });

    app.run().unwrap();
}

你可以通过添加过滤器来限制文件类型:

#![allow(unused)]
fn main() {
    btn.set_callback(|_| {
        let mut dialog = dialog::NativeFileChooser::new(dialog::NativeFileChooserType::BrowseMultiFile);
        dialog.set_filter("*.{txt,rs,toml}");
        dialog.show();
        println!("{:?}", dialog.filenames());
    });
}

这将让对话框只能选取.txt.rs.toml文件。

FLTL提供的文件选择器 FLTK's own file chooser

FLTK也提供了自己的文件选择器:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(400, 300);

    let mut btn = button::Button::default()
        .with_size(100, 30)
        .with_label("Select file")
        .center_of_parent();

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

    btn.set_callback(|_| {
        let mut dialog = dialog::FileChooser::new(
            ".",                            /*对话框弹出时所在目录*/
            "*.{txt,rs,toml}",              /*文件类型限定*/
            dialog::FileChooserType::Multi, /*对话框类型*/
            "Select file:",                 /*对话框标题*/
        );
        dialog.show();
        while dialog.shown() {
            app::wait();
        }
        if dialog.count() > 1 {
            for i in 1..=dialog.count() { // values start at 1
                println!(" VALUE[{}]: '{}'", i, dialog.value(i).unwrap());
            }
        }
    });

    app.run().unwrap();
}

image

使用file_chooser()dir_chooser()方法可以简化一些操作:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(400, 300);

    let mut btn = button::Button::default()
        .with_size(100, 30)
        .with_label("Select file")
        .center_of_parent();

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

    btn.set_callback(|_| {
        let file = dialog::file_chooser(
            "Choose File",
            "*.rs",
            /*start dir*/ ".",
            /*relative*/ true,
        );
        if let Some(file) = file {
            println!("{}", file);
        }
    });

    app.run().unwrap();
}

帮助文档对话框 Help dialog

FLTK提供了一个帮助文档对话框,可以用来将HTML文件转换为文档显示出来:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(400, 300);

    let mut btn = button::Button::default()
        .with_size(100, 30)
        .with_label("Show dialog")
        .center_of_parent();

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

    btn.set_callback(|_| {
        let mut help = dialog::HelpDialog::new(100, 100, 400, 300);
        help.set_value("<h2>Hello world</h2>"); // this takes html
        help.show();
        while help.shown() {
            app::wait();
        }
    });

    app.run().unwrap();
}

也可以用HelpDialog::load(path_to_html_file)方法加载一个HTML文件:

image

提示对话框 Alert dialogs

FLTK还提供了下面几种使用很方便的对话框类型,只需要调用相关函数即可显示:

  • message
  • alert
  • choice
  • input
  • password (类似于input,但会隐藏输入的内容)

显示一个简单的message对话框:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(400, 300);

    let mut btn = button::Button::default()
        .with_size(100, 30)
        .with_label("Show dialog")
        .center_of_parent();

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

    btn.set_callback(|_| {
        dialog::message_default("Message");
    });

    app.run().unwrap();
}

执行这段代码,将在默认位置(大致在鼠标位置)显示一个message对话框。如果你想手动设置显示的坐标,可以使用message()函数:

#![allow(unused)]
fn main() {
    btn.set_callback(|_| {
        dialog::message(100, 100, "Message");
    });
}

前面提到的这些函数有是两个变体变体的,其中一个是有 _default() 后缀,它不需要设置坐标,另一个没有后缀的则需要手动输入坐标。 有些对话框会返回一个值,比如choiceinputpasswordinputpassword返回用户输入的文本,而choice则返回选择值的索引:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(400, 300);

    let mut btn = button::Button::default()
        .with_size(100, 30)
        .with_label("Show dialog")
        .center_of_parent();

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

    btn.set_callback(|_| {
        // password() 和 input() 需要第二个参数来表示默认显示的值
        let pass = dialog::password_default("Enter password:", "");
        if let Some(pass) = pass {
            println!("{}", pass);
        }
    });

    app.run().unwrap();
}

image

使用choice的示例:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(400, 300);

    let mut btn = button::Button::default()
        .with_size(100, 30)
        .with_label("Show dialog")
        .center_of_parent();

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

    btn.set_callback(|_| {
        let choice = dialog::choice_default("Would you like to save", "No", "Yes", "Cancel");
        println!("{}", choice);
    });

    app.run().unwrap();
}

在用户选择按钮后将在控制台打印出选项索引,选择No将打印0,Yes将打印1,Cancel将打印2。

image

你可能已经发现这些对话框没有标题。你可以在调用对话框前调用这个函数来为对话框添加标题:

#![allow(unused)]
fn main() {
        dialog::message_title("Exit!");
        let choice = dialog::choice_default("Would you like to save", "No", "Yes", "Cancel");
}

你也可以在程序的开头调用dialog::message_title_default()来设置所有对话框的默认标题:

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

fn main() {
    let app = app::App::default();
    dialog::message_title_default("My App!");
    let mut wind = window::Window::default().with_size(400, 300);

    let mut btn = button::Button::default()
        .with_size(100, 30)
        .with_label("Show dialog")
        .center_of_parent();

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

    btn.set_callback(|_| {
        let choice = dialog::choice_default("Would you like to save", "No", "Yes", "Cancel");
        println!("{}", choice);
    });

    app.run().unwrap();
}

image

自定义对话框 Custom dialogs

上面这些对话框可能并不是你想要的样子,尤其是颜色和字体。如果你的程序是高度风格化的,你绝对还需要自定义对话框。对话框基本是在程序运行期间生成的一个模型窗口(Modal Windows),我们想要制作自定义对话框,只要制作一个窗口即可,它的主题与你为程序设置的主题将是一致的:

use fltk::{
    app, button,
    enums::{Color, Font, FrameType},
    frame, group, input,
    prelude::*,
    window,
};

fn style_button(btn: &mut button::Button) {
    btn.set_color(Color::Cyan);
    btn.set_frame(FrameType::RFlatBox);
    btn.clear_visible_focus();
}

pub fn show_dialog() -> MyDialog {
    MyDialog::default()
}

pub struct MyDialog {
    inp: input::Input,
}

impl MyDialog {
    pub fn default() -> Self {
        let mut win = window::Window::default()
            .with_size(400, 100)
            .with_label("My Dialog");
        win.set_color(Color::from_rgb(240, 240, 240));
        let mut pack = group::Pack::default()
            .with_size(300, 30)
            .center_of_parent()
            .with_type(group::PackType::Horizontal);
        pack.set_spacing(20);
        frame::Frame::default()
            .with_size(80, 0)
            .with_label("Enter name:");
        let mut inp = input::Input::default().with_size(100, 0);
        inp.set_frame(FrameType::FlatBox);
        let mut ok = button::Button::default().with_size(80, 0).with_label("Ok");
        style_button(&mut ok);
        pack.end();
        win.end();
        win.make_modal(true);
        win.show();
        ok.set_callback({
            let mut win = win.clone();
            move |_| {
                win.hide();
            }
        });
        while win.shown() {
            app::wait();
        }
        Self { inp }
    }
    pub fn value(&self) -> String {
        self.inp.value()
    }
}

fn main() {
    let a = app::App::default();
    app::set_font(Font::Times);
    let mut win = window::Window::default().with_size(600, 400);
    win.set_color(Color::from_rgb(240, 240, 240));
    let mut btn = button::Button::default()
        .with_size(80, 30)
        .with_label("Click")
        .center_of_parent();
    style_button(&mut btn);
    let mut frame = frame::Frame::new(btn.x() - 40, btn.y() - 100, btn.w() + 80, 30, None);
    frame.set_frame(FrameType::BorderBox);
    frame.set_color(Color::Red.inactive());
    win.end();
    win.show();
    btn.set_callback(move |_| {
        let d = show_dialog();
        frame.set_label(&d.value());
    });
    a.run().unwrap();
}

image

打印对话框 Printer dialog

FLTK还提供了打印对话框,它会调用你的系统平台的本地打印机对话框:

#![allow(unused)]
fn main() {
use fltk::{prelude::*, *};
let mut but = button::Button::default();
but.set_callback(|widget| {
    let mut printer = printer::Printer::default();
    if printer.begin_job(1).is_ok() {
        printer.begin_page().ok();
        let (width, height) = printer.printable_rect();
        draw::set_draw_color(enums::Color::Black);
        draw::set_line_style(draw::LineStyle::Solid, 2);
        draw::draw_rect(0, 0, width, height);
        draw::set_font(enums::Font::Courier, 12);
        printer.set_origin(width / 2, height / 2);
        printer.print_widget(widget, -widget.width() / 2, -widget.height() / 2);
        printer.end_page().ok();
        printer.end_job();
    }
});
}

这里打印了按钮的图像并指定了它的位置。你可以传递任何组件(像TextEditor组件之类的)作为被打印的组件。

图像 Images

FLTK支持矢量图和位图,提供下面几种开箱即用的图像类型:

  • BmpImage
  • JpegImage
  • GifImage
  • PngImage
  • SvgImage
  • Pixmap
  • RgbImage
  • XpmImage
  • XbmImage
  • PnmImage

它还提供了两个helper types:

  • SharedImage:它包装了上述所有的类型,使用时不需要提供图像的类型。
  • TiledImage:它提供了任何具体类型的平铺图像(Tiled Image)。

图像类型均实现了ImageExt Trait,该Trait中定义了缩放和检索图像元数据的方法。 可以通过向图像类型的load()函数传递图像路径来显示图像,对于某些类型,可以使用from_data()接收图像数据来构建图像:

#![allow(unused)]
fn main() {
/// 需要图像路径
let image = image::SvgImage::load("screenshots/RustLogo.svg").unwrap();

/// 需要图像数据
let image= image::SvgImage::from_data(&data).unwrap();
}

可以通过使用WidgetExt::set_image()/set_image_scaled()set_deimage()/set_deimage_scaled()(用于deactivated/grayed image)在其他组件上使用Image组件:

use fltk::{app, enums::FrameType, frame::Frame, image::SvgImage, prelude::*, window::Window};

fn main() {
    let app = app::App::default().with_scheme(app::Scheme::Gleam);
    let mut wind = Window::new(100, 100, 400, 300, "Hello from rust");

    let mut frame = Frame::default().with_size(360, 260).center_of(&wind);
    frame.set_frame(FrameType::EngravedBox);
    let mut image = SvgImage::load("screenshots/RustLogo.svg").unwrap();
    image.scale(200, 200, true, true);
    frame.set_image(Some(image));

    wind.make_resizable(true);
    wind.end();
    wind.show();

    app.run().unwrap();
}

或者通过WidgetExt::draw()方法:

use fltk::{app, enums::FrameType, frame::Frame, image::SvgImage, prelude::*, window::Window};

fn main() {
    let app = app::App::default().with_scheme(app::Scheme::Gleam);
    let mut wind = Window::new(100, 100, 400, 300, "Hello from rust");

    let mut frame = Frame::default().with_size(360, 260).center_of(&wind);
    frame.set_frame(FrameType::EngravedBox);
    let mut image = SvgImage::load("screenshots/RustLogo.svg").unwrap();
    frame.draw(move |f| {
        image.scale(f.w(), f.h(), true, true);
        image.draw(f.x() + 40, f.y(), f.w(), f.h());
    });

    wind.make_resizable(true);
    wind.end();
    wind.show();

    app.run().unwrap();
}

svg

使用Image作为你的程序的图标或背景将让你的程序更具风格化,更加美观(在FLTK中你可能需要使用其他主题或自定义)。

事件 Events

在之前章节的示例中,我们处理事件的方法主要是使用回调(Callback)。但是我们可以根据具体的使用情况选择其他方法,FLTK提供的处理事件的方式有这几种:

  • set_callback()方法,在点击按钮时自动触发。
  • handle()方法,用于进行细粒度的事件处理。
  • emit()方法,接收一个sender和一个message将触发的事件类型发送,之后在event loop中处理事件。
  • 我们还可以自定义一个可以在另一个组件的处理方法中被处理的事件。

设置回调 Callback

WidgetExt trait 中定义了set_callback方法。

使用闭包

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();
    but.set_callback(|_| println!("The button was clicked!"));
    app.run().unwrap();
}

这里闭包捕获的环境是设置回调的组件自身的&mut self

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();
    but.set_callback(|b| b.set_label("Clicked!"));
    app.run().unwrap();
}

你的按钮何时执行回调方法,点击时?还是鼠标松开时?你需要设置触发器来决定何时执行回调,set_callback()方法会设置默认的触发器,不同组件的触发器可能不同。例如按钮组件的触发器便是,当它具有鼠标焦点时的点击或按下回车。 某个组件的触发器是可以通过set_trigger()方法改变的。改变按钮的触发方式可能没有意义,但是对于Input组建来说,触发器可以被设置为CallbackTrigger::Changed,这可以使Input组件在状态改变时就触发回调:

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    let mut inp = input::Input::default()
        .with_size(160, 30)
        .center_of_parent();
    win.end();
    win.show();
    inp.set_trigger(enums::CallbackTrigger::Changed);
    inp.set_callback(|i| println!("{}", i.value()));
    a.run().unwrap();
}

在这个示例中,用户每输入一个字符都会打印一次。

使用闭包的好处便是因为它能够捕获环境,你可以将闭包环境作用域中的其他变量传递进去:

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();
    but.set_callback(move |_| {
        my_window.set_label("button was pressed");
    });
    app.run().unwrap();
}

菜单中,事件处理是在每个MenuItem上进行的。

使用方法对象 Function Object

如果你喜欢的话你也可以直接传递函数对象:

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

fn button_cb(w: &mut impl WidgetExt) {
    w.set_label("Clicked");
}

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();
    but.set_callback(button_cb);
    app.run().unwrap();
}

我们使用&mut impl WidgetExt,以便让所有组件都能使用这个回调。或者,你可以直接使用&mut button::Button只让Button使用。 这种方法的一个缺点是,有时候你必须维护全局状态:

extern crate lazy_static;

use fltk::{prelude::*, *};
use std::sync::Mutex;

#[derive(Default)]
struct State {
    count: i32,
}

impl State {
    fn increment(&mut self) {
        self.count += 1;
    }
}

lazy_static::lazy_static! {
    static ref STATE: Mutex<State> = Mutex::new(State::default());
}


fn button_cb(_w: &mut button::Button) {
    let mut state = STATE.lock().unwrap();
    state.increment();
}

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, "Increment!");
    my_window.end();
    my_window.show();
    
    but.set_callback(button_cb);
    
    app.run().unwrap();
}

这里我们用了lazy_static,当然也有其他的crate来用来进行状态管理。

同样,对菜单来说,在MenuExt::add()/insert()MenuItem::add()/insert()方法中,我们可以使用&mut impl MenuExt来设置MenuMenu Item的回调。

使用处理方法 handle method

handle方法接收一个有事件参数的闭包,并在处理后返回一个bool。这个返回值让FLTK知道该事件是否被处理。 它的使用是这样的:

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();

    but.handle(|_, event| {
        println!("The event: {:?}", event);
        false
    });
    
    app.run().unwrap();
}

这段代码将打印出event,但并不做其他处理,所以我们返回false。很明显,我们应该做一些有用的处理,所以我们把它改成这样:

#![allow(unused)]
fn main() {
    but.handle(|_, event| match event {
        Event::Push => {
            println!("I was pushed!");
            true
        },
        _ => false,
    });
}

在这里,我们处理事件Event然后返回true,将其他事件将被忽略并返回false

另一个例子:

#![allow(unused)]
fn main() {
    but.handle(|b, event| match event {
        Event::Push => {
            b.set_label("Pushed");
            true
        },
        _ => false,
    });
}

使用messages

这允许我们创建Channel和Sender Receiver的结构,在触发后发送Message(Message必须是Send + Sync),并在event loop中处理。这样做的好处是,当我们需要将我们的一些变量传递到闭包或线程中时,不必再使用智能指针来包装它们。

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();

    let (s, r) = app::channel();
    
    but.emit(s, true);
    // 相当于 but.set_callback(move |_| s.send(true))

    while app.wait() {
        if let Some(msg) = r.recv() {
            match msg {
                true => println!("Clicked"),
                false => (), // 什么都不做
            }
        }
    }
}

跟之前的例子一样,Messages 可以在event loop中被接受,另外你也可以在后台线程或app::add_idle()的回调中接收Message

#![allow(unused)]
fn main() {
    app::add_idle(move || {
        if let Some(msg) = r.recv() {
            match msg {
                true => println!("Clicked"),
                false => (), // 这里不做任何事
            }
        }
    });
}

这里也不限于使用fltk channel,你可以使用任何channel。例如,这个例子用使用了std channel

#![allow(unused)]
fn main() {
let (s, r) = std::sync::mpsc::channel::<Message>();
btn.set_callback(move |_| {
    s.send(Message::SomeMessage).unwrap();
});
}

类似于emit()方法,你也可以定义一个适用于所有组件的send()方法,:

use std::sync::mpsc::Sender;

pub trait SenderWidget<W, T>
where
    W: WidgetExt,
    T: Send + Sync + Clone + 'static,
{
    fn send(&mut self, sender: Sender<T>, msg: T);
}

impl<W, T> SenderWidget<W, T> for W
where
    W: WidgetExt,
    T: Send + Sync + Clone + 'static,
{
    fn send(&mut self, sender: Sender<T>, msg: T) {
        self.set_callback(move |_| {
            sender.send(msg.clone()).unwrap();
        });
    }
}

fn main() {
    let btn = button::Button::default();
    let (s, r) = std::sync::mpsc::channel::<Message>();
    btn.send(s.clone(), Message::SomeMessage);
}

创建自己的事件

FLTK在enums::Event中预先定义了29个事件。我们还可以使用调用app::handle(impl Into<i32>, window)创建我们自己的事件。handle函数以任意一个大于30的i32类型值作为信号标识,最好提前定义好信号标识。我们可以在另一个组件的handle()方法中处理事件,注意这个组件需要放在传递给app::handle的那个窗口内部。 在下面的例子中,我们创建了一个带有FrameButton的窗口。Button的回调函数在执行时,通过app::handle_main函数发送一个CHANGED事件。该CHANGED信号在Framehandle方法中被接收到并做出处理:

use fltk::{app, button::*, enums::*, frame::*, group::*, prelude::*, window::*};
use std::cell::RefCell;
use std::rc::Rc;

pub struct MyEvent;

impl MyEvent {
    const CHANGED: i32 = 40;
}

#[derive(Clone)]
pub struct Counter {
    count: Rc<RefCell<i32>>,
}

impl Counter {
    pub fn new(val: i32) -> Self {
        Counter {
            count: Rc::from(RefCell::from(val)),
        }
    }

    pub fn increment(&mut self) {
        *self.count.borrow_mut() += 1;
        app::handle_main(MyEvent::CHANGED).unwrap();
    }

    pub fn decrement(&mut self) {
        *self.count.borrow_mut() -= 1;
        app::handle_main(MyEvent::CHANGED).unwrap();
    }

    pub fn value(&self) -> i32 {
        *self.count.borrow()
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let app = app::App::default();
    let counter = Counter::new(0);
    let mut wind = Window::default().with_size(160, 200).with_label("Counter");
    let mut pack = Pack::default().with_size(120, 140).center_of(&wind);
    pack.set_spacing(10);
    let mut but_inc = Button::default().with_size(0, 40).with_label("+");
    let mut frame = Frame::default()
        .with_size(0, 40)
        .with_label(&counter.clone().value().to_string());
    let mut but_dec = Button::default().with_size(0, 40).with_label("-");
    pack.end();
    wind.end();
    wind.show();

    but_inc.set_callback({
        let mut c = counter.clone();
        move |_| c.increment()
    });

    but_dec.set_callback({
        let mut c = counter.clone();
        move |_| c.decrement()
    });
    
    frame.handle(move |f, ev| {
        if ev == MyEvent::CHANGED.into() {
            f.set_label(&counter.clone().value().to_string());
            true
        } else {
            false
        }
    });

    Ok(app.run()?)
}

发送的i32信号可以是动态创建的,也可以把它存在一个局部或全局常量中,或者存放在一个枚举中。

优点

  • 无开销。
  • 信号的处理方式与其他任何FLTK事件一样。
  • app::handle函数可以返回一个bool,表示该事件是否被处理。
  • 允许在事件循环之外处理自定义信号/事件。
  • 允许在程序中使用MVC或SVU架构。

缺点

  • 信号只能在一个组件的处理方法中处理。
  • 该信号在事件循环中是不可访问的(为解决,可以使用WidgetExt::emit或本节前面部分描述的channel)。

拖放 Drag & Drop

FLTK支持拖放的事件类型。如果你为组件实现了拖放事件,你就可以拖动组件,也可以把文件拖入程序框内。如果还想在组件上绘图,处理Event::Drag事件将会帮你做到。

拖动组件

通常情况下,你可以拖动窗口边框来移动窗口。现在我们将窗口设置为无边框,然后再为窗口本身实现拖放事件:

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default().with_size(400, 400);
    wind.set_color(enums::Color::White);
    wind.set_border(false);
    wind.end();
    wind.show();

    wind.handle({
        let mut x = 0;
        let mut y = 0;
        move |w, ev| match ev {
            enums::Event::Push => {
                let coords = app::event_coords();
                x = coords.0;
                y = coords.1;
                true
            }
            enums::Event::Drag => {
                w.set_pos(app::event_x_root() - x, app::event_y_root() - y);
                true
            }
            _ => false,
        }
    });

    app.run().unwrap();
}

拖动文件

将一个文件拖入程序框中会触发Paste事件,并将文件的路径传入app::event_text()。因此,当我们处理文件拖放时,我们需要在Event::Paste中捕获路径,这里我们将检查文件是否存在,读取其内容并填充我们的text组件:

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

fn main() {
    let app = app::App::default();
    let buf = text::TextBuffer::default();
    let mut wind = window::Window::default().with_size(400, 400);
    let mut disp = text::TextDisplay::default_fill();
    wind.end();
    wind.show();

    disp.set_buffer(buf.clone());
    disp.handle({
        let mut dnd = false;
        let mut released = false;
        let buf = buf.clone();
        move |_, ev| match ev {
            Event::DndEnter => {
                dnd = true;
                true
            }
            Event::DndDrag => true,
            Event::DndRelease => {
                released = true;
                true
            }
            Event::Paste => {
                if dnd && released {
                    let path = app::event_text();
                    let path = path.trim();
                    let path = path.replace("file://", "");
                    let path = std::path::PathBuf::from(&path);
                    if path.exists() {
                        // 我们使用 timeout 来避免路径传入缓冲区
                        app::add_timeout3(0.0, {
                            let mut buf = buf.clone();
                            move |_| {
                                buf.load_file(&path).unwrap();
                            }
                        });
                    }
                    dnd = false;
                    released = false;
                    true
                } else {
                    false
                }
            }
            Event::DndLeave => {
                dnd = false;
                released = false;
                true
            }
            _ => false,
        }
    });
    app.run().unwrap();
}

如果你对文件的内容不感兴趣,你可以只取得文件的路径并显示:

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

fn main() {
    let app = app::App::default();
    let buf = text::TextBuffer::default();
    let mut wind = window::Window::default().with_size(400, 400);
    let mut disp = text::TextDisplay::default_fill();
    wind.end();
    wind.show();

    disp.set_buffer(buf.clone());
    disp.handle({
        let mut dnd = false;
        let mut released = false;
        let mut buf = buf.clone();
        move |_, ev| match ev {
            Event::DndEnter => {
                dnd = true;
                true
            }
            Event::DndDrag => true,
            Event::DndRelease => {
                released = true;
                true
            }
            Event::Paste => {
                if dnd && released {
                    let path = app::event_text();
                    buf.append(&path);
                    dnd = false;
                    released = false;
                    true
                } else {
                    false
                }
            }
            Event::DndLeave => {
                dnd = false;
                released = false;
                true
            }
            _ => false,
        }
    });
    app.run().unwrap();
}

拖动绘图

我们已经学会了如何在屏幕内通过拖放绘制组件,现在我们还可以响应屏幕外的事件,比如用鼠标绘画这些。我们会将屏幕外事件,如鼠标的移动坐标等等,复制到组件中,然后进行绘制。一个更详细的例子可以在绘图 Drawing中看到。

状态管理器 State management

FLTK没有固化的某种形式的状态管理方法或程序架构,这些部分交由开发者自行设计选择。fltk-rs仓库和本书中的所有示例几乎都使用了回调(Callback)和消息(message)的方式来处理事件和管理状态,你能找它们的很多例子。 这些都在事件 Event中讨论过。

此外,为了简单起见,所有的示例都是在main函数中处理一切。您可以创建自己的App结构,将主窗口和你维护的数据状态放在里面:

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

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

struct MyApp {
    app: app::App,
    main_win: window::Window,
    frame: frame::Frame,
    count: i32,
    receiver: app::Receiver<Message>,
}

impl MyApp {
    pub fn new() -> Self {
        let count = 0;
        let app = app::App::default();
        let (s, receiver) = app::channel();
        let mut main_win = window::Window::default().with_size(400, 300);
        let col = group::Flex::default()
            .with_size(100, 200)
            .column()
            .center_of_parent();
        let mut inc = button::Button::default().with_label("+");
        inc.emit(s, Message::Inc);
        let frame = frame::Frame::default().with_label(&count.to_string());
        let mut dec = button::Button::default().with_label("-");
        dec.emit(s, Message::Dec);
        col.end();
        main_win.end();
        main_win.show();
        Self {
            app,
            main_win,
            frame,
            count,
            receiver,
        }
    }

    pub fn run(mut self) {
        while self.app.wait() {
            if let Some(msg) = self.receiver.recv() {
                match msg {
                    Message::Inc => self.count += 1,
                    Message::Dec => self.count -= 1,
                }
                self.frame.set_label(&self.count.to_string());
            }
        }
    }
}

fn main() {
    let a = MyApp::new();
    a.run();
}

辅助 crates

Rust的Crate生态为我们提供了很多用于进行状态管理的Crate。在fltk-rs GitHub组织下有2个crate,它们提供了几种程序架构设计和状态管理的方法:

提供了一个类似Elm的SVU架构。它是反应式(reactive)的,本质上不可变,每次发送消息都会让视图进行重绘,参见Elm的设计。

类似于即时模式的GUI界面,所有事件都在事件循环中处理。实际上它也是反应式的,但它是可变且无状态的。使用它可以避免触发事件引起视图重绘。

这两个crate都尽量避免使用回调方法,由于Rust的生命周期和借用检查机制,处理回调极其麻烦。你需要使用内部可变性的智能指针,才能够在回调中借用。

你可以看一下这两个crate,或许会启发你的灵感。

下面是分别使用这两个Crate实现计时器的示例:

Flemish

use flemish::{
    button::Button, color_themes, frame::Frame, group::Flex, prelude::*, OnEvent, Sandbox, Settings,
};

pub fn main() {
    Counter::new().run(Settings {
        size: (300, 100),
        resizable: true,
        color_map: Some(color_themes::BLACK_THEME),
        ..Default::default()
    })
}

#[derive(Default)]
struct Counter {
    value: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    IncrementPressed,
    DecrementPressed,
}

impl Sandbox for Counter {
    type Message = Message;

    fn new() -> Self {
        Self::default()
    }

    fn title(&self) -> String {
        String::from("Counter - fltk-rs")
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::IncrementPressed => {
                self.value += 1;
            }
            Message::DecrementPressed => {
                self.value -= 1;
            }
        }
    }

    fn view(&mut self) {
        let col = Flex::default_fill().column();
        Button::default()
            .with_label("Increment")
            .on_event(Message::IncrementPressed);
        Frame::default().with_label(&self.value.to_string());
        Button::default()
            .with_label("Decrement")
            .on_event(Message::DecrementPressed);
        col.end();
    }
}

fltk-evented

use fltk::{app, button::Button, frame::Frame, group::Flex, prelude::*, window::Window};
use fltk_evented::Listener;

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

    let mut wind = Window::default()
        .with_size(160, 200)
        .center_screen()
        .with_label("Counter");
    let flex = Flex::default()
        .with_size(120, 160)
        .center_of_parent()
        .column();
    let but_inc: Listener<_> = Button::default().with_label("+").into();
    let mut frame = Frame::default();
    let but_dec: Listener<_> = Button::default().with_label("-").into();
    flex.end();
    wind.end();
    wind.show();

    let mut val = 0;
    frame.set_label(&val.to_string());

    while a.wait() {
        if but_inc.triggered() {
            val += 1;
            frame.set_label(&val.to_string());
        }

        if but_dec.triggered() {
            val -= 1;
            frame.set_label(&val.to_string());
        }
    }
}

布局 Layouts

FLTK-rs提供了这些开箱即用的布局组件:

  • Flex
  • Pack
  • Grid
  • 组件相对定位

Flex

Flex组件可以让你灵活的布局。它在group mod中定义,实现了GroupExt Trait。可以使用set_typewith_type方法选择Flex的布局形式。比如列(Column)和行(Row)布局:

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

fn main() {
    let a = app::App::default().with_scheme(app::Scheme::Gtk);
    let mut win = window::Window::default().with_size(400, 300);
    let mut flex = group::Flex::new(0, 0, 400, 300, None);
    flex.set_type(group::FlexType::Column);
    let expanding = button::Button::default().with_label("Expanding");
    let normal = button::Button::default().with_label("Normal");
    flex.fixed(&normal, 30);
    flex.end();
    win.end();
    win.show();
    a.run().unwrap();
}

fixed方法 (在1.4.6版本前是 set_size)方法 接收一个放在Flex内部的组件,将其大小(高度或宽度)设置为传递的值,示例中设置高度为30。因为这是一个Column类型的Flex,所以传入的值代表组件的高度。 示例中另一个按钮大小是变的,因为没有为它设置尺寸。参见这个完整的例子:

示例1

image

Packs

Pack组件同样在group mod中,并实现了GroupExt trait。类似的,也有两种形式的Pack,Vertical PackHorizontal Pack,Pack默认是Vertical,它需要设置子组件的高度,而Horizontal Pack需要设置它的子组件的宽度,比如这个示例:

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

fn main() {
    let app = app::App::default();
    
    let mut my_window = window::Window::default().with_size(400, 300);
    let mut hpack = group::Pack::default().with_size(190, 40).center_of(&my_window);
    hpack.set_type(group::PackType::Horizontal);
    hpack.set_spacing(30);
    let _but1 = button::Button::default().with_size(80, 0).with_label("Button1");
    let _but2 = button::Button::default().with_size(80, 0).with_label("Button2");
    hpack.end();
    my_window.end();
    my_window.show();

    app.run().unwrap();
}

我们在窗口内创建了一个Pack,并在其中创建2个按钮。我们不需要设置按钮的坐标。你也可以像 FLTK仓库的示例 中的Calculator一样,将Pack互相嵌套。可以试试Pack::auto_layout()方法自动布局:

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

fn main() {
    let app = app::App::default();
    
    let mut my_window = window::Window::default().with_size(400, 300);
    let mut hpack = group::Pack::new(0, 200, 400, 100, "");
    hpack.set_type(group::PackType::Horizontal);
    hpack.set_spacing(30);
    let _but1 = button::Button::default().with_label("Button1");
    let _but2 = button::Button::default().with_label("Button2");
    hpack.end();
    hpack.auto_layout();
    my_window.end();
    my_window.show();

    app.run().unwrap();
}

这种情况下,我们甚至不需要设置按钮的大小。

image

Grid

Grid Crate实现在另一个Crate中的。它需要使用Grid::set_layout(&mut self, rows, columns)来设置一个Layout。然后通过Grid::insert(&mut self, row, column)Grid::insert_ext(&mut self, row, column, row_span, column_span)方法添加组件。

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

fn main() {
    let a = app::App::default().with_scheme(app::Scheme::Gtk);
    let mut win = window::Window::default().with_size(500, 300);
    let mut grid = Grid::default_fill();
    // 若为 "true" 便会显示网格的框线和数字
    grid.debug(false); 
    // 设置Grid为 5 行,5 列
    grid.set_layout(5, 5); 
    // 设置组件和所在的行列
    grid.insert(&mut button::Button::default().with_label("Click"), 0, 1); 
    // 设置组件和所在的行列以及行高列宽
    grid.insert_ext(&mut button::Button::default().with_label("Button 2"), 2, 1, 3, 1); 
    win.end();
    win.show();
    a.run().unwrap();
}

Grid example

image

Relative positioning

WidgetExt Trait中定义了几个构造方法,允许我们相基于其他组件的大小和位置构建组件。这类似于Qt中Qml的锚定(anchoring):

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

fn main() {
    let app = app::App::default();
    let mut wind = window::Window::default()
        .with_size(160, 200)
        .center_screen()
        .with_label("Counter");
    let mut frame = frame::Frame::default()
        .with_size(100, 40)
        .center_of(&wind)
        .with_label("0");
    let mut but_inc = button::Button::default()
        .size_of(&frame)
        .above_of(&frame, 0)
        .with_label("+");
    let mut but_dec = button::Button::default()
        .size_of(&frame)
        .below_of(&frame, 0)
        .with_label("-");
    wind.end();
    wind.show();
    app.run().unwrap();
}

counter

(我们跳过了一些主题的设计)

这些方法是:

  • above_of(&widget, padding): 将该组件放在所传递的组件上面
  • below_of(&widget, padding): 将该组件放在所传递的组件下面
  • right_of(&widget, padding): 将该组件放在所传递的组件右边
  • left_of(&widget, padding):将该组件置于所传递的组件左边
  • center_of(&widget): 将组件放置在所传递的组件的中间(包括x和y轴)
  • center_of_parent(): 将组件放在父组件的中间(包括x轴和y轴)
  • center_x(&widget): 将组件放置在所传递的组件的中心(X轴)
  • center_y(&widget): 将组件放置在所传递的组件的中心(Y轴)
  • size_of(&widget): 构建与所传组件相同大小的组件
  • size_of_parent(): 构建与其父组件相同大小的组件

样式 Style

FLTK为你的应用程序提供了丰富的自定义选项,包括改变程序的总体主题,到自定义颜色、字体、Frame、自定义绘图...等等。

颜色 Colors

FLTK可以处理True color。一些常用的颜色在enums::Color中列举出来方便使用:

  • Black
  • White
  • Red
  • Blue
  • Cyan ...etc.

你也可以使用下列方法构建其他的颜色:

  • by_index()方法使用fltk的colormap选取颜色。取值范围是0到255。
#![allow(unused)]
fn main() {
let red = Color::by_index(88);
}

colormap

  • from_hex()方法需要传入一个24bit的十六进制RGB值。
#![allow(unused)]
fn main() {
const RED: Color = Color::from_hex(0xff0000); // 注意它是一个Const函数
}
  • from_rgb()方法需要传入3个代表R、G、B的值:
#![allow(unused)]
fn main() {
const RED: Color = Color::from_rgb(255, 0, 0); // 注意它是一个Const函数
}

Color Enum还提供了一些方便的方法,使用.darker().lighter().inactive()等方法可以在所选颜色的基础上生成色调有些变化的颜色,常用作阴影或按钮点击反馈等:

#![allow(unused)]
fn main() {
let col = Color::from_rgb(176, 100, 50).lighter();
}

如果你喜欢使用html十六进制字符串生成颜色,可以使用from_hex_str()方法生成:

#![allow(unused)]
fn main() {
let col = Color::from_hex_str("#ff0000");
}

边框类型 FrameTypes

FLTK提供了丰富的边框类型。这些可以在enumsmod下找到: image

你可以用WidgetExt::set_frame()来为组件选择边框。一些组件或Trait也支持set_down_frame()方法:

use fltk::{app, enums::FrameType, frame::Frame, image::SvgImage, prelude::*, window::Window};

fn main() {
    let app = app::App::default().with_scheme(app::Scheme::Gleam);
    let mut wind = Window::new(100, 100, 400, 300, "Hello from rust");

    let mut frame = Frame::default().with_size(360, 260).center_of(&wind);
    frame.set_frame(FrameType::EngravedBox);
    let mut image = SvgImage::load("screenshots/RustLogo.svg").unwrap();
    image.scale(200, 200, true, true);
    frame.set_image(Some(image));

    wind.make_resizable(true);
    wind.end();
    wind.show();

    app.run().unwrap();
}

image

在这里,我们将Frame的FrameType设置为EngravedBox,你可以看到图片周围出现的边框。

ButtonExt Trait支持使用set_down_frame()

#![allow(unused)]
fn main() {
btn1.set_frame(enums::FrameType::RFlatBox);
btn1.set_down_frame(enums::FrameType::RFlatBox);
}

此外,我们可以使用app::set_frame_type_cb()来改变我们选择的相应FrameTypes的绘制方式:

use fltk::{
    enums::{Color, FrameType},
    prelude::*,
    *
};

fn down_box(x: i32, y: i32, w: i32, h: i32, c: Color) {
    draw::draw_box(FrameType::RFlatBox, x, y, w, h, Color::BackGround2);
    draw::draw_box(FrameType::RoundedFrame, x - 10, y, w + 20, h, c);
}

fn main() {
    let app = app::App::default();
    app::set_frame_type_cb(FrameType::DownBox, down_box, 0, 0, 0, 0);
    let mut w = window::Window::default().with_size(480, 230).with_label("Gui");
    w.set_color(Color::from_u32(0xf5f5f5));

    let mut txf = input::Input::default().with_size(160, 30).center_of_parent();    
    txf.set_color(Color::Cyan.darker());

    w.show();

    app.run().unwrap();
}

image

这就用自定义的down_box绘制方法改变了默认的DownBox。我们还可以在绘制方法中使用ImageExt::draw()来绘制图像(比如画一个svg图像,以获得可缩放的圆角边框)。

字体 Fonts

FLTK自带了16种字体,可以在enums::Font中找到:

  • Helvetica
  • HelveticaBold
  • HelveticaItalic
  • HelveticaBoldItalic
  • Courier
  • CourierBold
  • CourierItalic
  • CourierBoldItalic
  • Times
  • TimesBold
  • TimesItalic
  • TimesBoldItalic
  • Symbol
  • Screen
  • ScreenBold
  • Zapfdingbats

FLTK也允许加载系统默认字体或者将字体文件编译进二进制文件中。

系统字体依赖于用户的系统,默认情况下FLTK并不会加载。但你可以用App::load_system_fonts()方法来让程序加载系统字体。 然后可以使用app::fonts()函数获取加载到的字体,或者用app::font_count()app::font_name()app::font_index()函数查看字体的数量,名称等。 查询到后,便可以使用Font::by_index()Font::by_name()方法来为程序应用字体。

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

fn main() {
    let app = app::App::default().load_system_fonts();
    // 要加载指定路径的字体的话,参见 App::load_font() 函数
    let fonts = app::fonts();
    // println!("{:?}", fonts);
    let mut wind = window::Window::default().with_size(400, 300);
    let mut frame = frame::Frame::default().size_of(&wind);
    frame.set_label_size(30);
    wind.set_color(enums::Color::White);
    wind.end();
    wind.show();
    println!("The system has {} fonts!\nStarting slideshow!", fonts.len());
    let mut i = 0;
    while app.wait() {
        if i == fonts.len() {
            i = 0;
        }
        frame.set_label(&format!("[{}]", fonts[i]));
        frame.set_label_font(enums::Font::by_index(i));
        app::sleep(0.5);
        i += 1;
    }
}

如果你想加载一个自己的字体,你可以选择使用Font::load_font()Font::set_font()方法:

use fltk::{app, enums::Font, button::Button, frame::Frame, prelude::*, window::Window};

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

    let font = Font::load_font("angelina.ttf").unwrap();
    Font::set_font(Font::Helvetica, &font);
    app::set_font_size(24);

    let mut wind = Window::default().with_size(400, 300);
    let mut frame = Frame::default().with_size(200, 100).center_of(&wind);
    let mut but = Button::new(160, 210, 80, 40, "Click me!");
    wind.end();
    wind.show();

    but.set_callback(move |_| frame.set_label("Hello world"));

    app.run().unwrap();
}

load_font()会加载.ttf格式的字体,然后我们使用set_font()用我们这个字体替换FLTK的默认字体Font::Helvetica

image

绘制事物 Drawing things

fltk-rsdraw mod中提供了可以绘制自定义元素的函数。但是只有当绘制函数的调用是在特定的上下文中时,例如在WidgetBase::draw()方法中或在Offscreen上下文中,绘制才有效:

在组件上绘制

注意,我们在组件的draw方法中使用了draw

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

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    win.end();
    win.show();

    win.draw(|w| {
        use draw::*;
        // 白色窗口
        draw_rect_fill(0, 0, w.w(), w.h(), enums::Color::White);
        // 画一个蓝色的圆
        set_draw_color(enums::Color::Blue.inactive());
        draw_pie(w.w() / 2 - 50, w.h() / 2 - 50, 100, 100, 0.0, 360.0);
        // 让文字旋转一定角度
        set_draw_color(enums::Color::Red);
        set_font(enums::Font::Courier, 16);
        draw_text_angled(45, "Hello World", w.w() / 2, w.h() / 2);
    });

    a.run().unwrap();
}

draw

我们用了整个窗口当绘制的画板,任何其他组件理论上都可以进行绘制。还有许多其他函数可以让你绘制直线、矩形、弧线、饼、循环、多边形,甚至图像。

屏幕外事件的绘制

有时可能会需要通过绘制来响应一些事件,例如拖动鼠标时,在屏幕上绘制出鼠标的轨迹。在这种情况下,你可以使用draw::Offscreen来做到这一点。我们所用组件的draw方法只是复制屏幕外事件的内容,例如鼠标的坐标,而绘制是在组件的handle方法中进行的。

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();

    // 用白色填充
    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();
}

注意,这里我们用offs.begin()开始了OffScreen上下文,用offs.end()表示上下文结束。只有在上下文内,我们才能调用Offscreen绘图函数:

image

自定义风格 Styling

FLTK提供了自定义程序风格的很多方法(不然实在有点丑)。我们可以设置颜色,不同的字体,自定义绘制组件等等。自定义风格用到了所有这些。我们可以使用WidgetExt中定义的方法为每个组件单独设置风格,也可以使用app模块中的函数为程序全局设置风格。

WidgetExt

WidgetExt Trait的大多数方法与修改边框、标签、组件颜色、文本颜色、字体和文本大小这些自定义的功能有关。 对相应的属性,都提供了settergetter方法,可以在[WidgetExt](https://docs.rs/fltk/*/fltk/prelude/trait.WidgetExt.html)找到。

看看这个示例:

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();

    // 设置风格
    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);
    // 风格应用结束

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

    app.run().unwrap();
}

counter

理论上所有组件都支持在其中显示图像,参见 图像 章节。

Global styling

全局风格化方法可以在 app mod中找到。先看看如何改变程序的主题:

#![allow(unused)]
fn main() {
use fltk::{prelude::*, enums::*, *};
let app = app::App::default().with_scheme(app::Scheme::Plastic);
}

FLTK本身提供四个主题:

  • Base
  • Gtk
  • Gleam
  • Plastic

这个例子设置了程序的颜色、默认字体、默认边框和是否在组件上显示焦点:

use fltk::{app, button::Button, enums, frame::Frame, prelude::*, window::Window};

fn main() {
    let app = app::App::default();
    app::set_background_color(170, 189, 206);
    app::set_background2_color(255, 255, 255);
    app::set_foreground_color(0, 0, 0);
    app::set_selection_color(255, 160,  63);
    app::set_inactive_color(130, 149, 166);
    app::set_font(enums::Font::Times);
    
    let mut wind = Window::default().with_size(400, 300);
    let mut frame = Frame::default().with_size(200, 100).center_of(&wind);
    let mut but = Button::new(160, 210, 80, 40, "Click me!");
    wind.end();
    wind.show();

    but.set_callback(move |_| frame.set_label("Hello world"));

    app.run().unwrap();
}

image

Custom Drawing

FLTK还提供了绘图基本图形(drawing primitives),这可以大大简化为组件自定义外观的步骤。我们使用接收一个闭包参数的draw()方法完成绘制。让我们来绘制一个自己的按钮,虽然FLTK已经提供了ShadowFrame框架类型,为了演示我们自己再做一个:

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

fn main() {
    let app = app::App::default();
    app::set_color(255, 255, 255); // 白色
    let mut my_window = window::Window::new(100, 100, 400, 300, "My Window");

    let mut but = button::Button::default()
        .with_pos(160, 210)
        .with_size(80, 40)
        .with_label("Button1");

    but.draw2(|b| {
        draw::set_draw_color(Color::Gray0);
        draw::draw_rectf(b.x() + 2, b.y() + 2, b.width(), b.height());
        draw::set_draw_color(Color::from_u32(0xF5F5DC));
        draw::draw_rectf(b.x(), b.y(), b.width(), b.height());
        draw::set_draw_color(Color::Black);
        draw::draw_text2(
            &b.label(),
            b.x(),
            b.y(),
            b.width(),
            b.height(),
            Align::Center,
        );
    });

    my_window.end();
    my_window.show();

    app.run().unwrap();
}

draw

draw()方法也支持在组件内部的绘制,你可以在下一节看到。

fltk-theme

这是一个FLTK主题crate,它提供了好几个预定义的主题,只需要加载就可以使用。

这里有很多好看的FLTK主题,或许可以挽留一下被界面劝退的你:

use fltk::{prelude::*, *};
use fltk_theme::{widget_themes, WidgetTheme, ThemeType};

fn main() {
    let a = app::App::default();
    let widget_theme = WidgetTheme::new(ThemeType::Aero);
    widget_theme.apply();
    let mut win = window::Window::default().with_size(400, 300);
    let mut btn = button::Button::new(160, 200, 80, 30, "Hello");
    btn.set_frame(widget_themes::OS_DEFAULT_BUTTON_UP_BOX);
    win.end();
    win.show();
    a.run().unwrap();
}

aqua-classic

动画 Animations

可以通过这几种机制在FLTK-rs中制作动画效果:

  • 利用事件循环 Event loop
  • 使用线程 Spawning threads
  • 超时 Timeouts

利用事件循环

fltk提供了app::wait()app::check(),允许使用一个阻塞操作更新UI:

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);
    // 我们的按钮占据了窗口的左侧
    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; // 绘制已经完成
        }
        while btn.w() != 0 {
            btn.set_size(btn.w() - 2, btn.h());
            app::sleep(0.016);
            btn.parent().unwrap().redraw();
            app::wait(); // 或 app::check();
        }
    });
    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);
}

使用线程

使用线程可以让我们不会阻塞主/ui线程:

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);
    // 我们的按钮占据了窗口的左侧
    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; // 绘制已经完成
        }
        std::thread::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(); // 唤醒UI线程进行绘制
                    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);
}

超时

fltk为重复性操作提供了timeout功能。使用timeout会像循环一样持续性执行一段代码,我们可以添加条件让它持续重复,或者删除timeout让它停止。

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

fn move_button(mut btn: button::Button, handle: app::TimeoutHandle) {
    btn.set_size(btn.w() - 2, btn.h());
    btn.parent().unwrap().redraw();
    if btn.w() == 20 {
        app::remove_timeout3(handle);
    } else {
        app::repeat_timeout3(0.016, handle);
    }
}

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default().with_size(400, 300);
    win.set_color(Color::White);
    let mut btn = button::Button::new(0, 0, 100, 300, None);
    style_btn(&mut btn);
    btn.clear_visible_focus();
    win.end();
    win.show();

    btn.set_callback(|b| {
        let btn = b.clone();
        app::add_timeout3(0.016, move |handle| {
            let btn = btn.clone();
            move_button(btn, handle)
        });
    });

    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);
}

这段代码在用户点击时添加timeout,当按钮宽度到达预定值时,删除timeout使其停止重复。

Accessibility

FLTK提供了一些开箱即用的无障碍功能:

  • 在ui元素之间和内部的键盘导航。

FLTK 会自动启用此功能。 根据组件的创建顺序,以及其是否获得焦点,您可以使用箭头键或Tab和Shift-Tab移动到下一个/上一个组件。 同样,相同的操作也适用于菜单项。

键盘快捷键。

Button widgets and Menu widgets provide a method which allows setting the keyboard shortcut:

#![allow(unused)]
fn main() {
use fltk::{prelude::*, *};

let mut menu = menu::MenuBar::default().with_size(800, 35);
menu.add(
    "&File/New...\t",
    Shortcut::Ctrl | 'n',
    menu::MenuFlag::Normal,
    |_m| {},
);

let mut btn = button::Button::new(100, 100, 80, 30, "Click me");
btn.set_shortcut(enums::Shortcut::Ctrl | 'b');
}

键盘可替代的鼠标操作。

FLTK 会自动启用此功能。 根据项目是否有默认的 CallbackTrigger::EnterKey 触发器,或使用 set_trigger 设置了触发器,在按下回车键是会触发回调。 例如,按钮如果有焦点,就会自动响应回车键。以下代码为组件设置了触发器:

#![allow(unused)]
fn main() {
use fltk::{prelude::*, *};

let mut inp = input::Input::new(10, 10, 160, 30, None);
inp.set_trigger(enums::CallbackTrigger::EnterKey);
inp.set_callback(|i| println!("You clicked enter, and the input's current text is: {}", i.value()));
}

IME 支持。

对于中文、日文和韩文等需要输入法编辑器(Input Method Editor, IME)的语言,IME会自动启用。在这种情况下,FLTK 使用操作系统提供的 IME。

为组件和自定义组件设置按键事件。

使用 WidgetExt::handle 方法,你可以自定义组件如何处理事件,包括按键事件。

#![allow(unused)]
fn main() {
use fltk::{prelude::*, *};

let mut win = window::Window::default().with_size(400, 300);
win.handle(|w, ev| {
    enums::Event::KeyUp => {
        let key = app::event_key();
        match key {
            enums::Key::End => app::quit(),     // 以quit为例
            _ => {
                if let Some(k) = key.to_char() {
                    match k {
                        'q' => app::quit(),
                        _ => (),
                    }
                }
            },
        }
        true
    }, 
    _ => false,
});
}

屏幕阅读器的支持

屏幕阅读器的支持集成在另一个 crate 中:

这个例子使用了 fltk-accesskit 编写了一个无障碍程序的示例:

例子:

#![windows_subsystem = "windows"]
use fltk::{prelude::*, *};
use fltk_accesskit::{AccessibilityContext, AccessibleApp};

fn main() {
    let a = app::App::default().with_scheme(app::Scheme::Oxy);
    let mut w = window::Window::default()
        .with_size(400, 300)
        .with_label("Hello fltk-accesskit");
    let col = group::Flex::default()
        .with_size(200, 100)
        .center_of_parent()
        .column();
    let inp = input::Input::default().with_id("inp").with_label("Enter name:");
    let mut btn = button::Button::default().with_label("Greet");
    let out = output::Output::default().with_id("out");
    col.end();
    w.end();
    w.make_resizable(true);
    w.show();

    btn.set_callback(btn_callback);

    let ac = AccessibilityContext::new(
        w,
        vec![Box::new(inp), Box::new(btn), Box::new(out)],
    );

    a.run_with_accessibility(ac).unwrap();
}

fn btn_callback(_btn: &mut button::Button) {
    let inp: input::Input = app::widget_from_id("inp").unwrap();
    let mut out: output::Output = app::widget_from_id("out").unwrap();
    let name = inp.value();
    if name.is_empty() {
        return;
    }
    out.set_value(&format!("Hello {}", name));
}

Accessible Trait 是为一些特定的组件实现的。 这个例子中,你需要实例化一个fltk_accesskit::AccessibilityContext,它需要你将根组件(主窗口),以及会被屏幕阅读器识别的组件作为参数。 最后你需要使用特殊的 run_with_accessibility 来运行 App结构。

可以在这里观看示例视频示例 YouTube.

FAQ

Build issues

Why does the build fails when I follow one of the tutorials?

The first tutorial uses the fltk-bundled feature flag, which is only supported for certain platforms since these are built using the Github Actions CI, namely:

  • Windows 10 x64 (msvc and gnu).
  • MacOS 12 x64 and aarch64.
  • Ubuntu 20.04 or later, x64 and aarch64.

If you're not running one of the aforementioned platforms, you'll have to remove the fltk-bundled feature flag in your Cargo.toml file:

[dependencies]
fltk = "^1.3"

Furthermore, the fltk-bundled flag assumes you have curl and tar installed (for Windows, they're available in the Native Tools Command Prompt).

Build fails on windows, why can't CMake find my toolchain?

If you're building using the MSVC toolchain, make sure you run your build (at least your initial build) using the Native Tools Command Prompt, which should appear once you start typing "native" in the start menu, choose the version corresponding to your installed Rust toolchain (x86 or x64). The Native Tools Command Prompt has all the environment variables set correctly for native development. cmake-rs which the bindings use might not be able to find the Visual Studio 2022 generator, in which case, you can try to use the fltk-bundled feature, or use ninja via the use-ninja feature. This requires installing Ninja which can be installed with Chocolatey, Scoop or manually.

If you're building for the GNU toolchain, make sure that Make is also installed, which usually comes installed in mingw64 toolchain.

Build fails on MacOS 11 with an Apple M1 chip, what can I do?

If you're getting "file too small to be an archive" error, you might be hitting this issues or this issue. MacOS's native C/C++ toolchain shouldn't have this issue, and can be installed by running xcode-select --install or by installing XCode. Make sure the corresponding Rust toolchain (aarch64-apple-darwin) is installed as well. You can uninstall other Rust apple-darwin toolchains or use cargo-lipo instead if you need universal/fat binaries.

If the linking fails because of this issue with older toolchains, it should work by using the fltk-shared feature (an issue with older compilers). Which would also generate a dynamic library which would need to be deployed with your application.

[dependencies]
fltk = { version = "^1.3", features = ["fltk-shared"] }

Why does my msys2 mingw built fltk app using, fltk-bundled, isn't self-contained and requires several dlls?

If you have installed libgdiplus via pacman, it would require those dependencies on other systems. If you're using the windows sdk-provided libgdiplus, it shouldn't require extra dlls. You can either uninstall libgdiplus that was installed via pacman, or or you can build using the feature flag: no-gdiplus.

This crate targets FLTK 1.4, while currently most distros distribute an older version of FLTK (1.3.5). You can try to install FLTK (C++) by building from source.

Build fails on Arch linux because of pango or cairo?

Pango changed its include paths which caused build failures across many projects. There are 2 solutions:

  • Use the no-pango feature. Downsides: loss of rtl and cjk language support.
  • Set the CFLAGS and CXXFLAGS to correct the global include paths.
export CFLAGS="-isystem /usr/include/harfbuzz -isystem /usr/include/cairo"
export CXXFLAGS="-isystem /usr/include/harfbuzz -isystem /usr/include/cairo"

How do I force CMake to use a certain C++ compiler?

FLTK works with all 3 major compilers. If you would like to change the C++ compiler that's chosen by default by CMake, you can change the CXX environment variable before running the build:

export CXX=/usr/bin/clang++
cargo run

CMake caches the C++ compiler variable after it's first run, so if the above failed because of a previous run, you would have to run cargo clean or you can manually delete the CMakeCache.txt file in the build directory.

Can I accelerate the build speed?

You can use the "use-ninja" feature flag if you have ninja installed.

Can I cache a previous build of the FLTK library?

You can use the fltk-bundled feature and use either the CFLTK_BUNDLE_DIR or CFLTK_BUNDLE_URL to point to the location of your cached cfltk and fltk libraries.

Deployment

How do I deploy my application?

Rust, by default, statically links your application. FLTK is built also for static linking. That means that the resulting executable can be directly deployed without the need to deploy other files along with it. If you want to create a WIN32 application, Mac OS Bundle or Linux AppImage, please check the question just below!

Why do I get a console window whenever I start my GUI app?

This is the default behavior of the toolchain, and is helpful for debugging purposes. It can be turned off easily by adding #![windows_subsystem = "windows"] at the beginning of your main.rs file if you're on windows. If you would like to keep the console window on debug builds, but not on release builds, you can use #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] instead.

For Mac OS and Linux, this is done by a post-build process to create a Mac OS Bundle or Linux AppImage respectively.

See cargo-bundle for an automated tool for creating Mac OS app bundles.

See here for directions on creating an AppImage for Linux.

Why is the size of my resulting executable larger than I had expected?

FLTK is known for it's small applications. Make sure you're building in release, and make sure symbols are stripped using the strip command in Unix-like systems. On Windows it's unnecessary since symbols would end up in the pdb file (which shouldn't be deployed).

If you need an even smaller size, try using opt-level="z":

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

Newer versions of cargo (>1.46) support automatically stripping binaries in the post-build phase:

cargo-features = ["strip"]

[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

Furthermore, you can build Rust's stdlib optimized for size (it comes optimized for speed by default). More info on that here

Can I cross-compile my application to a mobile platform or WASM?

FLTK currently doesn't support WASM nor iOS. It has experimental support for Android (YMMV). It is focused on desktop applications.

Licensing

Can I use this crate in a commercial application?

Yes. This crate has an MIT license which requires acknowledgment. FLTK (the C++ library) is licensed under the LGPL license with an exception allowing static linking for commercial/closed-source use. You can find the full terms of both licenses here:

Alignment

Why can't I align input or output text to the right?

FLTK has some known issues with text alignment.

Concurrency

Do you plan on supporting multithreading or async/await?

FLTK supports multithreaded and concurrent applications. See the examples dir and the fltk-rs demos repo for examples on usage with threads, messages, async_std and tokio (web-todo examples).

Should I explicitly call app::lock() and app::unlock()?

fltk-rs surrounds all mutating calls to widgets with a lock on the C++ wrapper side. Normally you wouldn't have to call app::lock() and app::unlock(). This depends however on the support of recursive mutexes in your system. If you notice haning in multithreaded applications, you might have to initialize threads (like xlib threads) by calling app::lock() once in your main thread. In that case, you can wrap widgets in an Arc or surround widget-mutating functions/methods with an app::lock and app::unlock. But that should rarely be required.

Windowing

Why does FLTK exit when I hit the escape key?

This is the default behavior in FLTK. You can easily override it by setting a callback for your main window:

#![allow(unused)]
fn main() {
    wind.set_callback(|_| {
        if fltk::app::event() == fltk::enums::Event::Close {
            app::quit(); // Which would close using the close button. You can also assign other keys to close the application
        }
    });
}

Panics/Crashes

My app panics when I try to handle events, how can I fix it?

This is due to a debug_assert which checks that the involved widget and the window are capable of handling events. Although most events would be handled correctly, some events require that the aforementioned conditions be met. Thus it is advisable to place your event handling code after the main drawing is done, i.e after calling your main window's show() method. Another point is that event handling and drawing should be done in the main thread. Panics accross FFI boundaries are undefined behavior, as such, the wrapper never throws. Furthermore, all panics which might arise in callbacks are caught on the Rust side using catch_unwind.

Memory and unsafety

How memory-safe is fltk-rs?

The callback mechanism consists of a closure as a void pointer with a shim which dereferences the void pointer into a function pointer and calls the function. This is technically undefined behavior, however most implementations permit it and it's the method used by most wrappers to handle callbacks across FFI boundaries. link

As stated before, panics accross FFI boundaries are undefined behavior, as such, the C++ wrapper never throws. Furthermore, all panics which might arise in callbacks are caught on the Rust side using catch_unwind.

FLTK manages it's own memory. Any widget is automatically owned by a parent which does the book-keeping as well and deletion, this is the enclosing widget implementing GroupExt such as windws etc. This is done in the C++ FLTK library itself. Any constructed widget calls the current() method which detects the enclosing group widget, and calls its add() method rending ownership to the group widget. Upon destruction of the group widget, all owned widgets are freed. Also all widgets are wrapped in a mutex for all mutating methods, and their lifetimes are tracked using an Fl_Widget_Tracker, That means widgets have interior mutability as if wrapped in an Arc<Mutex> and have a tracking pointer to detect deletion. Cloning a widget performs a memcpy of the underlying pointer and allows for interior mutability; it does not create a new widget. Images are reference-counted. All mutating methods are wrapped in locks. This locking might lead to some performance degradation as compared to the original FLTK library, it does allow for multithreaded applications, and is necessary in an FLTK (C++) application if it also required threading.

Overriding drawing methods will box data to be sent to the C++ library, so the data should optimally be limited to widgets or plain old data types to avoid unnecessary leaks if a custom drawn widget might be deleted during the lifetime of the program.

Can I get memory leaks with fltk-rs?

Non-parented widgets that can no longer be accessed are a memory leak. Otherwise, as mentioned in the previous section all parented widgets lifetimes' are managed by the parent. An example of a leaking widget:

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default();
    win.end();
    win.show();

    {
        button::Button::default(); // this leaks since it's not parented by the window, and has no handle in main
    }
}

A more subtle cause of leaks, is removing a widget from a group, then the scope ends without it being added to another group or deleted:

fn main() {
    let a = app::App::default();
    let mut win = window::Window::default();
    {
        button::Button::default(); // This doesn't leak since the parent is the window
    }
    win.end();
    win.show();

    {
        win.remove_by_index(0); // the button leaks here since it's removed and we no longer have access to it
    }
}

Why is fltk-rs using so much unsafe code?

Interfacing with C++ or C code can't be reasoned about by the Rust compiler, so the unsafe keyword is needed.

Is fltk-rs panic/exception-safe?

FLTK (C++) doesn't throw exceptions, neither do the C wrapper (cfltk) nor the fltk-sys crate. The higher level fltk crate, which wraps fltk-sys, is not exception-safe since it uses asserts internally after various operations to ensure memory-safety. An example is a widget constructor which checks that the returned pointer (from the C++ side) is not null from allocation failure. It also asserts all widget reads/writes are happening on valid (not deleted) widgets. Also any function sending a string across FFI is checked for interal null bytes. For such functions, the developer can perform a sanity check on passed strings to make sure they're valid UTF-8 strings, or check that a widget was not deleted prior to accessing a widget. That said, all functions passed as callbacks to be handled by the C++ side are exception-safe.

Are there any environment variables which can affect the build or behavior?

  • CFLTK_TOOLCHAIN=<path> allows passing the path to a CMake file acting as a CMAKE_TOOLCHAIN_FILE, this allows passing extra info to cmake if needed.
  • CFLTK_WAYLAND_ONLY=<1 or 0> allows building for wayland only without directly linking X11 libs nor relying on their headers for the build process. This only works with the use-wayland feature flag.
  • CFLTK_BUNDLE_DIR=<path> allows passing a path of prebuilt cfltk and fltk static libs, useful for when a customized build of fltk is needed, or for targetting other arches when building with the fltk-bundled flag.
  • CFLTK_BUNDLE_URL=<url> similar to above but allows passing a url which will directs the build script to download from the passed url.
  • FLTK_BACKEND=<x11 or wayland> allows choosing the backend of your hybrid X11/wayland FLTK app. This only works for apps built with use-wayland feature flag.

Contributing

Please refer to the CONTRIBUTING page for further information.