Seit einigen Tagen beschäftige ich mich mit PHP-Gtk, und bin auf einige Stolpersteine gestossen, die es GUI Anfängern wie mir schwer machen, ein vernünftiges erstes Ergebnis zu erzielen. Das Problem ist, dass das Gtk Toolkit zwar sehr gut dokumentiert ist, und es gibt auch viele anschauliche Beispiele in C++, aber bei PHP läuft die Verwendung der einzelnen Gtk Elemente eben doch ein wenig anders ab, und ich habe eine gewisse Zeit gebraucht bis ich hinter die Logik des ganzen geblickt habe. Mangels entsprechend ausführlicher Tutorialseiten (vor allem in deutsch) bin ich schliesslich im IRCNet im Channel #php auf einen hilfsbereiten französischen Zeitgenossen gestossen, der mir die ersten Schritte gezeigt hat. Dieses Tutorial ist eine Bilanz meiner bisherigen noch viel zu bescheidenen Kenntnisse von PHP-Gtk - ich hoffe dass es jemandem nützt und demjenigen den steinigen Weg, den ich hatte, etwas erleichtert. Aber genug geschwafelt jetzt.
Um mich mit PHP-Gtk anzufreunden habe ich ein kleines Tool zum Betrachten und (noch sehr eingeschränkten) Administrieren einer Nachrichtendatenbank geschrieben. Obwohl dieses Skript vorerst nur zum Lernen dient ist es dennoch nicht völlig sinnfrei, da ich in meinem Unternehmen u.a. für die Verwaltung der News eines Internetportals zuständig bin. Gerade beim chronologischen durchforsten der Nachrichten hat sich phpMyAdmin als unpraktisch erwiesen, da der eigentliche Meldungsinhalt einer Nachricht viel zu lang ist um in der Tabelle, die phpMyAdmin ausgibt, vernünftig gelesen zu werden.
Damit das Skript funktioniert braucht man die aktuellste PHP Version, eine aktuelle GTK+ 1.2 (nicht 1.3) Version für Un*X Derivate, PHP-Gtk (in meinem Falle habe ich Version 0.4 verwendet) sowie MySQL. Win32 User haben es auf jeden Fall leichter, auf der PHP-Gtk Seite gibt es PHP, GTK+ und PHP-Gtk als Komplettpaket.
Ich gehe hier jetzt nicht gross auf die Installation der Komponenten ein, das haben andere schon zur Genüge getan. Auch die Installation von PHP-Gtk ist auf der Downloadseite gut beschrieben.
PHP-Gtk Skripte werden nicht im Browser aufgerufen sondern als CGI an der Konsole unter X (bzw. Eingabeaufforderung unter Win32) gestartet! PHP muss dementsprechend installiert sein.
Um eine "realistische" Umgebung zu schaffen, habe ich ein kleines PHP Shellskript geschrieben welches die benötigte MySQL-Tabelle erzeugt und mit ziemlich sinnlosem Zeug füllt.
Also, Datenbank läuft, PHP funktioniert, PHP-Gtk ist sauber installiert? Dann mal los...
Screenshot des fertigen Programmes
Erläuterung des Screenshots:
Das obere Textfeld enthält den Nachrichtentext der aktuell ausgewählten Nachricht, danach kommt eine Reihe mit 6 Buttons deren Funktion klar sein sollte, darunter ein sogenanntes "Label", das alle relevanten Informationen zur ausgewählten Nachricht anzeigt, zuletzt kommt die Liste aller Nachrichten des aktiven Tages, sortiert nach ihrer ID, unterteilt in ID, Überschrift und Rubrik.
Das komplette Listing ist im folgenden abgedruckt, es kann ausserdem unter dem unten angegebenen Link heruntergeladen werden. Eine Schritt für Schritt Besprechung des Quellcodes folgt auf der nächsten Seite.
<?php
// Startup //////////////////////////////////////////////////////
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN')
dl('php_gtk.dll');
else
dl('php_gtk.so');
// Init //////////////////////////////////////////////////////
mysql_connect("localhost", "root", "passwort");
mysql_select_db("test");
$verschiebung = 0;
// Support functions //////////////////////////////////////////////////////
function sonderzeichen($string)
{
$string = str_replace("ä", "ae", $string);
$string = str_replace("ü", "ue", $string);
$string = str_replace("ö", "oe", $string);
$string = str_replace("Ä", "Ae", $string);
$string = str_replace("Ü", "Ue", $string);
$string = str_replace("Ö", "Oe", $string);
$string = str_replace("ß", "ss", $string);
$string = str_replace("´", "", $string);
return $string;
}
// GUI init //////////////////////////////////////////////////////
$window = &new GtkWindow();
$window->connect_object('destroy', array('gtk', 'main_quit'));
$window->connect('delete-event', 'delete_event');
$window->set_usize(800, 600);
$window->set_title('GPMNA');
$window->set_name('MainWindow');
$box1 = &new GtkVBox(false, 10);
$window->add($box1);
$table = &new GtkTable(4, 2, false);
$table->set_row_spacing(0,2);
$table->set_col_spacing(0,2);
$box1->pack_start($table, TRUE, TRUE, 0);
$text = &new GtkText (NULL, NULL);
$text->set_editable(FALSE);
$text->set_word_wrap(TRUE);
$table->attach($text, 0, 1, 0, 1,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
0, 0);
$vscrollbar = &new GtkVScrollbar($text->vadj);
$table->attach($vscrollbar, 1, 2, 0, 1,
GTK_FILL,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
0, 0);
$table2 = &new GtkTable(1, 6, false);
$table2->set_col_spacing(0,0);
$table->attach($table2, 0, 1, 1, 2, 0, 0, 0);
$button = &new GtkButton('Beenden');
$button->connect_object('clicked', array('gtk', 'main_quit'));
$table2->attach($button, 0, 1, 0, 1, 0, 0, 0);
$prevday = &new GtkButton('Einen Tag zurueck');
$prevday->connect_object('clicked', 'lade_prevday');
$table2->attach($prevday, 1, 2, 0, 1, 0, 0, 0, 0);
$nextday = &new GtkButton('Einen Tag vor');
$nextday->connect_object('clicked', 'lade_nextday');
$table2->attach($nextday, 2, 3, 0, 1, 0, 0, 0, 0);
$prevweek = &new GtkButton('Eine Woche zurueck');
$prevweek->connect_object('clicked', 'lade_prevweek');
$table2->attach($prevweek, 3, 4, 0, 1, 0, 0, 0, 0);
$nextweek = &new GtkButton('Eine Woche vor');
$nextweek->connect_object('clicked', 'lade_nextweek');
$table2->attach($nextweek, 4, 5, 0, 1, 0, 0, 0, 0);
$nextweek = &new GtkButton('Loeschen');
$nextweek->connect_object('clicked', 'loesche_eintrag');
$table2->attach($nextweek, 5, 6, 0, 1, 0, 0, 0, 0);
$label = &new GtkLabel('');
$label->set_text("Bereit.");
$table->attach($label, 0, 1, 2, 3, 0, 0, 0);
$listtitles = array("ID", "Ueberschrift", "Rubrik");
$list = &new GtkCList(3, $listtitles);
$list->set_selection_mode(GTK_SELECTION_BROWSE);
$list->set_sort_column(0);
$list->set_auto_sort(TRUE);
$list->connect_object('select-row', 'list_change');
$list->set_column_width(0, 55);
$list->set_column_width(1, 570);
//$list->set_column_width(2, 100);
lade_day(0);
$scrolled_window = &new GtkScrolledWindow();
$scrolled_window->set_policy(GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
$scrolled_window->add_with_viewport($list);
$table->attach($scrolled_window, 0, 1, 3, 4,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
0, 0);
$window->show_all();
Gtk::main();
// Functions connected to Gtk objects //////////////////////////////////////////////////////
function delete_event()
{
return false;
}
function loesche_eintrag()
{
GLOBAL $list;
GLOBAL $verschiebung;
$row = $list->selection[0];
$id = $list->get_text($row, 0);
$result = mysql_query("DELETE from nachrichten WHERE id = '$id'");
lade_day($verschiebung);
return false;
}
function list_change()
{
GLOBAL $list;
GLOBAL $label;
$row = $list->selection[0];
$id = $list->get_text($row, 0);
$result = mysql_query("SELECT text, datum, zeit, rubrik
from nachrichten WHERE id = '$id'");
$a = mysql_fetch_array($result);
$string = $a["text"];
$string = sonderzeichen($string);
GLOBAL $text;
$length = $text->get_length();
$text->backward_delete($length);
$text->freeze();
$text->insert(NULL, NULL, NULL, $string, -1);
$text->thaw();
$length = $text->get_length();
$label->set_text("ID $id, Laenge $length, Datum ".$a["datum"].", ".
$a["zeit"]." Uhr, Rubrik ".$a["rubrik"]);
return false;
}
function lade_day($verschiebung)
{
GLOBAL $list;
$tag = date("Y-m-d", mktime(0, 0, 0, date("m"),
date("d") + $verschiebung,
date("Y")));
$list->clear();
$list->freeze();
$result = mysql_query("SELECT id, ueberschrift, rubrik from nachrichten
WHERE datum = '$tag'");
while ($a = mysql_fetch_array($result))
{
$a["ueberschrift"] = sonderzeichen($a["ueberschrift"]);
$thisarray = array($a["id"], $a["ueberschrift"], $a["rubrik"]);
$list->append($thisarray);
}
$list->thaw();
return false;
}
function lade_prevday()
{
GLOBAL $verschiebung;
$verschiebung--;
lade_day($verschiebung);
return false;
}
function lade_nextday()
{
GLOBAL $verschiebung;
$verschiebung++;
lade_day($verschiebung);
return false;
}
function lade_prevweek()
{
GLOBAL $verschiebung;
$verschiebung = $verschiebung - 7;
lade_day($verschiebung);
return false;
}
function lade_nextweek()
{
GLOBAL $verschiebung;
$verschiebung = $verschiebung + 7;
lade_day($verschiebung);
return false;
}
?>
Quellcode
<?php
// Startup //////////////////////////////////////////////////////
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN')
dl('php_gtk.dll');
else
dl('php_gtk.so');
Hier wird lediglich überprüft, auf welcher Plattform wir uns befinden, und dementsprechend wird die passende Bibliothek geladen. Unter Windows muss die Bibliothek offensichtlich im selben Verzeichnis wie das Skript liegen, unter Linux erwartet PHP die Bibliotheken (php_gtk.so und php_gtk.la) im Verzeichnis /usr/local/lib/php/extensions/no-debug-non-zts-20001222 obwohl ich --prefix=/usr/local bei der Installation von PHP-Gtk angegeben hatte. Egal, nachdem ich die Dateien dorthin kopiert hatte, liefs.
// Init //////////////////////////////////////////////////////
mysql_connect("localhost", "root", "passwort");
mysql_select_db("test");
$verschiebung = 0;
// Support functions //////////////////////////////////////////////////////
function sonderzeichen($string)
{
$string = str_replace("ä", "ae", $string);
$string = str_replace("ü", "ue", $string);
$string = str_replace("ö", "oe", $string);
$string = str_replace("Ä", "Ae", $string);
$string = str_replace("Ü", "Ue", $string);
$string = str_replace("Ö", "Oe", $string);
$string = str_replace("ß", "ss", $string);
$string = str_replace("´", "", $string);
return $string;
}
Bis hierher noch keinerlei Gtk. Die MySQL Verbindung ist klar, die Sonderzeichenfunktion brauchen wir, weil PHP-Gtk etwas seltsam auf deutsche Umlaute reagiert, nämlich gar nicht. Das Programm startet zwar, aber Elemente, die Umlaute enthalten, werden nicht angezeigt, oder genauer: ein Button mit dem Text "Drück mich" wird zwar als Button angezeigt, jedoch völlig ohne den Text. Hoffen wir dass dies ein Bug ist, kein Feature.
// GUI init //////////////////////////////////////////////////////
$window = &new GtkWindow();
$window->connect_object('destroy', array('gtk', 'main_quit'));
$window->set_usize(800, 600);
$window->set_title('GPMNA');
$window->set_name('MainWindow');
Zuallererst einmal wird das Fenster erschaffen. Da das Gtk Toolkit komplett objektorientiert ist, wird jedes Element auch als PHP Objekt angelegt. Als nächstes wird dieses Fensterobjekt mit einem Event verbunden. Dieses verbinden von Events mit Funktionen ist eine zentrale Grundlage des Gtk Toolkits. Man kann sich das ähnlich wie bei JavaScript vorstellen.
Wenn ich einen Link folgendermassen deklariere:
<a href="#" OnClick="irgendeinefunktion();">Blubb</a>
dann habe ich den Event OnClick (also Mausklick) mit der Funktion irgendeinefunktion() verbunden. Genau dies tut $window->connect_object. In diesem Falle verbinde ich den Event destroy (Schliessen des Fensters) mit array('gtk', 'main_quit'). Normalerweise verbindet man diese Events mit selbstgeschriebenen Funktionen. In diesem Fall löst der Aufruf der Array-Funktion wohl eine interne Gtk Funktion auf, die für das Schliessen des Fensters und das Beenden des Skripts sorgt.
Jetzt setzen wir noch ein paar Eigenschaften unseres Fensters. Die Funktion set_usize() legt die horizontale und vertikale Grösse des Fensters fest, set_title die im Fensterkopf angezeigte Überschrift und set_name den internen Namen.
$box1 = &new GtkVBox(false, 0);
$window->add($box1);
Eine weitere Grundlage des Gtk ist die Verschachtelung von Elementen, denn in dem bis jetzt angelegten Fenster könnten wir nur ein Element (z.B. einen Button anlegen). Um in unserem Fenster weitere Elemente einbauen zu können, brauchen wir erstmal eine Box. Eine Box ist ein unsichtbarer Container, in den wir weitere Elemente "packen" können, und zwar gibt es horizontale und vertikale Boxen, in denen Elemente (natürlich auch weitere Boxen) entweder von links nach rechts (bzw. oben nach unten) oder aber von rechts nach links (bzw. unten nach oben) angeordnet werden können.
Die Parameter false und 0 in unserem Beispiel geben an, das Unterelemente der Box nicht homogen angeordnet werden und einen Abstand von 0 Pixeln zueinander haben. Da unsere Box nur ein Unterelement haben wird, sind diese Werte hier sowieso unerheblich. Dann müssen wir noch dafür sorgen, dass die Box ein Unterelement unseres Fensters ist, sonst würde sie nämlich im Nirgendwo "befestigt" sein. Das erledigt der Befehl add. Erstellen wir nun das eben erwähnte Unterelement:
$table = &new GtkTable(4, 2, false);
$table->set_row_spacing(0,2);
$table->set_col_spacing(0,2);
$box1->pack_start($table, TRUE, TRUE, 0);
Wir erstellen eine Table. Dies ist wie die Box ein für sich genommen unsichtbares Containerelement, dass es (genau wie Tabellen in HTML) erlaubt, weitere Unterelemente an einem definierten Raster auszurichten. Bei der Erzeugung unserer Tabelle geben wir an, dass sie 4 Spalten und 2 Reihen erhalten soll und dass die Zellen nicht homogen gehalten werden sollen. Homogen bedeutet in diesem Falle, dass automatisch alle Zellen genau so gross sind wie die Zelle mit dem grössten Inhalt. Dann legen wir das Spacing, also den Abstand der Tabellenzellen zueinander, fest, und zwar zuerst für die Reihe 0 den Abstand 2, dann für die Spalte 0 den Abstand 2. Das macht in unserem Falle aber gerade für die Zeile wenig Sinn und soll nur die Anwendungsweise zeigen. Damit auch die Tabelle nicht im Nirgendwo hängt, packen wir sie in die zuvor erzeugte Box. Die Funktion pack_start packt ein Element an den Anfang einer Box, pack_end würde ans Ende anfügen. Dies spielt natürlich nur dann eine Rolle wenn schon Elemente in einer Box vorhanden sind.
Also, kurze Zwischenbilanz: Wir haben ein Fenster erstellt, dorthinein haben wir eine Box platziert, und in diese wiederum haben wir eine Tabelle mit 4 Zeilen und 2 Reihen gepackt. Jetzt werden wir endlich Elemente erstellen, die man auch sieht, und werden diese an der Tabelle ausrichten.
$text = &new GtkText(NULL, NULL);
$text->set_editable(FALSE);
$text->set_word_wrap(TRUE);
$table->attach($text, 0, 1, 0, 1,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
0, 0);
Wir erzeugen zunächst einmal ein Textfeld, in dem wir den Inhalt der ausgeählten Nachricht anzeigen können. Die beiden Parameter, die wir beim Erzeugen angeben, sind die horizontale und vertikale Ausrichtung der Box. Wir setzen beides auf NULL und lassen das Textfeld somit selbst über seine Ausrichtung entscheiden - in unserem Design spielt das sowieso keine Rolle. Da wir das Textfeld nur zum Anzeigen und nicht zum Bearbeiten von Text verwenden wollen, setzten wir die Eigenschaft editable auf FALSE. Damit Wörter sauber umgebrochen werden und nicht mitten in einem Wort umgebrochen wird, setzen wir das Wordwrapping des Textfeldes auf TRUE. Jetzt kommt des wichtigste: Wir teilen dem Textfeld mit, wo in der Tabelle es platziert werden soll. Da wir es in der obersten linken Ecke platzieren wollen, geben wir dem attach Befehl unserer Tabelle nach Angabe des zu platzierenden Objekts die gewünschen Koordinaten mit, in diesem Falle 0, 1, 0, 1, was bedeutet "Objekt $text soll sich von Spalte 0 bis 1 und von Zeile 0 bis 1 erstrecken", es füllt somit also genau eine Tabellenzelle aus. Die weiteren Parameter GTK_EXPAND, GTK_SHRINK und GTK_FILL geben an, ob und wie das hinzugefügte Objekt mit wächst und schrumpft, wenn die Tabellengrösse geändert wird. Ich muss gestehen dass ich hier noch nicht durch die Logik durchblicke - jedenfalls klappt es so ;). Die beiden letzten Parameter geben das Padding, also den horizontalen bzw. vertikalen Abstand des Objekts zu umgebenden Objekten an. Die Ähnlichkeit mit HTML Tabellen dürfte die Funktionsweise klarmachen.
$vscrollbar = &new GtkVScrollbar($text->vadj);
$table->attach($vscrollbar, 1, 2, 0, 1,
GTK_FILL,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
0, 0);
Da unser Textfeld von Haus aus keine Scrolleiste (für den Fall dass mehr Inhalt als sichtbare Zeilen in das Textfeld geladen wird) mitbringt, müssen wir noch ein Objekt vom Typ GtkVScrollbar erzeugen. Schon beim Erstellen des Objekts übergeben wir den vadj Parameter des Textfeldes, damit die Scrollleiste mit dem Textfeld korrekt verbunden wird. Dann wird die fertige Scrollleiste nur noch wie zuvor das Textfeld an eine sinnvolle Position rechts neben dem Textfeld platziert, sie soll sich also von Spalte 1 bis 2 und (genau wie das Textfeld) von Zeile 0 bis 1 erstrecken. Obwohl das ja die klassische Position einer Scrolleiste ist, ist sie keinesfalls vorgeschrieben. Die Leiste könnte sich an völlig anderer Position optisch losgelöst vom Textfeld befinden, und trotzdem könnte man das Textfeld weiterhin scrollen - wahrscheinlich würde sich die Bedienung dann aber sehr seltsam "anfühlen". Nun zur Buttonleiste:
$table2 = &new GtkTable(1, 6, false);
$table->attach($table2, 0, 1, 1, 2, 0, 0, 0);
Genau, erstmal erstellen wir wieder eine Tabelle, um die Buttons sauber nebeneinander platzieren zu können. Das lässt sich wahrscheinlich etwas einfacher auch mit einer horizontalen GtkBox realisieren, aber ehrlichgesagt ist mir das erst jetzt eingefallen, und diese Vorgehensweise zeigt die Verschachtelung einer Tabelle in einer anderen, also warum nicht (ausserdem: wenn ich mir die Arbeit schon gemacht habe, werde ich sie euch nicht vorenthalten ;). Jedenfalls genau die selbe Vorgehensweise wie beim Erstellen der ersten Tabelle: Objekt erschaffen (auf Spacing verzichten wir hier) und an gewünschter Position der ersten Tabelle einfügen, und zwar von Spalte 0 bis Spalte 1 und Zeile 1 bis Zeile 2.
Jetzt können wir die Buttons in die zweite Tabelle einfügen.
$beenden = &new GtkButton('Beenden');
$beenden->connect_object('clicked', array('gtk', 'main_quit'));
$table2->attach($beenden, 0, 1, 0, 1, 0, 0, 0);
Ich erkläre hier nur den ersten Button, die anderen werden genauso eingebaut. Erstmal wird das GtkButton wieder erschaffen, dann mit einer Funktion verbunden (hier wieder dieses seltsame Array welches das Programm beendet), und zwar über den Event clicked (= Button wurde gedrückt), und schliesslich an geeigneter Stelle in der zweiten Tabelle positioniert (Von Zeile 0 bis 1 und Spalte 0 bis 1).
Die anderen Buttons werden jetzt nach dem selben Muster hinzugefügt und jeweils eine Spalte rechts vom Vorgänger platziert:
$prevday = &new GtkButton('Einen Tag zurueck');
$prevday->connect_object('clicked', 'lade_prevday');
$table2->attach($prevday, 1, 2, 0, 1, 0, 0, 0, 0);
$nextday = &new GtkButton('Einen Tag vor');
$nextday->connect_object('clicked', 'lade_nextday');
$table2->attach($nextday, 2, 3, 0, 1, 0, 0, 0, 0);
$prevweek = &new GtkButton('Eine Woche zurueck');
$prevweek->connect_object('clicked', 'lade_prevweek');
$table2->attach($prevweek, 3, 4, 0, 1, 0, 0, 0, 0);
$nextweek = &new GtkButton('Eine Woche vor');
$nextweek->connect_object('clicked', 'lade_nextweek');
$table2->attach($nextweek, 4, 5, 0, 1, 0, 0, 0, 0);
$delete = &new GtkButton('Loeschen');
$delete->connect_object('clicked', 'loesche_eintrag');
$table2->attach($delete, 5, 6, 0, 1, 0, 0, 0, 0);
Die Funktionen, mit denen die einzelnen Buttons hier connected werden, werden wir weiter unten schreiben.
Jetzt fügen wir noch ein Label (das ist ein Darstellungsfeld für wenig Textinhalt) unterhalb der Tabelle, die die Buttons beinhaltet, ein, um dort später einige Informationen über die aktuell ausgewählte Nachricht anzeigen zu können:
$label = &new GtkLabel('');
$label->set_text("Bereit.");
$table->attach($label, 0, 1, 2, 3, 0, 0, 0);
Nach dem Erzeugen des Objekts weisen wir dem Label noch einen ersten Textinhalt mit der Funktion set_text zu, dann fügen wir es in die dritte Reihe der Tabelle ein - jetzt wieder die erste Tabelle, die zweite war nur für die Anordnung der Buttons gedacht.
Kommen wir jetzt zu der Liste, die die verschiedenen Newsbeiträge anzeigen wird. Das Objekt GtkCList ist schon etwas komplexer, wir gehen die Erzeugung deshalb Schritt für Schritt durch:
$listtitles = array("ID", "Ueberschrift", "Rubrik");
Eine GtkCList ist ja eigentlich eine Tabelle. Wir legen diese dreispaltig an, und um die Spalten benennen zu können, muss zunächst ein Array mit den drei Spaltennamen angelegt werden.
$list = &new GtkCList(3, $listtitles);
Jetzt erzeugen wir die Liste, geben an dass sie 3 Spalten haben soll, und übergeben das soeben erzeugte Array, aus dem dann die Spaltentitel erzeugt werden.
$list->set_selection_mode(GTK_SELECTION_BROWSE);
Hier legen wir den Auswahlmodus (selection_mode) der Liste fest. Es gibt 4 Modi: GTK_SELECTION_SINGLE, GTK_SELECTION_BROWSE, GTK_SELECTION_MULTIPLE und GTK_SELECTION_EXTENDED. Bei den ersten beiden Modi kann immer nur eine Zeile der Liste gleichzeitig gewählt sein, wobei bei GTK_SELECTION_SINGLE nur ein "vollständiger" Mausklick auf eine Zeile diese auch auswählt, während man bei GTK_SELECTION_BROWSE mit gedrückter Maustaste durch die Liste "surfen" kann und immer die Zeile ausgewählt wird, in der sich der Mauszeiger gerade befindet. Bei den beiden anderen Modi kann mehr als ein Element auf einmal angewählten werden, wobei man bei GTK_SELECTION_EXTENDED mehrere Elemente mit der weit verbreiteten Methode auswählt, nämlich in dem man mittels Shift-Taste ganze Bereiche und mittels Strg-Taste einzelne Einträge der Auswahl hinzufügt. Bei der Verwendung von GTK_SELECTION_MULTIPLE dagegen fügt schon ein einfacher Klick auf ein Element dieses der Auswahl hinzu, ein Klick auf ein schon ausgewähltes Element entfernt es wieder - eher nervig als sinnvoll.
Wir entscheiden uns hier für GTK_SELECTION_BROWSE, da diese Methode die schnellste und angenehmste darstellt, um die Liste zu durchforsten.
$list->set_sort_column(0);
$list->set_auto_sort(TRUE);
Nun sorgen wir noch dafür, dass wir immer eine sauber sortierte Liste haben. Mit dem Befehl set_sort_column legen wir fest, dass die Spalte, an der sich die Sortierung orientieren soll, die erste (also Spalte 0) sein soll, das ist die mit den IDs, und weiterhin legen wir fest, dass die Liste jedesmal, wenn ein Element hinzugefügt wird, automatisch nachsortiert werden soll.
$list->set_column_width(0, 55);
$list->set_column_width(1, 570);
Hier passen wir die Breite der einzelnen Spalten sinnvoll an. Der erste Parameter von set_column_width gibt die zu verändernde Spalte an, der zweite Parameter den Wert der gewünschten Breite in Pixeln. Die dritte Spalte muss hier nicht angepasst werden, denn deren Wert ergibt sich nach Festlegung der beiden anderen von selbst.
$list->connect_object('select-row', 'list_change');
Schlussendlich verbinden wir den Event select-row (Eine Zeile wird ausgewählt) mit der Funktion list_change (die wir gleich definieren werden, versprochen).
Jetzt ist die Liste eigentlich fertig, aber auch das GtkCList Objekt bringt von Haus aus leider keine Scrollleiste mit. Deshalb müssen wir (anders als bei der Textbox!) unser Listenobjekt in ein GtkScrolledWindow einfügen. Der Begriff Window ist etwas verwirrend, es handelt sich hier nicht um ein regelrechtes Programmfenster wie wir es zu Beginn erstellt haben, sondern um ein unsichtbares Feld mit einer Scrolleiste. Wenn die Elemente, die man diesem Feld hinzufügt, grösser als das Feld selbst sind, kann man die Scrolleiste benutzen, um, naja, halt zu scrollen.
Wir erstellen also das GtkScrolledWindow:
$scrolled_window = &new GtkScrolledWindow();
$scrolled_window->set_policy(GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
$scrolled_window->add_with_viewport($list);
Über die Funktion set_policy legen wir fest, dass die horizontale bzw. vertikale Scrolleiste bei Bedarf automatisch erscheint. Es gibt noch GTK_POLICY_ALWAYS und GTK_POLICY_NEVER - die Namen sprechen für sich.
Im letzten Schritt fügen wir unsere Liste dem GtkScrolledWindow via add_with_viewport hinzu.
Jetzt muss noch dieses Window mit der enthaltenen Liste in unsere Tabelle eingefügt werden:
$table->attach($scrolled_window, 0, 1, 3, 4,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
GTK_EXPAND | GTK_SHRINK | GTK_FILL,
0, 0);
So, an dieser Stelle steht unser Gtk Gerüst eigentlich. Jetzt müssen wir noch definieren dass alle Elemente in unserem Programmfenster auch angezeigt werden:
$window->show_all();
...und die Main Routine der Gtk Bibliothek starten:
Gtk::main();
?>
Und schon haben wir eine lauffähige PHP-Gtk Anwendung. Die einzelnen Funktionen, die aufgerufen werden, wenn z.B. ein Button gedrückt wird, sind zwar noch nicht vorhanden, trotzdem kann man das Skript schon mittels
# /usr/local/bin/php -q /pfad/zum/gpnma-skript.php
starten. Wenn man einen Event auslöst, z.B. durch drücken eines Buttons, wird PHP lediglich an der Konsole einen Fehler ausgeben.
Im zweiten Teil des Walthroughs werden wir nun die Funktionen definieren, die die einzelnen Events der Gtk-Elemente abfangen.
Mit Gtk Objekten verknüpfte Funktionen sind ganz 'normale' PHP Funktionen. Hier zum Beispiel die nicht wirklich aufregende Funktion, die wir vorhin mit dem delete Event des Hauptfensters verbunden haben:
// Functions connected to Gtk objects ///
function delete_event()
{
return false;
}
Ok, kommen wir zu einer etwas anspruchsvolleren Funktion, nämlich der, die wir mit dem Button Loeschen verbunden haben und die nach drücken des Buttons die aktuell ausgewählte Nachricht aus der Datenbank entfernen soll:
function loesche_eintrag()
{
GLOBAL $list;
GLOBAL $verschiebung;
$row = $list->selection[0];
$id = $list->get_text($row, 0);
$result = mysql_query("DELETE from nachrichten WHERE id = '$id'");
lade_day($verschiebung);
return false;
}
Um den richtigen Eintrag zu löschen, brauchen wir die ID der aktuell in der Liste ausgewählten Nachricht. Die ID steht jeweils in der ersten Spalte der Liste und kann von dort ausgelesen werden. Um in dieser Funktion darauf zugreifen zu können, müssen wir unsere Liste der Funktion durch GLOBAL erst bekanntmachen, ausserdem globalisieren wir auch noch die Variable $verschiebung - was es damit auf sich hat wird später klar. Ersteinmal finden wir heraus, welche Zeile in der Liste momentan ausgewählt ist, indem wir den ersten Wert des Arrays selection des Objekts $list auslesen. selection ist deshalb ein Array, weil bei einigen Listentypen auch mehrere Zeilen gleichzeitig ausgewählt sein können und die Nummern der ausgewählten Zeilen in diesem Array gespeichert werden. Da in unserer Liste immer nur eine Zeile gleichzeitig ausgewählt sein kann, muss der Wert an Position 0 im Array gespeichert sein. Den Wert der ID bekommen wir nun, indem wir die Funktion get_text unter Angabe der Reihe (die wir eben erfahren haben) und der Spalte (da es sich um die erste Spalte handelt hier 0) aufrufen - somit bekommen wir den Inhalt der durch dieses Koordinatenpaar definierten Zelle zurückgeliefert, eben unsere ID. Damit können wir dann den entsprechenden MySQL Aufruf ausführen, um diesen Eintrag zu löschen.
Damit unsere Liste jetzt den gelöschten Eintrag nicht mehr anzeigt, müssen die Einträge der Datenbank neu eingelesen werden, dass erledigt der lade_day Aufruf - dazu später wie gesagt mehr.
Schreiben wir jetzt eine Funktion die den Fall abfängt, dass eine Zeile in der Liste ausgewählt wird:
function list_change()
{
GLOBAL $list;
GLOBAL $label;
$row = $list->selection[0];
$id = $list->get_text($row, 0);
$result = mysql_query("SELECT text, datum, zeit, rubrik
from nachrichten WHERE id = '$id'");
$a = mysql_fetch_array($result);
$string = $a["text"];
$string = sonderzeichen($string);
GLOBAL $text;
$length = $text->get_length();
$text->backward_delete($length);
$text->freeze();
$text->insert(NULL, NULL, NULL, $string, -1);
$text->thaw();
$length = $text->get_length();
$label->set_text("ID $id, Laenge $length, Datum ".$a["datum"].", ".
$a["zeit"]." Uhr, Rubrik ".$a["rubrik"]);
return false;
}
Zuerst wieder das selbe Spiel wie eben: Die Elemente der Benutzeroberfläche, die wir verändern oder auslesen wollen, müssen der Funktion durch GLOBAL bekannt gemacht werden - in diesem Fall die Liste, das Label und das Textfeld (weiter unten). Dann holen wir uns genau wie eben die ID der aktuell ausgewählten Nachricht mit $list->selection und $list->get_text. Diese ID brauchen wir dann, um einige Werte aus der Datenbank auszulesen: den Nachrichtentext, das Datum der Nachricht, die Zeit und die Rubrik. Damit wie eingangs erwähnt Gtk keine Probleme mit Umlauten macht, filtern wir den Text der Nachricht durch unsere sonderzeichen Funktion.
Zunächst wollen wir den Text der Nachricht im grossen Textfenster anzeigen lassen. Da ja unter Umständen schon ein Text dort angezeigt wird, müssen wir das Nachrichtenfeld ersteinmal löschen. Das geht leider nicht mit einem einzigen Befehl. Wir müssen erst die Anzahl Zeichen auslesen, die momentan im Feld geladen sind (über den Befehl get_length), und dann mit dem Befehl backward_delete genau diese Anzahl Zeichen von der aktuellen Position im Text rückwärts löschen. Da unser Textfeld nicht editierbar ist, kann die aktuelle Position im Text (die Position eines sozusagen imaginären Zeigers, der, wenn das Textfeld editierbar wäre, gar nicht mehr so imaginär, sondern eher schwarz, senkrecht und blickend wäre) immer nur am Ende liegen. Wir löschen also auf jeden Fall alle Zeichen.
Jetzt werden wir den neuen Text in das Feld laden. Bevor wir das tun, werden wir das Textfeld 'einfrieren'. Das ist nicht unbedingt erforderlich, auf jeden Fall aber sauberer: Wenn ein Element eingefroren ist, werden Änderungen erst wieder sichtbar, wenn es mittels thaw() wieder freigegeben wird. Das ist sinnvoll wenn mehrere komplexe Änderungen an einem Element vorgenommen werden, damit der User kein 'flackern' des Elements bei jeder Änderungen sieht. Das einfrieren eines Elements geschieht mittels Aufruf der Funktion freeze. Nicht jedes Element kann eingefroren werden.
Um dann den neuen Nachrichtentext (gespeichert in $string) in das Feld einzufügen benutzt man die Funktion insert. insert erwartet 5 Parameter: den zu verwendenden Font, die Schriftfarbe, die Hintergrundfarbe der Schrift, den Textstring und die Anzahl der Zeichen die vom Textstring verwendet werden sollen. Font und Schriftfarben interessieren hier ersteinmal nicht, deshalb setzen wir diese Parameter alle auf NULL. Wir übergeben als vierten Parameter den Text der Nachricht, bei der Anzahl der Zeichen übergeben wir einfach -1, damit der komplette String übernommen wird. Nachdem das Textfeld gefüllt wurde können wir es jetzt mittels $text->thaw() wieder freigeben.
Jetzt muss noch das Labelfeld geupdated werden. Zuerst holen wir uns noch die Anzahl der momentan im Textfeld geladenen Zeichen mittels get_length(), dann benutzen wir die Funktion set_text des Labelobjekts, um unseren neuen Informationsstring (der den alten ersetzt) in das Label zu schreiben.
Wir schliessen die Funktion wie schon zuvor mit return false ab.
Kommen wir jetzt zu der Funktion, die unsere Liste mit den IDs, Überschriften und Rubriken der Nachrichten des gewählten Tages füllt:
function lade_day($verschiebung)
{
GLOBAL $list;
$tag = date("Y-m-d", mktime(0, 0, 0, date("m"),
date("d") + $verschiebung,
date("Y")));
$list->clear();
$list->freeze();
$result = mysql_query("SELECT id, ueberschrift, rubrik from nachrichten
WHERE datum = '$tag'");
while ($a = mysql_fetch_array($result))
{
$a["ueberschrift"] = sonderzeichen($a["ueberschrift"]);
$thisarray = array($a["id"], $a["ueberschrift"], $a["rubrik"]);
$list->append($thisarray);
}
$list->thaw();
return false;
}
Da wir die Liste verändern müssen, machen wir sie mittels GLOBAL unserer Funktion bekannt. Dann finden wir das Datum heraus, dass wir uns anzeigen lassen wollen, ausgehend vom heutigen Datum abzüglich oder zuzüglich der $verschiebung (welche mittels der Buttons Einen Tag zurueck, Einen Tag vor, Eine Woche zurueck und Eine Woche vor eingestellt wird). Wir löschen die Liste und frieren sie ein, dann holen wir aus der Datenbank die Datensätze des berechneten Tages.
Wir arbeiten dann die Datensätze mittels mysql_fetch_array Zeile für Zeile durch. In der Schleife ersetzen wir in der Überschrift sicherheitshalber die Sonderzeichen, dann erzeugen wir ein Array aus drei Elementen (ID, Überschrift, Rubrik) und hängen dieses Array mittels append an die Liste an. Nachdem die Liste auf diese Weise gefüllt wurde, geben wir sie via thaw wieder frei.
Jetzt fehlen nur noch unsere restlichen Buttons. Ein Klick auf einen der Buttons Einen Tag zurueck, Einen Tag vor, Eine Woche zurueck oder Eine Woche vor muss nur die $verschiebung des Datums um 1 bzw. 7 erhöhen bzw. vermindern und die Funktion lade_day aufrufen, damit die aktuellen Datensätze in die Liste geschrieben werden:
function lade_prevday()
{
GLOBAL $verschiebung;
$verschiebung--;
lade_day($verschiebung);
return false;
}
function lade_nextday()
{
GLOBAL $verschiebung;
$verschiebung++;
lade_day($verschiebung);
return false;
}
function lade_prevweek()
{
GLOBAL $verschiebung;
$verschiebung = $verschiebung - 7;
lade_day($verschiebung);
return false;
}
function lade_nextweek()
{
GLOBAL $verschiebung;
$verschiebung = $verschiebung + 7;
lade_day($verschiebung);
return false;
}
Mit dem Aufruf
# /usr/local/bin/php -q /pfad/zum/gpnma-skript.php
unter X wird das Skript gestartet.
Ich habe das Skript nach bestem Wissen und Gewissen gestestet - sollte sich dennoch irgendwo der Fehlerteufel eingeschlichen haben, würde ich mich über eine entsprechende eMail freuen. Auch Lob und Kritik sind willkommen, und ich bin gerne bereit auch weiterführende Fragen zu beantworten - allerdings entdecke ich PHP-Gtk auch gerade erst und kann nicht versprechen, jede Frage beantworten zu können. Meine eMail-Adresse lautet manuel@kiessling.net, der Link zu meiner Homepage steht unten.
Homepage des Autors
Dokumentation von GTK
Homepage von PHP-GTK
Deutsche PHP-GTK Seite
GTK Download Seite
Shellskript zum anlegen und füllen der MySQL Tabelle
|