Textbasierte Benutzeroberflächen zu erstellen, ist nicht besonders kompliziert. Die vorgestellten Frameworks erfordern zwar zunächst etwas Einarbeitungszeit, ermöglichen aber sehr viel Freiheit beim Design.
Der Einsatz textbasierter Benutzeroberflächen gehört im Vergleich zu grafischen Oberflächen im Alltag eher zur Ausnahme. Allerdings finden sich immer wieder prominente Vertreter, die schon seit vielen Jahren ihren festen Platz auf vielen Rechnern haben. Dazu gehört sicher der Norton Commander sowie das freie Pendant Midnight Commander. Beide Programme haben einen Ruf als Dateimanager erworben, die schnell und intuitiv arbeiten.
Programme mit einer textbasierten Oberfläche wirken oft auf den ersten Blick recht simpel. Meist sorgt ein wenig Farbe und eine andere Anordnung der Ausgabe auf dem Bildschirm nur für bessere Übersicht. Sie sind auf ihre eigentliche Aufgabe exakt zugeschnitten. Vorteilhaft wirken sich die kleinere Programmgröße und der geringere Ressourcenbedarf aus. Daher finden sich solche Programme zum Beispiel in der Konfiguration des BIOS und auf Embedded Systemen wieder.
Als primäres Eingabewerkzeug kommt meist die Tastatur zum Einsatz. Damit ermöglicht den problemlosen Betrieb auf entfernten Rechnern, zum Beispiel über eine verschlüsselte Verbindung via SSH. Die Datenmenge, die dabei zwischen dem lokalen und dem entfernten System fließt und die damit verbundene Latenz beim Übertragen der Daten fällt deutlich geringer als bei einer GUI.
Zum Bedienen einer GUI aus der Ferne benötigen Sie andere Lösungen: Eine Verbindung mit X11-Weiterleitung (ssh -X), den Einsatz eines VNC-Clients (beispielsweise Vinagre für Gnome) oder das Erweitern des Computers mit Spezial-Hardware, wie einer Remote-Management-Karte. Einen detaillierteren Überblick zu Text User Interfaces im Allgemeinen gibt die Wikipedia [1].
Zwischenschicht
Seit dem Beginn der EDV kommen Terminals – Konsole genannt – bei der Ein- und Ausgabe von Daten an Rechenmaschinen zum Einsatz. Terminals ersetzten die an Großrechnern verwendeten Fernschreiber. Die Gerätedateien für Terminals unter Unix/Linux heißen daher in Anlehnung an diese Vergangenheit heute tty als Kurzform für Teletype, das englische Wort für Fernschreiber) (Abbildung 1).
Die meisten von uns verwenden heute eher selten echte Terminals und stattdessen die Emulation eines Textterminals in der graphischen Oberfläche, zum Beispiel Programme wie Xterm oder das Gnome Terminal. Linux-Systeme unterstützen verschiedene Terminalvarianten. Das Ansteuern erfolgt über eine spezielle Softwarebibliothek und -datenbank – über die Termcap Library [2] oder Terminfo [3].
Termcap steht für terminal capability, zu deutsch etwa Terminalfähigkeiten. Die Bibliothek hält alle Informationen in der Textdatei /etc/termcap vor, wohingegen Terminfo für jeden Terminaltyp dessen Eigenschaften im Verzeichnis /lib/terminfo/ als separate Binärdatei ablegt.
Beide Bibliotheken beschreiben, über welche Eigenschaften ein Text-Terminal verfügt. Dazu zählen nicht nur die Schrifteigenschaften (normal, fett, invertiert, blinkend oder unterstrichen), sondern auch das Adressieren und Positionieren des Cursors sowie das Neuzeichnen des Bildschirms (Refresh).
Ein Programm kommuniziert nicht mehr direkt mit dem Terminal, sondern über diese Zwischenschicht, welche die Ein- und Ausgaben korrekt an die verwendete Terminalvariante übersetzt und weiterleitet. Auf diese Weise braucht sich das Programm nicht mehr mit allen Terminalvarianten auszukennen. Das vereinfacht das Schreiben portablen Programmcodes erheblich.
Ansteuern des Terminals
Programmierer haben es heute relativ leicht. Natürlich könnten Sie Termcap oder Terminfo direkt ansprechen. Einfacher geht es aber, wenn Sie eine der Bibliotheken Ncurses [4] oder Newt [5] nutzen, und diese die Kommunikation mit Termcap oder Terminfo übernehmen. Gleichzeitig bringen Ncurses und Newt alle Voraussetzungen zum Erzeugen von Masken und Menüs mit.
Als Programmiersprache kommt der Einfachheit halber die Skriptsprache Python [6] zum Einsatz. Das Übersetzen des Programmcodes in ein ausführbares Programm erfolgt automatisch zur Laufzeit. Kompilieren im Vorfeld ist nicht erforderlich.
Um den Einsatz zu illustrieren und als Grundlage für eigene Experimente enthält dieser Artikel sowie der darauf folgende zweite Teil kleinere Beispielprogramme. Achten Sie bei der Übernahme des Beispielcodes darauf, dass Sie die Einrückung der einzelnen Anweisungen beibehalten. Python unterscheidet dadurch die einzelnen Programmblöcke. Klammern – wie beispielsweise in C/C++ für einzelne Funktionsblöcke – kommen dafür nicht zum Einsatz.
Flexibel oder schnell
Zum Entwickeln von Software mit Python/Ncurses stehen unterschiedliche Wege offen; Zum einen haben Sie die Möglichkeit, die Bibliothek mit dem Python-Modul curses direkt anzusprechen. Deutlich bequemer geht das Programmieren aber mit den Python-Modulen Urwid [7] und Pythondialog [8]. Für letzteres heißt das Python-Modul dialog und stellt eine vollständige Klasse mit den benötigten Fensterwidgets bereit.
Für Oberflächen mit Newt steht zunächst das Paket python-newt bereit. Das eigentliche Modul heißt dann kurioserweise snack – nicht zu verwechseln mit dem Snack Sound Kit [9] zum Erstellen von Audioapplikationen.
Aufgrund der untschiedlichen Herangehensweisen der einzelnen Module stellt dieser Artikel zuerst das Curses-Modul und Urwid vor, die beiden sehr viel Flexibilität beim Programmieren bieten. der nächste Teil behandelt dann Python-dialog und Python-newt. Mit diesen erzielen Sie sehr schnell Ergebnisse aufgrund der vorgefertigten Elemente.
Python/Ncurses
Ncurses bietet Ihnen alle Freiheiten, um Informationen auf dem Textbildschirm an beliebiger Position auszugeben. Einfach ist es nicht, da es gilt, etliche Details beim Ansteuern des Terminals zu berücksichtigen. Einen generischen Rahmen für Python-Ncurses-Programme finden Sie in Listing 1.
Listing 1
import curses win=curses.initscr() curses.start_color() curses.noecho() curses.cbreak() win.keypad(1) # ... eigentliches Programm ... win.keypad(0) curses.echo() curses.nocbreak() curses.endwin()
Zunächst laden Sie in Zeile 1 das Modul und initialisieren es (Zeile 3). Über die darauf folgenden Zeilen bestimmen Sie die Terminalvariante und legen verschiedene, interne Datenstrukturen an. In der Variable win befindet sich ein Fensterobjekt, das den gesamten Terminalbildschirm repräsentiert. Alle Aktionen im Fenster – zum Beispiel das Ändern der Cursorposition, die Ausgabe oder das Löschen von Text – ermöglichen Funktionen, die zu diesem Objekt gehören.
Möchten Sie zusätzliche Farben zu verwenden, erfordert dies den Aufruf der Funktion curses.start_color() (Zeile 4). Die Funktion noecho() (Zeile 5) schaltet die direkte Ausgabe von Tastendrücken auf dem Bildschirm ab. Normalerweise werten Textprogramme die Tastendrücke erst aus und entscheiden dann, was sie auf dem Bildschirm ausgeben. Prominentes Beispiel dafür ist der Texteditor Vim, der über mehrere Aktionsmodi verfügt. Je nach aktiviertem Modus interpretiert die Software die Tasten anders.
Textterminals arbeiten normalerweise zeilenorientiert. Sie sammeln die Eingabe in einem Puffer und schicke sie erst bei Betätigen der Eingabe-Taste an die Anwendung. Um dieses Verhalten abzuschalten und alle Tastendrücke sofort zu erhalten, aktivieren Sie im Programm den Cbreak-Modus (Zeile 6).
Mit dem Einschalten des Keypad-Modus (Zeile 7) übermittelt das Modul für alle Sondertasten bereits vordefinierte Tastencodes, die das Auswerten der Tastendrücke vereinfachen, zum Beispiel curses.KEY_DOWN für die Pfeiltaste nach unten.
Um die Applikation zu beenden, genügt der Aufruf der Komplementärfunktionen (Zeilen 11 bis 13), in denen Sie auf den “buffered mode” umstellen und wieder dafür sorgen, dass das Terminal alle Zeichen direkt ausgibt. Abschließend erfolgt der Aufruf der Funktion curses.endwin() (Zeile 14), um das Terminal auf den Originalzustand zurückzusetzen (analog zur Funktion curses.initscr()).
Unsere Beispielapplikation (Abbildung 2) verfügt über eine farbige Titel- und Statuszeile und ein kleines, zweites Fenster zur Textausgabe. Auf Tastendruck ändern sich die Farbe des Textes und des Hintergrunds im kleinen Fenster.
Listing 2
# -*- coding: utf-8 -*-
import curses, traceback, string
try: # curses initialisieren
win=curses.initscr()
curses.start_color()
curses.noecho()
curses.cbreak()
win.keypad(1)
# 5 Farbsets definieren
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_RED)
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE)
curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_GREEN)
curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_CYAN)
curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_YELLOW)
# Breite und Höhe des Fensters ermitteln
(height, width) = win.getmaxyx()
# oberste Zeile füllen: Text
headline = " Fenster 1 (Hoehe %s, Breite %s)" % (height, width)
headline = string.ljust(headline, width, ' ')
win.addstr(0, 0, headline, curses.color_pair(1))
running = 4
while running:
win.refresh()
vwin = curses.newwin(7,25,5, (width - 25)/ 2)
vwin.clear()
# oberste Zeile von Fenster 2 füllen, Text mittig
headline2 = string.center(" Fenster 2 ", 23, ' ')
vwin.addstr(1, 1, headline2, curses.color_pair(5))
vwin.addstr(3,1,"Etwas Text in einem\n separaten Fenster", curses.color_pair(running))
vwin.box()
vwin.refresh()
# Statuszeile füllen, Text linksbündig
bottomline = " Statuszeile | Durchlauf %s von %s " % (5 - running, 4)
bottomline = string.ljust(bottomline, width, ' ')
win.addstr(height - 5, 0, bottomline, curses.color_pair(2))
# auf Tastendruck warten
comm_ch = win.getch()
running = running - 1
win.clear()
win.refresh()
# Programm ohne Fehlermeldung beenden
win.keypad(0)
curses.echo()
curses.nocbreak()
curses.endwin() # curses beenden
except:
# Programm mit Fehlermeldung beenden
win.keypad(0)
curses.echo()
curses.nocbreak()
curses.endwin()
traceback.print_exc() # Fehlermeldung ausgeben
Als erstes ergänzen Sie den Programmrahmen um die Encoding-Deklaration (Zeile 1) sowie den Import der zusätzlich benötigten Python-Module traceback und string (Zeile 3). Traceback ist eine Art digitaler Hängematte – es dient zum vernünftigen Abfangen und Übermitteln auftretender Fehler im Programmablauf. Das String-Modul stellt die Funktionen für Zeichenketten bereit.
Die gesamte Applikation schließen Sie am besten in ein Try-Except-Konstrukt ein, um bei Fehlern zur Laufzeit ein möglichst sauberes Beenden des Programms zu gestatten (Zeilen 5 und 58). Die Hauptanweisungen des Programms liegen im Try-Zweig. Tritt dort ein Fehler auf, so springt das Programm in den Except-Zweig. Den Except-Zweig füllen Sie mit den gleichen Anweisungen wie zum regulären Beenden, jedoch ergänzt um die Ausgabe der Fehlermeldung (Zeile 64). Der Traceback-Aufruf zeigt uns die genaue Position im Python-Skript, an der der Fehler auftrat.
Curses biete die Möglichkeit, Farbwerte zu Paaren für den Vorder- und Hintergrund zu kombinieren. Die Zeilen 13 bis 17 definieren 5 Sets. Auf diese greifen Sie später noch zu. Jede Applikation braucht zudem Daten darüber, wie groß das Fenster ist, in dem sie agiert. Mit dem Funktionsaufruf in Zeile 20 ermitteln Sie die Breite und Höhe des Fensters, dass heißt, wieviele Zeilen und Spalten das Terminal bereitstellt.
Die Zeilen 23 und 24 sorgen dafür, dass der Text für die Titelzeile linksbündig ausgerichtet auf dem Bildschirm erscheint. Der Einfachheit halber enthält Text nur die Höhe und Breite des Fensters. Zeile 25 veranlasst das Hinzufügen des Textes zum ausgewählten Fenster, beginnend an der angegebenen Position. Die Werte 0, 0 bezeichnen die obere, linke Ecke des Fensters. Der vierte Parameter im Funktionsaufruf wählt das erste, von uns bereits vorher definierte Farbset aus (weiße Schrift auf rotem Hintergrund).
Erfrischend
Damit Veränderungen im virtuellen Fenster auf dem Bildschirm erscheinen, gilt es, diesen zu aktualisieren. Dazu dient der Aufruf in Zeile 30, der zu Beginn jedes Schleifendurchlaufs stattfindet (Zeile 28 bis 49).
Die Zeilen 31 bis 39 erstellen ein Unterfenster, das ebenfalls eine Überschrift bekommt (Zeilen 34 und 35). Der Text der Überschrift ist diesmal mittig ausgerichtet und erscheint in den Farben schwarz auf gelb (Farbkombination 5). Zeile 37 sorgt dafür, dass der Text im Unterfenster erscheint, die Farbe wechselt dabei in Abhängigkeit des Schleifendurchlaufs. Die Zeilen 38 und 39 aktualisieren den Text im Unterfenster.
Die Zeilen 42 bis 44 erzeugen eine Statuszeile, die die Nummer des aktuellen Schleifendurchlauf darstellt. Der Text steht hier wieder linksbündig und erscheint als weiße Schrift auf blauem Grund (Farbkombination 2).
Ab Zeile 47 wartet die Applikation auf einen Tastendruck vom Benutzer. Hat das Programm die maximale Anzahl Schleifendurchläufe erreicht, so endet die Schleife, und es durchläuft den Code ab Zeile 50. Dieser veranlasst das Löschen des Bildschirminhaltes und nachfolgend das Aktualisieren des Fensters (Zeile 51). Ab Zeile 53 beendet sich das Skript wie oben beschrieben.
Für die Programmiersprache C ist das recht schick in einem Wikibook [10] zusammengestellt. Zum Einstieg ist das vielleicht zu komplex, aber als Referenz zum Nachschlagen durchaus gut geeignet.
In Netz findet sich ein Tutorial [11] für das Curses-Modul. Weitere Informationen zu allen Funktionen finden sich in der ausführlichen Python-Dokumentation [12]. Eine Zeitlang existierte ein Paket curses-extra vom Entwickler Riccardo Galli, welches bereits vordefinierte Fenster zur Eingabe und Auswahl sowie Knöpfe und Scrollbalken bot. Derzeit existiert aber kein aktuelles Paket dafür.
Urwid
Wer vorgefertigte Elemente sucht, dem empfiehlt sich ein Blick auf Urwid. Das recht aktive Projekt genießt noch nicht den Bekanntheitsgrad wie curses oder dialog. Es ermöglicht aber, mit wenig Programmcode zu sehr brauchbaren Ergebnissen zu kommen. Es bringt keine fertigen Dialogboxen mit, aber eine eine Reihe intelligent programmierter Text- und Füllobjekte.
Um das Sichern oder Wiederherstellen des Bildschirminhalts brauchen Sie sich nicht zu kümmern. Wichtig ist die Funktion urwid.MainLoop(), die eine Hauptschleife im Programm erzeugt und auf die Tastendrücke des Benutzers reagiert. Mit dem Aufruf loop.run() aktivieren Sie diese Schleife. Rufen Sie die ohne Parameter auf, gelingt das Abbrechen der Schleife nur mit [Strg]+[C].
Text, den Sie ausgeben möchten, binden Sie zunächst an ein Textobjekt der Klasse urwid.Text(). Um daraus eine Box zu erzeugen, packen Sie dieses Objekt in ein Füllobjekt (urwid.Filler()).
Listing 3
# -*- coding: utf-8 -*-
import urwid
def show_or_exit(input):
# Eingabe prüfen
if input in ('q', 'Q', 'esc'):
raise urwid.ExitMainLoop()
# Tastendruck darstellen
txt.set_text('Taste ' + repr(input))
# Textobjekt erzeugen und im Textfeld oben anordnen
txt = urwid.Text(" Hello World! ")
fill = urwid.Filler(txt, 'top')
# Hauptschleife festlegen und aufrufen
loop = urwid.MainLoop(fill, unhandled_input=show_or_exit)
loop.run()
Das kleine Hallo-Welt-Programm (Listing 3) beinhaltet die beiden Elemente. Es erzeugt zunächst ein Textobjekt (Zeile 14), welches einer Box in der obersten Zeile erscheint (Zeile 15). Da für die Box keine Größenangaben vorliegen, nimmt sie den gesamten Inhalt des Terminals ein.
Die Zeilen 18 und 19 erzeugen die Hauptschleife loop, und mit loop.run() startet diese. Der erste Parameter bindet die Hauptschleife an das Textfeld. Der zweite Parameter legt fest, welche Funktion einen Tastendruck behandelt. Hier ist es die lokale Funktion show_or_exit(), welche in Zeilen 5 bis 11 definiert ist.
De Funktion show_or_exit() prüft zunächst, ob die Eingabe [Q],[Umschalt]+[Q] oder [Esc] entspricht. Trifft das zu, beendet sich das Skript mit Hilfe von urwid.ExitMainLoop(). Für alle anderen Tastendrücke füllt das Programm das Textfeld mit dem Wert der gedrückten Taste und aktualisiert den Bildschirm.
Eingaben abfangen
Äußerst angenehm ist die Art und Weise, wie Urwid Tastendrücke verarbeitet. Im Gegensatz zu den anderen vorgestellten Toolkits fällt es hier am leichtesten, Tasten abzufragen (Abbildung 3). Das beinhaltet auch die Sondertasten, die das Modul in Form ihres Namens als Zeichenkette zurückgibt – zum Beispiel f9 für die Funktionstaste [F9]. Ebenso flexibel reagiert es auf das Verändern der Fenstergröße (window resize) – es passt sich problemlos an die neue Größe an.
Möchten Sie Farbkombinationen einsetzen, definieren Sie zunächst eine Palette, deren Einträge aus mindestens drei Komponenten bestehen – dem Namen der Kombination sowie der Vorder- und der Hintergrundfarbe (Listing 4).
Listing 4
palette = [
('kopfzeile', 'white', 'dark red'),
('hintergrund', 'white', 'dark blue')
]
Zum Einfärben des Textes übermitteln Sie den Namen der Farbkombination und den String in einem Tupel an das Textwidget. Der Hauptschleife machen Sie die verwendete Farbpalette bekannt, in dem Sie sie als zweites Argument übergeben.
Auf der Webseite des Projekts gibt es neben einem sehr schönen Tutorial und einer ausführlichen Bibliotheksdokumentation eine Anzahl komplexer Beispiele. Das beinhaltet neben einem Farbentest für bis zu 256 Farben (Abbildung 4) einen einfachen Texteditor, Fonts in unterschiedlichen Größen, Taschenrechner und auch Dialogboxen.
Fazit
Beim Gestalten der Benutzeroberfläche bieten Curses und Urwid viel Flexibilität, wobei letzteres vieles einfacher macht. Hier brauchen Sie sich weniger mit Nebensächlichkeiten zum Ansteuern des Bildschirms befassen.
Bei Python-dialog und Newt die im Mittelpunkt des nächsten Teils stehen, haben Sie die Auswahl zwischen vielen vorgefertigten Elementen. Das führt noch schneller zu ordentlichen Ergebnissen, allerdings um den Preis der Flexibilität.
Infos
[1] Text User Interfaces: http://en.wikipedia.org/wiki/Text_user_interface
[2] Termcap Library: http://www.gnu.org/software/termutils/manual/termcap-1.3/termcap.html
[3] Terminfo (englisch): http://en.wikipedia.org/wiki/Terminfo
[4] Ncurses: http://www.gnu.org/software/ncurses/ncurses.html
[5] Newt: http://en.wikipedia.org/wiki/Newt_%28programming_library%29
[6] Python: http://www.python.org
[7] Urwid: http://excess.org/urwid/tutorial.html
[8] Python-dialog: http://pythondialog.sourceforge.net/
[9] The Snack Sound Kit http://www.speech.kth.se/snack/
[10] Ncurses mit C (WikiBook): http://de.wikibooks.org/wiki/Ncurses
[11] Curses-Howto: A.M. Kuchling, Eric S. Raymond “Curses Programming with Python” http://docs.python.org/howto/curses.html#curses-howto
[12] Python-Dokumentation zu curses: http://docs.python.org/library/curses.html









