Aus Linux-Magazin 07/2023

Planet Rust – Episode 4: GUIs

© NASA

In Rust gibt es nicht die eine Oberflächenbibliothek, die Sie wie bei anderen Sprachen üblich bei jedem Programm automatisch verwenden. Wir sorgen für ein wenig mehr Klarheit auf diesem Gebiet.

Irgendwann kommt für jedes professionelle Softwarehaus der Zeitpunkt, sich von einem liebgewonnenen Tool trennen zu müssen, da es nicht mehr in die aktuelle Anwendungswelt passt. Vor mehreren Jahren war ich an einem solchen Auswahlprojekt beteiligt, in dem es um nicht weniger als die “Oberflächenbibliothek der Zukunft” ging. Wir fragten uns also, wie eine möglichst objektive Auswahl aussehen könnte. Nach längerem Hin und Her kam ein Kollege auf auf die Idee: “Machen wir es so wie bei den Autotests im Fernsehen. Wir definieren einen Parcours aus verschiedenen Aufgaben und stoppen die Zeit, die wir jeweils mit einer bestimmten Bibliothek dafür brauchen.”

Das klang für mich im ersten Moment etwas komisch, stellte sich aber als überaus praktisch heraus. Mit der Abarbeitung der einzelnen Aufgaben erkannten wir sehr schnell, wofür die eine Bibliothek besser geeignet war als die andere. In diesem Zusammenhang stießen wir auf die Webseite von Eugen Kiss “The 7 Tasks” [1]. Er beschreibt sieben typische Aufgaben für Oberflächen: vom Hochzählen mit einem Button (Counter) bis zum freien Zeichnen von Kreisen (Circle Drawer) (Abbildung 1).

Abbildung 1: Zu den typischen Aufgaben für Oberflächen gehören unter anderem ein Counter und ein Timer.

Abbildung 1: Zu den typischen Aufgaben für Oberflächen gehören unter anderem ein Counter und ein Timer.

Auf der Webseite finden Sie neben der ausführlichen Erklärung der einzelnen Aufgaben die entsprechenden Lösungen für unterschiedliche Programmiersprachen [2] und GUI-Bibliotheken (Abbildung 2).

Abbildung 2: Einige der "7 Tasks" in Rust mit der Bibliothek Egui finden Sie auf Github.

Abbildung 2: Einige der “7 Tasks” in Rust mit der Bibliothek Egui finden Sie auf Github.

Oberflächen mit Rust

Die verschiedenen GUI-Bibliotheken für Rust lassen sich grob in vier Kategorien einteilen (siehe Tabelle “Zugriff auf Bibliotheken”): Einerseits existieren Bibliotheken, die den Zugriff von Rust auf plattformspezifische APIs ermöglichen. Andererseits erlauben weitere Bibliotheken, auf gängige plattformunabhängige APIs zuzugreifen. Die dritte Kategorie enthält Bibliotheken für Rust, die auf Web-/DOM-Frameworks basieren. Schließlich gibt es zudem Bibliotheken, die mit Rust und primär für Rust entwickelt wurden.

Dieser Artikel konzentriert sich auf zwei einfache Beispiele: Eines mit FLTK [3], einer gängigen plattformunabhängigen Bibliothek, und das andere mit der Rust-Bibliothek Egui [4]. Das Spannende an Egui ist die andere, sehr direkte Herangehensweise. Mehr über die GUI und Rust finden Sie im Blog “Rust: state of GUI” [5].

Plattform

Bibliothek

Beschreibung

URL

Zugriff auf plattformspezifische Bibliotheken

MacOS, iOS

Cacao

Zugriff auf AppKit und UIKit

https://github.com/ryanmcgrath/cacao

Win32

Winsafe

Windows-API und GUI mit Rust

https://github.com/rodrigocfd/winsafe

Zugriff auf plattformunabhängige Bibliotheken

FLTK

Fltk-rs

eigentlich C++-GUI-Toolkit für Linux, Microsoft Windows und MacOS

https://github.com/fltk-rs/fltk-rs

GTK

Gtk-rs

Anbindung von Rust an die Bibliotheken von Gnome, wie Glib, Cairo, GTK 3/4

https://gtk-rs.org

Qt

Cxx-qt

Verwendung von Qt zusammen mit Rust

https://kdab.github.io/cxx-qt/book/

Auf Basis von Web/DOM-Frameworks

Tauri

Web-Apps mit Rust

https://tauri.app

Für Rust entwickelt

Fui

Model-View-View-Model-Framework für Rust

https://github.com/marek-g/rust-fui

Iced

einfach zu programmierende GUI für unterschiedliche Plattformen

https://github.com/iced-rs/iced

KAS

GUI-Toolkit mit Controls

https://github.com/kas-gui/kas

Egui

Oberflächenbibliothek mit direktem Zugriff für unterschiedliche Plattformen und das Web

https://github.com/emilk/egui

Zugriff auf FLTK

Sollten Sie bereits mit anderen GUIs programmiert haben, wirkt die GUI-Bibliothek FLTK leichtgewichtig und ist einfach zu verstehen. Die Grundlage dafür bildet eine in C++ geschriebene Bibliothek. Die Rust-Version verwendet die sogenannte FFI-Schnittstelle (Foreign Function Interface), um auf die Basisfunktionen zuzugreifen. Listing 1 zeigt eine Umsetzung der ersten Aufgabe der “7 Tasks” von Eugen Kiss. Mit jedem Klick auf einen Button wird eine Zahl hochgezählt.

Listing 1

fltk_first.rs

use fltk::prelude::*;
use fltk::app::{App, Scheme};
use fltk::button::Button;
use fltk::output::Output;
use fltk::window::Window;
const WIDGET_HEIGHT: i32 = 25;
const WIDGET_WIDTH: i32 = 70;
const PADDING: i32 = 10;
fn main() {
  let app = App::default().with_scheme(Scheme::Gtk);
  let mut win = Window::new(100, 100, WIDGET_WIDTH*2 + 3*PADDING, WIDGET_HEIGHT + PADDING*2, "Counter");
  let mut output = Output::new(PADDING, PADDING, WIDGET_WIDTH, WIDGET_HEIGHT, "");
  output.set_value("0");
  let mut button = Button::default()
  .with_size(WIDGET_WIDTH, WIDGET_HEIGHT)
  .right_of(&output, PADDING)
  .with_label("Count");
  let mut value = 0;
  button.set_callback(move |_| {
      value += 1;
      output.set_value(&format!("{}", value))
    }
  );
  win.end();
  win.show();
  app.run().unwrap();
}
let app = App::default().with_scheme(Scheme::Gtk);

Für die Oberfläche erzeugen Sie zunächst ein Objekt vom Typ »App«. Es ist die Klammer um die gesamte GUI. Mit »app.run().unwrap();« startet Rust, nachdem alle Inhalte definiert sind, die Anzeige und Verarbeitung der Oberfläche (Listing 1, Zeile 26). Rust bleibt so lange bei der Abarbeitung dieses Befehls, bis Sie die Oberfläche schließen. Erst danach kommen die restlichen Befehle in der Funktion »main()« (Zeile 9). Dieses Ablaufverhalten sollten Sie bitte beim Aufbau des Programms einkalkulieren.

FLTK bietet verschiedene Styles (Schemes) an. Die Einstellung »with_scheme(Scheme::Gtk);« bei der App in diesem Beispiel lässt die Oberfläche wie ein im Linux-Bereich häufig verwendetes Gtk-Toolkit aussehen (Listing 1, letzte Zeile).

Zusätzlich zur App brauchen Sie ein Objekt vom Typ »Window«, in dem am Ende die Oberfläche zu sehen ist (Zeile 11). FLTK verwendet die Fensterobjekte der jeweiligen Betriebssystemplattform: Unter Windows HWND, bei MacOS NSWindow und XWindow unter Linux.

Für das Counter-Beispiel fehlen noch zwei Controls (Widgets), eines zur Ausgabe der Zahl (»output«) und als ein weiteres der Befehlsknopf (»button«) zum Drücken. Beim Ausgabe-Widget »output« reicht zur Definition die Größe (»WIDGET_WIDTH, WIDGET_HEIGHT«, ab Zeile 15).

Widgets lassen sich entweder direkt mit der Funktion »new()« erzeugen oder mit einem Builder-Pattern, wie es beim Widget »button« zu sehen ist (Zeile 14). Dabei erzeugen Sie im ersten Schritt ein Grundgerüst und fügen daraufhin alle gewünschten Änderungen hinzu.

Um festzulegen, was bei einem Klick auf den Button passieren soll, übergeben Sie dem Button eine Funktion mit der Methode »set_callback« (ab Zeile 19). Diese ruft FLTK auf, sobald ein Klick-Event für den Button eintrifft. In Listing 1 sieht das etwas kompliziert aus, da in solchen Fällen die Funktion gleich im Befehl definiert wird und nicht wie gewohnt mit dem Befehl »fn« irgendwo im Programm. Diese anonymen Funktionen, Closure genannt, haben ihre Besonderheiten, auf die ich in einer späteren Folge von Planet Rust detaillierter eingehen möchte. Zusätzlich steht noch der Befehl »move« vor der anonymen Funktion. Dahinter steckt ein Auftrag an die Speicherverwaltung von Rust. Darauf komme ich ebenfalls ausführlich in einem weiteren Artikel zurück.

FLTK eignet sich sehr gut zum schnellen Erstellen von Oberflächen, weil es bereits viele grundlegende Widgets mitbringt. Es arbeitet wie die meisten GUI-Bibliotheken im Retain Mode. Sie definieren zuerst alle Elemente, und die Abarbeitung findet später in einer Schleife statt, die bei FLTK innerhalb der Methode »run« läuft. Bei der nächsten Bibliothek Egui ist das ganz anders.

Oberflächen mit <C>Egui<C>

Im Gegensatz zu FLTK und anderen GUI-APIs arbeitet Egui statt im Retain Mode im Immediate Mode. Wie eben gezeigt, erstellen Sie beispielsweise im Retain Mode einen Button zusammen mit einer Callback-Funktion. Möchten Sie etwa die Beschriftung des Buttons ändern, müssten Sie irgendwo eine Referenz darauf gespeichert haben. Im Immediate Mode verhält sich das anders: Das Programm durchläuft die Befehle zum Zeichnen und Interagieren mit dem Button bei jeder Bildaktualisierung (zum Beispiel 60 Mal pro Sekunde). Das bedeutet, dass Sie dort sofort (immediate) eingreifen und einfach beim nächsten Durchlauf einen anderen Beschriftungstext angeben können. Sie brauchen keine Callback-Funktion und keine zentralen Referenzen auf GUI-Elemente.

Der Vorteil von GUI-Bibliotheken, die im Immediate Mode arbeiten, liegt darin, dass sie einfacher zu verwenden sind und sich in andere Frameworks wie Spiele- oder App-Engines ohne viel Aufwand integrieren lassen. Nachteilig wirkt sich dagegen die Mehrbelastung der CPU aus.

Eine Oberfläche mit Egui läuft überall dort, “wo man ein texturiertes Dreieck zeichnen kann”, wie es in dessen Dokumentation steht (Abbildung 3). Sie können eine solche Anwendung auf Ihrem Linux-Rechner laufen lassen, oder für das Web als Webassembly kompilieren. Dann haben Sie eine Webapp.

Abbildung 3: Die Beispiele der Bibliothek Egui lassen sich im Browser unter <a href="https://www.egui.rs/#demo" target="_blank">https://www.egui.rs/#demo</a> testen.

Abbildung 3: Die Beispiele der Bibliothek Egui lassen sich im Browser unter https://www.egui.rs/#demo testen.

Simpler Counter mit Egui

Listing 2 zeigt die Umsetzung des Counter-Beispiels mit Egui. Der erste Gedanke, der einem da kommen könnte: Egui soll doch einfach sein. Wieso für zwei Widgets so viele Programmzeilen? Die meisten Zeilen gehören zum “Drumherum”, nur fünf Zeilen zu Egui selbst.

Listing 2

counter_egui.rs

use eframe::epaint::Vec2;
use eframe::Theme;
pub struct Task1App {
  value: i32,
}
impl Default for Task1App {
  fn default() -> Self {
    Self {
      value: 0
    }
  }
}
impl Task1App {
  pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
    Default::default()
  }
}
impl eframe::App for Task1App {
  fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
    egui::CentralPanel::default().show(ctx,|ui| {
      ui.horizontal(|ui| {
        ui.label(format!("{}", self.value));
        if ui.button("Count").clicked() {
          self.value += 1;
        }
      })
    });
  }
}
fn main() {
  let native_options = eframe::NativeOptions{
    default_theme: Theme::Light,
    initial_window_size: Option::from(
      Vec2::new(160.0, 20.0)
    ),
    resizable: true,
    ..Default::default()
  };
  eframe::run_native(
    "Counter",
    native_options,
    Box::new(|cc| Box::new( Task1App::new(cc))),
  );
}

Zeile 22 erzeugt das eine Element, den Beschriftungstext. Die Variable »ui« ist der Bezug zur Oberfläche, und die Methode »label« fügt eine Beschriftung hinzu, in diesem Fall die Anzahl der Klicks, die beim Button angekommen sind. Die Methode »button« fügt einen Button mit der Beschriftung »Count« hinzu (Zeile 23). Nach einem Klick darauf (»clicked«) erhöht Rust den Wert von »value« um eins (Zeile 24). Für alle gängigen Oberflächenelemente existieren ebenfalls Methoden, um diese einfach zu erzeugen. So viel zur eigentlichen Oberfläche, jetzt zum Drumherum.

Irgendwo müssen die Widgets positioniert sein. Dies erfolgt in Egui auf Panels, in diesem Fall im »CentralPanel«, da wir nur eine Fläche haben (Zeile 20). Darüber hinaus stehen in komplexeren Oberflächen noch »SidePanel« oder »TopBottomPanel« zur Verfügung.

Für die Bibliothek Egui gibt es unzählige Integrationen in Spieleframeworks oder andere Programme. Möchten Sie Egui als eigenständige GUI nutzen, bietet sich die Rust-Bibliothek »eframe« an. Der Datentyp »App« liefert alles Nötige. In der Funktion »App::update« erstellen Sie die Oberfläche. »eframe::run_native« startet eine Desktop-Anwendung, mit »eframe::start_web« könnten Sie sogar eine Webanwenung daraus machen. Die Arbeit übernehmen der Compiler und die Bibliotheken.

Siebzehn-und-Vier

Wie in der letzten Folge von Planet Rust angekündigt, ergänzen wir nun bei dem Beispielprogramm Siebzehn-und-Vier eine grafische Oberfläche (Abbildung 4).

Abbildung 4: Mit der Bibliothek Egui lassen sich die verschiedenen grafischen Elemente eines Siebzehn-und-Vier-Spiels umsetzen.

Abbildung 4: Mit der Bibliothek Egui lassen sich die verschiedenen grafischen Elemente eines Siebzehn-und-Vier-Spiels umsetzen.

Durch den Aufbau des Spiels als Lasagne-Applikation (mehr dazu finden Sie in der letzten Folge von Planet Rust [6]) macht das Programm alle Ein- und Ausgaben über die in der Schnittstelle (»trait«) »PresentationLayer« (Listing 3) definierten Funktionen.

Listing 3

Presentation Layer

pub trait PresentationLayer{
  fn show(&self, cards_player: &Vec<Card>, sum_player:i32, cards_bank: &Vec<Card>, sum_bank:i32)->();
  fn another_card(&self)->bool;
  fn show_winner(&self,player_win:bool)->();
}

Für die Kommandozeilenversion hat diese Schnittstelle die Struktur »PresentionLayerCli« realisiert. Für die grafische Oberfläche nutzen Sie die neue Struktur »PresentationLayerEgui«, die diese Aufgabe mit Egui erledigt. Die eigentliche Idee lautete: Alte Struktur durch neue austauschen – ruckzuck fertig. So einfach, wie ich es mir zu Beginn vorgestellt hatte, war es am Ende aber doch nicht.

Eine GUI fällt sehr anders aus als eine einfache Eingabeaufforderung aus. Bei der Eingabeaufforderung macht das Kartenspiel etwas, dann kommt die Anzeige oder die Abfrage und dann wieder das Programm. Im Grunde beschreibt dies eine klassische synchrone Vorgehensweise. Bei einer grafischen Oberfläche macht der Anwender etwas, anschließend reagiert das Programm darauf – also eine grundsätzlich andere Philosophie.

Um möglichst wenig am Spiel ändern zu müssen und damit von der Schnittstellenstruktur zu profitieren, überlegte ich mir, das Spiel und die Oberfläche parallel in jeweils eigenen Programmsträngen (Threads) laufen zu lassen. Beide unterhalten sich, wenn notwendig, und jeder kann seinen eigene Programmstruktur haben. In anderen Programmiersprachen könnte das sehr aufwendig sein, aber Rust bietet ein relativ einfaches und zuverlässiges Multithreading. Das Ergebnis können Sie sich auf Github [7] ansehen. Zusätzlich finden Sie alle Beispiele Download-Bereich dieses Artikels.

Fazit

In Rust gibt es für die unterschiedlichen Anforderungen eine Menge verschiedener Oberflächenbibliotheken. Angesichts der Menge von Rust-Schnittstellen für die unterschiedlichsten GUIs ist es sinnvoll, sich einmal einen Überblick zu verschaffen – die Palette reicht von betriebssystemspezifischen APIs über die Anbindung an Bekanntes wie FLTK bis hin zu kompletten Neuentwicklungen wie Egui. Durch die einfache Anbindung an C und C++ dürften in Zukunft mehr hinzukommen. Die etwas andere Herangehensweise von Egui finde ich wirklich spannend. Dass Rust-Programme durch Kompilieren in Webassembly einfach in jedem Browser laufen, könnte eine kostengünstige Alternative zu dem aktuell stattfindenden Webprogrammierchaos mit immer mehr Javascript-Frameworks, Werkzeugen und Generatoren sein. (csi)

Infos

  1. Eugen Kiss, “The 7 Tasks”: https://eugenkiss.github.io/7guis/tasks/
  2. 7 Tasks in Rust mit Egui: https://github.com/Rust-Ninja-Sabi/rust-egui-seven-tasks
  3. FLTK: https://www.fltk.org
  4. Egui: https://docs.rs/egui/latest/egui/
  5. “Rust: state of GUI”: https://kas-gui.github.io/blog/state-of-GUI-2022.html
  6. Planet Rust (Folge 3), Gerhard Völkl, “Weltraumnahrung”, LM 05/2023, S. 74: https://www.lm-online.de/48940
  7. Github-Repository zum Artikel: https://github.com/Rust-Ninja-Sabi/rust-twentyone-egui
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben