Wednesday, March 26, 2014

Getting Started with GtkAda - Drawing

Many widgets, like buttons, do all their drawing themselves. You just tell them the label you want to see, and they figure out what font to use, draw the button outline and focus rectangle, etc. Sometimes, it is necessary to do some custom drawing. In that case, a Gtk_Drawing_Area might be the right widget to use. It offers a canvas on which you can draw by connecting to the "draw" signal.

The contents of a widget often need to be partially or fully redrawn, e.g. when another window is moved and uncovers part of the widget, or when tie window containing it is resized. It is also possible to explicitly cause part or all of the widget to be redrawn, by calling Gtk.Widget.Queue_Draw() or its variants. GtkAda takes care of most of the details by providing a ready-to-use cairo context to the ::draw signal handler.

The following example shows a ::draw signal handler. It is a bit more complicated than the previous examples, since it also demonstrates input event handling by means of ::button-press and ::motion-notify handlers.

src/draw.adb
with Gtk.Window;       use Gtk.Window;
with Gtk.Frame;        use Gtk.Frame;
with Gtk.Button;       use Gtk.Button;
with Gtk.Drawing_Area; use Gtk.Drawing_Area;
with Gtkada.Handlers;  use Gtkada.Handlers;
with Gdk.Event;        use Gdk.Event;
with draw_cb; use draw_cb;

with Gtk.Main;
with Gtk.Enums;

procedure Draw is
   Win   : Gtk_Window;
   Frame : Gtk_Frame;
   Da    : Gtk_Drawing_Area;
begin
   --  Initialize GtkAda.
   Gtk.Main.Init;

   -- create a top level window
   Gtk_New (Win);
   Win.Set_Title ("Drawing Area");
   -- set the border width of the window
   Win.Set_Border_Width (8);
   -- connect the "destroy" signal
   Win.On_Destroy (main_quit'Access);

   -- create a frame
   Gtk_New (Frame);
   Frame.Set_Shadow_Type (Gtk.Enums.Shadow_In);
   Win.Add (Frame);

   Gtk_New (Da);
   -- set a minimum size
   Da.Set_Size_Request (100, 100);
   Frame.Add (Da);

   -- Signals used to handle the backing surface
   Da.On_Draw (draw_cb.draw_cb'Access);
   Da.On_Configure_Event (configure_event_cb'Access);
   -- Event signals
   Da.On_Motion_Notify_Event (motion_notify_event_cb'Access);
   Da.On_Button_Press_Event (button_press_event_cb'Access);

   -- Ask to receive events the drawing area doesn't normally
   -- subscribe to. In particular, we need to ask for the
   -- button press and motion notify events that want to handle.
   Da.Set_Events (Da.Get_Events or Button_Press_Mask or Pointer_Motion_Mask);

   -- Now that we are done packing our widgets, we show them all
   -- in one go, by calling Win.Show_All.
   -- This call recursively calls Show on all widgets
   -- that are contained in the window, directly or indirectly.
   Win.Show_All;

   -- All GTK applications must have a Gtk.Main.Main. Control ends here
   -- and waits for an event to occur (like a key press or a mouse event),
   -- until Gtk.Main.Main_Quit is called.
   Gtk.Main.Main;
end Draw;
src/draw_cb.ads:
with Gtk.Widget;  use Gtk.Widget;
with Gtk.Button;  use Gtk.Button;
with Glib.Object;

with Gdk.Event;
with Cairo;

package draw_cb is
   procedure main_quit (Self : access Gtk_Widget_Record'Class);

   function draw_cb
     (Self : access Gtk_Widget_Record'Class;
      Cr   : Cairo.Cairo_Context)
      return Boolean;

   -- Create a new surface of the appropriate size to store our scribbles
   function configure_event_cb
     (Self  : access Gtk_Widget_Record'Class;
      Event : Gdk.Event.Gdk_Event_Configure)
      return  Boolean;

   -- Handle motion events by continuing to draw if button 1 is
   -- still held down. The ::motion-notify signal handler receives
   -- a GdkEventMotion struct which contains this information.
   function motion_notify_event_cb
     (Self  : access Gtk_Widget_Record'Class;
      Event : Gdk.Event.Gdk_Event_Motion)
      return  Boolean;

   -- Handle button press events by either drawing a rectangle
   -- or clearing the surface, depending on which button was pressed.
   -- The ::button-press signal handler receives a GdkEventButton
   -- struct which contains this information.
   function button_press_event_cb
     (Self  : access Gtk_Widget_Record'Class;
      Event : Gdk.Event.Gdk_Event_Button)
      return  Boolean;
end draw_cb;
src/draw_cb.adb:
with Glib;       use Glib;

with Gdk.Types;  use Gdk.Types;
with Gdk.Window;
with Gtk.Main;

package body draw_cb is

   -- Button defintions from gtk-3.0/gdk/gdkevents.h
   -- The primary button. This is typically the left button, or the
   -- right button in a left-handed setup.
   Gdk_Button_Primary : constant := 1;
   --  The secondary button. This is typically the right mouse button, or the
   -- left button in a left-handed setup.
   Gdk_Button_Secondary : constant := 3;

   surface : Cairo.Cairo_Surface;
   use type Cairo.Cairo_Surface;

   procedure main_quit (Self : access Gtk_Widget_Record'Class) is
   begin
      if surface /= Cairo.Null_Surface then
         Cairo.Surface_Destroy (surface);
      end if;

      Gtk.Main.Main_Quit;
   end main_quit;

   procedure Clear_Surface is
      Cr : Cairo.Cairo_Context;
   begin
      Cr := Cairo.Create (surface);
      Cairo.Set_Source_Rgb (Cr, 1.0, 1.0, 1.0);
      Cairo.Paint (Cr);
      Cairo.Destroy (Cr);
   end Clear_Surface;

   -- Redraw the screen from the surface. Note that the ::draw
   -- signal receives a ready-to-be-used cairo_t that is already
   -- clipped to only draw the exposed areas of the widget
   function draw_cb
     (Self : access Gtk_Widget_Record'Class;
      Cr   : Cairo.Cairo_Context)
      return Boolean
   is
   begin
      Cairo.Set_Source_Surface (Cr, surface, 0.0, 0.0);
      Cairo.Paint (Cr);
      return False;
   end draw_cb;

   function configure_event_cb
     (Self  : access Gtk_Widget_Record'Class;
      Event : Gdk.Event.Gdk_Event_Configure)
      return  Boolean
   is
   begin
      if surface /= Cairo.Null_Surface then
         Cairo.Surface_Destroy (surface);
      end if;
      surface :=
         Gdk.Window.Create_Similar_Surface
           (Self.Get_Window,
            Cairo.Cairo_Content_Color,
            Self.Get_Allocated_Width,
            Self.Get_Allocated_Height);
      -- Initialize the surface to white
      Clear_Surface;

      -- We've handled the configure event, no need for further processing.
      return True;
   end configure_event_cb;

   -- Draw a rectangle on the surface at the given position
   procedure draw_brush
     (Self : access Gtk_Widget_Record'Class;
      x    : Gdouble;
      y    : Gdouble)
   is
      Cr : Cairo.Cairo_Context;
   begin

      -- Paint to the surface, where we store our state
      Cr := Cairo.Create (surface);
      Cairo.Rectangle (Cr, x - 3.0, y - 3.0, 6.0, 6.0);
      Cairo.Fill (Cr);
      Cairo.Destroy (Cr);

      -- Now invalidate the affected region of the drawing area.
      Self.Queue_Draw_Area (Gint (x - 3.0), Gint (y - 3.0), 6, 6);
   end draw_brush;

   function motion_notify_event_cb
     (Self  : access Gtk_Widget_Record'Class;
      Event : Gdk.Event.Gdk_Event_Motion)
      return  Boolean
   is
   begin
      -- paranoia check, in case we haven't gotten a configure event
      if surface = Cairo.Null_Surface then
         return False;
      end if;

      if (Event.State and Gdk.Types.Button1_Mask) > 0 then
         draw_brush (Self, Event.X, Event.Y);
      end if;

      -- We've handled it, stop processing
      return True;
   end motion_notify_event_cb;

   function button_press_event_cb
     (Self  : access Gtk_Widget_Record'Class;
      Event : Gdk.Event.Gdk_Event_Button)
      return  Boolean
   is
   begin
      -- paranoia check, in case we haven't gotten a configure event
      if surface = Cairo.Null_Surface then
         return False;
      end if;

      if Event.Button = Gdk_Button_Primary then
         draw_brush (Self, Event.X, Event.Y);
      elsif Event.Button = Gdk_Button_Secondary then
         Clear_Surface;
         Self.Queue_Draw;
      end if;
      -- We've handled the event, stop processing
      return True;
   end button_press_event_cb;
end draw_cb;
draw.gpr:
with "gtkada";

project Draw is

   for Source_Dirs use ("src");
   for Object_Dir use "obj";
   for Main use ("draw.adb");

   --  Enable Ada 2005.
   package Compiler is
      for Default_Switches ("ada") use ("-gnat05");
   end Compiler;

end Draw;
To compile:
gprbuild -P draw

This program is more complicated and it shows a problem with the current GtkAda (3.8.3) binding.

There is no button definition for GDK_BUTTON_PRIMARY and GDK_BUTTON_SECONDARY, etc., which have to be defined in package draw_cb ourselves by taking the definition from the gtk-3.0/gdk/gdkevents.h. As of 2013-03-26, the SVN source tree has been updated after I reported the bug, it now contains the button definitions in gdk-event.ads as Button_Primary, Button_Secondary and Button_MIddle, if you are using the SVN source after the day, you may want to use that definition directly.

The cairo surface variable is defined in the draw_cb package body, it has the similar effect of the C static definition that only procedures and functions in the package body could see it.

We use use type Cairo.Cairo_Surface; in the draw_cb package body, so that /= and = could be used directly on the type without exposing other Cairo definitions.

No comments: