Saturday, March 22, 2014

Getting started with GtkAda - Building user interfaces

When constructing a more complicated user interface, with dozens or hundreds of widgets, doing all the setup work in Ada code is cumbersome, and making changes becomes next to impossible.

Thankfully, GtkAda supports the separation of user interface layout from your business logic, by using UI descriptions in an XML format that can be parsed by the Gtk_Builder class.

This example is exactly like the grid example, but using the Gtk_Builder class.

src/builder.adb:
with Gtk.Builder; use Gtk.Builder;
with Gtk.Window; use Gtk.Window;
with Gtk.Button; use Gtk.Button;
with Gtkada.Handlers;  use Gtkada.Handlers;

with builder_cb; use builder_cb;
with Glib; use Glib;
with Glib.Error; use Glib.Error;
with Gtk.Main;

procedure Builder is
   Win   : Gtk_Window;
   Button : Gtk_Button;
   builder : Gtk_Builder;
   ret : GUint;
   error : aliased GError;
begin
   --  Initialize GtkAda.
   Gtk.Main.Init;

   -- construct a Gtk_Builder instance and load our UI description
   Gtk_New (Builder);
   ret := Builder.Add_From_File ("builder.ui", error'Access);

   -- connect signal handlers to the constructed widgets
   Win := Gtk_Window (Builder.Get_Object ("window"));
   -- connect the "destroy" signal
   Win.On_Destroy (main_quit'Access);

   button := Gtk_Button (Builder.Get_Object ("button1"));
   button.On_Clicked (button_clicked'Access);

   button := Gtk_Button (Builder.Get_Object ("button2"));
   button.On_Clicked (button_clicked'Access);

   button := Gtk_Button (Builder.Get_Object ("quit"));
   -- connect the "clicked" signal of the button to destroy function
   Widget_Callback.Object_Connect
     (Button,
      "clicked",
      Widget_Callback.To_Marshaller (button_quit'Access),
      Win);
   -- 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 Builder;
src/builder_cb.ads:
with Gtk.Widget;  use Gtk.Widget;
with Gtk.Button; use Gtk.Button;

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

   procedure button_clicked (Self : access Gtk_Button_Record'Class);
   procedure button_quit (Self : access Gtk_Widget_Record'Class);
end builder_cb;
src/builder_cb.adb:
with Ada.Text_IO; use Ada.Text_IO;
with Gtk.Main;

package body builder_cb is

   procedure main_quit (Self : access Gtk_Widget_Record'Class) is
   begin
      Gtk.Main.Main_Quit;
   end main_quit;

   procedure button_clicked (Self : access Gtk_Button_Record'Class) is
   begin
      Put_Line ("Hello World!");
   end button_clicked;

   procedure button_quit (Self : access Gtk_Widget_Record'Class) is
   begin
      Put_Line ("buttion_quit is called");
      Destroy (Self);
   end button_quit;
end builder_cb;
builder.gpr:
with "gtkada";

project Builder is

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

   -- Enable Ada 2005.
   package Compiler is
     for Default_Switches ("ada") use ("-gnat05");
   end Compiler;
end Builder
builder.ui:
<interface> 
  <object id="window" class="GtkWindow"> 
    <property name="visible">True</property> 
    <property name="title">Grid</property> 
    <property name="border-width">10</property> 
    <child> 
      <object id="grid" class="GtkGrid"> 
        <property name="visible">True</property> 
        <child> 
          <object id="button1" class="GtkButton"> 
            <property name="visible">True</property> 
            <property name="label">Button 1</property> 
          </object> 
          <packing> 
            <property name="left-attach">0</property> 
            <property name="top-attach">0</property> 
          </packing> 
        </child> 
        <child> 
          <object id="button2" class="GtkButton"> 
            <property name="visible">True</property> 
            <property name="label">Button 2</property> 
          </object> 
          <packing> 
            <property name="left-attach">1</property> 
            <property name="top-attach">0</property> 
          </packing> 
        </child> 
        <child> 
          <object id="quit" class="GtkButton"> 
            <property name="visible">True</property> 
            <property name="label">Quit</property> 
          </object> 
          <packing> 
            <property name="left-attach">0</property> 
            <property name="top-attach">1</property> 
            <property name="width">2</property> 
          </packing> 
        </child> 
      </object> 
      <packing> 
      </packing> 
    </child> 
  </object> 
</interface>
To compile:
gprbuild -P builder

Note that Gtk_Builder can also be used to construct objects that are not widgets, such as tree models, adjustments, etc. That is the reason the method we use here is called Get_Object and returns a Glib.Object.GObject instead of a Gtk_Widget. That is why we need to cast the return value of Get_Object to the corresponding type to pass the compilation.

Normally, you would pass a full path to Add_From_File to make the execution of your program independent of the current directory. A common location to install UI descriptions and similar data is /usr/share/appname.

It is also possible to embed the UI description in the source code as a string and use Add_From_String to load it. But keeping the UI description in a separate file has several advantages: It is then possible to make minor adjustments to the UI without recompiling your program, and, more importantly, graphical UI editors such as glade can load the file and allow you to create and modify your UI by point-and-click.

There is one thing in this example differ from the C version. In the C example, gtk_builder_add_from_file() is called with a NULL pointer for the error parameter, in GtkAda version, the Error is a must, otherwise, you will get constraint error:

raised CONSTRAINT_ERROR : gtk-builder.adb:151 access check failed

No comments: