Zeichnen mit Cairo

Bearbeiten

Cairo ist eine Vektorgrafikbibliothek mit direkter Unterstützung durch Gtk+. Mit Funktionen aus dieser Bibliothek können Sie Pfade zeichnen, die sich anschließend transformieren lassen und gezeichnete Elemente in SVG, PDF und anderen Formaten ausgeben. Wenn Sie eine externe Bibliothek wie RSVG dazu nehmen, können Sie sogar SVG-Dateien laden und ansehen. In diesem Kapitel geht es darum zu zeigen, wie man mit Cairo zeichnet und Zeichnungen rotieren lässt.


Diagramme nach Wahl

Bearbeiten

Das folgende Programm stellt ein Balken- und ein Tortendiagramm dar. Zwischen den Diagrammtypen schalten Sie mit einem Knopf in der Statusleiste um. Hier zunächst die XML-Beschreibung:

<?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="leinwand-1">
            <signal name="draw" handler="neu_malen" />
            </object>
        </child>
        <child>
            <object class="GtkHBox" id="hbox-layout-1">
            <property name="homogeneous">TRUE</property>
            <child>
                <object class="GtkButton" id="wechsel-ansicht-knopf">
                <property name="label">gtk-convert</property>
                <property name="use-stock">TRUE</property>
                <signal name="clicked" handler="wechsel_ansicht" />
                </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">Cairo auf einer Leinwand</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>

Innerhalb des Hauptfensters befindet unter anderem eine Zeichenfläche vom Typ GtkDrawingArea. In dieses Widget werden wir zeichnen. Immer, wenn ein Neuzeichnen erforderlich ist, wird automatisch das Signal „draw“ emittiert. Darauf reagieren wir mit der Callback-Funktion neu_malen().

Das Hauptprogramm besteht aus einigen Funktionen, die verschiedene Bereiche zeichnen und den Callback-Funktionen, um die Diagrammtypen umzuschalten wie auch das Neuzeichnen zu erledigen. Ebenfalls verwenden wir nicht mehr nur noch ein Builder-Objekt, um es den Callback-Funktionen mitzugeben, sondern eine C-Struktur, die unter anderem den Builder enthält sowie eine Kennzeichnung, welcher Diagrammtyp nun gezeichnet werden soll.

C
#include <gtk/gtk.h>
#include <glib/gprintf.h>
#include <math.h>

/* Ein Objekt, das allen Callbacks übergeben wird */
typedef struct _CallbackObjekt
{
    GtkBuilder *builder;
    GtkWidget *zeichenflaeche;
    gboolean tortendiagramm;
    
} CallbackObjekt;

/* Stimmabgaben */
static gint wahlen[3] = {50, 70, 22};

/* Achse zeichnen */
static void zeichne_achsen (cairo_t *cr, gint breite, gint hoehe)
{
    cairo_set_source_rgb (cr, 0, 0, 0);
    /* X-Achse */
    cairo_move_to (cr, 3, hoehe - 5);
    cairo_line_to (cr, breite - 3, hoehe - 5);
    
    /* Y-Achse */
    cairo_move_to (cr, 5, hoehe - 3);
    cairo_line_to (cr, 5, 3);
    cairo_stroke (cr);
}

static void balkendiagramm (cairo_t *cr, gint breite, gint hoehe)
{
    gint blockbreite = (breite - 20) / 6;
    gint blockhoehe;
    /* wahlen[0] */
    blockhoehe = (hoehe - 6) * wahlen[0] / 100;
    cairo_rectangle (cr, blockbreite,  (hoehe - 6) - blockhoehe, 
      blockbreite, blockhoehe);
    cairo_set_source_rgb (cr, 1, 0, 0);
    cairo_fill(cr);
    /* wahlen[1] */
    blockhoehe = (hoehe - 6) * wahlen[1] / 100;
    cairo_rectangle (cr, 3 * blockbreite,  (hoehe - 6) - blockhoehe,
      blockbreite, blockhoehe);
    cairo_set_source_rgb (cr, 0, 1, 0);
    cairo_fill(cr);
    /* wahlen[0] */
    blockhoehe = (hoehe - 6) * wahlen[2] / 100;
    cairo_rectangle (cr, 5 * blockbreite,  (hoehe - 6) - blockhoehe,
      blockbreite, blockhoehe);
    cairo_set_source_rgb (cr, 0, 0, 1);
    cairo_fill(cr);
}


static void tortendiagramm (cairo_t *cr, gint breite, gint hoehe)
{
    /* Winkel pro Stimme */
    double stimmwinkel = (2.0 * M_PI) / (wahlen[0] + wahlen[1] + wahlen[2]);
    double winkel0 = wahlen[0] * stimmwinkel;
    double winkel1 = wahlen[1] * stimmwinkel;
    double winkel2 = wahlen[2] * stimmwinkel;

    /* Mittelpunkt */
    double mitte_x = breite / 2.0;
    double mitte_y = hoehe / 2.0;
    
    /* Radius */
    double radius = (breite < hoehe ? breite : hoehe) / 3.0;
    
    /* Tortenstück für Partei 0 */
    cairo_set_source_rgb (cr, 1, 0, 0);
    cairo_move_to (cr, mitte_x, mitte_y);
    cairo_arc (cr, mitte_x, mitte_y, radius, 0.0, winkel0);
    cairo_fill(cr);

    /* Tortenstück für Partei 1 */
    cairo_set_source_rgb (cr, 0, 1, 0);
    cairo_move_to (cr, mitte_x, mitte_y);
    cairo_arc (cr, mitte_x, mitte_y, radius, winkel0, winkel0 + winkel1);
    cairo_fill(cr);
    
    /* Tortenstück für Partei 2 */
    cairo_set_source_rgb (cr, 0, 0, 1);
    cairo_move_to (cr, mitte_x, mitte_y);
    cairo_arc (cr, mitte_x, mitte_y, radius, winkel0 + winkel1,
      winkel0 + winkel1 + winkel2);
    cairo_fill(cr);
}


gboolean neu_malen (GtkWidget *zeichenflaeche, cairo_t *cr, CallbackObjekt *obj)
{
    gint breite, hoehe;
    breite = gtk_widget_get_allocated_width (zeichenflaeche);
    hoehe = gtk_widget_get_allocated_height (zeichenflaeche);
    if (obj->tortendiagramm)
    {
        tortendiagramm (cr, breite, hoehe);
    }
    else
    {
        zeichne_achsen (cr, breite, hoehe);
        balkendiagramm (cr, breite, hoehe);
    }
    return TRUE;
}

void wechsel_ansicht (GtkWidget *button, CallbackObjekt *obj)
{
    GtkLabel *textfeld;
    gint breite, hoehe;
    textfeld = GTK_LABEL(gtk_builder_get_object (obj->builder, "mein-label-1"));
    obj->tortendiagramm = !obj->tortendiagramm;
    breite = gtk_widget_get_allocated_width (obj->zeichenflaeche);
    hoehe = gtk_widget_get_allocated_height (obj->zeichenflaeche);
    gtk_label_set_text (textfeld, obj->tortendiagramm ? "Tortendiagramm" :
      "Balkendiagramm");
    gtk_widget_queue_draw_area (obj->zeichenflaeche, 0, 0, breite, hoehe);
}

int main (int argc, char *argv[])
{
    CallbackObjekt *callobj;
    GError *errors = NULL;
    GtkWidget *window;

    gtk_init (&argc, &argv);
    
    callobj = g_malloc (sizeof(CallbackObjekt));
    callobj->tortendiagramm = TRUE;
    
    callobj->builder = gtk_builder_new ();
    gtk_builder_add_from_file (callobj->builder, "cairo1.xml", &errors);
    gtk_builder_connect_signals (callobj->builder, callobj);
    window = GTK_WIDGET(gtk_builder_get_object (callobj->builder,
      "hauptfenster"));
    callobj->zeichenflaeche = GTK_WIDGET(gtk_builder_get_object (
      callobj->builder, "leinwand-1"));
    gtk_widget_set_size_request (callobj->zeichenflaeche, 200, 200);
    gtk_widget_show_all (window);
    gtk_main ();
    g_free (callobj);
    return 0;
}

In diesem Beispiel verwenden wir ein CallbackObjekt, um es allen Callback-Funktionen mitzugeben. Für dieses Objekt müssen wir mit g_malloc() Speicherplatz bereitstellen, ab da kann gtk_builder_connect_signals() dieses Objekt an alle Callback-Funktionen senden.

Eine dieser Callback-Funktionen ist wechsel_ansicht(). Diese Funktion wird aufgerufen, sobald der Anwender die Diagrammdarstellung wechseln möchte. In dieser Funktion wird die Ansicht geändert, zum Beispiel von Tortendiagramm auf Balkendiagramm gewechselt, das Textfeld mit einem passenden Hinweis beschrieben und dann mit gtk_widget_queue_draw_area() ein Neuzeichnen der Zeichenfläche angefordert. Dieser Funktion gibt man die Ausdehnung eines Rechtecks mit, das neu gezeichnet werden soll.

Sobald neu gezeichnet werden soll, wird die Callback-Funktion neu_malen() aufgerufen. Diese bekommt als Parameter die Zeichenfläche, einen Zeichen-Kontext und einen Verweis auf das Callback-Objekt mitgegeben. Das eigentliche Zeichnen wird an die Funktionen tortendiagramm(), zeichne_achsen() und balkendiagramm() weitergereicht. Hierzu wird lediglich der Zeichen-Kontext und die Größe der Zeichenfläche übergeben.

Die Funktion zeichne_achsen() setzt mit cairo_set_source_rgb() die Zeichenfarbe auf Schwarz, bewegt den Stift mit cairo_move_to() an eine Anfangsposition, zeichnet eine Linie mit cairo_line_to() bis zur angegebenen Endposition und stellt die so entstandenen Koordinatenachsen mit cairo_stroke() dar.

Balkendiagramme werden mit der Funktion „balkendiagramm()“ gezeichnet. Hier werden zunächst passende Balkenbreiten bestimmt. Um drei Balken zu zeichnen, wird der zur Verfügung stehende Platz in sechs Teile geteilt, damit immer ein Balken neben einer Lücke platziert werden kann. Dann sorgen wir mit cairo_rectangle() dafür, dass ein Rechteck gezeichnet wird. Das Rechteck soll gefüllt erscheinen, das erledigt cairo_fill() für uns.

Das Tortendiagramm wird mit der gleichnamigen Funktion gezeichnet. Hier wird zunächst der Winkel pro Wahlstimme ausgerechnet und dann die Winkel für die jeweiligen Parteien bestimmt. Um ein gefülltes Tortenstück zu zeichnen, legt man die Zeichenfarbe fest, dann beginnt man einen Pfad in der Mitte der Torte. An diesen Pfad wird ein Wegstück eines Kreises mit angegebenem Radius und Winkel mit cairo_arc() angefügt. Die Funktion cairo_fill() sorgt nun ihrerseits dafür, das der Pfad wieder zum Mittelpunkt geschlossen wird und füllt die entstandene Fläche mit der angegebenen Farbe.

Alles dreht sich

Bearbeiten

Im folgenden Beispiel zeigen wir anhand eines sich drehenden Textes, wie man Transformationen auf einen Pfad anwendet. Der Pfad ist Text, den der Benutzer in einer Editorzeile selbst eingeben kann, es würde aber mit jeder anderen Zeichnung auch funktionieren. Damit sich der Text selbstständig dreht, also animiert wird, muss in bestimmten Intervallen ein Zeitsignal an das Programm gesendet werden.

Zunächst die XML-Beschreibung des Programms:

<?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="leinwand-1">
            <signal name="draw" handler="neu_malen" />
            </object>
        </child>
        <child>
            <object class="GtkHBox" id="hbox-layout-1">
            <property name="homogeneous">TRUE</property>
            <child>
                <object class="GtkEntry" id="entry-1">
                <property name="text">Hallo</property>
                <property name="max-length">10</property>
                <signal name="activate" handler="neuer_text" />
                </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">Cairo auf einer Leinwand</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>

Auf der Zeichenfläche vom Typ GtkDrawingArea werden wir wie im ersten Beispiel die Zeichenoperationen ausführen. Die Editorzeile vom Typ GtkEntry legt die maximale Textlänge auf 10 Zeichen fest und sorgt dafür, dass immer, wenn der Anwender eingegebenen Text mit der Enter-Taste bestätigt die Callback-Funktion neuer_text() aufgerufen wird.

Hier das Programm dazu:

C
#include <gtk/gtk.h>
#include <glib/gprintf.h>
#include <math.h>
#include <stdlib.h>


/* Ein Objekt, das allen Callbacks übergeben wird */
typedef struct _CallbackObjekt
{
    GtkBuilder *builder;
    GtkWidget *zeichenflaeche;
    double winkel;
    gchar anzeigetext[20];
} CallbackObjekt;


gboolean neu_malen (GtkWidget *zeichenflaeche, cairo_t *cr, CallbackObjekt *obj)
{
    gint breite, hoehe;
    cairo_text_extents_t ausdehnung;
    
    breite = gtk_widget_get_allocated_width (zeichenflaeche);
    hoehe = gtk_widget_get_allocated_height (zeichenflaeche);
    cairo_set_source_rgb (cr, 1, 0, 0);
    /* Mittelpunkt festlegen */
    cairo_translate (cr, breite / 2.0, hoehe / 2.0);
    /* Font vergrößern */
    cairo_set_font_size (cr, 100);
    /* Text rotieren lassen */
    cairo_rotate (cr, obj->winkel);
    /* Textausdehnung ermitteln */
    cairo_text_extents (cr, obj->anzeigetext, &ausdehnung);
    /* Anfangspunkt für Text verschieben */
    cairo_move_to (cr, -ausdehnung.width / 2.0, ausdehnung.height / 2.0);
    /* Text ausgeben */ 
    cairo_show_text (cr, obj->anzeigetext);
    return TRUE;
}


gboolean zeitgeber (CallbackObjekt *obj)
{
    gint breite, hoehe;
    breite = gtk_widget_get_allocated_width (obj->zeichenflaeche);
    hoehe = gtk_widget_get_allocated_height (obj->zeichenflaeche);
    /* Winkel vergrößern */
    obj->winkel = obj->winkel + 0.063;
    /* Zum Zeichnen auffordern */
    gtk_widget_queue_draw_area (obj->zeichenflaeche, 0, 0, breite, hoehe);
    return TRUE;
}


void neuer_text (GtkEntry *texteditor, CallbackObjekt *obj)
{
    /* neuen Text eintragen */
    g_snprintf (obj->anzeigetext, 20, "%s", gtk_entry_get_text (texteditor));
}


int main (int argc, char *argv[])
{
    CallbackObjekt *callobj;
    GError *errors = NULL;
    GtkWidget *window;
    GtkEntry *editor;

    gtk_init (&argc, &argv);
    callobj = g_malloc (sizeof(CallbackObjekt));
    callobj->builder = gtk_builder_new ();
    callobj->winkel = 0.0;
    gtk_builder_add_from_file (callobj->builder, "cairo2.xml", &errors);
    window = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "hauptfenster"));
    editor = GTK_ENTRY(gtk_builder_get_object (callobj->builder, "entry-1"));
    g_snprintf (callobj->anzeigetext, 20, "%s", gtk_entry_get_text (editor));
    gtk_builder_connect_signals (callobj->builder, callobj);
    callobj->zeichenflaeche = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "leinwand-1"));
    gtk_widget_set_size_request (callobj->zeichenflaeche, 400, 400);
    gtk_widget_show_all (window);
    g_timeout_add (80, (GSourceFunc)zeitgeber, callobj);
    gtk_main ();
    g_free (callobj);
    return 0;
}

Das hier verwendete CallbackObjekt enthält den Winkel und den anzuzeigenden Text. Dieser Text wird erstmals in der Funktion main() gesetzt, die den Text aus dem Builder-Objekt liest.

In regelmäßigen Abständen soll eine Funktion aufgerufen werden, die den Winkel des darzustellenden Textes verändert, so dass dieser sich dreht. Hierfür wird ein Wecker mit der Funktion g_timeout_add() gestellt. Ihr wird eine Zeit in Millisekunden übergeben, eine Callback-Funktion, die aufgerufen wird, wenn der Wecker klingelt und das CallbackObjekt. Die Funktion zeitgeber(), kann einen Wert zurückgeben. Ist dieser TRUE, dann wird der Wecker erneut gestellt.

Die Callback-Funktion zeitgeber() hat nun nichts weiter zu tun, als den Winkel um einen kleinen Bereich weiter zu drehen und zum Neuzeichnen aufzufordern.

Die Funktion neu_malen() legt nun mit cairo_source_rgb() die Zeichenfarbe auf Rot fest. Wir wollen um den Mittelpunkt der Zeichenfläche drehen. Da eine Rotation immer um den Nullpunkt passiert, müssen wir mit cairo_translate() den Nullpunkt auf die Mitte der Zeichenfläche verlagern. Sonst wäre dieser die linke obere Ecke. Damit man den Text auch groß sieht, legen wir die Schriftsatzgröße auf 100 fest, und rotieren dann die Zeichenfläche um den angegebenen Winkel. Bitte beachten Sie, dass dieser Winkel im Uhrzeigersinn notiert wird. In technischen Anwendungen werden positive Winkel zumeist gegen den Uhrzeigersinn aufgefasst. Anschließend fragen wir nach, welche Ausmaße der aktuelle Text haben wird. Die Mitte des Textes soll genau im neuen Nullpunkt liegen, deswegen müssen wir den Text etwas nach links unten rücken. Mit cairo_show_text() wird dann der eigentliche Text auf den Bildschirm gebracht.


Zusammenfassung

Bearbeiten

In diesem Kapitel haben Sie die Grundlagen des Zeichnens mit Cairo kennen gelernt. Sie kennen nun das Widget GtkDrawingArea als Zeichenfläche und einige Zeichenoperationen und Transformationen von Cairo. Ebenfalls kennen Sie nun eine Möglichkeit, zeitgesteuert Aufgaben von Ihrem Programm erledigen zu lassen.