Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Web Development

Programming Graphical Applications with Gtk2-Perl, Part 2


December, 2003: Programming Graphical Applications with Gtk2-Perl, Part 2

Gavin works for the domain registry CentralNic, and was a coauthor of Professional Perl Programming (Wrox Press, 2001). He can be contacted at [email protected].


Gtk+ is a toolkit for creating graphical applications on X11 systems. It has also been ported to Windows and BeOS, and runs on Mac OS X using Apple's X11 server. It was originally created for the GIMP, a popular image-manipulation program, and later became the toolkit for GNOME, the GNU Network Object Model Environment.

In Part 1 of this article, published in TPJ last month, I gave a brief overview of the event-based programming model and discussed some of the most common widgets available. This month, I'll complete the exploration of the Gtk+ widget set and also explore some of the handy time-saving features of Gtk2-Perl.

Container Widgets Continued: Panes

Panes in Gtk+ are analogous to frames in HTML—they break the window into two separate parts, with a bar between them that can be dragged to resize the two parts.

Pane widgets come in two flavors: horizontal and vertical. They are both created in the same way:

my $hpaned = Gtk2::HPaned->new;
my $vpaned = Gtk2::VPaned->new;

Panes can contain precisely two widgets—to reflect this, you add widgets to it using the add1() and add2() methods:

$hpaned->add1($left_widget);
$hpaned->add2($right_widget);

or

$vpaned->add1($top_widget);
$vpaned->add2($bottom_widget);

Listing 8 shows how the paned widgets can be used. The resulting widgets are shown in Figure 8.

Scrolled Windows

Scrolled windows are handy when you want to place a widget inside a scrollable area. Almost all applications have something like this because inevitably, the data the program uses will grow so that it can't all be displayed at once.

Scrolled windows are often used in conjunction with another kind of container called a viewport. Viewports are rarely used without a scrolled window, so I won't talk about them, but you do need to know about them.

Listing 9 is a simple example of scrolled window usage, which results in the window shown in Figure 9. The scrolled window allows us to view the butterfly, even though it's much larger than the window size.

Some widgets have their own scrollbars and don't need a viewport. These include the Gtk2::TextView and Gtk2::TreeView widgets. For these, you just need to use the add() method.

The add_with_viewport() method creates a new viewport, adds the image to it, and then adds it to the scrolled window. The set_policy() method tells the widget under what conditions it should display horizontal and vertical scrollbars, in that order. When set to "automatic," it will display a scrollbar when the child widget is larger than the scrolled window. Other values are "always" and "never."

Event Boxes

Event boxes allow you to add callbacks to widgets for events and signals that they don't support. For example, a Gtk2::Image cannot have any signals connected to it. However, if you place the image inside an event box, you can connect a signal to the event box, as shown in Listing 10 and the corresponding Figure 10.

Manipulating Graphics with Gtk2::Gdk::Pixbuf

The gdk-pixbuf libraries (namespaced as Gtk2::Gdk::Pixbuf in Perl) provide a flexible and convenient way to handle graphics in Gtk+. gdk-pixbuf supports a wide range of image formats, including PNG, JPG, GIF, and XPM. It also has support for animations.

The GdkPixbuf provides some methods for basic image manipulation. However, you may find that libraries such as GD and Image::Magick will be better for more complicated tasks.

Resizing an Image

To resize a pixbuf, use the scale_simple() method:

$pixbuf->scale_simple(
        $destination_pixbuf,
        $new_width,
        $new_height,
        $scaling_type
);

This will resize $pixbuf and place the resulting image into $destination_pixbuf. If you don't want to create a new object, just replace $destination_pixbuf with $pixbuf. $new_width and $new_height are the dimensions desired, in pixels. $scaling_type determines the algorithm to be used: "bilinear" produces better quality images, but is slower than "nearest."

If you want to resize the pixbuf into a new pixbuf object, you must make sure that the destination pixbuf has the same properties as the source. To do this, use the various get_*() methods as arguments to the constructor:

my $destination_pixbuf = Gtk2::Gdk::Pixbuf->new(
        $source_pixbuf->get_colorspace,
        $source_pixbuf->get_has_alpha,
        $source_pixbuf->get_bits_per_sample,
        $source_pixbuf->get_width,
        $source_pixbuf->get_height,
);

Changing the Brightness of a pixbuf

To change the brightness of an image, use the saturate_and_pixelate() method:

$pixbuf->saturate_and_pixelate(
        $destination_pixbuf,
        2,
        0
);

The second argument is the factor by which to brighten or darken the image: If it's less than 1.0, the image is darkened; if it's higher, then the image is brightened. The last argument determines whether the image is "pixelated." This draws a checkerboard over the image as though it were disabled. In this case, we don't want that, so it's set to 0.

As with the example above, the destination pixbuf must have the same properties as the source.

Copying a Region from a pixbuf

You can copy a region of the image out of a pixbuf using the copy_area() method:

$source_pixbuf->copy_area(
        $src_x,
        $src_y,
        $width,
        $height,
        $destination_pixbuf,
        $dest_x,
        $dest_y
);

$src_x and $src_y are the horiztonal and vertical coordinates of the top left corner of the region to be copied. $width and $height define its dimensions. $dest_x and $dest_y are the coordinates in $destination_pixbuf where the copied area will be positioned.

Writing pixbuf Data to Disk

You can write a pixbuf to disk in any format that gdk-pixbuf supports. For example:

$pixbuf->save('image.png' 'png');

writes the pixbuf in PNG format.

Getting the pixbuf of a Stock Icon

Should you need the pixbuf of a stock icon, you can use the render_icon() method of any available widget:

my $stock_pixbuf = $window->render_icon($stock_id, $stock_size);

See "More Information" for references to the allowed values for $stock_id and $stock_size.

Glade

The Rapid Application Development (RAD) tool Glade is a WYSIWYG graphical interface designer. Using it, you can construct an interface in minutes that would take hours to hand craft.

The Glade program produces UI description files in XML format, and the Gtk2::GladeXML module is capable of reading these XML files and constructing your interface at runtime. The advantage in this is immediate—it provides for a separation between logic and presentation that will make your job a lot easier in the future. And if you happen to work with a Human Computer Interface (HCI) expert, they can have complete control over the interface without ever having to touch a line of code, and you can get on with the job of making the program work without having to constantly revise the interface.

Here's a simplified example of how to use a Glade XML file in your program:

#!/usr/bin/perl
use Gtk2 -init;
use Gtk2::GladeXML;
use strict;

my $gladexml = Gtk2::GladeXML->new('example.glade');

$gladexml->signal_autoconnect_from_package('main');

Gtk2->main;

exit;

# this is called when the main window is closed:
sub on_main_window_delete_event {
        Gtk2->main_quit;
}

[snip]

This program takes a Glade file called "example.glade" and creates an interface. When designing your interface, you can define the names of subroutines to handle widget signals, and the signal_autoconnect_from_package() method tells Gtk2::GladeXML which package name the handlers can be found in.

Each widget in the Glade file has a unique name, for example, my_main_window or label5. You can get the widget object for a specific object using the get_widget() method:

my $label = $gladexml->get_widget('label5');
$label->set_text('Hello World!');

You may find that just experimenting with Glade will be a good learning exercise. Glade is also an excellent prototyping utility, so that even if you don't plan to make use of Gtk2::GladeXML, you will find it invaluable for planning how your program will be organized.

Gtk2::SimpleList

As well as providing Perl bindings for the Gtk+ widget set, Gtk2-Perl comes with a couple of extra modules that provide a much more Perlish way of working with some of the more complex widgets. An example is the Model, View, Controller (MVC) system for list and tree widgets. While being extremely powerful, they're a pain to work with, especially since the average application will rarely make use of the full features available.

Gtk2::SimpleList is designed to make working with lists easy. Listing 11 is an example of its usage. You can see what this program looks like in Figure 11.

Gtk2::SimpleList acts as a wrapper around the standard Gtk2::TreeView and inherits all of the latter's methods, so that once your list is created you can manipulate it in the usual way. Data is represented as a Perl list of lists (an array of array references), and can be modified directly without having to redraw the entire list.

In the constructor, we are required to define the columns in the list we will use. We do this using a pseudohash where the column titles are the keys, and their types are the values. The allowed values are "text" for a text string, "int" for an integer, "double" for a double-precision floating-point value and "pixbuf" for a Gtk2::Gdk::Pixbuf object.

We can populate the data in the list either by writing to the $list->{data} value or using a subroutine.

In addition to the methods it inherits from Gtk2::TreeView, Gtk2::SimpleList also provides:

$list = Gtk2::SimpleList->new_from_treeview($treeview, [column_data]);

This creates a list using an already created treeview widget. This is handy for when you're using Glade and such a widget has already been created for you.

@indices = $list->get_selected_indices;

This returns an list of numbers representing the currently selected rows in the list.

$list->set_data_array($arrayref);

This method populates the list data array. $arrayref is a reference to an array of array references.

$list->select(@rows);
$list->unselect(@rows);

These methods select and unselect the rows identified in the @rows array.

$list->set_column_editable($column, $editable);

Cells in the list can be edited by the user—this method can be used to enable or disable this feature. $column is the index of the column, and $editable should be a true value to enable editing, or false to disable it.

Gtk2::SimpleMenu

This module makes it easy to create hierarchical application menus (the File, Edit, View menu at the top of each window). The constructor takes as an argument a reference to an array containing a tree of menus, submenus, and items. (See Listing 12; you can see the resulting program in Figure 12.)

Each item and submenu is represented by an anonymous hash, containing data needed to set up the menu. The various item_types include Branch for a submenu, LastBranch for a right-justified menu, Item for a standard item, StockItem for an item with a stock item (which is then specified in the extra_data key), and RadioItem and CheckItem, which create items with radio buttons and checkboxes, respectively.

You can define a callback for when the item is clicked using the callback key, but you can also specify a callback_action, which is then passed as an argument to the function defined by the default_callback argument in the constructor.

Creating Your Own Widgets

It is very easy to create custom widgets with Gtk2-Perl. All you need to do is create a package that inherits from an existing Gtk+ widget, and extend the functionality. Listing 13 is an example of a simple extension to Gtk2::Button. The resulting program is shown in Figure 13.

The first thing we need to do is declare what package we want to inherit from. This is as easy as

use base 'Gtk2::SomeWidget';

Then we need to override the constructor. But we need to get hold of a Gtk2::SomeWidget object so we can play with it. The $package->SUPER::new expression does this for us—then we can re-bless() it into our own package and start playing.

The Gtk2::SimpleList and Gtk2::SimpleMenu widgets are examples of this kind of custom widget, which is derived from an existing Gtk+ widget. There's also the Gtk2::PodViewer widget, available from CPAN, which provides a Perl POD rendering widget based on Gtk2::TextView.

More Information

This article cannot really cover every aspect of Gtk2-Perl programming, but I hope it has given you enough of an introduction that you will want to find out more. There is certainly a steep learning curve, but once you've reached the top I'm sure you'll find that it was worth it!

To help you on your way, I have compiled a reasonably complete list of references that should make your Gtk2-Perl programming much less painful.

Gtk+ Documentation

There is no direct POD documentation for Gtk2-Perl. This is because the Perl bindings adhere as closely as possible to the original C implementation. The C API is fully documented and is available in two forms:

Online. You can read the full Gtk+ documentation at http://www .gtk.org/api/. This includes documentation for Gtk+, the Gtk+ stock icon set, the Glib general-purpose utility library, GdkPixbuf, and Pango.

Devhelp. Devhelp is an API documentation browser that works natively with the Gtk+ documentation system. As well as having a full index of the documentation, you can also search the function reference. It is available from http://www.imendio.com/projects/ devhelp/, and I thoroughly recommend it.

If you're not familiar with C you may have trouble at first translating the examples from C to Perl. Fear not! The Gtk2-Perl team has documented the C to Perl mapping in the Gtk2::api POD document.

Tutorials

There are a number of tutorials that will help the learning process. Stephen Wilhelm's tutorial on the old Gtk-Perl 1.x series still works with Gtk2-Perl in 90 percent of the examples, and is a pretty complete guide to the available widgets. The URL for his tutorial is http://jodrell.net/files/gtk-perl-tutorial/.

Ross McFarland, one of the Gtk2-Perl developers, has also written a tutorial that is available at http://gtk2-perl.sourceforge.net/ doc/intro/.

General Information

The main Gtk+ web site is at http://www.gtk.org/ and the Gtk2-Perl site is http://gtk2-perl.sf.net/. The Gtk2-Perl site has a great deal of extra information including documentation for Glib, as well as build utilities like ExtUtils::PkgConfig (the perl bindings pkg-config), and information on developing XS bindings and the internals of Gtk2-Perl.

Asking Questions

Gtk2-Perl has a mailing list at http://mail.gnome.org/mailman/ listinfo/gtk-perl-list. There is also an active IRC channel—join #gtk-perl on irc.gnome.org and ask your question.

Installing Gtk2-Perl

If you like what you see here and want to get started with Gtk2-Perl, the first thing you'll need to do is install the Gtk2-Perl modules. You can do this by downloading and compiling the source libraries from the Gtk2-Perl web site, but you can also use the CPAN bundle. With this, installaton is as simple as issuing this command:

perl -MCPAN -e 'install Bundle::Gnome2'

This will install the latest versions of all the available Gtk2-Perl libraries.

TPJ



Listing 8

#!/usr/bin/perl
use Gtk2 -init;
use strict;

my $window = Gtk2::Window->new;
$window->set_title('Paned Example');
$window->signal_connect('delete_event', sub { Gtk2->main_quit });
$window->set_border_width(8);

my $vpaned = Gtk2::VPaned->new;
$vpaned->set_border_width(8);

$vpaned->add1(Gtk2::Button->new('Top Pane'));
$vpaned->add2(Gtk2::Button->new('bottom Pane'));

my $frame = Gtk2::Frame->new('Right Pane');
$frame->add($vpaned);

my $hpaned = Gtk2::HPaned->new;
$hpaned->set_border_width(8);

$hpaned->add1(Gtk2::Button->new('Left Pane'));
$hpaned->add2($frame);

$window->add($hpaned);

$window->show_all;

Gtk2->main;
Back to article


Listing 9
#!/usr/bin/perl
use Gtk2 -init;
use strict;

my $window = Gtk2::Window->new;
$window->set_title('Scrolled Window Example');
$window->signal_connect('delete_event', sub { Gtk2->main_quit });
$window->set_border_width(8);

my $image = Gtk2::Image->new_from_file('butterfly.jpg');

my $scrwin = Gtk2::ScrolledWindow->new;
$scrwin->set_policy('automatic', 'automatic');

$scrwin->add_with_viewport($image);

$window->add($scrwin);

$window->show_all;

Gtk2->main;
Back to article


Listing 10
#!/usr/bin/perl
use Gtk2 -init;
use strict;

my $window = Gtk2::Window->new;
$window->set_title('Event Box Example');
$window->signal_connect('delete_event', sub { Gtk2->main_quit });
$window->set_border_width(8);

my $image = Gtk2::Image->new_from_stock('gtk-dialog-info', 'dialog');

# a Gtk2::Image widget can't have the 'clicked' signal, so this causes
# an error:
$image->signal_connect('clicked', \&clicked);

my $box = Gtk2::EventBox->new;

# but this works!
$box->signal_connect('button_release_event', \&clicked);

$box->add($image);

$window->add($box);

$window->show_all;

Gtk2->main;

sub clicked {
        print "someone clicked me!\n";
}
Back to article


Listing 11
#!/usr/bin/perl
use Gtk2 -init;
use Gtk2::SimpleList;
use strict;

my $window = Gtk2::Window->new;
$window->set_title('List Example');
$window->set_default_size(300, 200);
$window->signal_connect('delete_event', sub { Gtk2->main_quit });
$window->set_border_width(8);

my $list = Gtk2::SimpleList->new(
        'Browser Name'  => 'text',
        'Version'       => 'text',
        'Uses Gecko?'   => 'bool',
        'Icon'          => 'pixbuf'
);

$list->set_column_editable(0, 1);

@{$list->{data}} = (
        [ 'Epiphany',   '1.0.4', 1, Gtk2::Gdk::Pixbuf->new_from_file('epiphany.png') ],
        [ 'Galeon',     '1.3.9', 1, Gtk2::Gdk::Pixbuf->new_from_file('galeon.png') ],
        [ 'Konqueror',  '3.1.4', 0, Gtk2::Gdk::Pixbuf->new_from_file('konqueror.png') ],
);

my $scrwin = Gtk2::ScrolledWindow->new;
$scrwin->set_policy('automatic', 'automatic');

$scrwin->add_with_viewport($list);

my $button = Gtk2::Button->new_from_stock('gtk-ok');
$button->signal_connect('clicked', \&clicked);

my $vbox = Gtk2::VBox->new;
$vbox->set_spacing(8);

$vbox->pack_start($scrwin, 1, 1, 0);
$vbox->pack_start($button, 0, 1, 0);

$window->add($vbox);

$window->show_all;

Gtk2->main;

sub clicked {
        my $selection = ($list->get_selected_indices)[0];
        my @row = @{(@{$list->{data}})[$selection]};
        printf(
                "You selected %s, which is at version %s and %s Gecko.\n",
                $row[0],
                $row[1],
                ($row[2] == 1 ? "uses" : "doesn't use")
        );
        Gtk2->main_quit;
}
Back to article


Listing 12
#!/usr/bin/perl
use Gtk2 -init;
use Gtk2::SimpleMenu;
use strict;

my $window = Gtk2::Window->new;
$window->set_title('Menu Example');
$window->set_default_size(300, 200);
$window->signal_connect('delete_event', sub { Gtk2->main_quit });

my $vbox = Gtk2::VBox->new;

my $menu_tree = [
        _File => {
                item_type => '<Branch>',
                children => [
                        _New => {
                                item_type       => '<StockItem>',
                                callback        => \&new_document,
                                accelerator     => '<ctrl>N',
                                extra_data      => 'gtk-new',
                        },
                        _Save => {
                                item_type       => '<StockItem>',
                                callback        => \&save_document,
                                accelerator     => '<ctrl>S',
                                extra_data      => 'gtk-save',
                        },
                        _Quit => {
                                item_type       => '<StockItem>',
                                callback        => sub { Gtk2->main_quit },
                                accelerator     => '<ctrl>Q',
                                extra_data      => 'gtk-quit',
                        },
                ],
        },
        _Edit => {
                item_type => '<Branch>',
                children => [
                        _Cut => {
                                item_type       => '<StockItem>',
                                callback_action => 0,
                                accelerator     => '<ctrl>X',
                                extra_data      => 'gtk-cut',
                        },
                        _Copy => {
                                item_type       => '<StockItem>',
                                callback_action => 1,
                                accelerator     => '<ctrl>C',
                                extra_data      => 'gtk-copy',
                        },
                        _Paste => {
                                item_type       => '<StockItem>',
                                callback_action => 2,
                                accelerator     => '<ctrl>V',
                                extra_data      => 'gtk-paste',
                        },
                ],
        },
        _Help => {
                item_type => '<LastBranch>',
                children => [
                        _Help => {
                                item_type       => '<StockItem>',
                                callback_action => 3,
                                accelerator     => '<ctrl>H',
                                extra_data      => 'gtk-help',
                        }
                ],
        },
];

my $menu = Gtk2::SimpleMenu->new (
                menu_tree               => $menu_tree,
                default_callback        => \&default_callback,
        );

$vbox->pack_start($menu->{widget}, 0, 0, 0);
$vbox->pack_start(Gtk2::Label->new('Rest of application  here'), 1, 1, 0);

$window->add($vbox);

$window->show_all;

Gtk2->main;

sub new_document {
        print "user wants a new document.\n";
}

sub save_document {
        print "user wants to save.\n";
}

sub default_callback {
        my (undef, $callback_action, $menu_item) = @_;
        print "callback action number $callback_action\n";
}
Back to article


Listing 13
#!/usr/bin/perl
use Gtk2 -init;
use strict;

my $window = Gtk2::Window->new;
$window->set_title('Custom Widget Example');
$window->set_default_size(150, 100);
$window->signal_connect('delete_event', sub { Gtk2->main_quit });
$window->set_border_width(8);

my $button = Gtk2::FunnyButton->new(
        normal  => 'Click me!',
        over    => 'Go on, click me!',
        clicked => 'Click me harder!',
);

$window->add($button);

$window->show_all;

Gtk2->main;

package Gtk2::FunnyButton;
use base 'Gtk2::Button';
use strict;

sub new {
        my ($package, %args) = @_;
        my $self = $package->SUPER::new;
        bless($self, $package);
        $self->{labels} = \%args;
        $self->add(Gtk2::Label->new($self->{labels}{'normal'}));
        $self->signal_connect('enter_notify_event', sub { $self->child->set_text($self->{labels}{'over'}) });
        $self->signal_connect('leave_notify_event', sub { $self->child->set_text($self->{labels}{'normal'}) });
        $self->signal_connect('clicked', sub { $self->child->set_text($self->{labels}{'clicked'}) });
        return $self;
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.