GTK mit Builder: Pixbuf

Aus Wikibooks
Zur Navigation springen Zur Suche springen

Bilder darstellen und malen mit Pixbufs[Bearbeiten]

Pixbufs sind Speicherbereiche, die ein Bild enthalten plus einiger zusätzlicher Informationen. Bilddaten können mit den dazugehörigen Funktionen geladen und gespeichert werden, außerdem hat man direkten Zugriff auf den Bildspeicher, den man so Bit für Bit manipulieren kann. In Client-Server-Grafikanwendungen repräsentieren Pixbufs die clientseitigen Grafiken, die nicht übertragen werden.

Bilder laden[Bearbeiten]

Die folgende Anwendung zeigt, wie man Bilder mit Hilfe von Pixbufs lädt. Ein Bild wird in einen scrollbaren Bereich geladen, so dass unter Umständen nicht sofort das gesamte Bild zu sehen ist. Mit einem Knopf kann ein Bild über einen Dateidialog ausgewählt werden. Es wird dabei sicher gestellt, dass im Dateidialog nur Bilddateien angezeigt werden.

Hier zunächst die XML-Beschreibung:

Align=none
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <object class="GtkWindow" id="hauptfenster" >
    <signal name="destroy" handler="gtk_main_quit"/>
    <child>
        <object class="GtkVBox" id="vbox-layout">
        <property name="homogeneous">FALSE</property>
        <child>
            <object class="GtkScrolledWindow" id="scrollfenster-1">
            <property name="hscrollbar-policy">GTK_POLICY_ALWAYS</property>
            <property name="vscrollbar-policy">GTK_POLICY_ALWAYS</property>
            <child>
                <object class="GtkViewport" id="viewport-1">
                <child>
                    <object class="GtkImage" id="bildbereich-1" />
                </child>
                </object>
            </child>
            </object>
        </child>
        <child>
            <object class="GtkHBox" id="hbox-layout-1">
            <property name="homogeneous">TRUE</property>
            <child>
                <object class="GtkButton" id="datei-laden-knopf">
                <property name="label">gtk-floppy</property>
                <property name="use-stock">TRUE</property>
                <signal name="clicked" handler="datei_laden" />
                </object>
                <packing>
                    <property name="expand">FALSE</property>
                    <property name="fill">FALSE</property>
                </packing>
            </child>
            <child>
                <object class="GtkLabel" id="mein-label-1">
                <property name="label">Bildbetrachter</property>
                </object>
                <packing>
                    <property name="expand">FALSE</property>
                    <property name="fill">FALSE</property>
                </packing>
            </child>
            </object>
            <packing>
                <property name="expand">FALSE</property>
                <property name="fill">FALSE</property>
            </packing>
        </child>
        </object>
    </child>
    </object>
</interface>

In ein scrollbaren Fenster vom Typ GtkScrolledWindow, das je einen horizontalen und vertikalen Schieberegler hat, wird indirekt ein Bildbereich vom Typ GtkImage eingefügt. Indirekt deswegen, weil dieser Bildbereich selbst nicht über die Eigenschaft verfügt, scrollbar zu sein. Als Verbindungsstück zwischen den beiden Widgets dient der Adapter vom Typ GtkViewport, der enthaltenen Widgets die Fähigkeit verleiht, scrollbar zu sein.

Die Programmdatei hierzu:

C
#include <gtk/gtk.h>

typedef struct _CallbackObjekt
{
    GtkBuilder *builder;
    GtkImage *bild;
    GtkLabel *textfeld;
} CallbackObjekt;

void datei_laden (GtkWidget *button, CallbackObjekt *obj)
{
    GtkWidget *dateidialog;
    GtkFileFilter *filter;
    /* Nur Bildformate in der Dateiauswahl anzeigen */
    filter = gtk_file_filter_new ();
    gtk_file_filter_add_pixbuf_formats (filter);
    gtk_file_filter_set_name (filter, "Bildformate");
    /* Dialog öffnen, um Datei auszuwählen */
    dateidialog = gtk_file_chooser_dialog_new ("Bild öffnen",
        NULL, 
        GTK_FILE_CHOOSER_ACTION_OPEN,
        GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
        GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
        NULL);
    /* Filter einbringen */
    gtk_file_chooser_add_filter (GTK_FILE_CHOOSER(dateidialog), filter);
    if (gtk_dialog_run (GTK_DIALOG (dateidialog)) == GTK_RESPONSE_ACCEPT)
    {
        gchar *dateiname;
        dateiname = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dateidialog));
        /* Bild anzeigen */
        gtk_image_set_from_file (obj->bild, dateiname);
        gtk_label_set_text (obj->textfeld, dateiname);
        g_free (dateiname);
    }
    gtk_widget_destroy (dateidialog);
}

int main (int argc, char *argv[])
{
    CallbackObjekt *callobj;
    GError *errors = NULL;
    GtkWidget *window;
    
    gtk_init (&argc, &argv);
    callobj = g_malloc (sizeof(CallbackObjekt));
    callobj->builder = gtk_builder_new ();
    gtk_builder_add_from_file (callobj->builder, "pixbuf1.xml", &errors);
    gtk_builder_connect_signals (callobj->builder, callobj);
    window = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "hauptfenster"));
    callobj->bild = GTK_IMAGE(gtk_builder_get_object (callobj->builder, "bildbereich-1"));
    callobj->textfeld = GTK_LABEL(gtk_builder_get_object (callobj->builder, "mein-label-1"));
    gtk_widget_set_size_request (window, 200, 200);
    gtk_widget_show_all (window);
    gtk_main ();
    g_free (callobj);
    return 0;
}

In der Callback-Funktion datei_laden() wird zunächst ein Filter erzeugt, der alle Dateien berücksichtigt, die Pixbuf laden kann (gtk_file_filter_add_pixbuf_formats()). Diesem Filter wird ein Name zugewiesen, so dass man ihn im Dateidialog erkennen kann. Anschließend wird der eigentliche Dateidialog mit gtk_file_chooser_dialog_new() erzeugt. Dieser soll zwei Knöpfe haben, einen „Öffnen“-Knopf mit einem eingebetteten Piktogramm und einen „Abbrechen“-Knopf. Drückt der Anwender nach der Auswahl auf den Öffnen-Knopf, so soll GTK_RESPONSE_ACCEPT als Wert von gtk_dialog_run() zurückgegeben werden. Im anderen Fall wird GTK_RESPONSE_CANCEL zurückgegeben. Der Filter wird mit dem Dialog über den Aufruf von gtk_file_chooser_add_filter() verknüpft.

Bestätigt nun der Anwender den Dialog, wird der Dateiname aus dem Dialog ermittelt und das Bild mit gtk_image_set_from_file() anhand dieses Dateinamens geladen. In diesem GtkImage steckt nun auch unser eigentliches Pixbuf. In der Textzeile soll noch der Dateiname angezeigt werden, dann wird der Dialog wieder gelöscht.

Jetzt haben wir einen universellen Leser für bekannte Dateiformate.

Bildbearbeitung[Bearbeiten]

Mit Pixbufs hat man sofort die Möglichkeit an der Hand, Bilder zu Bearbeiten. Die Funktionen rund um Pixbufs stellen Ihnen allerdings keine Grafikprimitiven zum Zeichnen von Linien, Kreisen und dergleichen zur Verfügung. Allerdings haben Sie die Möglichkeit, jedes einzelne Pixel zu setzen und Pixbufs mit Cairo-Funktionen zu bearbeiten. Wir stellen Ihnen in dem folgenden Beispiel eine Anwendung vor, bei der Sie selbst Grafiken Pixel für Pixel in einer frei wählbaren Farbe malen können. Den Farbauswahldialog erreichen Sie über den Mausknopf drei, das ist zumeist die rechte Maustaste. Malen können sie mit der linken Maustaste.

Hier zunächst die XML-Beschreibung:

Align=none
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <object class="GtkWindow" id="hauptfenster" >
    <signal name="destroy" handler="gtk_main_quit"/>
    <child>
        <object class="GtkVBox" id="vbox-layout">
        <property name="homogeneous">FALSE</property>
        <child>
            <object class="GtkDrawingArea" id="malbereich-1" >
            <signal name="draw" handler="neu_malen" />
            <signal name="configure-event" handler="neu_konfigurieren" />
            <signal name="motion-notify-event" handler="maus_bewegt_sich" />
            <signal name="button-press-event" handler="mausknopf_gedrueckt" />
            </object>
        </child>
        <child>
            <object class="GtkLabel" id="mein-label-1">
            <property name="label">Malerei</property>
            </object>
            <packing>
                <property name="expand">FALSE</property>
                <property name="fill">FALSE</property>
            </packing>
        </child>
        </object>
    </child>
    </object>
</interface>

Diese Beschreibungsdatei enthält eine Malfläche vom Typ GtkDrawingArea. Die Malfläche reagiert auf vier Signale, die Aufforderung zum Neuzeichnen („draw“), Veränderung der Größe („configure-event“), Mausbewegungen („motion-notify-event“) und Maustastendrücke („button-press-event“). Die zwei letzten Signale müssen im Programm explizit freigeschaltet werden.

Hier das Programm:

C
#include <gtk/gtk.h>

typedef struct _CallbackObjekt
{
    GtkBuilder *builder;
    guchar *daten;
    GdkPixbuf *pixbuf;
    GtkLabel *textfeld;
    gint zeilenlaenge;
    guchar rot, gruen, blau;
} CallbackObjekt;

static void male_pixel (CallbackObjekt *obj, gint x, gint y)
{
    if (!obj->daten)
        return;
    if (x < 0)
        return;
    if (y < 0)
        return;
    obj->daten[y * obj->zeilenlaenge + 3 * x] = obj->rot;
    obj->daten[y * obj->zeilenlaenge + 3 * x + 1] = obj->gruen;
    obj->daten[y * obj->zeilenlaenge + 3 * x + 2] = obj->blau;
}

gboolean neu_malen (GtkWidget *malflaeche, cairo_t *cr, CallbackObjekt *obj)
{
    gdk_cairo_set_source_pixbuf (cr, obj->pixbuf, 0, 0);
    cairo_paint (cr);
    return TRUE;
}

gboolean neu_konfigurieren (GtkWidget *malflaeche,
	GdkEventConfigure *ereignis, CallbackObjekt *obj)
{
    gint breite, hoehe;
    breite = ereignis->width;
    hoehe = ereignis->height;
    obj->zeilenlaenge = 3 * breite;
    if (obj->pixbuf)
        g_object_unref (obj->pixbuf);
    if (obj->daten)
        g_free (obj->daten);
    /* Speicher für RGB-Daten (3 Bytes pro Pixel) anfordern */
    obj->daten = g_malloc (obj->zeilenlaenge * hoehe);
    obj->pixbuf = gdk_pixbuf_new_from_data (obj->daten, GDK_COLORSPACE_RGB, FALSE, 8, breite, hoehe, obj->zeilenlaenge, NULL, NULL);
    return FALSE;
}

gboolean maus_bewegt_sich (GtkWidget *malflaeche, GdkEventMotion *ereignis,
	CallbackObjekt *obj)
{
    gint x, y;
    gchar statustext[128];
    x = (gint) ereignis->x;
    y = (gint) ereignis->y;

    if (ereignis->state & GDK_BUTTON1_MASK)
    {
        male_pixel (obj, x, y);
        gtk_widget_queue_draw (malflaeche);
    } 
    
    g_snprintf (statustext, 128, "X: %d, Y: %d", x, y);
    gtk_label_set_text (obj->textfeld, statustext);
    return TRUE;
}

gboolean mausknopf_gedrueckt (GtkWidget *malflaeche, GdkEventButton *ereignis,
	CallbackObjekt *obj)
{
    gint x, y;
    x = (gint) ereignis->x;
    y = (gint) ereignis->y;
    if (ereignis->button == 1)
    {
        /* Pixel malen */
        male_pixel (obj, x, y);
        gtk_widget_queue_draw (malflaeche);
    }
    else if (ereignis->button == 3)
    {
        /* Farbdialog aufrufen, Farbe neu setzen */
        GtkWidget *dialog;
        GtkColorSelection *farbwaehler;
        GdkColor farbe;
        farbe.pixel = 0;
        farbe.red = obj->rot * 65535 / 255;
        farbe.green = obj->gruen * 65535 / 255;
        farbe.blue = obj->blau * 65535 / 255;
        dialog = gtk_color_selection_dialog_new ("Wählen Sie bitte eine Farbe aus");
        farbwaehler = GTK_COLOR_SELECTION(gtk_color_selection_dialog_get_color_selection(GTK_COLOR_SELECTION_DIALOG(dialog)));
        gtk_color_selection_set_current_color (farbwaehler, &farbe);
        gtk_dialog_run (GTK_DIALOG(dialog));
        gtk_color_selection_get_current_color (farbwaehler, &farbe);
        gtk_widget_destroy (dialog);
        obj->rot = 255 * farbe.red / 65535;
        obj->gruen = 255 * farbe.green / 65535;
        obj->blau = 255 * farbe.blue / 65535;
    }
    return TRUE;
}


int main (int argc, char *argv[])
{
    CallbackObjekt *callobj;
    GError *errors = NULL;
    GtkWidget *window, *malflaeche;
    
    gtk_init (&argc, &argv);
    callobj = g_malloc (sizeof(CallbackObjekt));
    callobj->daten = NULL;
    callobj->pixbuf = NULL;
    callobj->builder = gtk_builder_new ();
    callobj->rot = 255;
    callobj->gruen = 100;
    callobj->blau = 64;
    gtk_builder_add_from_file (callobj->builder, "pixbuf2.xml", &errors);
    gtk_builder_connect_signals (callobj->builder, callobj);
    callobj->textfeld =  GTK_LABEL(gtk_builder_get_object (callobj->builder, "mein-label-1"));
    window = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "hauptfenster"));
    gtk_widget_set_size_request (window, 200, 200);
    malflaeche = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "malbereich-1"));
    gtk_widget_add_events (malflaeche, GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK);
    gtk_widget_show_all (window);
    gtk_main ();
    g_free (callobj);
    return 0;
}

Pixbufs selber enthalten die Bilddaten als Folgen von vorzeichenlosen Bytes. Die einzelnen Pixel haben dabei einen roten, grünen und blauen Anteil, der jeweils ein Byte ausmacht. Ein Grafikpixel besteht also aus drei aufeinander folgenden Bytes. Danach folgen wieder drei Bytes für das nächste Pixel und so fort, bis eine Zeile voll ist. Die Zeilenlänge, also die Anzahl der Bytes pro Farbzeile, ist dabei 3 * Anzahl der Pixel bei 24 Bit Farbtiefe, wie man es für ein RGB-Bild benötigt. Hier zwei Beispiele, um Pixelmanipulationen vorzunehmen. Das Vorgehen ist in der Funktion male_pixel() implementiert:

  • Möchte man das nullte Pixel verändern, dann schreibt man an die Stelle Null den Wert für Rot, an die Stelle Eins den Wert für Grün und an die Stelle Zwei den Wert für Blau.
  • Möchte man das zweite Pixel der dritten Zeile verändern, dann nimmt man im Datenarray 3 * Zeilenlänge + 2 * 3, und schreibt ab dort 3 Bytes mit den RGB-Farbinformationen hinein.

Die Callback-Funktion neu_konfigurieren() wird unter anderem aufgerufen, wenn die Größe der Malfläche verändert wird. In diesem Fall erzeugen wir einen neuen Datenarray, der so groß ist, dass er die gesamte zur Verfügung stehende Malfläche ausfüllen kann. Falls es schon ein Datenarray gibt, wird es gelöscht, um etwas Speicher zu sparen. Anschließend wird mit gdk_pixbuf_new_from_data() mit diesem Datenarray ein neues Pixbuf-Objekt erzeugt. Diese Funktion erwartet das Datenarray, den Farbraum, wobei hier zur Zeit als einzige Auswahl der angegebene RGB-Farbraum möglich ist, eine Angabe darüber, ob ein transparenter Bereich zur Verfügung steht, die Anzahl der Bits pro Farbelement, Breite und Höhe des Bildes, und die Zeilenlänge. Die letzten beiden Parameter können Sie mit einer Funktion füllen, die aufgerufen wird, wenn das Pixbuf gelöscht wird sowie Daten, die diese Funktion zusätzlich erhält. Jetzt haben wir ein Datenarray, in das wir hineinschreiben können und ein Pixbuf, das wir anzeigen, verändern und speichern können.

In der Callback-Funktion neu_malen() sehen Sie auch sogleich, wie man ein Pixbuf auf den Bildschirm bringt, nämlich mit Hilfe von Cairo. Dort könnten Sie auch weitere Cairo-Funktionen einsetzen, um das Bild zu manipulieren. Mit gdk_cairo_set_source_pixmap() wird das Pixmap Cairo übergeben, es wird durch einen Aufruf von cairo_paint() gezeichnet.

Wenn sich die Maus über der Malfläche bewegt, dann wird die Callback-Funktion maus_bewegt_sich() aufgerufen. Im Ereignis-Parameter steht unter anderem drin, wo sich die Maus gerade aufhält und welcher Mausknopf gleichzeitig gedrückt wurde. Wurde während der Bewegung der Mausknopf 1 gedrückt, dann soll an diese Stelle mit der aktuellen Farbe ein Punkt gemalt werden. Anschließend wird zum Neuzeichnen aufgefordert und der Statustext verändert.

Wird mit der Maus auf eine stelle im Malbereich geklickt, dann wird die Callback-Funktion mausknopf_gedrückt() aufgerufen. Der Ereignis-Parameter enthält ebenfalls die Position des Mauszeigers und gibt Auskunft darüber, welcher Knopf gedrückt wurde. Wenn der Mausknopf 1 gedrückt wurde, dann soll an diese Stelle ein Pixel gemalt werden. Wurde hingegen Mausknopf 3 gedrückt, dann soll ein Farbdialog aufgerufen werden.

Mit dem Farbdialog wird über eine Struktur vom Typ GdkColor kommuniziert. Diese verwendet 16 Bit Farbtiefe pro Farbkanal, wir verwenden in unserer male_pixel()-Funktion nur 8 Bit. Da wir die aktuelle Zeichenfarbe vorgeben wollen, müssen wir sie umrechnen. Anschließend starten wir den Dialog mit gtk_color_selection_dialog_new() und holen uns mit gtk_color_selection_dialog_get_color_selection() das Widget, das für die eigentliche Farbauswahl verantwortlich ist. Diesem wird nun unsere Farbe vom Typ GdkColor“ übermittelt. Der Dialog wird dann mit gtk_dialog_run() gestartet. Wird er beendet, so holen wir uns mit gtk_color_selection_get_current_color() die ausgewählte Farbe und konvertieren sie wieder in 8 Bit.

Damit wir Mausbewegungen und Maustastenereignisse gemeldet bekommen, werden mit gtk_widget_add_events() Benachrichtigungen darüber eingeschaltet. Wenn Sie Mausbewegungen verfolgen wollen, bei denen der Anwender eine Taste klicken können soll, muss GDK_BUTTON_PRESS_MASK zwingend eingeschaltet werden, auch, wenn sie nicht gesondert auf Maustastendrücke reagieren wollen. Sonst ist das entsprechende Feld in der Ereignisstruktur vom Typ GdkEventMotion nicht zu gebrauchen.

Zusammenfassung[Bearbeiten]

In diesem Kapitel haben Sie Pixbufs auf zwei Weisen kennen gelernt, nämlich einmal „versteckt“ in einem GtkImage und einmal so, dass Sie jedes Pixel anfassen durften. Ganz nebenbei haben wir Ihnen einige Widgets wie GtkImage, GtkScrolledArea, GtkViewport und andere vorgestellt und Sie mit einigen Details der Ereignisbehandlung vertraut gemacht.