17.7.4. Tk::Thumbnail
To appreciate the power of Photos,
let's examine a Thumbnail widget. Thumbnails are shrunken
images of larger (or zoomed images of smaller) pictures, typically
arranged in a tabular format. Looking at Figure 17-11, Tk::Thumbnail's rendered POD
documentation, we see the thumbnail width and height is selectable,
the images can be labeled, and there's a
-command option so we can supply a
<Button-1> callback. The list of images (of
any supported format) can be a mixture of filenames and/or existing
Photo images. Tk::Thumbnail is responsible for disposing of any
Photos it creates, thus it uses an OnDestroy
callback to perform object cleanup.
Figure 17-11. POD documentation for Tk::Thumbnail
Given a desired thumbnail width and
height, Tk::Thumbnail takes each image in turn and either shrinks it
(via the copy -subsample
method, described shortly) or enlarges it (via the
copy -zoom method). This means
that images of any size and format can be combined in the same
Thumbnail widget.
This sample code, tkthumb, created Figure 17-12:
use Tk::Thumbnail;
my $mw = MainWindow->new;
my $skel = $mw->Photo(-file => 'tkskel.gif');
my $tn = $mw->Thumbnail(
-images => ['mouse.xbm', $skel, <*.ppm>],
-command => sub {
my $i = $_[0]->cget(-image);
print "args=@_, image=$i\n";
},
@ARGV);
$tn->pack;
The list of images consists of an XBM filename of the small mouse
cursor, a large GIF Photo image, and a file glob of lots of
medium-sized PPM images of the neko. In real life, the
-command callback might actually do something when
clicked over a thumbnail, but in tkthumb, it
simply prints an informative message. Finally, note that
@ARGV holds the command-line arguments, so we can
run the program with different options and experiment with the
Thumbnail widget.
Figure 17-12. A mixture of image formats and sizes in one Thumbnail
From the discussions in Chapter 14, "Creating Custom Widgets in Pure Perl/Tk", it should be
clear that Tk::Thumbnail is a derived mega-widget based on Tk::Table.
By the time Populate is called, Perl/Tk has
already created the Table, $self, for us and
blessed it into class Tk::Thumbnail.
$Tk::Thumbnail::VERSION = '1.0';
package Tk::Thumbnail;
use Carp;
use File::Basename;
use Tk::widgets qw/Table JPEG PNG TIFF/;
use base qw/Tk::Derived Tk::Table/;
use subs qw/free_photos/;
use strict;
Construct Tk::Widget 'Thumbnail';
Populate removes from the argument hash those
options that are specific to the Thumbnail and needed only within
itself. Scrollbars have proven to be distracting, so they're
disabled unless the user specifically requests them. The thumbnail
pixel dimensions also have default values unless overridden by
options on the constructor call.
sub Populate {
my($self, $args) = @_;
my $img = delete $args->{-images}; # reference to array of images
my $lbl = delete $args->{-labels}; # display file names IFF true
my $pxx = delete $args->{-width}; # thumbnail pixel width
my $pxy = delete $args->{-height}; # thumbnail pixel height
$args->{-scrollbars} = '' unless defined $args->{-scrollbars};
$pxx ||= 32;
$pxy ||= 32;
croak "Tk::Thumbnail: -images argument is required." unless defined $img;
$self->SUPER::Populate($args);
This code computes the minimum dimensions required to fit all the
thumbnail images in a square Table.
my $count = scalar @$img;
my $rows = int(sqrt $count);
$rows++ if $rows * $rows != $count;
For each element of the image array, we invoke
UNIVERSAL::isa to test if it's already a
Photo or if we need to create the Photo ourselves. (We could have
used the ref function, but isa
will detect derived Photo classes.) Once we have a Photo reference in
$photo, we determine its width and height so we
know whether to shrink or expand it to thumbnail size.
THUMB:
foreach my $r (1 .. $rows) {
foreach my $c (1 .. $rows) {
last THUMB if --$count < 0;
my $i = @$img[$#$img - $count];
my ($photo, $w, $h);
$photo = UNIVERSAL::isa($i, 'Tk::Photo') ? $i :
$self->Photo(-file => $i);
($w, $h) = ($photo->width, $photo->height);
We start by making an empty Photo, $subsample,
which will receive pixels from the main Photo,
$photo. To shrink a picture, we subsample it,
which means we extract every nth pixel from the
source Photo before copying to the destination Photo. For example, if
the source Photo is 64x64, we subsample every other pixel to reduce
it to a 32x32 thumbnail. If the source Photo is 320x320, we subsample
every tenth bit, and so on. We do this for both x and y. The special
value -1 tells us not to subsample a particular dimension, keeping
its size unchanged, and gives us one way to change the
Thumbnail's aspect ratio.
my $subsample = $self->Photo;
my $sw = $pxx == -1 ? 1 : ($w / $pxx);
my $sh = $pxy == -1 ? 1 : ($h / $pxy);
So the variables $sw and $sh
are typically integers greater than one, specifying how many pixels
to subsample in width and height. If that's the case, invoke a
subsample copy from $photo to
$subsample, creating a shrunken thumbnail. But if
the source Photo is smaller than the thumbnail size, the subsample
width and height are fractional and we need to enlarge the Photo, so
we zoom in for a closer look.
Finally, a little bookkeeping is required. We're responsible
for deleting every thumbnail when the widget is destroyed, so we keep
a list of Photos in an instance variable.
if ($sw >= 1 and $sh >= 1) {
$subsample->copy($photo, -subsample => ($sw, $sh));
} else {
$subsample->copy($photo, -zoom => (1 / $sw, 1 / $sh));
}
push @{$self->{photos}}, $subsample;
This uneventful code stuffs the thumbnail image in a Label and
put s it in the Table widget, optionally adding a
Label with the thumbnail's filename. The
bind statement arranges for the user's
-command callback (if any) to be invoked on a
<Button-1> event over the thumbnail. Lastly,
delete the source Photo, assuming we created it in the first place.
my $f = $self->Frame;
my $l = $f->Label(-image => $subsample)->grid;
my $file = $photo->cget(-file);
$l->bind('<Button-1>' => [$self => 'Callback', '-command',
$l, $file]);
$f->Label(-text => basename($file))->grid if $lbl;
$self->put($r, $c, $f);
$photo->delete unless UNIVERSAL::isa($i, 'Tk::Photo');
} # forend columns
} #forend rows
Here's a rather typical ConfigSpecs call.
See Chapter 14, "Creating Custom Widgets in Pure Perl/Tk" for details.
$self->ConfigSpecs(
-font => ['DESCENDANTS',
'font', 'Font', 'fixed'],
-background => [['DESCENDANTS', 'SELF'],
'background', 'Background', undef],
-command => ['CALLBACK',
'command', 'Command', undef],
);
When an OnDestroy callback is invoked, it's
guaranteed to have an intact mega-widget hash, including all instance
variables. The object method free_photos then has
a chance to delete all the thumbnail Photos.
$self->OnDestroy([$self => 'free_photos']);
} # end Populate
sub free_photos {
# Free all our subsampled Photo images.
foreach my $photo (@{$_[0]->{photos}}) {
$photo->delete;
}
} # end free_photos
1;
Figure 17-13 and Figure 17-14
demonstrate the flexibility of the Photo copy
method.
Figure 17-13. tkthumb -width 8 -height 8
Figure 17-14. tkthumb -width 64 -height 32 -labels 1 -background white