=head1 Gtk+2 - ein erweiterbares GUI-Toolkit =head2 Abstract Gtk+2 (das BIMP BoolBit, Version 2) durchlief bei der Entwicklung zu Version 2 tiefgreifende Änderungen. Das Typsystem wurde stark abstrahiert, so daß sich Schnittstellen zu anderen Sprachen im wesentlichen auf das Bereitstellen des Objektsystems beschränken. In diesem Dokument wird das Toolkit gtk+2 kurz vorgestellt, wobei Erfahrung mit GUI-Programmierung vorausgesetzt wird. Im Wesentlichen geht es darum, einige besondere Techniken vorzustellen sowie das Toolkit von Perl und C aus selbst zu erweitern. =head2 Gtk2 und Glib, eine Übersicht =head3 Übersicht über die G-Module Es gibt eine Menge Bibliotheken, deren Namen mit G anfängt und im Dunstkreis von Gtk+ stehen. Vielleicht wäre es sinnvoll gewesen, eine neue G-Hierarchie in Perl zu begründen, so muß man sich mit einer teilweise nicht sehr logischen Namensgebung zurechtzufinden. =over 4 =item Glib Glib ist die Schnittstelle zur C (sowie zu C, C und C). Es bildet die Grundlage für das Objekt- und Typ- und Signalsystem, bietet Hilfsfunktionen zum Umwandeln von Dateinamen, Logging usf. =item Gtk2 Dieses Modul ist eine Sammelstelle für diverse Bibliotheken: neben C auch C, C und C. Sie reicht für fast alle Anwendungen aus. =item Gtk2::GLExt, Gtk2::GladeXML, Gtk2::TrayIcon und Gtk2::Spell Bieten Integration von OpenGL, GladeXML, EggTrayIcon und Spell-checking für Text-Widgets. =item Gnome2 Schnittstelle zur C und vielem mehr. ... muß man nicht benutzen, kann mir aber vorstellen, daß sie für die Erstellung von Gnome-Applikationen ganz nützlich sind :) =for latex \begin{sloppypar} =item Gnome2::Canvas, Gnome2::GConf, Gnome2::PanelApplet, Gnome2::Print, Gnome2::Rsvg, Gnome2::VFS, Gnome2::Vte, Gnome2::Wnck. Jede Menge Gnome-Zeugs. =for latex \end{sloppypar} =back =head3 Die Widgets von Gtk2 sind GObjects Wie jedes andere Toolkit auch, bietet Gtk2 eine Menge vorgefertigter I: I, I, I und andere ``einfache'' Widgets, aber auch komplexe, wie das I- (komplett in Unicode, einbettbare Objekte) oder das I- (Listen und Bäume im MVC-Modell) Widget. =for html =for comment Scheisslatex ignoriert natürlich alle Daten wie Auflösung.. grr... =for latex \begin{center}\includegraphics[scale=0.4]{gtk2-textwidget.png}\end{center} =for perlpoint \IMAGE{src="gtk2-textwidget.png"} Alle diese Widgets sind sogenannte I (davon abgeleitet sind in Gtk2 dann sogenannte I, GUI-Objekte), und haben folgende Features: =over 4 =item Eine Klasse Jedes GObject ist eine Instanz einer Klasse (C), eine Datenstruktur, die Informationen über Methoden u.ä. speichert. Eine C ist allerdings nur ein Spezialfall eines Cs: Die Glib bildet ein komplettes Typsystem mit Klassen, Aufzählungen, Flags, einfachen Datentypen wie Integer oder Fließkommazahlen oder auch C-structs an. All dies ist zur Laufzeit in einer Form verfügbar, in der sich auch neue - zur Compilezeit nicht bekannte - Datentypen verwenden lassen. Schnittstellen für andere Sprachen werden dadurch einfach, da man ``lediglich'' die Konzepte ``Datentyp'', ``Konverter'', ``Klasse'', ``Methode'', ``Bitset'' usw. umsetzen muß. Konkrete Instanzen dieser Typen funktionieren zur Laufzeit automatisch. Bei einer gut geschriebenen C-Bibliothek, die auf Glib bzw. GObject aufsetzt, muß man eigentlich nur die Initialisierungsfunktion aufrufen, die die Datentypen registriert, und kann dann sofort von Perl darauf zugreifen - ohne eine einzige XS-Funktion geschrieben zu haben. =item Methoden Jedes GObjekt hat mindestens Methoden zum Erzeugen (C) und Zerstören (C), Setzen und Lesen von Properties und eine Spezialität von GObject - C. Letzteres ist eine Art Anfrage an ein Objekt, sich freiwillig aufzulösen. Gtk2 fügt noch einige weitere hinzu, wie die C-Methode, die aktiv versucht, ein Objekt zu zerstören: In GUI-Programmen ist es manchmal schwer, alle Referenzen im richtigen Moment aufzulösen, da sie häufig zirkulär sind. Manche Objekte ``leben'' auch ganz eigenständig, z.B. wird ein Toplevel-Fenster im allgemeinen vom Benutzer ``referenziert'', was sich aber nicht notwendigerweise im Referenz-Zähler niederschlägt. Um solche Objekte loszuwerden, kann man sie bitten, sich zu I. Dabei gibt es alle Referenzen auf andere Objekte frei und bittet diese, sich ebenfalls zu destroyen. Dadurch werden zirkuläre Referenzen effektiv aufgebrochen. Methoden kann man in Perl nicht direkt implementieren, dafür aber eigene Signale, die beliebige Parameter oder Rückgabewerte besitzen können und mehr oder weniger direkt von C oder anderen Sprachen aus aufgerufen werden können. =item Signale Signale ähneln Methoden, Callbacks oder sogenannten ``Delegates''. Wie Methoden sind sie fest an eine Klasse gebunden und rufen (in C) eine Methode der Klasse auf, sobald sie ausgelöst werden. Ein Signal kann von jedem ausgelöst werden, insbesondere lösen aber Ereignisse (Events) in Gtk2 Signale aus. Der Signal-Handler eines Objektes verarbeitet das Ereignis (und beendet die Signalauslieferung) oder tut nichts, woraufhin Gtk2 ein anderes Objekt als Empfänger sucht. Zusätzlich kann man vor oder nach dem eigentlichen Signal-Handler des Objektes weitere Callbacks einklinken. Z.B. löst das Text-Widget beim Mausklick rechts ein C-Signal aus, das normalerweise ein Popup-Menü generiert. Das kann man verändern oder ganz überschreiben. Und wenn man Mausklicks abfangen möchte, kann man das C-Signal überschreiben und so Mausklicks vom Text-Widget fernhalten. =item Properties (Eigenschaften) Properties sind von der jeweiligen Klasse definierte Eigenschaften, die das Objektverhalten beeinflussen. Sie werden von der Klasse festgelegt und auch von ihr benutzt. =item Schlüssel-Wert-Paare Damit andere Benutzer mit einem Objekt Daten verknüpfen können, kann man an ein GObject noch beliebige Schlüssel-Wert-Paare (sogar mit Destruktor) packen, wobei sich dies von Perl aus auf Integer beschränkt, da das Glib-Modul jedes GObject automatisch mit einem Perl-Hash verknüpft und diesen auf Perl-Ebene zur Speicherung zur Verfügung stellt. =back =head3 Informationen über Typen und Klassen zur Laufzeit abfragen Die Typinformationen, wie Werte von Aufzählungen, Namen von Datentypen, Objekt-Signale, -Properties uvm., sind alle zur Laufzeit abfragbar. Dadurch wird das Schreiben von Klassenbrowsern oder RAD-Tools sehr einfach. Aber auch beim Entwickeln von Programmen ist das nützlich, da man nicht immer die Dokumentation zu Rate ziehen muss. Z.B. liefert C<< $obj->list_properties >> (oder C Klassenname) eine Liste von Arrayrefs mit Name, Typ, Flags und Beschreibung zurück, die man sich z.B. mit folgender Funktion ausgeben lassen kann: use Gtk2; sub gobj_props { for ($_[0]->list_properties) { printf "%-16s %-24s %-24s %s\n", $_->{name}, $_->{type}, (join ":", @{$_->{flags}}), $_->{descr}; } } Der Aufruf C (oder mit einem C-Object als Argument) liefert folgende Properties, die alle Gtk2-Widgets besitzen: user-data gpointer readable:writable Anonymous User Data Pointer name Glib::String readable:writable The name of the widget parent Gtk2::Container readable:writable The parent widget of this widget. Must be a Container widget width-request Glib::Int readable:writable Override for width request of the widget, or -1 if natural request should be used ... extension-events Gtk2::Gdk::ExtensionMode readable:writable The mask that decides what kind of extension events this widget gets no-show-all Glib::Boolean readable:writable Whether gtk_widget_show_all() should not affect this widget Signale lassen sich ebenso abfragen: sub gobj_signals($) { # Signale haben Argumente und Parameter, daher Dumpvalue use Dumpvalue; Dumpvalue->new (bareStringify => 0)->dumpValue ($_) for Glib::Type->list_signals ($_[0]); } gobj_signals "Gtk2::Widget"; ... 'signal_id' => 23 'signal_name' => 'button-press-event' 'itype' => 'Gtk2::Widget' 'param_types' => ARRAY(0x83b4f28) 0 'Gtk2::Gdk::Event' 'return_type' => 'Glib::Boolean' 'signal_flags' => [ run-last ] -> 2 ... Das C-Signal möchte also ein Event als einziges Argument und liefert einen Wahrheitswert. Die Abstammung von C ist schnell gefunden: warn join " ", Glib::Type->list_ancestors ("Gtk2::Window"); Gtk2::Window Gtk2::Bin Gtk2::Container Gtk2::Widget Gtk2::Object Glib::Object at gtk.pl line xx. # und list_interfaces wollte ich auch erwähnen: warn join " ", Glib::Type->list_interfaces ("Gtk2::Window"); Gtk2::Atk::ImplementorIface at gtk.pl line xx. Wenn man gtk+ von C her kennt (oder eben die C-Dokumentation benutzt), kann man aus dem C-Typnamen den Namen in Perl erfahren: warn Glib::Type->package_from_cname ("GtkWindowPosition"); Gtk2::WindowPosition at gtk.pl line xx. warn Glib::Type->package_from_cname ("GdkPixbuf"); Gtk2::Gdk::Pixbuf at gtk.pl line xx. Aufzählungen und Flags kann man ebenso hinterfragen: sub g_vals($) { for (Glib::Type->list_values ($_[0])) { printf "%-30s %s\n", $_->{name}, $_->{nick}; } } g_vals "Gtk2::Gdk::EventMask"; GDK_EXPOSURE_MASK exposure-mask GDK_POINTER_MOTION_MASK pointer-motion-mask ... GDK_SCROLL_MASK scroll-mask GDK_ALL_EVENTS_MASK all-events-mask =head4 Aufzählungen und Flags Natürlich kann man zur Laufzeit auch eigene Typen hinzufügen. Z.B. einen neuen Flag-Typ: Glib::Type->register_flags (Meine::Flags => 'eins', # implizit 1<<0 'zwei', # implizit 1<<1 ['drei' => 15 ], # explizit 15 ['vier' => 35 ], # explizit 35 'fuenf', # implizit 1<<4 ); Diese neuen Typen sind gleichwertig zu den vorhandenen, können also von C oder anderen Programmiersprachen aus benutzt werden. Aufzählungen und Flags sind in Perl überladen. Das ist besonders hilfreich in Event-Handlern: $widget->signal_connect (button_press_event => sub { if ($_[1]->type eq "button-press") { if ($_[1]->button == 1) { if ($_[1]->state == "shift-mask") { # Umschalt-Klick if ($_[1]->state * "shift-mask") { # oder '&' # Umschalt-Klick (eventuell plus anderen Modifiern) if ($_[1]->state * ["control-mask", "shift-mask"]) { # oder '&' # Klick + Umschalt ODER Strg (eventuell plus anderen Modifieern) if ($_[1]->state >= ["control-mask", "shift-mask"]) { # Klick + Umschalt UND Strg (Übermenge) Oder beim Manipulieren von Event-Masken: $widget->set (events => $widget->get ('events') - [qw(key_press_mask button-motion-mask)] ); Auch eigene Klassen sind möglich, siehe Teil 2 :) =head3 Beispiel: Session-Management Häufig gibt es in C sogenannte I (``Bequemlichkeitsfunktionen''). Sofern sie sinnvoll sind, wurden sie bei Glib und Gtk2 umgesetzt. So gibt es z.B. eine C- und eine C-Methode, die bei Gtk2-Widgets die Event-Maske verändern. Doch für alle Properties kann man die generischen C- und C-Methoden verwenden. Damit läßt sich ein einfaches Session-Management schreiben: use Scalar::Util; my %get = ( window_size => sub { [ ($_[0]->allocation->values)[2,3] ] }, column_size => sub { $_[0]->get("width") || $_[0]->get("fixed_width")}, modelsortorder => sub { [ $_[0]->get_sort_column_id ] }, ); my %set = ( window_size => sub { $_[0]->set_default_size (@{$_[1]}) }, column_size => sub { $_[0]->set (fixed_width => $_[1]) }, modelsortorder => sub { $_[0]->set_sort_column_id (@{$_[1]}) }, ); my $state = Storable::retrieve "session.dat"; # Alte Session-Daten laden my %widget; sub state { my ($widget, $class, %attr) = @_; while (my ($k, $v) = each %attr) { $v = $state->{$class}{$k} if exists $state->{$class} && exists $state->{$class}{$k}; $set{$k} ? $set{$k}->($widget, $v) : $widget->set ($k => $v); } $widget{$widget} = [$widget, $class, \%attr]; Scalar::Util::weaken $widget{$widget}[0]; } Naja, I so einfach ist es nicht: Man kann damit schon recht viel anfangen, und es sind zugegebenermaßen mehr als drei Zeilen. Was macht der Code? Es gibt Eigenschaften, die man über eine Session hinweg retten möchte, aber keine Properties sind. Für diese Eigenschaften (z.B. C) definiere ich eigene C- und C-Funktionen. Die eigentliche Schnittstelle ist die C-Funktion. Sie wird so benutzt: my $window = new Gtk2::Window; state $window, "game::window", window_size => [600, 500]; my $hpane = new Gtk2::HPaned; state $hpane, "game::hpane", position => 500; Diese beiden Widgets werden in einem (nicht sehr fiktiven) Go-Programm benutzt um ein Spiel darzustellen. Der erste Aufruf von C sagt: Beim Widget vom Typ C (ein willkürlicher Name) soll die Property C von einer Session in die nächste Übertragen werden. Falls keine Sessiondaten existieren, nimm als Default C<[600, 500]>. Der zweite Aufruf macht das gleiche für die C-Property des C-Widgets. Beides zusammen sorgt dafür, daß neue Spielfenster in der gleichen Größe erscheinen wie bei der letzten Benutzung und daß die horizontale Unterteiling (links Spielbrett, rechts Chat-Bereich) ebenfalls erhalten bleibt. In der Realität benutze ich eine etwas erweiterte C-Funktion mit einem C<$instance>-Argument: manche Benutzer möchten z.B. verschiedene Chat-Räume in verschieden großen Fenstern darstellen. Das zusätzliche Argument dient der Unterscheidung: wird ein neuer Raum betreten, werden die Werte des Raum-Typs benutzt; wird ein bekannter Raum betreten, werden dessen bekannte Werte benutzt. Die Funktion liest übrigens nur Werte aus den Sessiondaten C<$state> aus, aber irgendwie müssen sie auch dort abgelegt werden. Dies geschieht in meinen Programmen meistens durch Benutzeraufforderung: der Benutzer stellt sein gewünschtes Layout zusammen und drückt dann C o.ä., worauf dann folgende Funktion aufgerufen wird. sub save_state { for (grep $_, values %widget) { my ($widget, $class, $attr) = @$_; next unless $widget; # widget schon tot... $widget->realize if $widget->can ("realize"); while (my ($k, $v) = each %$attr) { $state->{$class}{$k} = $get{$k} ? $get{$k}->($widget) : $widget->get ($k); } } Storable::nstore $state, "session.dat"; } Sie geht alle Widgets durch, die mit C behandelt wurden, prüft, ob sie noch existieren, sorgt (mit C) dafür, daß die Eigenschaften auch mit sinnvollen Werten gefüllt sind, und speichert dann die entsprechenden Eigenschaften in C<$state>. =head3 Beispiel: Rapid Application Development mit Glade -- mein Weg Ich will es nicht verschweigen: Ich I (hoffentlich ist das deutlich genug) GUI-Programmierung. Nichts ist langweiliger bzw. nerviger, als Dialogboxen zusammenzustöpseln oder Widgets in der Hierarchie zu verschieben. Glücklicherweise gibt es RAD-Tools, mit denen man sich seine Applikation zusammenklicken kann. Was mich an RAD-Tools allerdings stört ist ihre Starrheit: meistens wollen sie gleich den Quelltext schreiben, inklusive des Hauptprogramms. Und häufig ist es schwierig, generelle Klassen zu definieren, z.B. ein Chat-Room-Widget, von dem es zur Laufzeit beliebig viele geben kann. Auch I -- ein (das!) Gtk+ und Gtk+2 RAD-Tool -- stellt hier keine Ausnahme dar. Zum Glück kann man Glade auch etwas anders zum Entwickeln benutzen, und diesen anderen Weg (ich kenne niemanden, der Glade so benutzt), möchte ich hier beschreiben. Zuallererst ist es notwendig, die Widget oder die UI in Glade zu erstellen und als GladeXML-Datei zu speichern. Z.B. habe ich für ein Bilder-Anzeige-Programm (C, reiner Beta-Code) einen Ausdrucken-Dialog erstellt: =for html =for latex \begin{center}\includegraphics{gtk2-cv.png}\end{center} =for perlpoint \IMAGE{src="gtk2-cv.png"} Die Beschreibung ist in der XML-Datei C gespeichert. Darin habe ich den einzelnen Widgets Namen gegeben, z.B. heißt das Dialogfenster C, das Option-Menü zum Auswählen der Papiergröße C usw. Den ganzen Dialog kapsele ich in einem Perl-Modul namens C ab, mit insgesamt 70 Zeilen und zwei Methoden. Die kürzeste davon benutzt ein anderes Modul C zum Ausdrucken und ist völlig uninteressant :) Die längste ist die C-Methode, die ein neues PrintDialog-Widget erzeugt. Zuerst der Standardkopf: use Gtk2; use Gtk2::GladeXML; # uvm. sub new { my $class = shift; my $self = bless { @_ }, $class; Als nächstes wird die GladeXML-Datei geladen: $self->{dialog} = my $d = new Gtk2::GladeXML ".../cv.glade", "PrintDialog"; Mit C wird ein Teilbaum aus dem Widget-Baum geladen, und zwar der mit dem C-Namen. So kann man viele verschiedene Dialoge in einer Datei ablegen. Das Laden ist nicht unbedingt eine schnelle Angelegenheit, ist aber in den meisten Fällen flott genug, um bei jedem Aufruf von neuem gemacht zu werden (und man hat sowieso keine Wahl :) Das Ergebnis ist I Widget, sondern ein C-Objekt, das die Methode C besitzt, mit der man anhand des Namens einzelne Widgets ``herausgreifen'' kann. Damit fülle ich den Dialog mit Leben: $d->get_widget ("destination")->set ( text => $ENV{CV_PRINT_DESTINATION} || "| lpr"); my $menu = $d->get_widget ("papersize")->get_menu; for (Gtk2::CV::PostScript->papersizes) { my ($code, $name, $w, $h) = @$_; $menu->append (my $item = new Gtk2::MenuItem $name); $item->set_name ($code); } $menu->show_all; $d->get_widget ("papersize")->set_history (0); $d->get_widget ("PrintDialog")->signal_connect (close => sub { $_[0]->destroy; }); $d->get_widget ("PrintDialog")->signal_connect (response => sub { if ($_[1] eq "ok") { $self->print ( size => (Gtk2::CV::PostScript->papersizes) [$d->get_widget ("papersize")->get_history], margin => $d->get_widget ("margin")->get_value, ... destination => $d->get_widget ("destination")->get ("text"), binary => $d->get_widget ("encoding_binary")->get ("active"), ); } $_[0]->destroy; }); return $self; # Ende } Jetzt wird klar, warum das Ding C<$d> heißt und nicht einen längeren Namen trägt. Wer das GladeXML-Modul kennt, weiß, daß es noch viel ``einfacher'' geht, z.B. mit C. Leider weiß man in den Callbacks, die diese Methode mit den Widgets verbindet, nichts mehr über das ursprüngliche C-Objekt, so daß sie mir recht nutzlos erscheint, wenn man nicht alles in globalen Variablen ablegen will (was häufig ausreicht, aber nicht gerade sauber ist). Daher habe ich die Funktion C geschrieben. Sie geht einen etwas anderen Weg und erlaubt es, die Signal-Callbacks als Closures anzugeben. Damit wäre das Namensraum-Problem gelöst. Leider muss man dazu jedem Signal-Handler einen Namen geben, und das ist so umständlich, daß ich die Funktion selbst nie benutzt habe und es einfach ``per Hand'' anhand der Widget-Namen mache. Dies alles heißt nicht, daß man es nicht einfacher anders besser machen könnte. Es ist einfach ``mein Weg'', Glade zu benutzen. Und wenn ich noch etwas darüber nachdenke, muß ich sagen, daß es mit Glade nicht wirklich einfacher ist, Widgets umzustellen oder GUIs zu designen. *Seufz*. =head2 Teil 2 - Gtk+ als solide Grundlage für Eigenentwicklungen Gtk+ eignet sich hervorragend für Eigenentwicklungen - auch größere. Belegt wird dies dadurch, daß man es vollständig von Perl aus erweitern kann und nicht auf einige vorgefertigte Klassen beschränkt ist. Außerdem kann man Widgets aus beliebigen Sprachen miteinander integrieren und sogar Teile in Perl und einer anderen Sprache schreiben, was große Anwendungen einfacher macht: zeitkritische Teile und Widgets werden (z.B.) in C implementiert, während Perl bequem für die GUI-Logik benutzt werden kann. =head3 Perl-Objekte vs. Glib-Objekte Meiner Meinung nach ist der Hauptgrund für das typisch ``skripthafte'' Aussehen vieler Perl/Tk (oder Perl/Gtk oder Perl/Qt oder auch Tcl/Tk) Programme die ausschließliche Verwendung von Standard-Widgets: Die Programme sehen aus wie aus dem Baukasten, weil sie nur aus Baukastenteilen bestehen. ``Richtige'' Anwendungen (wie z.B. Gimp) benutzen aber eigene Widgets, sofern das Sinn macht. Viele Toolkits machen es einem sehr schwer, eigene Widgets zu programmieren. Bei Gtk2 kann man zwischen einfachen Ableiten und ``echten'' Gtk2-Widgets wählen. =head4 Die Perl-Methode Die ``Perl-Methode'' besteht darin, eine neue Klasse von einem bestehenden Widget abzuleiten. Das ist einfach und erfordert keinerlei Umdenken: package IntegerWidget; use base Gtk2::Entry; sub new { my ($class, %prop) = @_; my $self = $class->SUPER::new; $self->set_text ($prop{text}); $self->signal_connect (insert_text => sub { my ($self, $text, $length, $position) = @_; $text =~ y/0-9//cd; ($text, $position); }); $self; } ... $container->add (new IntegerWidget text => 50); Das geht mit fast jedem Toolkit. Aber ein richtig neues Widget kommt dabei nicht heraus. Trotzdem ist dieses Vorgehen meistens nützlich, wenn man eine große Menge an Widgets benötigt, die mehr oder weniger eine speziell konfigurierte Version eines Standard-Widgets ist. Einige mehr oder minder wichtige Dinge kann man damit allerdings nicht implementieren: eigene Signale und Eigenschaften. Sicher kann man eigene C-Methoden schreiben, aber sie verhalten sich nicht wie Glib-Properties (das Session-Management-Beispiel müßte speziell abgeändert werden). Und Signale kann man durch Callbacks implementieren, aber der Nutzer des Widgets muß dann auf C verzichten und das Widget verhält sich insgesamt nicht wie andere Widgets, Und schließlich kann man nur sehr schlecht selbst zeichnen und auf Ereignisse (z.B. C) reagieren. =head4 Die Glib-Methode Mit der ``Glib-Methode'' wird keine normale Perl-Klasse erstellt, sondern eine echte Glib-Klasse (und zusätzlich eine Perl-Klasse, aber eben keine ganz normale). Dabei hilft einem das C-Modul, das ähnlich wie ein Pragma wirkt: package IntegerWidget; use Glib::Object::Subclass Gtk2::Entry, signals => { insert_text => sub { my ($self, $text, $length, $position) = @_; $text =~ y/0-9//cd; $self->signal_chain_from_overridden ($text, $length, $position); }, }; C ist die Basisklasse und mit C werden vorhandene Signale überschrieben bzw. neue deklariert. Überschreibt man ein vorhandenes Signal (wie hier) kann man das ursprüngliche Signal mit C aufrufen, was einem C-Aufruf in Perl entspricht. Das Modul setzt C<@ISA> und liefert auch gleich einige Methoden, z.B. C. Der Grund liegt darin, daß die C-Methoden der existierenden C-Widgets das Klassenargument ignorieren, wie auch die C-Funktionen, die sie aufrufen, kein Klassenargument besitzen. Bei der Umsetzung von gtk+ in Perl dachte man, die normalen Konstruktoren seien wichtiger, da sie häufiger aufgerufen werden. =for latex \begin{sloppypar} Die von C gelieferte C-Methode ruft lediglich C auf. Das ist der generische Objekt-Konstruktor, mit dem man jedes Glib-Objekt (bzw. auch davon abgeleitete) erzeugen kann. Er akzeptiert C<< property => wert >>-Paare, was für dieses Beispiel ausreicht, man muss also nichts weiter implementieren. =for latex \end{sloppypar} Interessant wird es, wenn man zusätzliche Properties und Signale registriert: package IntegerWidget; use Glib::Object::Subclass Gtk2::Entry, signals => { # neues signal, range-exception range_exception => { class_closure => sub { }, flags => [qw(run-first)], param_types => [Glib::Integer], }, # vorhandene Signale überschreiben insert_text => sub { ... }, changed => sub { my ($self) = @_; my $text = $self->get_text; $self->signal_emit (range_exception => $text) if $text < $self->{min} || $text > $self->{max}; }, }, properties => [ Glib::ParamSpec->string ( min => "Minimum", "Values smaller than this value cause a range_exception", 0, [qw(readable writable)] ), Glib::ParamSpec->string ( max => "Maximum", "Values larger than this value cause a range_exception", 0, [qw(readable writable)] ), ], Hier wird ein neues Signal C eingeführt (das standardmäßig garnichts macht). Es wird ausgelöst, wenn die eingegebene Zahl kleiner oder größer als die Properties C oder C ist, was in der C-Methode festgestellt wird. Wird eine Property gelesen, ruft Glib die C-Funktion auf. Sie ist in Grossbuchstaben geschrieben, weil sie nur von Glib und nie vom eigenen Code aus aufgerufen wird, ähnlich wie C. Genauso wird beim Verändern die C-Funktion aufgerufen. C liefert zwei Default-Implementierung, die die Werte aus C<$self->{name}> lesen bzw. dort ablegen. Man kann diese Funktionen natürlich überschreiben: sub SET_PROPERTY { my ($self, $pspec, $newval) = @_; $self->{$pspec->get_name} = $newval; # nicht so sinnvoll: warn "max wurde geändert!!!" if $pspec->get_name eq "max"; # sehr sinnvoll: $self->emit_signal ("changed"); } Wenn C oder C geändert werden, löst diese Implementierung ein C-Signal aus, um die neuen Grenzen zu prüfen. Die großgeschriebenen Funtkionen sind übrigens genau das: Funktionen. Sie sollten niemals ihre SUPER-Implementierung aufrufen, das erledigt Glib automatisch. =for latex \begin{sloppypar} Die beiden anderen Funktionen, die Glib aufruft sind C und C. Sie sollten das Objekt initialisieren bzw. auflösen. I sollte man C implementieren, da C intern von Glib benutzt wird und mehrmals aufgerufen werden kann. =for latex \end{sloppypar} =head4 Referenzprobleme Jetzt zu einem kleinen aber feinen Problem. Was ist der Unterschied der beiden folgenden INIT_INSTANCE-Funktionen? use Glib::Object::Subclass Gtk2::Frame; sub INIT_INSTANCE { my ($self) = @_; $self->add ($self->{entry} = new Gtk2::Entry); $self->{entry}->signal_connect (changed => sub { warn $self->{entry}->get_text; }); } sub INIT_INSTANCE { my ($self) = @_; $self->add ($self->{entry} = new Gtk2::Entry); $self->{entry}->signal_connect (changed => sub { warn $_[0]->get_text; }); } Nun, die obere Version erzeugt eine neue zirkuläre Referenz: Die Closure im ersten Fall referenziert das äußere C<$self>. Wie löst man diese Referenzen auf? Das größte Problem neuer Widgets sind zusätzliche Referenzen, die das Widget daran hindern, sich aufzulösen. Gtk2 löst dieses Problem durch ein explizites C-Signal: Wird es ausgelöst, sollte das Objekt möglichst alle Referenzen auf andere Objekte freigeben. Fällt dabei der Referenz-Zähler auf 0 ab, wird das Objekt aufgelöst. Container lösen dabei rekursiv das C-Signal auf die in ihnen enthaltenen Objekte auf. Dabei werden normalerweise alle Referenzen aufgelöst, da z.B. Signal-Handler ebenfalls entfernt werden, die Selbstreferenz im obigen Beispiel ist dabei also eigentlich kein Problem. Viele Perl-Widgets sind aber versteckte Container, da man häufig zusammengesetzte Widgets erzeugt, wie im obigen Beispiel. Dann kann es passieren, das auch nach einem C ein eingebettetes Widget eine Referenz auf den Vater enthält, und der hält gleichermaßen eine Referenz (C<< $self->{entry} >>). Dies kann man umgehen, indem man bei einem C einfach %$self löscht: $self->signal_connect (destroy => sub { %{$_[0]} = () }); Das ist etwas radikal, daher gibt es zwei sauberere Lösungen. Einmal kann man bei einem C wirklich sauber aufräumen: $self->signal_connect (destroy => sub { (delete $self->{entry})->destroy; # etc. für andere Widgets }); Oder man verhindert zusätzliche Referenzen auf Perl-Seite, was natürlich nur bei einfachen Referenzen geht: use Scalar::Util (); Scalar::Util::weaken $self->{entry}; =head4 Beispiel: ein Go-Brett Für einen Go-Server-Client benötige ich ein Widget, das ein Go-Spielbrett darstellt (das Bild zeigt es beim Auszählen der Punkte eines Spiels). =for html =for latex \begin{center}\includegraphics[scale=0.3]{gtk2-kgsueme.png}\end{center} =for perlpoint \IMAGE{src="gtk2-kgsueme.png"} Der Code für Board-Widget fängt so an: use Glib::Object::Subclass Gtk2::AspectFrame, properties => [ Glib::ParamSpec->IV ( "size", "Board Size", "The Go Board size, 2..38", 2, 38, 19, [qw(construct-only writable readable)], ), Glib::ParamSpec->IV ( "cursor-mask", "cursor mask", "The mask used to show a cursor", 0, 1<<30, 0, [qw(writable readable)], ), Glib::ParamSpec->IV ( "cursor-value", "cursor value", "The value used to show a cursor", 0, 1<<30, 0, [qw(writable readable)], ), ], signals => { "button-press" => { flags => [qw/run-first/], return_type => undef, # void return param_types => [Glib::Int, Glib::Int, Glib::Int], }, "button-release" => { flags => [qw/run-first/], return_type => undef, # void return param_types => [Glib::Int, Glib::Int, Glib::Int], }, destroy => sub { $_[0]->signal_chain_from_overridden; %{$_[0]} = (); }, }; Das Brett wird also von einer C abgeleitet: Das Brett ist zwar nicht ganz quadratisch, hat aber immer dasselbe Seitenverhältnis von ca. 11:10, so daß sich ein C anbietet. Als erstes werden drei Properties definiert: die Größe (C), die die Anzahl der Linienschnittpunkte angibt. Sie kann nur beim Erzeugen gesetzt werden (Flag C), nicht mehr später. Dies vereinfacht den Code etwas. Die beiden anderen Properties definieren das Aussehen des ``Cursors'': wenn man mit der Maus über das Brett fährt, kann man damit z.B. einen halbdurchscheinenden Stein anzeigen. Dann folgen zwei neue Signale, die ausgelöst werden, wenn man einen Mausknopf auf dem Brett drückt bzw. losläßt. Sie haben drei Integer-Parameter: Die Nummer des Mausknopfes und die Koordinaten des Klicks. =for latex \begin{sloppypar} Das C-Signal wird überschrieben, das C<%$self> brutal löscht. Da ich von C ableite weiß ich, daß ich kein anderes Perl-Widget in der Hierarchie über mir stören kann, da Gtk2-Widgets selbst den Perl-Hash C<%$self> nicht antasten. =for latex \end{sloppypar} Danach folgen einige Konstantendefinitionen für Steine, z.B. C oder C, die zusammenaddiert das Aussehen einer Brettposition festlegen oder die relative Größe von Steinen (schwarze Steine sind etwas größer) sowie Funktionen zum Laden von Bildern. Interessanter wird dann die C-Funktion: sub INIT_INSTANCE { my $self = shift; @::black_img or load_images; $self->double_buffered (0); $self->set (border_width => 0, shadow_type => 'none', obey_child => 0, ratio => TRAD_RATIO); $self->set(cursor_mask => MARK_B | MARK_W, cursor_value => MARK_B | MARK_GRAYED); $self->add ($self->{canvas} = new Gtk2::DrawingArea); $self->{canvas}->signal_connect ( configure_event => sub { $self->configure_event ($_[1]) }); $self->{canvas}->signal_connect ( motion_notify_event => sub { $self->motion }); $self->{canvas}->signal_connect ( leave_notify_event => sub { $self->cursor (0) }); $self->{canvas}->signal_connect ( button_press_event => sub { $self->button ("press", $_[1]) }); $self->{canvas}->signal_connect ( button_release_event => sub { $self->button ("release", $_[1]) }); $self->{canvas}->set_events ([ @{ $self->{canvas}->get_events }, 'leave-notify-mask', 'button-press-mask', 'button-release-mask', 'pointer-motion-mask', 'pointer-motion-hint-mask' ]); } Hier wird eine C eingebettet, die das eigentliche Bild darstellt. Eine C ist eine Art leeres Widget, das selbst nichts zeichnet, aber an der Größenbelegung teilnimmt und eine hervorragende Grundlage für eigene Widgets ist. Das Go-Widget verwendet einen Trick: Die Brett-Grafik befindet sich im X-Server (Windows: keine Ahnung) und wird automatisch vom Server gezeichnet, das Widget muss also keine C-Events behandeln, sondern ``nur'' zeichnen. Als Beispiel für ein Widget, das C-Events selbst behandelt, sei C genannt, das ein komplettes Widget inklusive Size-Allocation und Expose in Perl implementiert. Als nächstes die C-Funktion: sub SET_PROPERTY { my ($self, $pspec, $newval) = @_; $pspec = $pspec->get_name; $self->cursor (0) if $pspec =~ /^cursor/; $self->{$pspec} = $newval; $self->cursor (1) if $pspec =~ /^cursor/; } Sie macht das gleiche wie die normale C-Funktion, nur bei den Cursor-Attributen wird der Cursor neugezeichnet (was die Funktion C erledigt). Dann folgt der Handler für das C-Signal, das erzeugt wird, wenn sich Größe oder Position des Widgets ändern: sub configure_event { my ($self, $event) = @_; $self->{window} = $self->{canvas}->window; my $drawable = $self->{window}; $drawable->set_back_pixmap (undef, 0); $self->{idle} ||= add Glib::Idle sub { delete $self->{stack}; $self->{width} = $self->{canvas}->allocation->width; $self->{height} = $self->{canvas}->allocation->height; $self->draw_background; $self->draw_board (delete $self->{board}, 0) if $self->{board}; $self->{window}->clear_area (0, 0, $self->{width}, $self->{height}); delete $self->{idle}; # Handler lief 0; # Lösche Handler }; 1; } Sie löscht die Background-Pixmap, da sie nicht mehr gültig ist und neugezeichnet wird. Da das initiale Zeichnen des Brettes clientseitig geschieht und das Zeichnen und der Transfer zum Server einige Zeit in Anspruch nimmt, geschieht es nicht synchron zum Signal, sondern später, in einem Idle-Handler. Dieser wird aufgerufen, wenn die Hauptschleife von Gtk2 alle anstehenden Ereignisse abgearbeitet hat. Das Gdk-Fenster, das man mit C<< $self->{canvas}->window >> erhält, kann erst hier abgefragt werden und nicht schon in C, da zum Zeitpunkt von C noch keine Gdk-Fenster für die Widgets erzeugt wurden sondern erst, nachdem sie das erste mal angezeigt wurden (bzw. etwas früher, wenn sie I werden). Im Idle-Handler werden noch andere Daten invalidiert (C<< $self->{stack} >> speichert die vorberechneten Steine), die neue Größe abgefragt und der Bretthintergrund gezeichnet sowie die Steine gemalt und gesetzt. Am Ende wird dem X-Server mit C mitgeteilt, daß er die Grafik auffrischen soll. Danach folgen einige hundert Zeilen Code, der die Grafik zeichnet (und dazu im wesentlichen C benutzt). Und dann: sub do_button_press { my ($self, $button, $x, $y) = @_; } sub do_button_release { my ($self, $button, $x, $y) = @_; } Als der Code geschrieben wurde, gab es noch kein C-Feld für Signale und Signale wurden immer als Perl-Methoden aufgerufen, mit einem vorangestellten C (``weil Python das auch so macht'' :). Und abschließend noch die Methode, die die Mausklick-Signale auslöst: sub button { my ($self, $type, $event) = @_; $self->motion; if ($self->{cursorpos}) { $self->signal_emit ("button-$type", $event->button, @{$self->{cursorpos}}); } } C<< $self->motion >> bewegt den Cursor, falls das notwendig ist, und, falls die Maus über einer gültigen Brettposition stelt, wird ein Signal ausgelöst. Wenn man so wollte, könnte man von C/C++/XS aus jetzt ein neues Go-Brett erzeugen: GtkWidget *w = g_object_new ( "Gtk2__GoBoard", # ':' wird zu '_' "size", 19 ); ... und im wesentlichen so benutzen wie jedes andere Gtk2-Widget. =head3 Objekte aus anderen Sprachen nutzen Obwohl es eine aufregende Idee wäre, Python-Widgets in Perl und umgekehrt zu benutzen, ist dies eher unrealistisch: man müßte zuerst Python und Perl im selben Prozess vereinigen, was wahrscheinlich möglich ist, aber kostspielig. Deshalb beschränke ich mich auf Widgets/Objects aus C oder C++, die Vorgehensweise ist aber übertragbar auf andere Sprachen. Dazu muß man zwei Schritte durchführen: Erstens die Bibliothek oder den Code in den Prozess linken, zweitens die Cs beim Glib-Modul registrieren. Am einfachsten (bzw. portabelsten) geschieht dies mit einem kleinen XS-Modul. Möchte man z.B. den C aus der C, schreibt man ein kleines Modul (oder benutzt C und C), das man gegen die C linkt. In dessen BOOT-Section schreibt man: BOOT: gperl_register_object (GIMP_TYPE_COLOR_BUTTON, "Gimp::ColorButton"); =for latex \begin{sloppypar} Damit ist die Verbindung zwischen C und Perl hergestellt, und man kann den C sofort benutzen: =for latex \end{sloppypar} use MeinXSModul; my $button = Gimp::ColorButton->Glib::Object::new (...); Ähnlich verfährt man mit anderen Datentypen (z.B. Flags und Aufzählungen), die man registrieren möchte: gperl_register_fundamental (GDK_TYPE_CAP_STYLE, "Gtk2::Gdk::CapStyle"); Sobald sie registriert sind, kann man sie wie gewohnt von Perl aus nutzen. =head2 Verweise =over 4 =item * http://www.gtk.org/ Die Dokumentation zu Gtk+, Glib usw. =item * perldoc Glib, Glib::Object, Gtk2, Gtk2:: Das Gtk2-Modul hat für jedes Widget eine eine C-Seite, die (kurz) die Hierarchiezugehörigkeit, Methoden, Signale und Properties auflistet. =item * apt-get install devhelp-book-gtk2 # oder devhelp-books Installiert unter Debian I, einen interaktiven Help-Browser und das Gtk2-Buch (dazu gehört auch GObject, Glib-Doku uvm.). Sehr hilfreich beim Programmieren. =item * Gtk2::PodViewer, Gtk2::GoBoard, Gtk2::CV::Schnauzer Verschiedene Gtk2-Widgets in Perl. =back =head2 Danke ... an denjenigen, der Latex (a.k.a. ``the bag of dirty broken hacks'') endlich mal fixt.