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.
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?
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.
Am elegantesten bewegen wir so eine Hintergrund-Bitmap mit der Maus. Dazu müssen wir drei Ereignisse abfangen:
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>
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).
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
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
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:
|
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:wenddafü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 |
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. |
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: