Re: Updating a TextView with the results of a long, processor-intensive process



On Wed, 2004-04-14 at 17:31, Bob Wilkinson wrote:
I have 

  Glib::Idle->add( sub {while (Gtk2->events_pending()) {Gtk2->main_iteration()}});

at the bottom of my GUI code

Er, no.  The main loop runs an idle when it has no events pending. 
Thus, your handler will be invoked when the event loop is empty, and
will do nothing because there are no events pending.

If you want to use the "chew up events" trick, it works like this:

  for (0..$one_hundred_beellyun_dollars) {
     some_operation_that_takes_a_relatively_long_time();
     # run the main loop a few times to update the ui.
     Gtk2->main_iteration while Gtk2->events_pending;
  }



a) put the processing into a child process, communicating via a pipe; then
watch the pipe for events, and update accordingly in the io watch callback.  
this is what you're trying to set up.  (the worker thread model is similar to
this, except you have more communication options.)

Rather than just blather on about how it should work, i decided to write
a little proof of concept example.  This code actually works; the
scroll-to-bottom thing doesn't look so great, but i can't be arsed to
figure out why, as it's getting late and i haven't been sleeping much
lately.  I'll leave that as an exercise for the reader.  :-)

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
#!/usr/bin/perl -w

use strict;
use Glib 1.040, qw(TRUE FALSE);
use Gtk2 -init;
use IO::File;

my @worklist = ();

# <boilerplate type="window with a scrolling textview and a button">
my $window = Gtk2::Window->new;
$window->signal_connect (delete_event => sub {exit});
$window->set_default_size (400, 300);
my $vbox = Gtk2::VBox->new;
$window->add ($vbox);
my $scroller = Gtk2::ScrolledWindow->new;
$vbox->add ($scroller);
my $textview = Gtk2::TextView->new;
my $buffer = $textview->get_buffer;
$buffer->create_mark ('end', $buffer->get_end_iter, FALSE);
$buffer->signal_connect (insert_text => sub {
        $textview->scroll_to_mark ($buffer->get_mark ('end'),
                                   0.0, TRUE, 0, 0.5);
});
$scroller->add ($textview);
my $button = Gtk2::Button->new ('go');
$button->signal_connect (clicked => \&button_handler);
$vbox->pack_start ($button, FALSE, FALSE, 0);
$window->show_all;
Gtk2->main;
exit;
# </boilerplate>


sub button_handler {
        # add stuff to the work list, and start processing it.
        @worklist = qw(three two one);
        kick_off_job ();
        # at this point, the processing is happening in the child,
        # and we just return to the main loop.
}

# this is called "kick off job" because we start the processing
# and return while the processing is going.
sub kick_off_job {
        unless (@worklist) {
                # the worklist is empty, so we have no more work to do.
                my $buffer = $textview->get_buffer;
                $buffer->insert ($buffer->get_end_iter,
                                 'list is empty!');
                # re-enable the button, so we can start over.
                $button->set_sensitive (TRUE);
                return;
        }

        # if we're still here, we have stuff in the work list.
        # make sure the button is disabled, so we don't get more
        # stuff put on the queue.  (this isn't entirely necessary,
        # but you may want it this way.)
        $button->set_sensitive (FALSE);

        # let's process the next item.
        my $foo = shift @worklist;

        # we use IO::File to get a unique file handle.  this is very
        # important if we're handling more than one job at a time
        # (which this code does not do, but you may want to allow that
        # if you are on a multiprocessor system, where you can actually
        # run processes simultaneously to improve throughput).
        my $fh = new IO::File;

        # fork a copy of ourselves, and read the child's stdout.
        my $pid = $fh->open ("-|");
        die "can't fork: $!\n" unless defined $pid;
        if ($pid == 0) {
                # in child.  pretend to do some work, printing out
                # status lines as we go.
                warn "starting!\n"; # will show up on stderr
                print "Starting $foo...\n";  # to be eaten by parent.
                for (0..9) {
                        print "$_\n";
                        select undef, undef, undef, 0.5;
                }
                warn "Done!\n";
                print "Finished $foo!\n";
                # important!  do not continue to run or Very Bad Things
                # can and will happen!
                exit;
        } else {
                # in parent.  install an io watch for this stream and
                # return immediately to the main caller, who will return
                # immediately to the event loop.  the callback will be
                # invoked whenever something interesting happens.
                Glib::IO->add_watch (fileno $fh, [qw/in hup err/],
                                     \&watch_callback, $fh);
        }
}


sub watch_callback {
        my ($fd, $condition, $fh) = @_;
        if ($condition >= 'in') {
                # there's data available for reading.  we have no
                # guarantee that all the data is there, just that
                # some is there.  however, we know that the child
                # will be writing full lines, so we'll assume that
                # we have lines and will just use <>.
                my $data = scalar <$fh>;
                if (defined $data) {
                        # do something useful with the text.
                        my $buffer = $textview->get_buffer;
                        $buffer->insert ($buffer->get_end_iter, $data);
                }
        }
        if ($condition >= 'hup' or $condition >= 'err') {
                # End Of File, Hang UP, or ERRor.  that means
                # we're finished.
                $fh->close;
                $fh = undef;
        }

        if ($fh) {
                # the file handle is still open, so return TRUE to
                # stay installed and be called again.
                return TRUE;
        } else {
                # we're finished with this job.  start another one,
                # if there are any, and uninstall ourselves.  if there's
                # more in the queue, kick_off_job() will install a new
                # watch for the new file handle.
                kick_off_job ();
                return FALSE;
        }
}

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

-- 
muppet <scott asofyet org>




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