Libraries 3: Grafikprogrammierung mit Freebasic und GTK


Im Prinzip haben Sie im letzten Kapitel gelernt, sich mittels Glade beliebige Widgets zu einer Oberfläche zusammenzuklicken und sie mit einem Freebasic-Programm zu verbinden. Beim Widget zum Zeichnen von Bildern, wie man es auch ausserhalb von GTK mittels SCREEN, LINE usw. machen kann, gibt es auch ein Widget namens "Drawing Area". Damit müsste man doch alles in dieser Richtung tun können? Nun, im Fall dieses Widgets gibt es noch ein paar Dinge im Hintergrund, die man wissen (oder sich erarbeiten) sollte. Und damit will sich dieses Kapitel beschäftigen.

Übrigens hier ein Tipp am Rande: Nachdem Sie nun das erste GTK-Kapitel durchgearbeitet haben, würde ich Ihnen empfehlen, die Entwicklerversion von GTK zu installieren. Die hat nämlich den unschätzbaren Vorteil, dass die komplette API-Dokumentation gleich mitinstalliert wird.

Die GDK-Bibliothek: Pixmaps, Pixbufs und Windows

Innerhalb GTK selbst gibt es keine plot- oder line-Befehle. GTK ist eine Widget-Bibliothek. Zeichenbefehle gehören zu den s.g. "Drawing Primitives" und diese sind in einer Bibliothek zusammengefasst, die man GDK (Gimp Drawing Kit) bezeichnet. Dazu gibt es auch eine GDK-Dokumentation. GDK steht uns mit GTK ohne Weiteres zur Verfügung.

Gehen wir in das GDK Reference Manual unter "Drawing Primitives", finden wir wunderbare Befehle zum Zeichnen, z.B.:

Aber damit fängt das Elend an: Was sind diese ganzen neuen Klassen? GdkDrawable? GdkCC? GdkPixbuf? Und welche Objekte muss man hier übergeben, damit ich die Befehle benutzen kann?

GdkDrawable:Eine Bitmap, auf die man zeichnen kann. Sie gibt es in drei Formen: GdkWindow (nicht zu verwechseln mit GtkWindow!), GdkPixmap und GdkPixbuf sind GdkDrawables.
GdkWindow: Ein einfaches GdkDrawable, das immer sichtbar ist. Im Drawing-Area-Widget ist ein GdkWindow, in das man zeichnen kann. Das Gezeichnete wird sofort sichtbar.
GdkPixmap: Ein einfaches GdkDrawable, das nicht sichtbar ist. Also das, was in der gfxlib ein Screenbuffer ist, den man mit CreateImage erzeugt und in den man zeichnen kann.
GdkPixbuf: Ebenfalls ein GdkDrawable, das nicht sichtbar ist, allerdings ein viel mächtigeres als GdkPixmap. GdkPixbuf ist wohl in der Entwicklung später entstanden, hat seine eigene Teilbibliothek und Dokumentation und Routinen zum Laden und Speichern in verschiedensten Formaten.
GdkCC ist ein Graphics Context, der eine Vielzahl Voreinstellungen für's Zeichnen festhält, z.B. Clipping-Region (wo, an welchen Rändern, wird das Zeichnen abgeschnitten?), Linienattribute, Vorder- und Hintergrundsfarbe. Es macht Sinn, diesen Kontext nicht völlig neu zu definieren, sondern ihn von einem bestehenden Kontext zu übernehmen. Das geht mit der Methode gdk_gc_new(GdkDrawable ptr d). Und d ist dabei irgendein GdkDrawable.
Windows und Widgets Viele Widgets besitzen eine eigene Zeichenfläche, ihr eigenes GdkWindwow. Z.B. kann man durchaus einen Button mit Gdk-Zeichenbefehlen "bemalen". Wir steuern es einfach mit widget->window an. Folglich liegt es auch nahe, z.B. unseren Kontext aus dem Widget-Window des Hauptfensters zu bekommen:
context=gdk_gc_new(toplevel->window).
Es ist ziemlich zweckmässig, in einfacheren Programmen diesen context als globale Variable mitzuführen.

Zeichen-Ereignisse: expose_event-Management

Die Frage des "Wie" haben wir beim Zeichnen gelöst: Mit den Gdk"Primitives". Die Frage des "Wohin" wohl auch: Auf das GdkWindow unseres Drawing-Area-Widgets. Fein. Bleibt die für GUI-Programmierung typische Frage: "Wann"?

Fast allen GUI-Toolkits ist gemeinsam, dass ihre Zeichenflächen eine Methode (Callback-Funktion) bereithalten müssen, die für den Refresh zuständig ist: Falls ein Fenster oder Menü oder sonst etwas den Zeichenbereich überlappt und dann wieder verschwindet, ist der Zeicheninhalt erstmal zerstört. Der Refresh muss nun den ganzen Bereich oder zumindest den zerstörten Bereich wiederherstellen.

Im Fall von GTK übernimmt dies das Signal "expose_event". Das müssen wir unserem Drawing-Area-Widget mitgeben. Und die drangehängte Methode ist dann die, in der wir tunlichst den Inhalt der Zeichenfläche zeichnen sollte.

Das könnte dann ungefähr so aussehen:

-----------------------------------
Ausschnitt aus der XML-Datei:
-----------------------------------

<child>
          <widget class="GtkDrawingArea" id="drawingarea1">
            <property name="width_request">800</property>
            <property name="height_request">600</property>
            <property name="visible">True</property>
<signal name="expose_event" handler="on_drawingarea1_expose_event"/>
          </widget>
          <packing>
            <property name="x">16</property>
            <property name="y">48</property>
          </packing>
        </child>

-----------------------------------
Im Programm:
-----------------------------------

declare sub on_drawingarea1_expose_event           cdecl alias "on_drawingarea1_expose_event" (byval widget as GtkWidget  ptr, byval user_data as gpointer)

(...)

' ------------------------------------------------------------------------------------------------------------


sub on_drawingarea1_expose_event cdecl(byval widget as GtkWidget ptr, byval user_data as gpointer) export

  'contex global definiert, siehe Text.
  dim as gboolean filled = true
  gdk_draw_rectangle(widget->window,context,filled,0,0,500,500)

end sub

Das funktioniert auch soweit. In der Zeichenfläche erscheint ein Rechteck. Aber es ist nicht gut. Das Zeichnen der Fläche kann ja einige Zeit in Anspruch nehmen. Nehmen wir z.B. ein Programm zum Zeichnen von Fraktalen. Da dauert das Zeichnen schon mal ein paar Minuten. Inakzeptabel, dass die Maschine jedes Mal das Fraktal von vorne berechnet, wenn mal ein Menü zuklappt.

Es empfiehlt sich, nicht direkt in die Drawing-Area zu zeichnen. Sondern eine Pixmap als Hintergrundpuffer zu benutzen. (Es muss eine Pixmap sein. Ein Pixbuf wäre theoretisch günstiger, aber man kommt später in Schwierigkeiten, sie geeignet zu konvertieren.)

Das Beste ist also, ein Puffer-Objekt zu schaffen, das eine pixmap enthält und sich um das Management dieses Puffers kümmert. Im expose-Event wird dieser Puffer nur noch ins Widget verschoben:

sub on_drawingarea1_expose_event cdecl(byval widget as GtkWidget ptr, byval user_data as gpointer) export

  dim as integer wwidth,wheight
dim as byte ptr x1,x2
  dim as integer i


  with area1
    if (.init0) then _
      dim as integer width1,height1

      gtk_data.window_width=wwidth
      gtk_data.window_height=wheight

      'Überschreibe drawingarea schwarz
      area1.setcolor(0,0,0,0,0,0)
      gdk_draw_rectangle(widget->window,.context,TRUE,0,0,wwidth-1,wheight-1)
      if (wwidth>area1.map_nx or wheight>area1.map_ny) then _
        wwidth=area1.map_nx:wheight=area1.map_ny
      gdk_draw_drawable(widget->window,.context,.map,.map_x,.map_y,0,0,.map_nx,.map_ny)

  end with

end sub

Der Puffer heisst im Beispiel area1 und ich habe die Klasse tomagtkmap genannt und sie hat bei mir folgenden Aufbau:

type tomagtkmap

  map as GdkPixmap ptr
  map_nx as integer
  map_ny as integer
  map_x as integer
  map_y as integer
  init0 as gboolean
  map_ncolor as integer

  context as GdkGC ptr
  fgcolor as _gdkcolor ptr
  bgcolor as _gdkcolor ptr


  declare constructor()
  declare sub init(nwidth as integer, nheight as integer, ncolorbit as integer)
  declare Sub init1()
  declare sub close()
  declare sub setcolor(fg_red as integer,fg_green as integer, fg_blue as integer, bg_red as integer, bg_green as integer, bg_blue as integer)
  declare sub rectangle(filled as gboolean, x as integer, y as integer, wwidth as integer, wheight as integer)

end type

dim shared area1 as tomagtkmap

Auf diese Art und Weise können alle Zeichenoperationen in area1 vorgenommen werden, völlig unabhängig davon, wann Screen-Refreshs stattfinden.

Die Bitmap, die area1 verwaltet, muss nicht die gleiche Grösse haben wie unser Sichtfenser, unser drawing-area-Widget. Es kann auch kleiner oder grösser sein. Im letzteren Fall können wir dafür sorgen, dass der Anwender die Bitmap am Widget quasi vorbeiziehen kann, area1 also so etwas wie eine grosse Karte ist, auf die wir mit einem Sichtfenster draufschauen.

Die GDK-Bibliothek: Mausaktionen auf Zeichenflächen

Am elegantesten bewegen wir so eine Hintergrund-Bitmap mit der Maus. Dazu müssen wir drei Ereignisse abfangen:

  1. Das Ereignis des Maustasten-Runterdrückens. Das heisst, das Ereignis, dass die Bitmap "gepackt" wird. Hier müssen wir einen Modus aktivieren, in dem die Mausbewegung in Bitmap-Bewegung umgesetzt wird.
  2. Das Ereignis des Mausziehens. Hier müssen wir die Bitmapbewegung selbst umsetzen.
  3. Das Ereignis des Maustastenloslassens. Hier müssen wir den "Verfolgungsmodus" wieder ausschalten.

Würden wir nicht einen speziellen Verschiebungsmodus vorsehen, würde die Ereignisbehandlung dafür sorgen, dass die Bitmap immer verschoben wird - auch bei losgelassener Maustaste.

Die drei zugehörigen Signale können wir problemlos in Glade oder der XML-Datei direkt dem Drawing Area-Widget zuordnen. Aber wir dürfen nicht vergessen, einzustellen, dass die Drawing Area auch auf diese Signale "lauscht". Wir müssen also die entsprechenden Ereignisse aktivieren:

<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_MOTION_MASK | GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK</property>
<signal name="button_press_event" handler="on_drawingarea1_button_press_event"/>
<signal name="motion_notify_event" handler="on_drawingarea1_motion_notify_event"/>
<signal name="button_release_event" handler="on_drawingarea1_button_release_event"/>

Ein kleines Testprogramm, wie sowas dann funktioniert:

' ---------------------------------------------------------------------
' Oma Tutorial, Teil 3, Kapitel 16
' Testprogramm zum Zeichnen mit GDK/GTK
' Zeichnet eine Testbitmap, die vor dem Sichtfenster
' mit der Maus verschoben werden kann
' ---------------------------------------------------------------------

#include "gtk/gtk.bi"
#include "gtk/libglade/glade-xml.bi"

#ifndef NULL
#define NULL 0
#endif

dim shared as GtkWidget ptr toplevel
dim shared xml as GladeXML ptr
dim shared systemstart as gboolean = TRUE
dim shared as _GdkColor ptr black0,white0,blue0


Sub brkmess(s as string)

  ? s
  sleep
  end

end sub

function min(x1 as integer, x2 as integer) as integer
  min=x1
  if (x2x1) then max=x2
end function



type tomagtkmap

  map as GdkPixmap ptr
  map_nx as integer
  map_ny as integer
  map_x as integer
  map_y as integer
  init0 as gboolean
  map_ncolor as integer

  context as GdkGC ptr
  fgcolor as _gdkcolor ptr
  bgcolor as _gdkcolor ptr


  declare constructor()
  declare sub init(nwidth as integer, nheight as integer, ncolorbit as integer)
  declare Sub init1()
  declare sub close()
  declare sub setcolor(fg_red as integer,fg_green as integer, fg_blue as integer, bg_red as integer, bg_green as integer, bg_blue as integer)
  declare sub rectangle(filled as gboolean, x as integer, y as integer, wwidth as integer, wheight as integer)

end type

'--------------------------------------------------------------------------------------------------


constructor tomagtkmap()
  init0=false
end constructor

'--------------------------------------------------------------------------------------------------


Sub tomagtkmap.init(nwidth as integer, nheight as integer, ncolorbit as integer)

  map_nx=nwidth
  map_ny=nheight
  map_ncolor=ncolorbit

  map=gdk_pixmap_new(NULL,map_nx,map_ny,24)
  if (map=NULL) then brkmess("omagtkmap_init():  gdk_pixmap_new() failed")

  fgcolor=allocate(Len(_GdkColor))
  bgcolor=allocate(Len(_GdkColor))


End Sub


Sub tomagtkmap.init1()

  context=gdk_gc_new(toplevel->window)
  setcolor(0,255,0,0,0,0)
  rectangle(TRUE,0,0,100,100)
  setcolor(255,0,0,0,0,0)
  rectangle(FALSE,100,100,170,170)
  setcolor(127,0,127,0,0,0)
  rectangle(FALSE,200,200,300,400)
  map_x=50
  map_y=0

end sub


'--------------------------------------------------------------------------------------------------

Sub tomagtkmap.close()

  deallocate fgcolor
  deallocate bgcolor

End Sub

'--------------------------------------------------------------------------------------------------

sub tomagtkmap.rectangle(filled as gboolean, x as integer, y as integer, wwidth as integer, wheight as integer)

  dim as GtkWidget ptr w
  'setcolor(255,0,0,255,0,0)
  w = glade_xml_get_widget( xml, "drawingarea1" )
  if (w=NULL) then brkmess("tomagtkmap.rectangle() Error widget not found")

  'context=gdk_gc_new(w)
  gdk_gc_set_foreground(context,fgcolor)
  gdk_gc_set_background(context,bgcolor)

  ? "Fill rectangle"
  gdk_draw_rectangle(map,context,filled,x,y,wwidth,wheight)

end sub

'--------------------------------------------------------------------------------------------------

sub tomagtkmap.setcolor(fg_red as integer,fg_green as integer, fg_blue as integer, bg_red as integer, bg_green as integer, bg_blue as integer)

  fgcolor->pixel=fg_red*&h10000+fg_green*&h100+fg_blue
  fgcolor->red=fg_red
  fgcolor->green=fg_green
  fgcolor->blue=fg_blue

  bgcolor->pixel=bg_red*&h10000+bg_green*&h100+bg_blue
  bgcolor->red=bg_red
  bgcolor->green=bg_green
  bgcolor->blue=bg_blue

end sub

'--------------------------------------------------------------------------------------------------

dim shared area1 as tomagtkmap

'--------------------------------------------------------------------------------------------------


type tgtk_data
  old_x as integer
  old_y as integer
  old_mapx as integer
  old_mapy as integer
end type

dim shared as tgtk_data gtk_data

'--------------------------------------------------------------------------------------------------

declare sub on_drawingarea1_expose_event           cdecl alias "on_drawingarea1_expose_event" (byval widget as GtkWidget  ptr, byval user_data as gpointer)

declare sub on_button1_clicked                        cdecl alias "on_button1_clicked" (byval object as GtkObject  ptr, byval user_data as gpointer)

declare function on_drawingarea1_button_press_event cdecl alias "on_drawingarea1_button_press_event" (byval object as GtkWidget ptr,  byval button as GdkEventButton ptr, byval user_data as gpointer) as gboolean

declare function on_drawingarea1_motion_notify_event cdecl alias "on_drawingarea1_motion_notify_event" (byval object as GtkWidget ptr,  byval button as GdkEventMotion ptr, byval user_data as gpointer) as gboolean




' ------------------------------------------------------------------------------------------------------------

sub color_init()

  black0=allocate(Len(_GdkColor))
  white0=allocate(Len(_GdkColor))
  blue0=allocate(Len(_GdkColor))

  black0->pixel=&hFF000000
  black0->red=0
  black0->green=0
  black0->blue=0

  white0->pixel=&hFFFFFFFF
  white0->red=&hFF
  white0->green=&hFF
  white0->blue=&hFF

  blue0->pixel=&hFF0000FF
  blue0->red=0
  blue0->green=0
  blue0->blue=&hFF

end sub

sub color_close()

  deallocate black0
  deallocate white0
  deallocate blue0

end sub



' ------------------------------------------------------------------------------------------------------------


sub tomagtk_getwidgetsize(widgetname as string, byref wwidth as integer, byref wheight as integer)

  dim as GtkRequisition ptr g
  dim as GtkWidget ptr w

  w = glade_xml_get_widget( xml,widgetname)

  g=allocate(Len(GtkRequisition))

  gtk_widget_size_request(w,g)

  wwidth=g->width
  wheight=g->height

  deallocate(g)

end sub

' ------------------------------------------------------------------------------------------------------------


sub on_drawingarea1_expose_event cdecl(byval widget as GtkWidget ptr, byval user_data as gpointer) export

  dim as integer wwidth,wheight
  dim as GdkGC ptr context
  tomagtk_getwidgetsize("drawingarea1",wwidth,wheight)
  dim as byte ptr x1,x2
  dim as integer i

  with area1
    if (.init0) then _
      dim as integer width1,height1

      if (wwidth>area1.map_nx or wheight>area1.map_ny) then _
        wwidth=area1.map_nx:wheight=area1.map_ny
      ? wwidth,wheight

      'context=gdk_gc_new(toplevel->window)
      'gdk_gc_set_foreground(.context,area1.fgcolor)
      'gdk_gc_set_background(.context,area1.bgcolor)


      gdk_draw_drawable(widget->window,.context,.map,.map_x,.map_y,0,0,wwidth,wheight)

  end with

end sub


' ------------------------------------------------------------------------------------------------------------


sub on_button1_clicked  cdecl (byval object as GtkObject  ptr, byval user_data as gpointer) export

  '? "Hello!"
  dim as GtkWidget ptr w
  w = glade_xml_get_widget( xml, "drawingarea1" )

  area1.map_x=area1.map_x+20

  on_drawingarea1_expose_event(w,NULL)

end sub

' ------------------------------------------------------------------------------------------------------------

function on_drawingarea1_button_press_event cdecl (byval object as GtkWidget ptr,  byval button as GdkEventButton ptr, byval user_data as gpointer) as gboolean export

  ? "drawing area: Button pressed"
  ? button->x,button->y
  gtk_data.old_mapx=area1.map_x
  gtk_data.old_mapy=area1.map_y
  gtk_data.old_x=button->x
  gtk_data.old_y=button->y


  on_drawingarea1_button_press_event=false


end function


' ------------------------------------------------------------------------------------------------------------


sub area1_move(x as integer, y as integer)

    dim as GtkWidget ptr w
    w = glade_xml_get_widget( xml, "drawingarea1" )

    area1.map_x=x
    if area1.map_x<0 then area1.map_x=0
    if area1.map_x>400 then area1.map_x=400

    area1.map_y=y
    if area1.map_y<0 then area1.map_y=0
    if area1.map_y>400 then area1.map_y=400


    on_drawingarea1_expose_event(w,NULL)


end sub



' ------------------------------------------------------------------------------------------------------------


function on_drawingarea1_motion_notify_event cdecl (byval object as GtkWidget ptr,  byval button as GdkEventMotion ptr, byval user_data as gpointer) as gboolean export

  dim as GdkModifierType state

  state=button->state

  if (state and GDK_BUTTON1_MASK) then
    ? "drawing area: Motion notify"
    ? button->x,button->y

    area1_move(gtk_data.old_mapx+button->x-gtk_data.old_x,gtk_data.old_mapy+button->y-gtk_data.old_y)

  end if

  on_drawingarea1_motion_notify_event=false

end function




' ------------------------------------------------------------------------------------------------------------


gtk_init( NULL, NULL )
area1.init(1000,700,32)
color_init
xml = glade_xml_new( "fract1.glade", NULL, NULL )
toplevel = glade_xml_get_widget( xml, "window1" )
gtk_widget_show_all( toplevel )
glade_xml_signal_autoconnect( xml )
area1.init1()
gtk_main( )


g_object_unref( xml )
area1.close
color_close
end

'-------------------------------------------------
'fract1.glade
'-------------------------------------------------

<?xml version="1.0"?>
<glade-interface>
  <!-- interface-requires gtk+ 2.16 -->
  <!-- interface-naming-policy toplevel-contextual -->
  <widget class="GtkWindow" id="window1">
    <property name="width_request">850</property>
    <property name="height_request">650</property>
    <property name="window_position">center</property>
    <child>
      <widget class="GtkFixed" id="fixed1">
        <property name="visible">True</property>
        <child>
          <widget class="GtkDrawingArea" id="drawingarea1">
            <property name="width_request">800</property>
            <property name="height_request">600</property>
            <property name="visible">True</property>
            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_MOTION_MASK | GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK</property>
            <signal name="expose_event" handler="on_drawingarea1_expose_event"/>
            <signal name="button_press_event" handler="on_drawingarea1_button_press_event"/>
            <signal name="motion_notify_event" handler="on_drawingarea1_motion_notify_event"/>
            <signal name="button_release_event" handler="on_drawingarea1_button_release_event"/>
          </widget>
          <packing>
            <property name="x">16</property>
            <property name="y">48</property>
          </packing>
        </child>
        <child>
          <widget class="GtkButton" id="button1">
            <property name="label" translatable="yes">Save</property>
            <property name="width_request">40</property>
            <property name="height_request">28</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_button1_clicked"/>
          </widget>
          <packing>
            <property name="x">38</property>
            <property name="y">13</property>
          </packing>
        </child>
        <child>
          <widget class="GtkRadioButton" id="move_map">
            <property name="label" translatable="yes">Mouse: Move Map</property>
            <property name="width_request">125</property>
            <property name="height_request">20</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">False</property>
            <property name="active">True</property>
            <property name="draw_indicator">True</property>
            <property name="group">select_frame</property>
            <signal name="clicked" handler="on_move_map_clicked"/>
          </widget>
          <packing>
            <property name="x">82</property>
            <property name="y">9</property>
          </packing>
        </child>
        <child>
          <widget class="GtkRadioButton" id="select_frame">
            <property name="label" translatable="yes">Mouse: Select Frame</property>
            <property name="width_request">125</property>
            <property name="height_request">23</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">False</property>
            <property name="active">True</property>
            <property name="draw_indicator">True</property>
            <property name="group">move_map</property>
            <signal name="clicked" handler="on_select_frame_clicked"/>
          </widget>
          <packing>
            <property name="x">81</property>
            <property name="y">23</property>
          </packing>
        </child>
        <child>
          <widget class="GtkEntry" id="entry_bitmheight">
            <property name="width_request">50</property>
            <property name="height_request">18</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="invisible_char">●</property>
            <signal name="key_press_event" handler="on_entry_bitmheight_key_press_event"/>
          </widget>
          <packing>
            <property name="x">284</property>
            <property name="y">25</property>
          </packing>
        </child>
        <child>
          <widget class="GtkEntry" id="entry_bitmwidth">
            <property name="width_request">50</property>
            <property name="height_request">18</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="invisible_char">●</property>
            <signal name="key_press_event" handler="on_entry_bitmwidth_key_press_event"/>
          </widget>
          <packing>
            <property name="x">284</property>
            <property name="y">8</property>
          </packing>
        </child>
        <child>
          <widget class="GtkLabel" id="bitmapbreite">
            <property name="width_request">85</property>
            <property name="height_request">23</property>
            <property name="visible">True</property>
            <property name="label" translatable="yes">Bitmap-Breite</property>
          </widget>
          <packing>
            <property name="x">205</property>
            <property name="y">5</property>
          </packing>
        </child>
        <child>
          <widget class="GtkLabel" id="bitmapbreite1">
            <property name="width_request">71</property>
            <property name="height_request">29</property>
            <property name="visible">True</property>
            <property name="label" translatable="yes">Bitmap-Höhe</property>
          </widget>
          <packing>
            <property name="x">212</property>
            <property name="y">18</property>
          </packing>
        </child>
        <child>
          <widget class="GtkButton" id="button_run">
            <property name="label" translatable="yes">Run</property>
            <property name="width_request">50</property>
            <property name="height_request">26</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_button_run_clicked"/>
          </widget>
          <packing>
            <property name="x">348</property>
            <property name="y">12</property>
          </packing>
        </child>
      </widget>
    </child>
  </widget>
  <widget class="GtkAboutDialog" id="aboutdialog1">
    <property name="border_width">5</property>
    <property name="type_hint">normal</property>
    <property name="has_separator">False</property>
    <property name="program_name">Glade</property>
    <child internal-child="vbox">
      <widget class="GtkVBox" id="dialog-vbox1">
        <property name="visible">True</property>
        <property name="events">GDK_BUTTON_MOTION_MASK | GDK_STRUCTURE_MASK</property>
        <property name="orientation">vertical</property>
        <property name="spacing">2</property>
        <child>
          <placeholder/>
        </child>
        <child internal-child="action_area">
          <widget class="GtkHButtonBox" id="dialog-action_area1">
            <property name="visible">True</property>
            <property name="layout_style">end</property>
          </widget>
          <packing>
            <property name="expand">False</property>
            <property name="pack_type">end</property>
            <property name="position">0</property>
          </packing>
        </child>
      </widget>
    </child>
  </widget>
</glade-interface>

Die GDK-Bibliothek: Bemerkung zu den Farben

Farben werden im Objekt _gdkcolor gespeichert. _gdkcolor enthält eine Variable "pixel" und dann nochmal drei Variablen red,green,blue. Ausschlaggebend ist "pixel". red,green,blue sollten konsistent befüllt werden, dienen aber nur Informationszwecken. Eine Routine setcolor() ist dringend anzuraten (siehe Testprogramm).

Die GDK-Bibliothek: Laden und Abspeichern von Bitmaps

Um eine Bitmap abzuspeichern, muss man die pixbuf-Teilbibliothek bemühen. Hier ist ein bisschen Orientierung gefragt: Ein Teil der pixbuf-Dokumentation steht innerhalb der zentralen GDK-Doku (nämlich alles, was mit dem Zeichnen selbst zu tun hat), ein Teil ist ausgelagert in einer eigenen Doku, insbesondere die Routinen, die sich mit dem Speichern und Laden beschäftigen.

Damit eine pixmap gespeichert werden kann, muss sie in einen neuen pixbuf kopiert werden. Dazu muss der pixbuf erstmal generiert werden. Das erledigt die Routine gdk_pixbuf_new(). Den Kopiervorgang lösen wir mit der Routine gdk_pixbuf_get_from_drawable() aus. Diese allerdings verlangt die explizite Angabe einer GdkColormap (ein Teil von GdkContext). Das macht aber nichts, denn diese Information steckt zum Glück in unserer pixmap mit drin und mit gdk_drawable_get_colormap() können wir sie da auch rausholen. Schliesslich speichern wir mit gdk_pixbuf_save() ab. Dabei stehen eine Vielzahl Formate zur Verfügung - wir arbeiten ja nicht umsonst mit dem Toolkit eines grossen Fotobearbeitungsprogramms.

Hier ein Beispiel für eine Speicherroutine:

sub tomagtkmap.save(fname as string)

  dim as GdkPixbuf ptr tempbuf
  dim as GError ptr ptr err1


  'Um abzuspeichern, muss die omagtkmap-pixmap in eine pixbuf transferiert werden.
  tempbuf=gdk_pixbuf_new(GDK_COLORSPACE_RGB,FALSE,8,map_nx,map_ny)
  if (tempbuf=NULL) then brkmess("tomagtkmap.save():  gdk_pixbuf_new() failed")

  gdk_pixbuf_get_from_drawable(tempbuf,map,gdk_drawable_get_colormap(map),0,0,0,0,map_nx,map_ny)
  gdk_pixbuf_save(tempbuf,fname,"png",err1,NULL)

  'Sag der Garbage Collection, dass sie den Speicher wieder freigeben kann
  gdk_pixbuf_unref(tempbuf)

end sub

gdk_pixbuf_unref() ist auch noch so eine Sache. Normalerweise hat GTK den unglaublichen Vorteil, dass man sich um Speicherverwaltung nicht die Bohne kümmern muss. Das gilt allerdings nur für Speicherplatz, der unmittelbar mit Widgets verknüpft ist und/oder lediglich einmal während der Programmlaufzeit vergeben wird. In diesem Fall funktioniert die s.g. Garbage Collection nicht: tempbuf würde bis zum Programmende bestehen bleiben, wenn man es nicht freigibt. Die Frage ist nur: Wie freigeben? delete und deallocate funktionieren in diesem Fall nicht. Daher unref().

Beim Laden funktioniert's quasi andersherum: Zunächst muss das Bild geladen und in einem pixbuf zwischengespeichert werden. Erst dann kann es in eine pixmap überführt werden. Da kann man dann auch schon etwas Zeit brauchen, bis man sich die geeigneten Routinen zusammengelesen hat.

sub tomagtkmap.load(fname as string)

dim as GdkPixbuf ptr tempbuf
  dim as GError ptr ptr err1


  'Um zu laden, muss das Bild zunaechst in einer pixbuf zwischengespeichert werden.
  tempbuf=gdk_pixbuf_new_from_file(fname,err1)
  if (tempbuf=NULL) then omagtk_msgbox("tomagtkmap.load(): File not found")

  gdk_pixbuf_render_to_drawable(tempbuf,map,context,0,0,0,0,map_nx,map_ny,GDK_RGB_DITHER_NONE,0,0)

end sub

Die GDK-Bibliothek: Sprites

Sprites sieht GDK explizit nicht vor. Aber mit pixbuf's und pixmap's ist das alles kein Problem. Allerdings gibt es gegenüber den bisherigen Ausführungen eine Besonderheit: Sprites müssen die Farbe "Durchsichtig" enthalten. In der professionellen Grafik behandelt man das mit einem "Alpha-Kanal", der sozusagen als vierter Farbkanal neben Rot, Grün und Blau die Durchsichtigkeit bestimmt. Wir brauchen also einen pixbuf mit Alphakanal.

Es liegt zunächst nahe, mit gdk_pixbuf_new() sich einen solchen zu beschaffen (has_alpha auf TRUE und bits_per_sample auf 32, statt auf 24.) Aber das ist gar nicht notwendig. Mit gdk_pixbuf_new_from_file() holt man sich sein Sprite ganz normal in den Speicher. Dann holt man sich die s.g. Clippingfarbe, die durchsichtig sein soll. Z.B. aus einem bestimmten Pixel des Sprites. Oder man setzt sie explizit auf einen bestimmten Wert. Daraufhin benutzen wir die Methode gdk_pixbuf_add_alpha(), um nachträglich einen Alpha-Kanal mit der entsprechenden Clippingfarbe einzurichten. Das Ganze als Load-Routine:

' -----------------------------------------------------------------------------------------------------

Function pixbuf_getblue(color0 as guint32) as integer

  Function=color0 mod &h100

End Function

' -----------------------------------------------------------------------------------------------------

Function pixbuf_getred(color0 as guint32) as integer

  Function=(color0 SHR 16) mod &h100

End Function

' -----------------------------------------------------------------------------------------------------

Function pixbuf_getgreen(color0 as guint32) as integer

  Function=(color0 SHR 8) mod &h100

End Function


' -----------------------------------------------------------------------------------------------------

Function pixbuf_getalpha(color0 as guint32) as integer

  Function=color0 SHR 24

End Function



' -----------------------------------------------------------------------------------------------------

Sub omagtk_pixbuf_load(byref pix as GdkPixbuf ptr, fname as string)

  'Loads data of a pixbuf from a png file and sets the clipping color to the color
  'of the most upper left pixel.
  'fname: Path to a bitmap file

  dim as zstring ptr fname1
  dim as integer found
  dim as GError ptr ptr gerr
  dim as guint32 clipcolor
  dim as integer ix,iy

  found=open (fname for input as #1)
  close #1
  if (found=2) then
    ? "omagtk_pixmap_load() bitmap file not found"
    ? "Name: ";fname
    sleep
    end
  end if
  fname1=allocate(LEN(fname)+2):*fname1=fname
  pix=gdk_pixbuf_new_from_file(fname1,gerr)
  if (pix=NULL) then brkmess("Error in omagtk_pixbuf_load(): pixbuf load failed")

  'Hier wird die Farbe des Pixels in der linken oberen Ecke hergenommen
  clipcolor=omagtk_pixbuf_getpixel(pix,0,0)
  'Kontrolliere die RGB-Werte der Transparentfarbe:
  ? pixbuf_getred(clipcolor)
  ? pixbuf_getgreen(clipcolor)
  ? pixbuf_getblue(clipcolor)

  pix=gdk_pixbuf_add_alpha(pix,TRUE,omagtk_pixbuf_getred(clipcolor), _
                                    omagtk_pixbuf_getgreen(clipcolor), _
                                    omagtk_pixbuf_getblue(clipcolor))

  deallocate fname1

END SUB

Ein Anwendungsbeispiel: Omafrac

Um die beiden GTK-Kapitel abzurunden, finden Sie am Schluss nun eine vollständige kleine GTK-Anwendung: Ein GUI-Programm zum Zeichnen von Fraktalen namens Omafrac.

Hier nur noch ein paar Bemerkungen zu einzelnen Problemlösungen innerhalb von Omafrac.
Klassen: Das Progrämmchen hat vier wichtige Klassen:
  • tomagtkmap: Für das Management der Hintergrundbitmap. Sie hat eine globale Instanz: area1
  • tomafrac: Für das Zeichnen der Fraktale. Sie hat eine globale Instanz: omafrac
  • tgtk_data: Hält alle globalen Daten, die für das GUI-Management (auch ausserhalb von area1) notwendig sind. Insbesondere ein Array mit den Widget-Pointern. Zuständig dafür, dass die internen Variablen in die Textfelder kommen und umgekehrt. Eine globale Instanz: gtk_data
  • tcolormap: Enthält alles für das Management der Farbpalette
Zoomlist Beim Zoomen werden die Daten des letzten Ausschnitts in einer verketteten Liste gespeichert. Ich habe dazu die omalist-Lib benutzt, die wir in einem der letzten Kapitel entwickelt hatten.
Optisches Zeichnen des Fraktals und blockierte main loop Im Normalfall würde das Zeichnen das ganzen Fraktals im Hintergrund geschehen. Mit dem Ergebnis, dass nach dem Drücken des Run-Knopfs im Regelfall erstmal gar nichts passiert. Das ist unschön. Besser ist, wenn man dem PC beim Zeichnen des Fraktals zusehen kann. Dazu kann man am Ende jeder gezeichneten Spalte ein expose-Event auslösen. Soweit so einfach.
Wir wollen das Zeichnen aber auch mit dem Stop-Knopf unterbrechen können. Dazu bilden wir ein stop-Flag innerhalb von gtk_data, das gesetzt wird, sobald der Stop-Knopf aktiviert wurde. Abgefragt wird es innerhalb der Fraktalschleife. So sollte es gehen. Tut es aber nicht. Denn die Fraktalzeichenroutine ist ja nur ein Aufruf vom run-Button aus und solange dieser Aufruf nicht fertig abgearbeitet wurde, kehren wir nicht in die GUI-Hauptschleife zurück. Solange wird dann aber auch die Betätigung irgendwelcher Knöpfe nicht abgearbeitet. Das Zeichnen blockiert also das ganze GUI. Wie können wir das verhindern? So ähnlich, wie wir in simple Freebasic mit einem sleep 10 dafür sorgen, dass noch andere Tasks zum Zuge kommen, so sorgen wir mit dem Einfügen der Zeile
while (gtk_events_pending):gtk_main_iteration:wend
dafür, dass die Hauptschleife (gtk_main_iteration) dann abgearbeitet wird, wenn GUI-Aktivitäten in der Warteschleife stecken.
Radio Buttons Klingt völlig einfach, aber Radio Buttons sind ein Widget-Typ, den ich nicht ganz in den Griff bekommen habe. Ihre zentrale Eigenschaft ist, dass sie gegenseitig abhängen: Aktiviere ich einen, werden alle anderen deaktiviert. Dazu müssen sie in einen Buttongroup gestellt werden. Diese kann man in Glade bilden. Naheliegend wäre, dieser Gruppe einen beliebigen Namen zu verpassen. Das geht aber nicht. Glade stellt als Gruppenname immer nur die Namen anderer Radiobuttons zur Verfügung. Wählt man direkt im XML-File einen anderen Namen als einen Buttonnamen, dann funktioniert die Knopf-Abstimmung gar nicht.
Nimmt man allerdings den Vorschlag von Glade an, funktioniert sie auch nicht: Alle Knöpfe sind gleichzeitig aktiviert. Es funktioniert bei drei oder mehr Knöpfen in der Gruppe erst dann, wenn man als Gruppennamen für alle Knöpfe nur einen Namen verwendet, z.B. den des ersten Buttons - auch für diesen Button selbst. Wenn wir also die Radiobuttons A,B,C verknüpfen wollen, müssen wir allen den Gruppennamen A geben - auch dem Button A, obwohl das Glade so gar nicht vorsieht.
Die Lösung ist nicht perfekt - wir bekommen eine assertion-Warning während dem Programmstart auf der Konsole, aber ansonsten funktioniert alles wie gewünscht.
Noch ein Aspekt ist, dass wir nicht das Signal "toggled" abfragen sollten, sondern das Signal "clicked". Und auch da ist es gut, zu überprüfen, ob sich der Status des Knopfes überhaupt geändert hat.
Hide oder Destroy? Bei Unterfenstern wie dem Farbauswahl- oder dem Fraktalparameterfenster möchte man möglichst wenig verwalten müssen. Daher empfiehlt sich, die Fenster nur einmal mitsamt allen zugehörigen Widgets zu erzeugen und ab da erzeugt zu lassen. Das Erscheinen und Verschwinden erledigt man dann mit show() und hide().
Die Idee ist solange gut, solange der User nicht auf den Schliessen-Knopf oben rechts drückt. Dann nämlich wird die Zerstörung des Fensters und sämtlicher Widgets unwiderruflich eingeleitet.
Die Wiederherstellung kann mit glade-xml etwas tricky sein. Man muss einen neuen xml-Handle erzeugen, hinter dem automatisch dann der ganze Widgetbaum hängt, der neu erzeugt werden musste. Ergo kommen wir nicht mehr mit einem xml-Handle aus, in dem wir alles finden. Jedes Fenster braucht seinen eigenen Handle. Schlimmer fast noch ist, dass wir für Refresh-Aktionen eine Liste von Zeigern auf alle Elemente in einem Fenster brauchen. Die müssen natürlich nach einem Destroy alle neu geschrieben werden. Es funktioniert, aber es ist viel Arbeit.
Ich habe zu spät herausgefunden, dass man sich die ganzen Unannehmlichkeiten auch sparen kann. In Glade selbst kann man bzgl. eines Fensters (GtkWindow) einige Parameter angeben, u.a. "Destroyable" bzw. "Entfernbar". Klicken wir das auf "Nein", wird der Schliessenknopf passiv und wir können alle User-Aktionen über einen expliziten Close-Button leiten, der lediglich hide() benutzt, die Widgets an sich unangetastet und jede Idee an einen Destroy in Rauch zerfliessen lässt. (Das Gleiche erreichen wir unter Hinzufügen der Zeile <property name="deletable">False</property>
Dynamische Formeln Das Fraktalprogramm soll ja ein überschaubares Beispielprogramm für GTK/GDK sein. Aus diesem Grund und aus Zeitgründen sind daher einige Teile nicht so ausgebaut, wie sie ausgebaut sein könnten. Reizvoll wäre es z.B., die Programmteile für die einzelnen Fraktaltypen nicht in einzelnen Include-Dateien unterzubringen, sondern in Textpuffern, also entry-Widgets. Was sollen sie denn da?
Nun, man könnte dem Programm einen Freebasic-Compiler mitgeben. Nur die reine fbc.exe. Nachdem der User seine neue Formel editiert hat, wird der Programmtext abgespeichert. Aber nicht als normale bas-Datei, sondern als Vorlage für eine DLL. Diese wird dann aus dem Hauptprogramm heraus kompiliert und anschliessend dynamisch eingebunden. Wie das geht, haben wir schon längst mal gelernt! Sozusagen eine dynamische Plugin-Schnittstelle. Da könnte man viel draus machen.

GTK+ - die neue Welt

Mit diesen beiden Kapiteln haben wir nur einen ersten Blick in die Welt von GTK+ geworfen. Es gibt noch viele Dinge, die man sich erarbeiten kann: