Re: How to construct a tree that every nodes has an icon



I wrote several examples to show how to use TreeView as list or as
tree. I hope this will help others to avoid ask the simple question again.

The purpose of these examples is to show the contents of certain
directory.First, the simplest one, show the contents of the directory as
a list.

   use Gtk2 '-init';
   use Glib qw(TRUE FALSE);
   use Cwd;
   use constant {
       FILE_ICON  => 0,
       FILE_NAME  => 1,
       FILE_MTIME => 2,
   };

   my $dir = shift || getcwd;
   unless ( -d $dir ) {
       die "$dir is not a directory or doesn't exist!\n";
   }
   my $window = Gtk2::Window->new('toplevel');
   $window->signal_connect('delete_event' => sub { Gtk2->main_quit; });
   ret_vbox($window, $dir);
   $window->show();
   Gtk2->main;

   sub ret_vbox {
       my $top = shift;
       my $dir = shift;
       $top->set_title('Directory contents of '. $dir);
       my $vbox = Gtk2::VBox->new(FALSE, 4);
       my $sw = Gtk2::ScrolledWindow->new;
       $sw->set_size_request (600, 400);
       $sw->set_policy ('automatic', 'automatic');
       # create TreeView
       my $model = create_model($dir);
       my $treeview = Gtk2::TreeView->new_with_model($model);
       add_columns($treeview);
       $sw->add($treeview);
       $vbox->pack_start($sw,TRUE,TRUE,0);
       $vbox->show_all();
       $top->add($vbox);
   }

   sub create_model {
       my $dir = shift;
       my $model = Gtk2::ListStore->new(
           'Glib::String', 'Glib::String', 'Glib::String'
       );
       opendir(DIR, $dir) or die "Can't open directory $dir: $!";
       foreach my $file( readdir(DIR) ) {
           next if $file =~ /^[.]/; # ignore '.', '..' and hiden files
           my $iter = $model->append();
           $model->set(
               $iter,
FILE_ICON, ( -d "$dir/$file" ? 'gtk-directory' : 'gtk-file' ),
               FILE_NAME, $file,
               FILE_MTIME,  scalar(localtime((stat("$dir/$file"))[9]))
           );
       }
       return $model;
   }

   sub add_columns {
       my $treeview = shift;
       ## Add first column
       my $column1 = Gtk2::TreeViewColumn->new();
       $column1->set_title('file name');
       ### first cell for column one
       my $icon = Gtk2::CellRendererPixbuf->new();
       $column1->pack_start($icon, FALSE);
       $column1->set_attributes( $icon, 'stock-id' => FILE_ICON );
       ### second cell for column one
       my $name = Gtk2::CellRendererText->new();
       $column1->pack_start($name, FALSE);
       $column1->set_attributes( $name, 'text' => FILE_NAME );
       $column1->set_sort_column_id(FILE_NAME);
       $treeview->append_column($column1);
       ## Add second column
       my $column2 = Gtk2::TreeViewColumn->new();
       $column2->set_title("modify time");
       my $mtime = Gtk2::CellRendererText->new();
       $column2->pack_start($mtime, FALSE);
       $column2->set_attributes($mtime, 'text' => FILE_MTIME);
       $column2->set_sort_column_id(FILE_MTIME);
       $treeview->append_column($column2);
   }

These are standard operations to construct a TreeView:
 1. create a TreeModel object using ListStore or TreeStore with several
    data types. Note that the columns of TreeModel may or may not
    appear in TreeViewColumn.
 2. Create a TreeView using the TreeModel
 3. Create one ore more TreeViewColumn and append them to the
    TreeView. For each TreeViewColumn, you can add one or more
    CellRender to it. Each CellRender can set attribute according
    to any column of TreeModel or set by a function.

The third one also can be done by `insert_column_with_attributes' or
`insert_column_with_data_func' if the column has only one cell. For
example, to insert the second column, it can be rewrite as:

    ## Add second column
    $treeview->insert_column_with_attributes(
        1, "modify time",
        Gtk2::CellRendererText->new(),
        'text' => FILE_MTIME
    );
    $treeview->get_column(1)->set_sort_column_id(FILE_MTIME);

To sort the column by mtime, the TreeModel have to change, The
FILE_MTIME should keep the mtime of the file as integer not the text.
To show the date more readable, I use Date::Format::strftime to show
the mtime:

   sub create_model {
       my $dir = shift;
       my $model = Gtk2::ListStore->new(
           'Glib::String', 'Glib::String', 'Glib::Int'
       );
       opendir(DIR, $dir) or die "Can't open directory $dir: $!";
       foreach my $file ( readdir(DIR) ) {
           my $iter = $model->append();
           $model->set(
               $iter,
FILE_ICON, ( -d "$dir/$file" ? 'gtk-directory' : 'gtk-file' ),
               FILE_NAME, $file,
               FILE_MTIME, (stat("$dir/$file"))[9]
           );
       }
       return $model;
   }

   sub add_columns {
       use Date::Format;
       my $treeview = shift;
       ## Add first column
       my $column1 = Gtk2::TreeViewColumn->new();
       $column1->set_title('file name');
       ### first cell for column one
       my $icon = Gtk2::CellRendererPixbuf->new();
       $column1->pack_start($icon, FALSE);
       $column1->set_attributes( $icon, 'stock-id' => FILE_ICON );
       ### second cell for column one
       my $name = Gtk2::CellRendererText->new();
       $column1->pack_start($name, FALSE);
       $column1->set_attributes( $name, 'text' => FILE_NAME );
       $column1->set_sort_column_id(FILE_NAME);
       $treeview->append_column($column1);
       ## Add second column
       $treeview->insert_column_with_data_func(
           1, "modify time",
           Gtk2::CellRendererText->new(),
           sub {
               my ($tree_column, $cell, $model, $iter) = @_;
               my ($mtime) = $model->get ($iter, FILE_MTIME);
               my @lc = localtime($mtime);
               $cell->set (text => strftime("%c", @lc));
           }
       );
       $treeview->get_column(1)->set_sort_column_id(FILE_MTIME);
   }

If we want display the contents of subdirectory as well, it is better
to use TreeStore instead of ListStore. What we have to do, it to
change the `create_model' functon:

   sub read_dir {
       my $model = shift;
       my $dir = shift;
       my $iter = shift;
       opendir(DIR, $dir) or die "Can't open directory $dir: $!";
       foreach my $file ( readdir(DIR) ) {
           next if $file =~ /^[.]/;
           my $full = "$dir/$file";
           my $iter_child = $model->append($iter);
           if ( -d $full ) {
               $model->set(
                   $iter_child,
                   FILE_ICON, 'gtk-directory',
                   FILE_NAME, $file,
                   FILE_MTIME, (stat("$dir/$file"))[9]
               );
               read_dir($model, $full, $iter_child);
           } else {
               $model->set(
                   $iter_child,
                   FILE_ICON, 'gtk-file',
                   FILE_NAME, $file,
                   FILE_MTIME, (stat("$dir/$file"))[9]
               );
           }
       }
   }

   sub create_model {
       my $dir = shift;
       my $model = Gtk2::TreeStore->new(
           'Glib::String', 'Glib::String', 'Glib::Int'
       );
       read_dir($model, $dir);
       return $model;
   }

The contents of the directory will read into memory at start up. This
may take quit a lot time if the directory contains many files. It is
better to expand the directory when we want to. So the signal
row-expanded is what we need. First we need add a column to TreeModel
so that we can easy get the full file name of the TreeIter.

   use constant {
       FILE_ICON  => 0,
       FILE_NAME  => 1,
       FILE_MTIME => 2,
   };

The TreeModel is very simple now:

   sub create_model {
       my $dir = shift;
       my $model = Gtk2::TreeStore->new(
           'Glib::String', 'Glib::String', 'Glib::String', 'Glib::Int'
       );
       return $model;
   }

Then we connect the signal and open the $dir in function `ret_vbox':

    $treeview->signal_connect( 'row-expanded' => \&expand_dir );
    open_dir($model, $dir, undef);

Add two new function:

   sub expand_dir {
       my ($treeview,$iter,$path,$self) = @_;
       my $model = $treeview->get_model();
       my $dir = $model->get($iter, FILE_FULE_NAME);
       my $first = $model->iter_nth_child($iter, 0);
       unless ( defined $model->get($first, FILE_NAME) ) {
           open_dir($model, $dir, $iter);
           $model->remove($first);
       }
       return FALSE;
   }

   sub open_dir {
       my ($model, $dir, $parent_iter) = @_;
       opendir(DIR, $dir) or die "Can't open directory $dir: $!";
       foreach my $file ( readdir(DIR) ) {
           next if $file =~ /^[.]/;
           my $full = "$dir/$file";
           my $iter = $model->append($parent_iter);
           $model->set(
               $iter,
               FILE_ICON, ( -d $full ? 'gtk-directory' : 'gtk-file' ),
               FILE_NAME, $file,
               FILE_FULE_NAME, $full,
               FILE_MTIME, (stat($full))[9]
           );
           if ( -d $full ) {
               my $dummy = $model->append($iter);
           }
       }
       my $dummy = $model->iter_nth_child($parent_iter, 0);
       unless ( $model->get($dummy, FILE_NAME) ) {
           $model->remove($dummy);
       }
       return $model;
   }

If you want the file list looks better, you may want sort the contents
by name but the directory always list first. What we need to do is to
add an sort_func to the TreeModel. Add the following lines to the
function `add_column':

    # Set customized sort funciton
    my $model = $treeview->get_model;
    $model->set_sort_func(
        FILE_NAME,
        sub {
            my ($model, $itera, $iterb) = @_;
            if ( $model->get($itera, FILE_NAME)
              && $model->get($iterb, FILE_NAME)
              && $model->get($itera, FILE_ICON)
              && $model->get($iterb, FILE_ICON)
                 ) {
      $model->get($itera, FILE_ICON) cmp $model->get($iterb, FILE_ICON)
   || $model->get($itera, FILE_NAME) cmp $model->get($iterb, FILE_NAME);
            }
        });
    $model->set_sort_column_id(FILE_NAME, $column1->get_sort_order);
    $model->sort_column_changed();

Ok, this is the final version(Encode module is used to support file
name with multibyte characters):

use Gtk2 '-init';
use Glib qw(TRUE FALSE);
use Encode qw/encode decode/;
use constant {
    FILE_ICON  => 0,
    FILE_NAME  => 1,
    FILE_FULE_NAME => 2,
    FILE_MTIME => 3,
};

my $window = Gtk2::Window->new('toplevel');
$window->signal_connect('delete_event' => sub { Gtk2->main_quit; });
ret_vbox($window);
$window->show();
Gtk2->main;

sub ret_vbox {
    my $top = shift;
    my $dir = '.';
    $top->set_title('Directory contents of '. $dir);
    my $vbox = Gtk2::VBox->new(FALSE, 4);
    my $sw = Gtk2::ScrolledWindow->new;
    $sw->set_size_request (600, 400);
    $sw->set_policy ('automatic', 'automatic');
    # create TreeView
    my $model = create_model($dir);
    my $treeview = Gtk2::TreeView->new_with_model($model);
    open_dir($model, $dir, undef);
    add_columns($treeview);
    $treeview->signal_connect( 'row-expanded' => \&expand_dir );
    $sw->add($treeview);
    $vbox->pack_start($sw,TRUE,TRUE,0);
    $vbox->show_all();
    $top->add($vbox);
}

sub create_model {
    my $dir = shift;
    my $model = Gtk2::TreeStore->new(
        'Glib::String', 'Glib::String', 'Glib::String', 'Glib::Int'
    );
    return $model;
}

sub expand_dir {
    my ($treeview,$iter,$path,$self) = @_;
    my $model = $treeview->get_model();
    my $dir = $model->get($iter, FILE_FULE_NAME);
    my $first = $model->iter_nth_child($iter, 0);
    unless ( defined $model->get($first, FILE_NAME) ) {
        open_dir($model, $dir, $iter);
        $model->remove($first);
    }
    return FALSE;
}

sub open_dir {
    my ($model, $dir, $parent_iter) = @_;
    opendir(DIR, $dir) or die "Can't open directory $dir: $!";
    foreach my $file ( readdir(DIR) ) {
        next if $file =~ /^[.]/;
        my $uni_name = decode('utf8', $file);
        my $full = "$dir/$uni_name";
        my $iter = $model->append($parent_iter);
        $model->set(
            $iter,
            FILE_ICON, ( -d $full ? 'gtk-directory' : 'gtk-file' ),
            FILE_NAME, $uni_name,
            FILE_FULE_NAME, $full,
            FILE_MTIME, (stat($full))[9]
        );
        if ( -d $full ) {
            my $dummy = $model->append($iter);
        }
    }
}

sub add_columns {
    use Date::Format;
    my $treeview = shift;
    my $column;
    ## Add first column
    my $column1 = Gtk2::TreeViewColumn->new();
    $column1->set_title('file name');
    ### first cell for column one
    my $icon = Gtk2::CellRendererPixbuf->new();
    $column1->pack_start($icon, FALSE);
    $column1->set_attributes( $icon, 'stock-id' => FILE_ICON );
    ### second cell for column one
    my $name = Gtk2::CellRendererText->new();
    $column1->pack_start($name, FALSE);
    $column1->set_attributes( $name, 'text' => FILE_NAME );
    $column1->set_sort_column_id(FILE_NAME);
    # Set customized sort funciton
    my $model = $treeview->get_model;
    $model->set_sort_func(
        FILE_NAME,
        sub {
            my ($model, $itera, $iterb) = @_;
            if ( $model->get($itera, FILE_NAME)
              && $model->get($iterb, FILE_NAME)
              && $model->get($itera, FILE_ICON)
              && $model->get($iterb, FILE_ICON) ) {
$model->get($itera, FILE_ICON) cmp $model->get($iterb, FILE_ICON) || $model->get($itera, FILE_NAME) cmp $model->get($iterb, FILE_NAME);
            }
        });
    $model->set_sort_column_id(FILE_NAME, $column1->get_sort_order);
    $model->sort_column_changed();
    $treeview->append_column($column1);
    ## Add second column
    $treeview->insert_column_with_data_func(
        1, "modify time",
        Gtk2::CellRendererText->new(),
        sub {
            my ($tree_column, $cell, $model, $iter) = @_;
            my ($mtime) = $model->get ($iter, FILE_MTIME);
            my @lc = localtime($mtime);
            $cell->set (text => strftime("%c", @lc));
        }
    );
    $treeview->get_column(1)->set_sort_column_id(FILE_MTIME);
}

On Mon, 30 Jul 2007 22:18:30 +0800, muppet <scott asofyet org> wrote:

Well, "ugly" is subjective, so what you're saying is unclear. Are you worried
that the icon will not indent with child nodes?  Have you *tried* it?
Sorry for my poor english. I know it is my fault, not gtk-perl. In fact, gtk-perl
is powerful, and interesting, and I spent all the days to learn it.

--
Best regards,
Ye Wenbin



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]