Zurück zur Homepage

Verwendung Standard Ruby GUI:TK

Der Standard für grafische Öberfläche, der in Rubyprogrammen am häufigsten erscheint, ist TK. TK wurde speziell als GUI für die in der Mitte achtziger entwickelte Tcl-Skriptsprache von John Ousterhout entwickelt, seitdem aber ist Toolkit fast auf allen gängigen Plattformen integriert worden, genau so kann Tk mit anderen Skriptsprachen wie Perl und Python verwendet werden. Tk's Widgets sind mit mehr modernenen GUI's vergleichbar und es unterstützt sogar MAC OS.

Installation

Ein von wesentlichen Vorteilen der Verwendung Ruby mit Tk ist es, dass Tk ein Standard Toolkit für Entwicklung GUI's mit Ruby ist, und es ist leicht und ohne großen Aufwand das Programmieren anzufangen. Ruby/Tk benötigt Tk als Selbstpaket sowie als Ruby/Tk Erweiterungsmodul. Sie können sich Source code für Tk bei der Tcl/Tk Homepage www.tcltk.org besorgen und dabei müssen Sie es dabei selbst kompilieren, aber es gibt schon fertige Binäres für Tk und zwar für alle gängige OS(Linux, Windows). Um sich leichter zu machen, empfiehlt es sich Standard Ruby Distribution für Windows bei der "Ruby Installer for Windows" Sobald Sie sie installieren, haben Sie bereits eine funktionfähige Tk Umgebung. Viele Linux Distributionen bieten auch Tcl/Tk als Standard Option bei der Installation des Betriebsystemes an. Die andere Möglichkeit Tk zu installieren, was allerdings ein wenig komplizierter ist, ist das Erweiterungsmodule für Ruby, die in Ruby's Source Code vorhanden sind. Wenn Sie sich selbst Ruby kompilieren würden, werden Ruby/Tk Erweiterungsmodule automatisch in Ihrem System installiert. Standard Ruby Installer für Windows also beinhaltet schon Ruby/Tk Erweiterungsmodule.

Ruby/Tk Basis

Ruby/Tk stellt eine Anzahl von Klassen zur Verfügung um verschiedene Tk Widgets zu präsentieren. Dabei sind Namen von Ruby/Tk Klassen für Widgets Tk Basis Klassen Namen ähnlich. Zum Beispiel "Entry-Widget" von Tk ist auch als TkEntry Klasse in Ruby/Tk vertreten. Ein typischer Aufbau von einem Ruby/Tk Programm ist eine Erzeugung eines "main" oder "root" Fensters(als Beispiel TkRoot-Klasse), dann werden andere Widgets zu dem "root" Fenster hinzugefügt und damit wird "user interface" aufgebaut, und am Schluss des Codes wird sogennante "main event loop" mit Tk.Mainloop aufgerufen. Das traditionelle Programm "Hello Word" für Ruby/Tk wird so aussehen:

require 'tk'
 root = Tkroot.new
 button = TkButton.new(root) {
 text "Hello, World!"
 command proc {puts "Ich sage Hello!" }
 }
 button.pack
 Tk.mainloop

Nun wird am MS-DOS-Promt diese Datei, die wir z.B. hello_world.rb nennen würden aufgerufen. Und so könnte es aussehen:

Nachdem Sie das Skript aufgerufen haben, erscheint GUI:

Und wenn Sie nun die Schalfläche drücken, sieht es so aus:

Die erste Zeile lädt einfach Ruby/Tk Erweiterung in den Ruby Interpreter und zweite Zeile errichtet "top-level window"(Hauptfenster) für das Programm. Schließlich komemen wir zum interesantesten Teil:

button = TkButton.new(root) {
 text "Hello, World!"
 command proc { puts "Ich sage Hello!" }
 }

Hier errichten wir ein Button,dessen Eltern's Widget das Hauptfenster ist. Genauso wie bei anderen GUI Toolkits, werden wir sehen, dass Ruby/Tk ein Modell verwendet, das sich auf "Zussamentsetzung" basiert, wo die Eltern's Widgets ein oder mehrere Kinders Widgets enthalten, die auch als "Behälter für andere Widgets zur Verfügunug stehen können. Dieser Codesteil zeigt nur einen Weg, wie man bei der Configuration von verschiedenen Optionen vorgehen kann. Innerhalb vom Codeblocks können Sie Methoden aufrufen, die das Aussehen des Erscheinens oder des Verhaltens von Widgets ändern. In unserem Beispiel die Methode text wurde verwendet um den Text in Button erscheinen zu lassen, während die command Methode mit einer Prozedur verbunden, des sogennanten "Callback"(mehr darüber wird später erklärt). Als abwechselnde aber gleichwertige Form zum Angeben von Widgets Optionen ist das, dass man sie als key, value (Hash-style Paare) verwendet werden als zweite und weitere Argumente für die new Funktion und das als Folgendes aussieht:

button = TkButton.new(root, text => "Hello, World!",
 command => proc { puts "Ich sage Hello!"})

Die zweite Zeile ist wichtig, weil sie dem Hauptfenster eine Anweisung gibt, den Button am richtigen Platzt zu setzen. Für diesen Beispiel ist das natürlich nicht so relevant, da das Hauptfenster nur ein Kind-Widget hat. Wie zum Beispiel wir später sehen werden, haben wirkliche Programme viel mehr komplizierte Layouts, die noch mehrere Ebenen haben und auch ihrerseits andrere Widgets beinhalten. Für diese Fälle werden wir auch zusätliche Argumente zur pack Methode übergeben, um hinzuweisen, wo die Widgets in Eltern-Widgets plaziert werden sollten, wie die Grösse des Buttons geändert werden könnte, wenn die Größe des Eltern's Widget geändert wurde und auch andere mit Layout zusamennhängende Aspekte.

Dieses Beispielprogramm endet, wie die meisten Ruby/Tk Programme es tun, mit dem Aufrufen Tk.mainloop;. Aber die Methode greift eingentlich schon am Beginn des Programmes. An dieser Stelle befindet sich das Programm in einer sogennanten unbegrenzten Schleife, wartet auf Ereignisse, die vom Anwender losgelöst werden und dann werden sie an den passenden "Handler" abgeschickt. Viele Programme, die mit Ruby/Tk entwickelt wurden, haben schon eine gewisse Struktur an Ereignissen, und dann wird der Code dafür geschrieben um diese Ereignisse zu führen. Das ist das Thema des follgenden Teil.

Integrieren von Ereignissen und Aufrufen in Ruby/Tk GUI's

Tk's Ereginiss Model ist auf zwei eng zusammenhängende Abläufe getrennt. Auf einer Seite das Betriebsystem erzeugt auf niedriger Stufe ein Ereigniss wie z.Beispiel: "der Mauscursor bewegt sich gerade in einem Fenster oder der Anwender drückt einfach eine Taste "S" auf der Tastatur". Auf anderer Seite ruft Tk in Ihrem Programm ein Ereigniss um darauf hinzuweisen, dass etwas Bedeutunsvolles in Widget geschehen ist.(z.B. Button wurde angeklickt). Für beide Fälle können Sie einen Codeblock oder einen Ruby Proc Objekt schreiben, das genau angibt, wie das Programm auf das Ereigniss oder den Aufruf reagiert.

Zuerst lassen Sie uns einen Blick werfen, wie die Methode bind verwendet wird um betriebsystemspezifische Ereignisse mit Ruby's Prozeduren zu verbinden, welche dementsprechend darauf reagiren. Die bind Methode wird in einfachsten Form für die Einbindung des Ereignisses und des Codeblockes eingesetzt. Daraufhin führt Tk das Codeblock, was als eine Antwort aufs entspreschende Ereigniss ist. Zum Beispiel um ein ButtonRelease Ereigniss für den ersten Mausbutton in irgendeinem Fenster zu packen, würden so schreiben:

someWidget.bind('ButtonRelease-1') {
...Codeblock...
 }

Für manche Ereignisstypen ist es ausreichend, den elementaren Ereignissnamen zu verwenden, wie z.B. "Configure" oder "Destroy", aber für andere brauchen wir mehr spezifischen Namen. Dabei kann der Eventsname auch zusätzliche modifiers  und details enthalten. Der "Modifier" ist ein "String" wie "Schift" "Control" oder "Alt", welches hindeutet, was dabei gedrükt wird. Im Detail wird eine von 1 bis 5 Nummer ausgewählt, welche auf die Nummer von Mausbutton hinweisen, oder kennzeichnen eine Taste auf die Tastatur. So z.B. um ein Event abzufangen, welches erzeugt wird, wenn der Anwender die Ctrl Taste festhält und dabei Rechtemausttaste betätigt(manchmal als Button 3 genannt wird) über das Fenster, würde man so geschrieben:

aWindow.bind('Control-ButtonPress-3', proc {
 puts "Wauuu!!!" })

Die Namen von diesen Events sind von den Namen entsprechenden X11 Eventstypes abgeleitet worden, hauptsächlich aus historischen Gründen; Tcl/Tk war ursprünglich für Unix Betriebsystem entwickelt und sein X Window System. Danach wurde Tk auf Windows und Macintosh portiert und diese Portierungen verwenden gleiche Eventsnamen für die Darstellung ihrer "native" Ereignissen, die auf jeweiliges System beheimatet sind. Das Beispielprogramm, das wir später entwicklen werden, verwendet ein paar Eventstypen, aber wenn Sie sich für die komplette Liste von aktuellen Eventsnamen, modifier und Details interessieren, sollten Sie "manual pages" für Tk's bind command zur Rate ziehen. Eine gute Online-Quelle für diese Art von Referenzen ist die Tcl/Tk Dokumentation auf "Tcl Developer Xchange Webseite(siehe hier)

Es ist natürlich nützlich, solche Art von "low-level" Events abfangen zu können, aber mehr häufiger werden Sie sich mit "high-level" Aktionen beschäftigen. Zum Beispiel Sie hätten gern einfach gewusst, wenn der Anwender auf den Help Button klickt, wobei Sie brauchen wirklich nicht zu wissen, wie es abläuft, wennn der Anwender mit linken Maustaste diesen Button anklickt und dann einige Millisekunden später den loslassen Viele Ruby/Tk Widgets können callbacks auslösen, wenn der Anwender sie aktiviert und Sie könnet command callback genau angeben, welche bestimmte Codeblocks oder Prozeduren aufgerufen werden müssen und was danach geschieht. Z.Beispiel mit einer beliebigen Option können Sie festlegen command callback Prozedure, wenn Sie ein Widget zaubern:

helpButton = TkButton.new(buttonFrame) {
 text "Help"
 command proc { showHelp }
 }

oder Sie können es später die Verwendung Widget's command Methode festlegen:

helpButton.command proc { showHelp }

Sie können aber auch disen Code so schreiben, seitdem command Methode es erlaubt:

helpButton = TkButton.new(buttonFrame) {
 text "Help"
 command { showHelp }
 }

Viele Widgets, wie TkCanvas, TkListbox und TkText können nicht verwendet werden, um z.B. ihren Inhalt im ihnen zugewiesenen Raum zu zeigen. Zum Beispiel, wenn Sie TkText Widget für die Darstellung eines langen Textes verwenden, werden höhstens nur ein Teil von Text sehen. Deswegen werden Sie auf Hauptwidget für ein paar Seiten einen typischen TkScrollbar beifügen. Damit wird es dem Anwender eine Möglichkeit gegeben, durch den gesamten Inhalt zu scrollen. Um alles das richtig zu machen und zwar das Zussammenspielen von "Scrollbalken" und Widgets wie TkText und TkListbox kann man also callbacks erzeugen wenn der Inhalt von Widgets vertikal oder gorisontal gescrollt werden muß. Um die callbacks mit Codeblocks zu verbinden können Sie Widget's xscrollcomamand or yscrollcommand Methode verwenden. Wir werden es uns später bei unserem Beispielprogramm ansehen, wie das alles funktioniert.

Arbeitsweise mit Ruby/Tk Layout Manager

Wenn Sie Ihres Programm etnwerfen, ist es wichtig zu verstehen, wie Ruby/Tk Events und Callback's funktionieren. Eebenso ein relevanter Aspekt beim Entwurf ist die Gestaltung von "User Interface". Viele Widgets dienen als interaktive Komponenten, when der Anwender darauf klickt oder tippt etwas um das Programm zu bringen, irgendwelche Aktion auszuführen. Andere Widgets, wie auch immer, sind mehr "passiv" und könnten als Behälter(Eltern) für schon anderer Widgets. Wir haben bereits gesehen, dass "top-level" "root" Widget ist einer von solchen Widgets und innerhalb des Hauptfensters können Sie TkFrame Widget verwenden. Damit werden die Kinder Widgets zussammengruppiert.

Der Layout Manager für den Behälter definiert im Grunde Leitfaden und wird zum Festlegen der Position und Größe im Hauptfenster vorhandenden Widgets Wie Sie sehen werden ,gleich nachdem Sie verstanden haben, wie Layout Manager funktioniert dass es Einiges an Experimentfreude gibt und Sie werden Möglichkeiten haben, um Ihre geistige Vorstellungen,was GUI's betrifft,in Code zu verwirklichen und sie richtig zu implementieren. Mit Ruby/Tk können Sie aus drei verschiedenen Layout Manager auswählen, obwohl Sie nicht brauchen denselben Layout zu verwenden. In Wirklichkeit für wichtigen GUI's ist es ziemlich wahrscheinlich, dass Sie mehrere Layots Manager verwenden werden.

Der einfachste Layout Manager ist placer, mit dem Sie lediglich Kinder Elementen plazieren würden, bzw. die Position und Größe festlegen. Auf ersten Blick könnte es angemessen klingeln, doch viele "GUI Builder" Tools dass sie Ihnen erlaubt Widgets auf Toolpallete wegzuzerren, oder fallen lassen auf dem Arbeitsfenster, verwenden diese Methode in dem Code, während sie den erzeuge. Nachteile diesen unflexsiblen festen Layout Managers werden offentsichtlich, wenn man sobald versucht, das Programm auf anderen Computer mit anderer Konfiguration und wahrscheinlich auch dass da ein anderes OS ist, zum Laufen zu bringen. Zum Beispiel, wenn Systemfonts verschieden sind, der Button erfordert auf Ihrem System für den Text nur 40 px Breite auf anderen System könnte es 60 px sein. Wenn Sie einen Textfeld fest zu diesem Button unmittelbar rechts verankert haben, werden jetzt die zwei Widgets sich gegenseitig überlappen. Da es so unflexsibel ist, werden Sie höhstwahrscheinlich nicht diesen Layout Manager im Praxis benutzen.

Der nächste Layout Manager ist grid, welcher seine Kinder Widgets in einer tabellenähnlichen Ordnung plaziert. Wenn man Kinder Widgets zu dem Elternfenster hinzufügt, legt man sozusagen Spalten und Reihen fest, die jetzt mit diesem Layout Manager für die Platzierung von Elementen verwendet werden künenen. Die Kinder Elemente werden so zugeordnet, dass alle Elementen in einer oder mehreren gleichen Spalten und Reihen mit gleichen Breite und Höhe zu finden sind. Um den schnellen Eindrück zu beschaffen, bietet sich hier ein Beispiel, mit grid Layout und mit 3 Reihen und 4 Spalten auf "Label" Widget:

require 'tk'
root = TkRoot.new
3.times { |r|
  4.times { |c|
   TkLabel.new(root) {
    text "row #{r}, column #{c}"
    }.grid('row' => r, 'column' => c,
      'padx' => 10, 'pady' => 10)
   }
  }
Tk.mainloop

Unten sieht man ein Ergebnis, wenn dieses kleines Programm laufen lässt:

Der grid Layout Manager ist aber mehr leistungsfähig, als man an diesem Beispeil erkennen lässt. Sie können sehen, dass man bei Grundeinstellungen so ist, dass grid jedes Kind Element in seiner Zelle zentriert, aber man kann zusätzliche Argumente verwenden und so kann grid Methode festsetzen, dass ein oder mehrere Seiten vom Kind-Element wie von einem "Kleber" ausgedehnt werden können. Außrdem grid Zellen können mehrere Reihen und Spalten umfassen. Um mehr darüber eine Information zu beschaffen, sollte man in Tk Referenz nachschauen.

Der letzte Layout Manager, den wir besprechen werden, ist packer. Ihn werden Sie häufiger verwenden, weil er sehr flexsibel sogar leichter zu benutzen ist. Der packer verwendet als ein Model zur Zuteilung der Position und Größe sozusagen "Hohlraum". Man kann sich einen leeren Raum vorstellen, bevor man die Kinder Element hinzugefügt werden. Wenn man danach das erste Kind-Element hinzufügt legt man die Position der Seiten vom den Rand des rechtwinkligen "Hohlraums"(links, rechts, oben, unten) fest. Der packer weist dem Kind-Element die gesamte Seite zu und dann verringert die Größe der Seite um die Menge, die das erste Kind-Element beansprucht hat. Das ist wichtig zu merken, dass der packer die ganze Seite des Raums für den neuen dazugekommenen Kind-Element zuteilt, selbst wenn das Kind-Element nicht braucht. Die aufeinanderfolgende Kind-Elemente werden also gegen ausgewählte Seiten des übriggebliebenen Raums drangepackt, bis es mehr den Platzt gibt.

Also ist es ganz relevant, zu verstehen, dass der packer Layout Manager zwischen packing space und display space unterscheidet. Der "Display-Raum" ist ein Teil des Raums, das bestimmtes Kind-Element für sich genommen hätte, um sich selbst korrekt zu präsentieren. Zum Beispiel ein "label" oder "button" Element hat ein "display space", das ein wenig weiter als das benötigte Raum für die Darstellung von Text auf "label" oder "button" Widgets ist. Das "packing space" ist dagegen das gesamte Raum das verfügbar für die Positionierung von Kind-Element im Hohlraum ist und das kann größe oder kleiner als "display space" für den Widget sein.

When "packing space" seine Maße als dazu benötigte "display space" überschreitet, tritt das "default" Verhalten ein und zwar wird das Kind-Element gerade ins Zentrum von "packin space" gesetzt und das übrige Raum von "packin space" wird anderen Seiten verschoben. Wenn Sie stattdessen genau so für das Kind-Element das ganze zur Verfügung stehende Raum füllen möchten, dann kann man den Parameter fill setzen mit drei Werten (x, y, both), die jeweilige Richtung hindeuten, in welcher das Element gefült wird. Wir werden das sehen später in unserem Beispielprogramm

Ein noch damit verwandter Parameter ist expand, welcher macht nicht anderes als dem Kind-Element hinweisen, wie es sich verhalten soll, wenn das Hauptfenster sich in seiner Dimensionen verändert. Default Einstellung beim expand Parameter ist false, gemeint ist damit nämlich das, wenn Hauptfenster sich vergrößert oder "schrumpft, wird der Kind-Widget seine Größe und Position beibehalten. Wenn man anstaat expand als true setzt, werden Kinder-Elemente auch ihre Dimensionen entsprechend and die neue Größe des Hauptfensters anpassen. Üblich ist es, wenn man fill Parameter als "both" festgesetzt hat für einen bestimmten Kind-Widget, dann sollte man expand als "true" für diesen Kind-Widget festlegen.

Als eine Vorführung, wie der pack Layout Manager funktioniert könnte es hilfreich sein an einer konkreten Aufgabe hinzuschauen. Erinnern Sie sich, dass wir uns mit einem leeren rechtwinkligen "Hohlraum". Nun lassen wir uns anfangen und fügen oben an der Seite des Raums ein Kind-Element hinzu(Widget 1 s.h.Abb.1):

Nach diesem Schritt hat sich der erste Kind-Widget quasi die Seite oben beanspucht. Ungeachtet, wie wir weitere Kind-Widgets packen, das ist der einzige Widget, welcher am oberen Rand des Raums angegrenzt sein kann. Der unterste Rand des ersten Elementes wird quasi "oberster" Rand des übriggebliebenes Hohlraums. Als Nächstes fügen wir am linken Rand des Raums noch ein Kind-Widget 2(s.h.Abb.2)

Wieder einmal verringert sich übriggebliebenes Raum, diesmal um die Breite des zweiten Kind-Widgets. Der unterste Rand des ersten Widgets immer noch der oberste Rand des Raums, aber der rechter Rand des zweiten Kind-Widgets wird nun als neue linke Seite des verbleibenden Hohlraum. Jetzt fügen wir den dritten Kind-Widget 3(s.h.Abb.3) hinzu, diesmal am untersten Rand des Raums.

Nachdem wir diesen dritten Widget zugefügt haben, schrumpft das übriggebliebenes Raum um die Höhe des dritten Widgets und dessen oberste Rand wird nun der neue unterste Rand des übrig. Raums. Als Letztes fügen wir den letzten Widget 4(s.h.Abb.4) hinzu, diesmal am rechten Rand.

Ruby/Tk Beispielprogramm

Unten wird Quellecode gezeigt, welchen Sie sich auch herunterladen können tk-xmlviewer

#!/usr/bin/ruby -w
 # Sample Application(Beispielprogramm) tk-xmlviewer.rb

 require "tk"
 require "nqxml/treeparser"

 class XMLViewer < TkRoot
 def createMenubar
   menubar = TkFrame.new(self)
   fileMenuButton = TkMenubutton.new(menubar,
       'text' => 'Datei',
       'underline' => 0)
   fileMenu = TkMenu.new(fileMenuButton, 'tearoff' => false)


   fileMenu.add('command',
      'label' => 'Öffnen',
      'command' => proc { openDocument },
      'underline' => 0,
      'accel' => 'Ctrl+O')
   self.bind('Control-o', proc { openDocument })


   fileMenu.add('command',
      'label' => 'Beenden',
      'command' => proc { exit },
      'underline' => 0,
      'accel' => 'Ctrl+Q')
   self.bind('Control-q', proc { exit })


   fileMenuButton.menu(fileMenu)
   fileMenuButton.pack('side' => 'left')


   helpMenuButton = TkMenubutton.new(menubar,
       'text' => 'Hilfe',
       'underline' => 0)
   helpMenu = TkMenu.new(helpMenubutton, 'tearoff' => false)


   helpMenu.add('command',
      'label' => 'Info',
      'command' => proc { showAboutBox })


   helpMenuButton.menu(helpMenu)
   helpMenuButton.pack('side' => 'right')
   menubar.pack('side' => 'top', 'fill' => 'x')
 end


   def createContents
   # Lists
   listBox = TkListbox.new(self) {
     selectmode 'single'
     background 'white'
   font 'courier 10 normal'
   }
   scrollBar = TkScrollbar.new(self) {
     command proc { |*args|
       listBox.yview(*args)
     }
   }
   rightSide = TkFrame.new(self)
   attributesForm = TkFrame.new(rightSide)
   attributesForm.pack('side' => 'top', 'fill' => 'x')
   TkFrame.new(rightSide).pack('side' => 'top', 'fill' => 'both',
     'expand' => true)
   listBox.yscrollcommand(proc { |first, last|
     scrollBar.set(first, last)
   })
   listBox.bind('ButtonRelease-1') {
     itemIndex = listBox.curselection[0]
     if itemIndex
       # Remove currently displayed attributes
     TkGrid.slaves(attributesForm, nil).each { |slave|
       TkGrid.forget(attributesForm, slave)
     }

     #Add labels und entry widgets for this entity's attributes
     entity = @entities[itemIndex]
     if entity.kind_of?(NQXML::NamedAttributes)
       keys = entity.attrs.keys.sort
       keys.each_index { |row|
         TkLabel.new(attributesForm) {
           text keys[row] + ":"
           justify 'left'
         }.grid('row' => row, 'column' =>0, 'sticky'=> 'nw')
         entry = TkEntry.new(attributesForm)
         entry.grid('row' => row, 'column' => 0, 'sticky' => 'e')
         entry.value = entity.attrs[keys[row]]
         TkGrid.rowconfigure(attributesForm, row, 'weight' => 1)
       }
       TkGrid.columnconfigure(attributesForm, 0, 'weight' => 1)
       TkGrid.columnconfigure(attributesForm, 1, 'weight' => 1)
     else
     end
   end
 }


  listBox.pack('side' => 'left', 'fill' => 'both', 'expand' => true)
  scrollBar.pack('side' => 'left', 'fill' => 'y')
  rightSide.pack('side' => 'left', 'fill' => 'both', 'expand' => true)


  @listBox = listBox
  @attributesForm = attributesForm
 end


 def initialize(root)
  # Initialize base class
  super


  # Main Window Title
  title 'TkXMLViewer'
  geometry '600x400'


  #Menu bar
  createMenubar
  createContents
 end


 def populateList(docRootNode, indent)
  entity = docRootNode.entity
  if entity.instance_of?(NQXML::Tag)
     @listBox.insert('end', ''*indent + entity.to_s)
     @entities.push(entity)
     docRootNode.children.each do |node|
       populateList(node, indent + 2)
     end
  elsif entity.instance_of?(NQXML::Text) &&
       entity.to_s.strip.length != 0
     @listBox.insert('end', ''*indent + entity.to_s)
     @entities.push(entity)
  end
 end


 def loadDocument(filename)
  @document = nil
  begin
     @document = NQXML::TreeParser.new(File.new(filename)).document
  rescue NQXML::ParserTree => ex
     Tk.messageBox('icon' => 'error', 'type' => 'ok',
       'title' => 'Fehler', 'parent' => self,
       'message' => "Das XML Dokument konnte nicht geparst werden")
  end
  if @document
     @listBox.delete(0, @listBox.size)
     @entities = []
     populateList(@document.rootNode, 0)
  end
 end


 def openDocument
  types = [["Alle Datei", "*"], ["XML Documents", "*.xml"]]
  filename = Tk.getOpenFile('filetypes' => types, 'parent' => self)
  if filename != ""
     loadDocument(filename)
  end
 end


 def showAboutBox
  Tk.messageBox('icon' => 'info', 'type' => 'ok',
      'title' => 'Über TkXMLViewer',
      'parent' => self,
      'message' => 'Ruby/Tk XML Viewer Application')
  end
 end


 # Run Application( Ausführen des Programms)
  root = XMLViewer.new
  Tk.mainloop

Die ersten zwei Zeilen sind einfach zum Importieren erforderlichen Tk und NQXML Modulen. Danach wird Hauptprogramm-XMLViewer Klasse initialisiert, abgeleitet von TkRoot. Die Methode dieser Klasse initialize bekommt als ein Argument root

 def initialize(root)
  # Initialize base class
   super


  # Main Window Title
   title 'TkXMLViewer'
   geometry '600x400'


  #Menu bar
   createMenubar
   createContents
 end

Die erste Zeile dieser Methode ruft super auf, um die Base-Klasse zu initialisieren, das ist sehr wichtig, ohhe das wird das Programm nicht zum Vorscheinen kommen. Die nächste zwei Zeilen rufen TkRoot's title und geometry Methode auf, beziehungsweise wird man der Titel des Hauptfensters und dessen anfängliche Breite und Höhe in pixel festgelegt. Diese zwei Methoden sind übrigens von Ruby/Tk Wm Module bereitgestellt, welche die Anzahl der Funktionen fürs Aufeinandererwirken mit "Window Manager" definieren.

Die letzte zwei Zeilen der initialize Methode rufen die andere XMLViewer Methode herbei. Damit werden Window's "Menüleiste" und Contents Raum dargestellt. Wir hätten wohl die Codes von diesen Methoden direkt in die initialize Methode einschließen können, aber das würde andere Teile der GUI Konstruktion sozusagen zerbrechen, also das ist viel besser und einfacher umfangreiche und schwierige Programme aufzubauen, wenn Codes in verschieden Methoden verteilt und so werden wir weiter dieser Logik folgen. Im Gegensatz zu einigen anderen Toolkits, wir werden es sehen, hat Ruby/Tk keine spezifische Klasse für die "Menüleiste"; stattdessen werden wir einfach einen TkFrame-Widget Behälter verwenden, ausgedehnt entlang der obersten Rande des Hauptfensters.

Die "pulldown" Menü von Ruby/Tk besteht aus dem TkMenubutton Objekt verbunden mit dem TkMenu Objekt. Der TkMenubutton Widget ist es, was man auf der Menü selbst sieht, der beinhaltet den Text, was der Name der Menü ist, wie z.B. Datei, Bearbeiten oder Hilfe. Wenn der Anwender aud den Button klickt, der damit verbundenen TkMenu Widget wird angezeigt. Sie können ohne weiteres eine odere mehrere Optionen zu TkMenu Widget hinzufügen, wobei wird die add Methode verwendet. Lassen wir uns an der Organisation unseres Beispielprogrammes ansehen, nälich an der "Datei" Menü:

fileMenuButton = TkMenubutton.new(menubar,
           'text' => 'Datei',
           'underline' => 0)

Die Menü's Buttons werden als Kind-Widget der Menüleiste selbst erzeugt. Das underline Attribut von TkMenubutton Widget ist eine "integer"(ganze Zahl), welche auf eine Buchstabe hinweist, die im Titel des Menübuttons als unterstriechen bezeichnet wird. Die Hervorhebung einer Buchstabe in Menütitle ist ein allgemein-verwendeter optischer Hinweis in GUI Programms, um sozusagen eine "Beschleunigung-Lösung" festzulegen, die uns erlaubt die Menü zu aktivieren, z.Beispiel in meisten Windows Applicationen würde die Tastekombination Alt+D die "Datei" Menü aktivieren.

Als Nächstes erzeugen wir TkMenuWidget, verbunden mit dem TkMenubutton und fügen für diese Menü einen ersten Eintrag:

fileMenu = TkMenu.new(fileMenuButton, 'tearoff' => false)

   fileMenu.add('command',
      'label' => 'Öffnen',
      'command' => proc { openDocument },
      'underline' => 0,
      'accel' => 'Ctrl+O')
   self.bind('Control-o', proc { openDocument })

Beachten Sie dabei, dass TkMenu als Kind-Widget von Menübutton erzeugt wird. An dieser Stelle legen wir fest, dass die Menüstil nicht "abreißbar" ist. Der erste Eintrag, den wir zu "Datei"-Menü hinzfügen ist "command"-Entrag für Öffne Befehl. Der erste Argument zu add Methode ist ein "string", der uns gerne verrät,um welche Type des Menüeintrages sich handelt; zusätzlich zum command Attribut gibt's noch einige Type, wie z.Beispiel für den "checkbutton" Eintrag ist das check, "radiobutton" Eintrag radio, "separator" separator und für "cascading sub-menu" cascade. Das command Attribut für diesen Menüentrag ist ein Ruby Proc Objekt, welches eine andere XMLViewer "Instanz"-Methode aufruft, in diesem Fall openDocument, welche wir uns noch ansehen werden. Das accel Attribut legt die Tastaturkombination, die uns eine Möglichkeit gibt, auf schnelleweise eine Datei auszuwählen und sie zu öffnen. Der String dafür steht hinter dem "Offnen" Menüeintrag, was aber nicht automatisch für diese "Beschleunigungskombination" eingebunden wird, wir müssen dafür die bind Methode verwenden Damit wird diese Tastaturkombination in unserem Hauptfenster bzw. Programm eingebunden.

Nun lassen wir uns, auf von uns definierte createContents Methode einen Blick werfen. Diese Methode baut einen Raum für einen Inhalt(Content) auf dem Hauptprogrammfenster auf, wobei er in zwei Teilen getrennt wird. Ein Teil auf der linken Seiteist für eine Aufstellung von XML Dokumentenknoten und auf der rechten Seite eine Aufstellung des Knotenattributes.

listBox = TkListbox.new(self) {
     selectmode 'single'
     background 'white'
   font 'courier 10 normal'
   }
   scrollBar = TkScrollbar.new(self) {
     command proc { |*args|
       listBox.yview(*args)
     }
   }
   listBox.yscrollcommand(proc { |first, last|
     scrollBar.set(first, last)
   })

Tk's "list-box" Widget zeigt eine Liste von "strings", von denen wir uns etwas aussuchen können. Unsere TkListox Instanz bestimmt das selectmode Attribut zu single, was darauf hinweist, dass nur einen Posten zur gleichen Zeit ausgewählt werden kann. Es gibt auch andere "selectionmode", die es mehrfach erlauben ,zur gleichen Zeit Posten auszuwählen(unter Posten versteht man z.Beispiel einen Eintrag in XML-Datei). Wit setzen auch das Attriubut font als "Courier mit der Größe 10" fest, anstatt eine Default Schriftart zu verwenden, welche gewöhnlich die systemabhängige und proportional-raumige Schriftart ist. Da die Liste von Posten(Einträgen) sehr lang sein könnte(zu viel um auf einer Seite des Bildschirmes zu passen), werden wir zu diesem "Listbox" einen TkScrollbar Widget hinzufügen. Der Codeblock oder eine Prozedure, woraus die "scrollbar's command Methode besteht, modifiziert die Ansicht des "listboxes", bzw. das Aufstellen von Posten, die in diesem Widget gezeigt werden. Die Anzahl von Parametern, die an "scrollbar" command Methode übergeben werden, variiert abhängig davon, wie oft der Anwender den Scrollbar betätigt hat. Zum Beispiel, wenn der Anwender die Position von "scrollbar" durch das Betätigen einer von zwei "Pfeiltasten" bestimmt, empfängt die command Methode drei Argumente(scroll,1,units). Um mehr Information über verschiedene Argumente für den TkScrollbar Widget zu bekommen, sollten Sie die Tk Reference Dokumentation zur Rate ziehen. Im Allgemeinen braucht man überhaupt nicht, sich damit zu beschäftigen, wie man für die "command" Methode passende Argumente besorgt, man gibt sie einfach der xview oder yview Methode über. So ähnlich wird ein Codeblock oder eine Prozedur an die yscrollcommmand Methode weitergeleitet, damit wird die Position und das Aufstellen von "scrollbar" , when sich "list contents" verändert, angepasst. Der nächste Abschnitt unseres Programmcodes legt die rechte Seite des Hauptfensters fest.

rightSide = TkFrame.new(self)
 attributesForm = TkFrame.new(rightSide)
 attributesForm.pack('side' => 'top', 'fill' => 'x')
 TkFrame.new(rightSide).pack('side' => 'top', 'fill' => 'both',
     'expand' => true)

Im Moment ist es nicht besonders interresant, weil die "attributes" Formular noch leer ist. Aber der nächste Teil von Programmcode, welcher mit der Auswahl von Einträgen in XML-Datei handelt, macht diese Absicht ein wenig deutlicher:

   listBox.bind('ButtonRelease-1') {
     itemIndex = listBox.curselection[0]
     if itemIndex
       # Remove currently displayed attributes
     TkGrid.slaves(attributesForm, nil).each { |slave|
       TkGrid.forget(attributesForm, slave)
     }

     #Add labels und entry widgets for this entity's attributes
     entity = @entities[itemIndex]
     if entity.kind_of?(NQXML::NamedAttributes)
       keys = entity.attrs.keys.sort
       keys.each_index { |row|
         TkLabel.new(attributesForm) {
           text keys[row] + ":"
           justify 'left'
         }.grid('row' => row, 'column' =>0, 'sticky'=> 'nw')
         entry = TkEntry.new(attributesForm)
         entry.grid('row' => row, 'column' => 0, 'sticky' => 'e')
         entry.value = entity.attrs[keys[row]]
         TkGrid.rowconfigure(attributesForm, row, 'weight' => 1)
       }
       TkGrid.columnconfigure(attributesForm, 0, 'weight' => 1)
       TkGrid.columnconfigure(attributesForm, 1, 'weight' => 1)
     else
     end
   end
   }

Dieser gesamte Codeblock ist mit ButtonRelease Ereignis festgebunden, wenn man mit linken Maustaste in Listbox auf eine von Einträgen klickt. Das ist das Ereignis, das erzeugt wird, wenn der Anwender einen von Listeinträgen auswählt und darauf klickt und dann einfach den linken Mausbutton auf den ausgewählten Eintrag loslässt. Wir beginnen mit dem Aufruf der TkListbox's curselection Methode um in einem Array Indexes von ausgewählten Einträgen zu speichern. Da es eine "singl-selection" Liste ist, erwarten wir nur einen ausgesuchten Eintrag und zwar als Element, das den Index "0" im Array hat. Als Nächstes löchen wir aktuellen attributes Formularsinhalt, erreichen wir es, wenn man TkGrid.slaves aufruft, die in der Form eines Arrays die Kind-Widgets von Formularen erhält, und dann wenn man TkGrid.forget aufruft, wird man mit der Hilfe each Methode durchgeforstet und gelöscht. Während die meisten GUI Toolkits die "parent-child" Terminologie verwendet, um auf den hierarchischen Aufbau von GUI' Containers zu verweisen, verwendet Tk Häufig Termine "master" und "slave", vor allem when man so einen Layout Manager als TkGrid einsetzt.

Der nächste Abschnitt dieses "Event-Handler" "schleift" über alle Attributes des aktuell ausgewählten Dokumentenknotens und fügt für jeden Knoten einen TkLabel und TkEntry Widgets hinzu. Merken Sie es, dass wir die Default-Einstellung für Ausrichtung von TkLabel und TkEntry Widgets nicht berüksichtigen. Gültige Werte für das justify Attribut sind left, center und right, aber die Grundeinstellung der "Label's" Ausrichtung ist center. Also weil wir gern sehen würden, dass "grid" Manager Spalten und Zeilen gleich behandeln wird, rufen wir TkGrid.rowconfigure und TkGrid.columnconfigure Module-Methode auf, um das weight Attribut für jede Spalte und Zeile auf "1" zu setzen.

Nun sehen wir uns noch weitere sogennante "lower-level" Methoden für unsere XMLViewer Klasse. Für den Anfang haben wir openDocument Methode, welche ausgeführt wird, sobald der Anwender den Offnen Eintrag in der Datei Menü auswählt oder "Shortkey" Ctrl+O verwendet:

def openDocument
  types = [["Alle Datei", "*"], ["XML Documents", "*.xml"]]
  filename = Tk.getOpenFile('filetypes' => types, 'parent' => self)
  if filename != ""
     loadDocument(filename)
  end
 end

Der wichtigste Teil dieser Funktion ist das Aufrufen von Tk.getOpenFile Widget und selbstdefinierter loadDocument Methode. Tk.getOpenFile ist eine Tk Module-Methode, die man ermöglicht, das systemspezifisches Datei-Dialogbox anzuzeigen, wo man sich existierende Dateinamen aussuchen kann. Ähnliche Funktion hat Tk.getSaveFile Modul-Methode, die man verwenden kann, wenn man eine von existierenden oder neuen Datei speichern will. Das filetypes Attribut setzt eine Liste von Dateitypen als eine Schilderung und ein Muster dafür fest. Das nächste Attribut parent legt den Besitzer owner des Fensters für den Datei Dialog fest. Angenommen, dass der Anwender nicht disen Dialog beendet und versorgt sich mit einem legitimen Dateinamen, dann wird unsere selbstdefinierte loadDocument Methode aufgerufen um die aktuelle, ausgesuchte XML-Datei auszulesen:

def loadDocument(filename)
  @document = nil
  begin
     @document = NQXML::TreeParser.new(File.new(filename)).document
  rescue NQXML::ParserTree => ex
     Tk.messageBox('icon' => 'error', 'type' => 'ok',
       'title' => 'Fehler', 'parent' => self,
       'message' => "Das XML Dokument konnte nicht geparst werden")
  end
  if @document
     @listBox.delete(0, @listBox.size)
     @entities = []
     populateList(@document.rootNode, 0)
  end
 end<

Wenn der XML Parser eine Ausnahmesituation meldet während des Auslesens von XML-Datei, kann man sich ein einfaches Dialogbox anzeigen lassen, dargestellt mit der Hilfe von der Tk.messageBox Module-Methode. Das icon Attribut kann ein von vier "strings" aufnehmen und zwar error, info, question, warning, um einen optischen Effekt zu liefern. Das type Attribut weist darauf hin, welcher der "Ende-Button" verwendet wird. Für unseren einfachen Fall werden wir einfach den Button als "OK" anzeigen lassen und beim Drüclken auf den Button wird dieser kleine Dialog beendet. Es gibt natürlich auch andere Optionen fürs type Attribut. Das sind abortretrycancel, okcancel, retrycancel, yesno und yesnocancel.

Angenommen, es gab keine Fehler beim Auslesen von XML-Document, die Instanz-Varible @document eine Referenz(einen Verweis) auf ein NQXML::Document Objekt besitzen. Wir rufen die delete Methode auf, um die aktuelle Liste zu löschen und dann wird die Methode populateList gestartet, welche die Liste mit dem neuen Documentinhalt ausfüllt:

def populateList(docRootNode, indent)
  entity = docRootNode.entity
  if entity.instance_of?(NQXML::Tag)
     @listBox.insert('end', ''*indent + entity.to_s)
     @entities.push(entity)
     docRootNode.children.each do |node|
       populateList(node, indent + 2)
     end
  elsif entity.instance_of?(NQXML::Text) &&
       entity.to_s.strip.length != 0
     @listBox.insert('end', ''*indent + entity.to_s)
     @entities.push(entity)
  end
 end

Andere GUI Toolkits, wir werden später es sehen, haben eine baumähnliche Struktur für die Dastellung von XML-Datein. Tk hat aber solche Möglichket nicht, es gibt allerdings eine TK's Erweiterung - "Tix". Mehr Information über Tix erhalten Sie im Abschnitt "Tk's Erweiterung: Tix und BLT". Da Tix nicht immer verfügbar ist, werden wir die nur ungefähr behandeln, aber wenn Sie sich keine Gelegenheit "vermissen" möchten, wie man den baumstrukturigen Widget in einer regulären TkListbox mit der Einrücken von Listeinträgen verwendet, sollten Sie sich diese Erweiterung näher ansehen. Die populateList Methode wird solange aufgerufen, bis das ganze Document dargestellt wird und anschließend sehen wir, wie die Applikation die Auswahl der Liste von Einträgen behandelt.

Und so sieht unsere fertige Ruby/Tk Applikation aus(das Programm wurde unter Windows XP getestet):

Für Linuxbegeisterten, zu denen ich mich auch zähle, würde es so aushehen:

Anwenden SpecTcl GUI Builder

GUI-Building Tool für Tk SpecTcl war ursprünglich von "Sun Microsystems" entwickelt worden. Seit schon geraumiger Zeit wird dieses Projekt von einer Gruppe Freiwilligen unter Leitung von Morten Jensen weiterentwicklet und gewartet. Obwohl die ursprüngliche Absicht von "SpecTcl" war, die auf "user interface" basierende Tcl Sourcecodes zu erzeugen, haben Entwickler seitdem eine Generation kompatiblen Codes für andere Skriptsprachen enwickelt(Perl, Python, und Ruby), welche Tk als GUI verwenden. Eine experimentale Version von Ruby "backend" für "SpecTcl", genannt als specRuby wurde von Conrad Schneiker entwickelt und gewartet von Jonathan Conway. Eine aktuelle Version kann man von RAA-specRuby herunterladen. Folgende Abbildungen zeigen das Programm selbst mit einem einfachen Tk "user interface":

und als ein Ergebnis, wenn man das testet:

Zur Zeit gibt's keine Homepage von "specRuby", aber wie gesagt das Projekt kann man in "RAA" finden. Um Ruby/Tk zu verwenden, muss selbstverständlich eine funktionierende Tcl/Tk Installation auf dem System vorhanden sein.

Verwendung der Tk Erweiterung: Tix und BLT

Zusätzlich zu Tk's sozusagen "Kern-Widgets", gibt's aus viele Widgets von "dritter Hand", die mit Tk compatibel sind. Während die Beschaffung von dieser Tk Erweiturung jemandem zu schaffen macht, ist aber das Resultat dieser Anstrengung eher verblüffend, als man nur Tk allein einsetzt. Einer villeicht der bekannsten Tk-compatiblen Erweiterungen ist Tix- "Tk Interface Extension" genannt (http://tix.sourceforge.net) Tix bietet einen hierarchischen List-Widget (baumstrukturige Liste),einen "notebook-widget" und auch "combo-box-widget" sowie andere widgets an. Sie können immer den Source-Code von Tix Homeseite herunterladen, aber um diese Erweiterung zu verwenden, müssen Sie Tcl/Tk Source-Code herunterladen und selbst kompilieren.

Eine andere bekannte Erweiterung für Tk ist BLT(www.tcltk.com/blt) Als Tix bringt BLT eine Vielfalt von neuen Möglichkeiten, meistens für die Erstellung von Diagrammen und Grafiken. Sie können den Source-Code oder schon vorkompilierte Binäries für Windows von der BLT Homeseite herunterladen, aber im Gegensatz zu Tix ist die Erstellungsprozedure ganz normal, wie man si aus der Unix-Welt sicher kennt.: configure, make und make install. Um Tix und BLT mit Ruby/Tk zu verwenden müssen Sie ein TclTk-ext Package von Hidetoshi Nagai. Das können Sie von RAA downloaden, aber alle Dokumentation dazu wird in Japanisch geschrieben.

Valid XHTML 1.1! Valid CSS!