QVINTVS · SCRIBET

wxRuby-Programmierung, Teil II: Widgets und Events

Nachdem wir letztes mal die Grundlagen in Angriff genommen haben, wird es jetzt Zeit, sich mit dem korrekten Code-Layout und den Grundlagen von Event-Handling in wxRuby zu befassen.

Zielsetzung

Am Ende dieses Blogposts sollte eine einfache Anwendung stehen, die den Nutzer nach seinem Namen fragt und diesen auf Anfrage in einer Dialogbox ausgibt.

Das korrekte Layout

Wie ich ja schon letztes Mal angedeutet habe, ist es in der wxRuby-Programmierung besser, sein Hauptfenster in eine ordentliche Klasse zu verpacken. Wie wir dort auch gesehen haben, ist Wx::Frame die wxRuby-Klasse für Fenster, daher ist es logisch, dass unser Hauptfenster eine Subklasse von Wx::Frame wird:

# -*- coding: utf-8 -*-

require "wx"

class MainFrame < Wx::Frame

end

class MyApp < Wx::App

  def on_init
    @mainwindow = MainFrame.new
    @mainwindow.show
  end

end

MyApp.new.main_loop

Der magische Kommentar zum Encoding sieht jetzt ein wenig anders aus, bewirkt aber genau dasselbe wie Encoding: UTF-8. Ihr braucht den nicht so zu übernehmen, ich benutze ihn nur so, weil Emacs ihn dann korrekt erkennt.

Von da aus arbeiten wir jetzt weiter. Zunächst einmal bringen wir die Anwendung auf den Stand, den wir schon beim letzten Mal erreicht hatten, d.h. mit einem “Hello, world” in der Titelzeile:

class MainFrame < Wx::Frame

  def initialize(parent = nil)
    super(parent, title: "Hello, world!", size: [400, 400])
  end

end

parent = nil benutzen wir, damit unser Fenster auch in anderen Kontexten als dem Hauptfenster benutzt werden kann, sollte das, wenn das Programm wächst und gedeiht, einmal erforderlich werden. super ruft die initialize-Methode der Klasse Wx::Frame auf und akzeptiert auch dementsprechende Argumente.

Schließlich werden wir beim weiteren Fortgang dieser Klasse noch sehr viele Dinge aus dem Wx-Namespace brauchen. Es empfiehlt sich also, dieses Modul einzubinden, damit wir uns das ständige Wx::... sparen können:

class MainFrame < Wx::Frame
  include Wx

  def initialize(parent = nil)
    super(parent, title: "Hello, world!", size: [400, 400])
  end

end

Widgets!

Was wäre eine GUI ohne Buttons, ohne Textfelder, ohne Listen? Diese sogenannten Controls oder Widgets bilden den Kernpunkt einer jeden GUI. Es ist also empfehlenswert, sich mit ihnen auszukennen, was genau wir in diesem Abschnitt behandeln werden.

Um das zu Anfang beschriebene Ziel zu erreichen, brauchen wir zunächst einmal ein Textfeld sowie zwei Buttons. Das Textfeld wird den Namen des Benutzers aufnehmen, der erste Button ihn abspeichern und der zweite ihn abrufen. Nichts weltbewegendes also, aber es wird den allgemeinen Gang der Dinge in wxRuby (hoffentlich) darlegen.

Die wxRuby-Dokumentation verrät uns im Abschnitt “Controls”, was wir benötigen:

Widgets werden direkt während der Erstellungsphase der GUI angelegt, daher erstellt man sie auch direkt in der initialize-Methode des Hauptfensters; der Übersichtlichkeit halber lagert man sie jedoch meistens in eine Extra-Methode aus, die aus #initialize heraus aufgerufen wird. Sie sollte private sein, damit man sie nicht von außen heraus noch einmal aufrufen kann, was zu seltsamen Effekten in der GUI führen würde.

class MainFrame < Wx::Frame
  include Wx

  def initialize(parent = nil)
    super(parent, title: "Hello, world!", size: [400, 400])
    create_widgets
  end

  private

  def create_widgets
    @text_ctrl   = TextCtrl.new(self, pos: [20, 20], size: [200, -1])
    @save_button = Button.new(self, pos: [240, 20], label: "Save")
    @show_button = Button.new(self, pos: [20, 50], label: "Show")
  end

end

Wer sich den Aufruf von Frame#new und den new-Methoden der Widgets ansieht, wird eine gewisse Ähnlichkeit feststellen. Diese Ähnlichkeit ist auch durchaus beabsichtigt, denn alle sichtbaren GUI-Elemente werden auf dieselbe Art und Weise erzeugt: Das erste Argument ist das Elternfenster (das übrigens auch ein Widget sein kann, etwa im Fall von Wx::Panel), danach folgt das Optionshash mit Informationen zu Positionierung und Betitelung. pos und size versteht Frame#new genauso wie die #new-Methoden der Widgets, aber viele Widgets fügen eigene Parameter zum Hash hinzu, Wx::Button etwa den Parameter label, der angibt, welcher Text auf dem Button zu sehen sein soll.

Die Zeile

@text_ctrl   = TextCtrl.new(self, pos: [20, 20], size: [200, -1])

lässt sich somit fast komplett verstehen: Erzeuge ein Textfeld (TextCtrl) im Hauptfenster (self) an der Position (20|20) ([20, 20], hierbei handelt es sich um Pixel-Koordinaten, die von der Ecke oben-links gezählt werden) mit der Breite von 200 Pixeln ([200,). Was aber bedeutet das ominöse -1? Antwort: Es weist wxRuby an, den Standardwert für ein Positions- oder Größenargument zu nehmen, sodass wir als Programmierer nicht raten müssen, wie hoch das Widget wohl sein sollte. size: [200, -1] bedeutet demnach “Mache das Widget 200 Pixel breit, behalte aber die Standardhöhe”.

Der Code lässt sich auch schon mit sichbaren Ergebnis ausführen:

[Bild nicht verfügbar]

Funktionstüchtig ist die GUI aber noch nicht. Die Button-Klicks bewirken noch nichts, und das ändern wir jetzo.

Events

In wxRuby läuft jede Aktion über sog. Events ab, die von wxRuby vom Nutzer angenommen und im Event loop oder Main loop abgearbeitet und an die verantwortlichen Stellen weiterdeligiert werden. Der aufmerksame Leser stellt fest, dass man eine wxRuby-Anwendung eben deshalb durch Aufruf der Methode App#main_loop startet — hiermit stößt man den Eventloop an.

Ein Event kann viel sein: Eine Mausbewegung, ein Klick, ein Tastenanschlag (Druck und Loslassen einer Taste werden übrigens separat verarbeitet), aber auch weniger offensichtliche Dinge wie das Minimieren oder Verschieben eines Fensters. wxRuby kennt eine schier unzählbare Menge von Events, die alle der Dokumentation im Abschnitt “Events” entnommen werden können.

Der Lebenslauf so eines Events sieht am Beispiel eines Mausklicks wie folgt aus:

  1. Der Nutzer klickt irgendwo auf dem Bildschirm.
  2. Der Fenstermanager reicht die Klickkoordinaten an den X-Server weiter (unter Windows findet dieser Schritt nicht statt).
  3. Der X-Server (oder die Windows-Runtime) schaut nach, ob der Klick über einem Fenster war und sendet dem Fenster eine Nachricht, eine sog. Window message mit der Position des Klicks.
  4. wxWidgets erhält die Nachricht und generiert ein Wx::MouseEvent daraus.
  5. Der Eventloop arbeitet ein gesendetes Event nach dem anderen ab (schließlich werden, insbesondere durch Mausbewegungen, ja noch mehr Events generiert) und erreicht irgendwann das Klickevent, welches er dann an das Widget sendet, über welchem der Klick getätigt wurde. Wird kein Widget gefunden, wird der Klick an das übergeordnete Widget, in der Regel das Hauptfenster (es könnte aber auch z.B. ein Wx::Panel sein), gesendet.
  6. Beim angesprochenen Widget wird der Eventhandler für das betroffene Event aufgerufen, der beliebigen Code ausführt. Wird kein Eventhandler gefunden, wird das Event verworfen.
  7. Der Eventhandler verwirft das nun abgearbeitete Event (alternativ: Er verwirft es nicht und reicht es an das übergeordnete Widget weiter).

Uns als wxRuby-Programmierer interessieren nur die beiden letzten Schritte, denn die ersten drei Schritte werden von lowlevel-Anwendungen wie dem X-Server bearbeitet, Schritte 4 und 5 werden von der durch wxRuby gewrappten C++-Bibliothek wxWidgets erledigt. Sie sollen nur dem Verständnis dienen, damit ihr einen Überblick darüber bekommt, wo so ein Event eigentlich herkommt.

Der für uns interessante Teil ist der Eventhandler. Wie man an der obigen Auflistung sehen kann, werden also jedem Widget nur die ihn betreffenden Events zugesendet, und diese kann man mithilfe eines Eventhandlers auffangen; ein Eventhandler ist dabei eigentlich nur ein Callback, d.h. man teilt wxRuby mit “ich bin an dem und dem Event interessiert”, z.B. einem Mausklick, und gibt dabei ein Stück Ruby-Code in Form eines Proc-Objekts oder eines Methodensymbols mit. Das hört sich jetzt weißgottwie schwierig an, aber es verhält sich mit der Sache ganz simpel: Konkret für unseren Save-Button sieht das so aus, dass wir das von wxRuby generierte BUTTON_CLICKED-Event auffangen und daraufhin den im TextCtrl stehenden Wert in einer Instanzvariablen abspeichern:

class MainFrame < Wx::Frame
  include Wx

  def initialize(parent = nil)
    super(parent, title: "Hello, world!", size: [400, 400])
    create_widgets
    setup_event_handlers
  end

  private

  def create_widgets
    @text_ctrl   = TextCtrl.new(self, pos: [20, 20], size: [200, -1])
    @save_button = Button.new(self, pos: [240, 20], label: "Save")
    @show_button = Button.new(self, pos: [20, 50], label: "Show")
  end

  def setup_event_handlers
    evt_button(@save_button){|event| @name = @text_ctrl.value}
  end

end

Für die Eventhandler erstellen wir ebenfalls eine separate, von #initialize aus aufgerufene Methode, um die Übersichtlichkeit zu wahren. evt_button erlaubt es uns, besagte BUTTON_CLICKED-Events aufzufangen und beliebigen Code mitzugeben, in welchem wir in diesem Falle den Wert des TextCtrls auslesen und abspeichern. wxRubys Eventhandler akzeptieren im Übrigen auch ein Symbol, welches als Methodenname interpretiert wird; für größere Mengen an Code lässt sich demnach soetwas realisieren:

def setup_event_handlers
  evt_button(@save_button, :on_save_button_clicked)
end

private

def on_save_button_clicked(event)
  #Event handling code...
end

Welche Events ein Widget abfangen kann, erfahrt ihr in der Dokumentation zum jeweiligen Widget im Abschnitt “Event handling”, wo für Buttons etwa das folgende vorzufinden ist:

evt_button(id) { | event | … } Process a EVT_COMMAND_BUTTON_CLICKED event,when the button is clicked.

Außerdem kann jedes Widget noch eine Reihe an allgemeinen Events wie z.B. Mausbewegungen abfangen, worauf ich in diesem Posting allerdings noch nicht eingehen werde.

Damit sind die Grundlagen zum Event-Handling erklärt. Ich präsentiere jetzt ohne Umschweife den vollständigen Code des Programms:

# -*- coding: utf-8 -*-

require "wx"

class MainFrame < Wx::Frame
  include Wx

  def initialize(parent = nil)
    super(parent, title: "Hello, world!", size: [400, 400])
    create_widgets
    setup_event_handlers
  end

  private

  def create_widgets
    @text_ctrl   = TextCtrl.new(self, pos: [20, 20], size: [200, -1])
    @save_button = Button.new(self, pos: [240, 20], label: "Save")
    @show_button = Button.new(self, pos: [20, 50], label: "Show")
  end

  def setup_event_handlers
    evt_button(@save_button, :on_save_button_clicked)
    evt_button(@show_button, :on_show_button_clicked)
  end

  # Event handling

  def on_save_button_clicked(event)
    @name = @text_ctrl.value
  end

  def on_show_button_clicked(event)
    md = MessageDialog.new(self, 
                           caption: "Your name", 
                           message: "Your name is: #{@name}",
                           style: ICON_INFORMATION | OK)
    md.show_modal
  end

end

class MyApp < Wx::App

  def on_init
    @mainwindow = MainFrame.new
    @mainwindow.show
  end

end

MyApp.new.main_loop

Vom Konzept her neu hinzugekommen ist lediglich MainFrame#on_show_button_clicked, was den gespeicherten Namen in einer Dialogbox anzeigt. Der Code ist relativ selbsterklärend, aber hier nochmal in aller Ausführlichkeit: Wx::MessageDialog ist die wxRuby-Klasse, die für anzuzeigende Nachrichtendialoge zuständig ist. Der erste Parameter der new-Methode ist wie üblich das Elternfenster, caption der Titel des Dialogs, message die Nachricht. Mithilfe von style teilt man wxRuby Typ und Aussehen des Dialogs mithilfe von Konstanten mit; Wx::OK bedeutet “Zeige einen einzelnen OK-Button” und Wx::ICON_INFORMATION “Zeige ein Informationsicon (auf Windows kommt hier ein blaues “i” heraus). wxRuby unterstützt an dieser Stelle leider keine Symbole, was jedoch in zukünftigen Versionen hoffentlich geschehen wird.

Zu guter Letzt ist da der Aufruf von MessageDialog#show_modal, was den Nachrichtendialog anzeigt, und zwar modal. Modales Anzeigen bedeutet, dass, während der Dialog geöffnet ist, keine Interaktion mit dem Elternfenster, in unserem Falle also dem Hauptfenster, möglich ist. Dies ist das in der Regel gewollte Verhalten, möchte man dies jedoch nicht, genügt ein Aufruf der Methode show anstelle von show_modal (was dann aber u.U. zu mehreren Instanzen des Dialogs führen kann, z.B. wenn ein Nutzer in unserem Beispielprogramm einfach den Show-Button nochmal anklickt, während er den Nachrichtendialog noch nicht geschlossen hat.

Das war’s dann wohl für dieses Mal; ich wünsche noch ein schönes Restwochenende und einen angehmen Tag der Deutschen Einheit.

Valete.