QVINTVS · SCRIBET

Signale in GLib

Ein kleiner Ausflug in die Signalverwaltung von GTK+/GLib und eine kleine Anleitung, wie man in Ruby eigene Signale definiert.

Das ruby-gnome2-Projekt entwickelt die Ruby-Bindings für das GUI-Toolkit GTK+ und dessen Abhängigkeiten, darunter die Basisbibliothek glib, die eine Basisklasse (GLib::Object in den Ruby-Bindings) definiert, von der alle Objekte im GTK-Projekt erben, außerdem wird sie aufgrund einiger nützlicher Fertigkeiten sogar von ein paar völlig GTK-fremden Projekten genutzt (bei denen es sich nicht einmal um GUIs handeln muss). Obwohl glib Teil des GTK-Projekts ist, kann es unabhängig von diesem genutzt werden.

Konkret stand ich heute vor dem Problem, für das Kartenwidget des OpenRubyRMK einen neuen Signaltyp zu definieren, der die Lowlevel-Signale für Mausklicks und Tastendrücke hinter leichter benutzbaren Signalen für den Anfang einer Auswahl, deren Erweiterung und Abschließen, versteckt. Nach einiger Recherche bin ich auf einen Beispielpost für die Python-Bindings von GTK+ gestoßen, der zwar nicht 1:1 auf die Ruby-Bindings übertragbar ist, aber mir die notwendigen Informationen lieferte, um den Rest mit Recherche und ausprobieren herauszufinden.

Events und Signale

Zunächst einmal habe ich dabei gelernt, dass Events und Signale unterschiedliche Dinge sind. Events werden von X (oder zukünftig hoffentlich Wayland) an eine Applikation verschickt. Das sind nur sehr grundlegende Dinge, wie „Mausbewegung“, „Tastendruck“, etc. GTK+ nimmt diese Events und reicht sie in Form von Signalen an die einzelnen Objekte weiter. Einige Signale mappen durchaus direkt auf Events:

mywidget.signal_connect(:button_press_event){...}

Das ist aber nicht notwendigerweise der Fall:

mymenuentry.signal_connect(:activate){...}

Das activate-Signal eines Menüeintrags ist ein Highlevel-Signal, das über die Maus- und Tastaturevents, mit denen man einen Menüeintrag aktivieren kann, abstrahiert und eine uniforme Möglichkeit der Behandlung zur Verfügung stellt.

Signale in GLib

Es geht jedoch noch weiter. Ein Signal muss nicht zwangsläufig überhaupt mit Events des Fenstersystems zusammenhängen, sondern kann auch aus völlig anderen Kontexten heraus erstellt werden — die Verwendung gestaltet sich dann jener von Rubys Observable-Modul sehr ähnlich. Aus dieser Überlegung heraus folgt, dass Signale nicht GTK-spezifisch sind, sondern viel weiter oben in der Objekthierarchie anzusiedeln sind — bei GLib.

Definition eigener Signale

Das hat zwei Konsequenzen: Zum einen kann man damit glib als schnellen, featurereichen, in C geschriebenen Ersatz für Rubys observer-Bibliothek benutzen (glib ist ja unabhängig von GTK). Zum anderen ist es aber auch nicht erforderlich, sich mit den GTK-Interna zur Behandlung von Events auseinanderzusetzen. Tatsächlich ist die Definition eigener Events sogar recht simpel:

require "glib2"

class Sender < GLib::Object

  # Neuen GLib-Typ bekannt machen
  type_register

  # Neues Signal deklarieren (Erklärung folgt)
  signal_new :mysignal, GLib::Signal::RUN_FIRST, nil, nil, Hash

  # Eine Methode definieren, die das Signal irgendwo verschickt
  def foo
    # ...
    signal_emit(:mysignal, :para1 => "Foo", :para2 => "Bar")
  end

  private

  # Standard-Handler definieren. Der Methodenname
  # folgt dabei immer dem Muster "signal_do_<signalname>".
  def signal_do_mysignal(hsh)
    p hsh
  end

end

Obiger Code erstellt eine Klasse Sender, die in der Lage ist, ein Signal namens mysignal zu verschicken. Kernstück dabei ist die Zeile

 signal_new :mysignal, GLib::Signal::RUN_FIRST, nil, nil, Hash

, die das neue Signal bei GLib bekannt macht. :mysignal ist dabei der Signalname (Überraschung!), RUN_FIRST legt fest, dass der Standardhandler des Objekt immer zuerst vor allen registrierten Callbacks aufgerufen werden soll (siehe die Dokumentation für eine Liste der möglichen Signalflaggen), das erste nil ist ein von den Ruby-Bindings bislang nicht unterstützter Parameter namens accumulator, das zweite nil legt den Rückgabewert fest (denkt daran, glib ist eine C-Bibliothek, die braucht sowas…) und Hash schließlich definiert den Typ des Parameters des Signals. Hashes bieten sich hier an, weil es damit möglich ist, beliebig viele Parameter in „benannter“ Form zu mitzugeben.

Anwendung findet das ganze dann in der Methode Sender#foo:

 signal_emit(:mysignal, :para1 => "Foo", :para2 => "Bar")

signal_emit ruft alle für ein bestimmtes Signal (mysignal) registrierten Callbacks auf und übergibt ihnen als Parameter sein zweites Argument, was in diesem Fall wie wir schon oben festgelegt haben, ein Hash sein muss (wir nutzen hier den syntaktischen Zucker von Ruby für Hash-Argumente aus).

In Aktion sieht das ganze dann so aus:

sender = Sender.new
sender.signal_connect(:mysignal){|emitter, hsh| p(hsh)}
sender.foo
# => {:para1 => "Foo", :para2 => "Bar"}

Wie erwartet wird der Hash-Parameter von signal_emit an den Callback durchgereicht. Das erste Argument des Callbacks, emitter, ist dabei das emittierende Objekt, was in Ruby aufgrund der Closure-Eigenschaften von Blöcken zwar eher weniger wichtig ist (emitter ist in diesem Fall nämlich dasselbe Objekt wie sender), aber dennoch bei der Definition desselben Signalhandlers für verschiedene Objekte eventuell nützlich sein kann.

Damit steht nun auch der Definition eigener Signale für eigene Widgets nichts mehr im Wege.

Valete.