Programmierung in Ruby

Der Leitfaden der Pragmatischen Programmierer

Ruby Tk



Das Ruby-Application-Archiv enthält einige Erweiterungen, um für Ruby eine graphische Benutzeroberfläche (graphical user interface, GUI) zur Verfügung zu stellen, inklusive Erweiterungen für Tcl/Tk, GTK, OpenGL und andere.

Die Tk-Erweiterung ist in der Haupt-Distribution erfasst und funktioniert sowohl auf Unix wie auch auf Windows. Dazu muss man Tk auf seinem System installiert haben. Tk ist ein sehr großes System und es wurden schon ganze Bände darüber geschrieben, also halten wir uns nicht damit auf, in Tk selber einzutauchen, sondern konzentrieren uns darauf, wie man von Ruby aus auf die Tk-Eigenschaften zugreifen kann. Man braucht aber eines dieser Referenz-Handbücher, wenn man unter Ruby effektiv mit Tk arbeiten will. Die Anbindung, die wir hier benutzen, ist sehr ähnlich der von Perl, also wäre wohl Learning Perl/Tk  oder Perl/Tk Pocket Reference  nützlich.

Tk arbeitet mit einem Kompositions-Modell --- dh. man fängt mit einem Container-Widget an (etwa TkFrame oder TkRoot) und packt da andere Widgets hinein, etwa Buttons oder Labels. Wenn man dann bereit ist, die GUI zu starten, ruft man Tk.mainloop auf. Die Tk-Engine übernimmt dann die Kontrolle über das Programm, zeigt die Widgets an und ruft als Reaktion auf GUI-Ereignisse die entsprechenden Code-Teile auf.

Eine einfache Tk-Applikation

Eine einfache Tk-Applikation könnte etwa so aussehen:

require 'tk'
root = TkRoot.new { title "Ex1" }
TkLabel.new(root) {
  text  'Hello, World!'
  pack  { padx 15 ; pady 15; side 'left' }
}
Tk.mainloop

Figur 15.1

Figur 15.1

Diesen Code sehen wir uns etwas genauer an. Nachdem das tk-Erweiterungs-Modul geladen ist, erzeugen wir auf Root-Ebene ein Frame (einen Rahmen) mit TkRoot.new. Dann machen wir ein Label-Widget als Kind des Root-Frames und setzen ein paar Optionen für dieses Label. Schließlich packen wir den Root-Frame und geben die Kontrolle an die Haupt-Ereignisschleife der GUI ab.

Es ist schon eine sinnvolle Angewohnheit, Root explizit anzugeben, aber man kann das auch weglassen --- zusammen mit den Extra-Optionen --- und das Ganze auf drei Zeilen eindampfen:

require 'tk'
TkLabel.new { text 'Hello, World!' }
Tk.mainloop

Das war dann alles! Bewaffnet mit einem der schon genannten Perl/Tk-Büchern kannst du nun all die ausgefeiltesten graphischen Benutzeroberflächen gestalten. Wenn du aber noch ein paar mehr Details hören willst: hier sind sie.

Widgets

Ein Widget zu erzeugen ist einfach. Nimm den Namen des Widget aus der Tk-Dokumentation und hänge vorne ein Tk dran. Zum Beispiel werden die Widgets Label, Button und Entry zu den Klassen TkLabel, TkButton und TkEntry. Man erzeugt eine Instanz eines Widgets mit new, genau wie bei jedem anderen Objekt. Wenn man kein Elternteil für das Widget angibt, wird es defaultmäßig an das Frame der Root-Ebene angehängt. Normalerweise gibt man aber immer ein Elternteil an, zusammen mit vielen anderen Optionen wie Farbe, Größe und so weiter. Wir müssen aber auch Informationen von dem Widget zurückbekommen, während unser Programm läuft, mit Callbacks oder mit gemeinsamen Datenbereichen.

Setzen von Widget-Optionen

Wenn man in einem Tk-Referenz-Handbuch nachsieht (etwas dem für Perl/Tk), findet man, dass die Optionen für die Widgets normalerweise mit einem Bindestrich anfangen --- genauso wie Kommandozeilen-Optionen. In Perl/Tk werden die Optionen in einem Hash an die Widgets weitergegeben. In Ruby kann man das genauso machen, man kann die Optionen aber auch mit einem Code-Block übergeben; der Name der Option wird als Methoden-Name in dem Block benutzt und die Argumente für die Option sind die Argumente für den Methodenaufruf. Widgets brauchen ein Elternteil als erstes Argument, gefolgt von einem optionalen Hash mit Argumenten oder dem Code-Block mit den Argumenten. Damit sind die folgenden beiden Formen äquivalent.

TkLabel.new(parent_widget) {
  text    'Hello, World!'
  pack('padx'  => 5,
       'pady'  => 5,
       'side'  => 'left')
}
# oder
TkLabel.new(parent_widget, text => 'Hello, World!').pack(...)

Eine kleine Falle lauert bei der Form mit dem Code-Block: Der Gültigkeitsbereich der Variablen ist nicht so, wie man sich das denkt. Der Block wird tatsächlich im Kontext des Widget-Objekts ausgeführt, nicht in dem des Aufrufers. Das heißt, dass die Instanz-Variablen der aufrufenden Funktion innerhalb des Blocks nicht verfügbar sind, aber die lokalen Variablen des umfassenden Bereichs und die Globalen Variablen sind es (auch wenn man sie natürlich nicht benutzt). Wir zeigen die Übergabe von Optionenen mit beiden Methoden im folgenden Beispiel.

Längen (wie in den padx- oder pady-Optionen in diesem Beispiel) sind in Pixeln angegeben, man kann aber auch andere Einheiten nehmen wenn man hinten ein ``c'' (Zentimeter), ``i'' (Inch), ``m'' (Millimeter) oder ``p'' (Point) anhängt.

Daten vom Widget holen

Man kan vom Widget Daten zurückerhalten über Callbacks oder über gebundene Variablen.

Callbacks sind einfach einzurichten. Die command-Option (siehe den TkButton-Aufruf in dem folgenden Beispiel) nimmt ein Proc-Objekt entgegen, das aufgerufen wird, wenn der Callback ausgelöst wird. Hier benutzen wir Kernel::proc um den {exit}-Block zu einem Proc zu konvertieren.

TkButton.new(bottom) {
  text "Ok"
  command proc { p mycheck.value; exit }
  pack('side'=>'left', 'padx'=>10, 'pady'=>10)
}

Außerdem können wir eine Ruby-Variable mit dem Wert eines Tk-Widgets verbinden, indem wir einen TkVariable-Proxy benutzen. Wir zeigen das in dem folgenden Beispiel. Beachte wie der TkCheckButton eingerichtet wird: Die Dokumentation sagt, dass die variable-Option ein var reference als Arument entgegennimmt. Dazu erzeugen wir eine Tk-Variablen-Referenz mit TkVariable.new. Der Zugriff auf mycheck.value liefert den String ``0'' oder ``1'', je nachdem ob die Check-Box markeirt ist oder nicht. Man kann denselben Mechanismus für alles benutzen, das eine Var-Referenz unterstützt, wie etwa Radio-Buttons und Text-Felder.

mycheck = TkVariable.new

TkCheckButton.new(top) {   variable mycheck   pack('padx'=>5, 'pady'=>5, 'side' => 'left') }

dynamisches Setzen/Abrufen von Optionen

Zusätzlich zum Setzen einer Widget-Option während der Erzeugung kann man ein Widget natürlich auch zur Laufzeit rekonfigurieren. Jedes Widget unterstützt die configure-Methode, die genauso wie new einen Hash oder einen Code-Block als Eingabe erwartet. Wir ändern unser erstes Beispiel so ab, dass es als Reaktion auf das Drücken eines Buttons den Label-Text ändert:

lbl = TkLabel.new(top) { justify 'center'
  text    'Hello, World!';
  pack('padx'=>5, 'pady'=>5, 'side' => 'top') }
TkButton.new(top) {
  text "Cancel"
  command proc { lbl.configure('text'=>"Goodbye, Cruel World!") }
  pack('side'=>'right', 'padx'=>10, 'pady'=>10)
}

Wenn jetzt der Cancel-Button gedrückt wird, ändert sich der Text des Labels direkt von ``Hello, World!'' in ``Goodbye, Cruel World!''.

Man kann außerdem spezielle Options-Werte eines Widgets abfragen mit cget:

require 'tk'
b = TkButton.new {
  text     "OK"
  justify  'left'
  border   5
}
b.cget('text') » "OK"
b.cget('justify') » "left"
b.cget('border') » 5

Beispiel-Applikation

Jetzt kommt ein etwas längeres Beispiel mit einer ernsthaften Application --- ein ``Kauderwelsch''-Generator. Wenn man einen Ausdruck wie ``Ruby rules'' intippt und den ``Pig It''-Button drückt, so wird das dierekt in Kauderwelsch übersetzt.

Figur 15.2

Figur 15.2

require 'tk'

class PigBox   def pig(word)     leadingCap = word =~ /^A-Z/     word.downcase     res = case word       when /^aeiouy/         word+"way"       when /^([^aeiouy]+)(.*)/         $2+$1+"ay"       else         word     end     leadingCap ? res.capitalize : res   end

  def showPig     @text.value = @text.value.split.collect{|w| pig(w)}.join(" ")   end

  def initialize     ph = { 'padx' => 10, 'pady' => 10 }     # common options     p = proc {showPig}

    @text = TkVariable.new     root = TkRoot.new { title "Pig" }     top = TkFrame.new(root)     TkLabel.new(top) {text    'Enter Text:' ; pack(ph) }     @entry = TkEntry.new(top, 'textvariable' => @text)     @entry.pack(ph)     TkButton.new(top) {text 'Pig It'; command p; pack ph}     TkButton.new(top) {text 'Exit'; command {proc exit}; pack ph}     top.pack('fill'=>'both', 'side' =>'top')   end end

PigBox.new Tk.mainloop

Sidebar: Platzierungs-Management

Im Beispiel-Code in diesem Kapitel gibt es Referenzen auf die Widget-Methode pack. Das ist, wie sich herausstellt, ein sehr wichtiger Aufruf, denn lässt man ihn weg, dann sieht man vom Widget --- nichts. pack ist ein Kommando, das dem Platzierungs-Manager erzählt, das Widget nach unseren Vorgaben zu platzieren. Platzierungs-Manager kennen drei Kommandos:

Kommando Platzierung
pack Flexibel, Platzierung nach Vorgaben
place Absolute Position
grid Position in einer Tabelle (Reihe/Spalte)

pack ist dabei das gebräuchlichste Kommando, deshalb nehmen wir das auch für unsere Beispiele.

Verknüpfung von Events

Unsere Widgets haben Kontakt mit der echten Welt: sie werden angeklickt, die Maus bewegt sich über ihnen, sie werden über den Tab angewählt. All diese Sachen (und noch viel mehr) erzeugen events, die wir auffangen können. Mit der bind-Methode des Widgets kann man ein Event eines speziellen Widgets mit einem Code-Block verknüpfen.

Wir haben zum Beispiel ein Button-Widget, das ein Bild anzeigt. Wir möchten, dass sich das Bild jedesmal ändert, wenn die Maus über dem Button ist.

image1 = TkPhotoImage.new { file "img1.gif" }
image2 = TkPhotoImage.new { file "img2.gif" }

b = TkButton.new(@root) {   image    image1   command  proc { doit } }

b.bind("Enter") { b.configure('image'=>image2) } b.bind("Leave") { b.configure('image'=>image1) }

Als erstes erzeugen wir mit TkPhotoImage zwei GIF-Bild-Objekte aus Dateien auf Festplatte. Als nächstes erzeugen wir einen Button (ganz schlau ``b'' genannt), der das Bild image1 anzeigt. Dann verknüpfen wir das ``Enter''-Event so, dass es dynamisch das Bild des Buttonns nach image2 ändert, und das ``Leave''-Event so, dass wieder das Bild image1 angezeigt wird.

Dieses Beispiel zeigt die einfachen Events ``Enter'' und ``Leave.'' Aber das an bind als Argument übergebene benamte Event kann aus mehreren Unter-Strings zusammengesetzt sein, getrennt durch Bindestriche in der Reihenfolge modifier-modifier-type-detail. Modifikatoren (modifier) werden in der Tk-Referenz aufgelistet und sind zum Beispiel Button1, Control, Alt, Shift und so weiter. Type ist der Name der Events (von der X11-Namens-Konvention) und umfasst Events wie ButtonPress, KeyPress und Expose. Detail ist entweder eine Zahl von 1 bis 5 für Buttons oder ein Tastencode für Tastatureingaben. So wird zum Beispiel eine Verknüpfung, die das Loslassen der linken Maustaste bei gleichzeitig gedrückter Strg-Taste überwacht, so angegeben:

Control-Button1-ButtonRelease
oder
Control-ButtonRelease-1

Der Event selber kann bestimmte Felder enthalten, wie etwa der Zeitpunkt des Events oder die x- und y-Position. bind kann mit Event-Feld-Codes diese Sachen an den Callback weiterreichen. Diese Event-Feld-Codes werden wie printf-Spezifikationen benutzt. Wenn man etwa die x- und y-Positionen bei einer Mausbewegung ermitteln will, gibt man beim Aufruf von bind drei Parameter an. Der zweite Parameter ist das Proc für den Callback und der dritte ist der String für das Event-Feld.

canvas.bind("Motion", proc{|x, y| do_motion (x, y)}, "%x %y")

Canvas

Tk stellt ein Canvas-Widget zur Verfügung, mit dem kann man zeichnen und PostScript-Output erzeugen. Hier ist ein kleines Stück Code (von der Distribution übernommen), das gerade Linien zeichnet. Beim Drücken und Halten der linken Maustaste fängt die Linie an und bleibt als ``Gummiband'' dran, während man die Maus bewegt. Wenn man den Maus-Button wieder loslässt, wird die Linie an dieser Position gezeichnet. Drückt man die Maustaste Nr. 2, so wird eine PostScript-Repräsentation der Zeichnung druckbereit ausgegeben.

require 'tk'

class Draw   def do_press(x, y)     @start_x = x     @start_y = y     @current_line = TkcLine.new(@canvas, x, y, x, y)   end   def do_motion(x, y)     if @current_line       @current_line.coords @start_x, @start_y, x, y     end   end   def do_release(x, y)     if @current_line       @current_line.coords @start_x, @start_y, x, y       @current_line.fill 'black'       @current_line = nil     end   end   def initialize(parent)     @canvas = TkCanvas.new(parent)     @canvas.pack     @start_x = @start_y = 0     @canvas.bind("1", proc{|e| do_press(e.x, e.y)})     @canvas.bind("2", proc{ puts @canvas.postscript({}) })     @canvas.bind("B1-Motion", proc{|x, y| do_motion(x, y)}, "%x %y")     @canvas.bind("ButtonRelease-1",                  proc{|x, y| do_release (x, y)}, "%x %y")   end end

root = TkRoot.new{ title 'Canvas' } Draw.new(root) Tk.mainloop

Mit ein paar Mausklicks hat man ganz schnell ein Meisterstück erschaffen:

Figur 15.3

Figur 15.3

``Wir konnten den Künstler nicht finden, also hängten wir das Bild...''

Scrolling

Wenn man nicht gerade sehr kleine Bilder malen will, ist das vorige Beispiel nicht besonders nützlich. TkCanvas, TkListbox und TkText können aber auch mit Scrollbars eingesetzt werden, so dass man auf einem Ausschnitt des ``großen Bildes'' zeichnen kann.

Die Kommunikation zwischen einem Scrollbar und einem Widget ist zweiseitig. Wenn man den Scrollbar bewegt, muss sich auch die Ansicht im Widget bewegen; und wenn die Ansicht im Widget sich aus irgendwelchen anderen Gründen ändert, muss sich auch der Scrollbar ändern, um die naue Position widerzuspiegeln.

Weil wir bis jetzt noch nicht so viel mit Listen gearbeitet haben, wird unser scrollendes Beispiel eine zu scrollende Textliste sein. Im folgenden Beispiel fangen wir an mit der Erzeugung einer guten alten TkListbox. Dann machen wir ein TkScrollbar. Der Callback des Scrollbar (gesetzt mit command) ruft die yview-Methode des Listen-Widgets auf, die dann den Wert des sichtbaren Ausschnitts der Liste in y-Richtung ändert.

Nachdem dieser Callback feststeht, gehen wir in die andere Richtung: wenn die Liste es notwendig findet, zu scrollen, dann setzen wir den passenden Bereich in dem Scrollbar mit TkScrollbar#set. Wir werden dasselbe Fragment in einem voll funktionfähigen Programm im nächsten Abschnitt benutzen.

list_w = TkListbox.new(frame, 'selectmode' => 'single')

scroll_bar = TkScrollbar.new(frame,                   'command' => proc { |*args| list_w.yview *args })

scroll_bar.pack('side' => 'left', 'fill' => 'y')

list_w.yscrollcommand(proc { |first,last|                              scroll_bar.set(first,last) })

Nur noch eine Sache

Wir könnten noch hunderte von Seiten über Tk schreiben, aber das wird ein anderes Buch. Das folgende Beispiel ist unser letztes Tk-Beispiel --- ein einfacher GIF-Bild-Betrachter. Man kann einen GIF-Dateinamen aus der Scroll-Liste auswählen und eine Thumb-Nail-Version des Bildes wird angezeigt. Da sind nur noch ein paar andere Dinge, die wir noch sagen wollen.

Hast du schon mal eine Applikation gesehen, die einen Busy-Cursor angezeigt und dann vergessen hat, ihn wieder auf normal zu schalten? Es gibt da einen hübschen Trick in Ruby, der so etwas verhindert. Erinnere dich daran, wie File.new einen Block benutzte, um sicherzustellen, dass die Datei nach Benutzung auch geschlossen wurde? Wir machen dasselbe mit der Methode busy, wie wir im nächsten Beispiel zeigen.

Dieses Programm zeigt auch ein paar einfache TkListbox-Manipulationen --- Hinzufügen von Elementen zu der Liste, das Einrichten eines Callbacks für das Loslassen des Maus-Buttons [Man nimmt das Loslassen und nicht das Drücken, weil das Widget erst nach dem Drücken überhaupt selektiert wird.] und die Rückgabe der aktuellen Auswahl.

Bis jetzt haben wir TkPhotoImage nur zum direkten Anzeigen der Bilder benutzt, aber man kann genauso gut zoomen, ein Subsample anfertigen oder Teile des Bildes anzeigen. Hier werden wir die Subsample-Fähigkeit benutzen um das Bild zur Ansicht zu verkleinern.

Figur 15.4

Figur 15.4

require 'tk'

def busy   begin     $root.cursor "watch" # Set a watch cursor     $root.update # Make sure it updates  the screen     yield # Call the associated block   ensure     $root.cursor "" # Back to original     $root.update   end end

$root = TkRoot.new {title 'Scroll List'} frame = TkFrame.new($root)

list_w = TkListbox.new(frame, 'selectmode' => 'single')

scroll_bar = TkScrollbar.new(frame,                   'command' => proc { |*args| list_w.yview *args })

scroll_bar.pack('side' => 'left', 'fill' => 'y')

list_w.yscrollcommand(proc { |first,last|                              scroll_bar.set(first,last) }) list_w.pack('side'=>'left')

image_w = TkPhotoImage.new TkLabel.new(frame, 'image' => image_w).pack('side'=>'left') frame.pack

list_contents = Dir["screenshots/gifs/*.gif"] list_contents.each {|x|   list_w.insert('end',x) # Insert each file name into the list } list_w.bind("ButtonRelease-1") {   index = list_w.curselection[0]   busy {     tmp_img = TkPhotoImage.new('file'=> list_contents[index])     scale   = tmp_img.height / 100     scale   = 1 if scale < 1     image_w.copy(tmp_img, 'subsample' => [scale,scale])     tmp_img = nil # Be sure to remove it, the     GC.start      # image may have been large   } }

Tk.mainloop

Zum Schluss noch ein Wort zur Garbage-Collection --- wir hatten bei uns ein paar wirklich große GIF-Dateien in Benutzung [Das waren technische Dokumentationen], als wir diesen Code getestet haben. Wir wollten diese riesigen Bilder nicht länger als nötig im Speicher mit uns herumschleppen, also setzten wir die Bild-Referenz auf nil und riefen danach gleich den Garbage-Collector auf, um den Müll zu beseitigen.

Übersetzungen aus der Perl/Tk-Dokumentation

Das wars dann, jetzt musst du alleine weiter kommen. In den meisten Fällen kann man die für Perl/Tk gemachte Dokumentation einfach übertragen. Aber da gibt es ein paar Ausnahmen: einige Methoden sind nicht implementiert und es gibt undokumentierte Extra-Funktionen. Bis ein Ruby/Tk Buch erscheint, muss man wohl am besten in Newsgroups nachfragen oder den Quellcode lesen.

Im Allgemeinen ist die Vorgehensweise aber einfach. Man muss nur dran denken, dass die Optionen als Hash oder als Code-Block übergeben werden und dass der Gültigkeitsbereich des Code-Blocks innerhalb des benutzten TkWidget liegt und nicht in der Klassen-Instanz.

Objekt-Erzeugung

Perl/Tk:  $widget = $parent->Widget( [ option => value ] )
Ruby:     widget = TkWidget.new(parent, option-hash)
          widget = TkWidget.new(parent) { code block }

Man braucht den Rückgabewert der frisch erzeugten Widgets nirgends zu speichern, aber es gibt ihn. Vergiss nicht, die Pack-Methode des Widgets aufzurufen (oder einen der anderen Platzierungsaufrufe) oder man sieht nichts.

Optionen

Perl/Tk:  -background => color
Ruby:     'background' => color
          { background color }

Denk dran, dass der Gültigkeitsbereich des Code-Blocks woanders liegt.

Variable-Referenzen

Perl/Tk:  -textvariable => \$variable
          -textvariable => varRef
Ruby:     ref = TkVariable.new
          'textvariable' => ref
          { textvariable ref }

Benutze TkVariable, um eine Ruby-Variable an den Wert eines Widgets zu binden. Man kann den value-Zugriff in TkVariable (TkVariable#value und TkVariable#value=) nehmen, um den Inhalt des Widgets direkt zu beeinflussen.


Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Übersetzung: Juergen Katins
Für das englische Original:
© 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0. That reference is available for download.
Diese Lizenz sowie das Original vom Herbst 2001 bilden die Grundlage der Übersetzung
Es wird darauf hingewiesen, dass sich die Lizenz des englischen Originals inzwischen geändert hat.
Für die deutsche Übersetzung:
© 2002 Jürgen Katins
Der Copyright-Eigner stellt folgende Lizenzen zur Verfügung:
Nicht-freie Lizenz:
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder. Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.
Freie Lizenz:
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".