Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

Hello Monks

I have a small TK GUI with an Entry field and a Table to display results. I am trying to implement what I call a "progressive search": at every keyboard stroke the database query is performed. As the query string is getting longer, the number of matches are reduced at any new keyboard stroke. I am using the following approach:

$EntryQuery->idletasks;#$EntryQuery is the Tk widget where the user is + typing $SearchField->bind("<Key>", sub { print "Searching for $QueryInput\n";#$QueryInput is a variable + linked with the Entry widget my $ResultsFinal=QueryDatabase($QueryInput); DeleteAllRowsInTable(); PrintingResultsInTable($ResultsFinal); });
Everything works fine... except that, if the subrutines take too long time to be processed, same "query string" (which is progressively getting longer) is searched at the very end of the process. If I search for the word "paragraph", for instance, typing one character after the other, I may get:

Searching for p Searching for pa Searching for par Searching for paragr Searching for paragra Searching for paragrap Searching for paragraph Searching for para # this should have been searched as 4th iteration, +or skipped as no more interesting (since the user has typed another l +etcharacterter) Searching for parag # this should have been searched as 5th iteration, + or skipped as no more interesting (since the user has typed another +character)

As you can imagine, in this way (if the user is typing quicker than the computer is able to process the subrutines), the user ends up with the results matching "parag" instead of "paragraph". Note that the performed subrutines (especially QueryDatabase) are very complex and may require some time to be processed.

What approach could I use to avoid this problem? I was thinking about breaking out of the sequence of the processed subrutines if the user is typing the next character (as this query is no interesting anymore). But I am not sure if this is a good approach. And if yes, which is the best method to achieve it? Thanks for any suggestions

Replies are listed 'Best First'.
Re: Tk progressive search
by zentara (Cardinal) on Jun 29, 2018 at 17:32 UTC
    Hi, I think what you are looking for is the Entry's validate option. Check out how the following example works.
    #!/usr/bin/perl use warnings; use strict; use Tk; my @vars; my $mw = tkinit; my $row = 0; $mw->Label( -text => 'Unit cost:', ) ->grid( -row => $row, -column => 0, -sticky => 'e' ); $mw->Entry( -textvariable => \$vars[ 0 ], -validate => 'key', -validatecommand => sub { multiply( $vars[ 1 ], @_ ) }, )->grid( -row => $row++, -column => 1, -sticky => 'w' ); $mw->Label( -text => 'Quantity:', ) ->grid( -row => $row, -column => 0, -sticky => 'e' ); $mw->Entry( -textvariable => \$vars[ 1 ], -validate => 'key', -validatecommand => sub { multiply( $vars[ 0 ], @_ ) }, )->grid( -row => $row++, -column => 1, -sticky => 'w' ); $mw->Label( -text => 'Total cost:', ) ->grid( -row => $row, -column => 0, -sticky => 'e' ); $mw->Entry( -textvariable => \$vars[ 2 ], ) ->grid( -row => $row++, -column => 1, -sticky => 'w' ); sub multiply { my ( $var, $proposed ) = @_; my $product; eval { $product = ( $var || 0 ) * ( $proposed || 0 ) }; $product = 0 if $@; $vars[ 2 ] = $product; return ( 1 ); } MainLoop;

    I'm not really a human, but I play one on earth. ..... an animated JAPH
Re: Tk progressive search
by kcott (Archbishop) on Jun 30, 2018 at 04:15 UTC

    Firstly, I concur with ++zentara's recommendation to use Tk::Entry's -validate option set to 'key'.

    In addition, I also suggest the following:

    • Cache your search results so that you only need to query the database once per unique search query.
    • Add a progress bar, or similar graphical feedback, to advise the user that something is happening.
    • Look at your database and see if there's anything you can do to speed up the queries.

    The following code implements all of that (except, of course, the database point). I've simply dummied up some data to simulate whatever's on your database; as well as a two second delay for new searches.

    When you run this, you can just type, for instance, 'qwerty' in the "Search" field: see how each (i.e. 'q', 'qw', ..., 'qwerty') is queried in turn and the progress bar runs separately for each. You can then hit "Backspace" several times and see the results (i.e. for 'qwert', 'qwer', ..., 'q') appear immediately because they're cached.

    #!/usr/bin/env perl use strict; use warnings; use constant { DB_DELAY => 2000, RESULTS_SEPARATOR => '=' x 40 . "\n", }; use Tk; use Tk::ProgressBar; { my $mw = MainWindow::->new(); sub _mw () { $mw } my ($progress_bar, $progress, $results); sub _progress_bar () { \$progress_bar } sub _progress () { \$progress } sub _results () { \$results } } { my $search_F = _mw->Frame->pack; $search_F->Label( -text => 'Search:', )->pack(-side => 'left'); $search_F->Entry( -validate => 'key', -validatecommand => \&run_query, )->pack(-side => 'left'); $search_F->Label( -text => 'Progress:', )->pack(-side => 'left'); ${+_progress} = 0; ${+_progress_bar} = $search_F->ProgressBar( -variable => _progress, -from => 0, -to => 100, -blocks => 10, -gap => 0, -colors => [ 0 => '#99ccff' ], -width => 20, -length => 200, )->pack(-side => 'left'); $search_F->Button( -text => 'Exit', -command => sub { exit }, )->pack(-side => 'left'); my $results_F = _mw->Frame->pack; ${+_results} = $results_F->Scrolled('Text', -scrollbars => 'osoe', )->pack; } MainLoop; sub run_query { my ($query) = @_; if (length $query) { ${+_results}->insert(end => "Query: $query\n"); ${+_results}->insert(end => "Results:\n"); for (@{get_results($query)}) { ${+_results}->insert(end => "$_\n"); } ${+_results}->insert(end => RESULTS_SEPARATOR); ${+_results}->yviewMoveto(1); } return 1; } { my @data; INIT { @data = qw{q qwe qwerty a asd asdfgh} } my %cache; sub get_results { my ($query) = @_; unless (exists $cache{$query}) { $cache{$query} = [ grep /\Q$query/, @data ]; my $step = int 0.5 + DB_DELAY / 10; for (my $i = 0; $i <= DB_DELAY; $i += $step) { _mw->after($step); ${+_progress} = $i / DB_DELAY * 100; ${+_progress_bar}->idletasks; } ${+_progress} = 0; } return $cache{$query}; } }

    You may have also noticed a complete absence of any global variables. These often become a source of all sorts of issues, particularly with Tk applications of any substance, and especially when callbacks are involved. Aim to avoid them wherever possible: which is pretty much everywhere.

    — Ken

Re: Tk progressive search
by tybalt89 (Monsignor) on Jun 30, 2018 at 00:34 UTC

    Here's an attempt at a solution.

    This uses Tk::IO so it requires the query to be in a separate process.
    I had no luck trying to use ->kill, so what this does is use a closure to ignore any lines from a query that is not the latest from showing up in the list.
    Slow queries are simulated with a sleep 1.

    Seems to work :)

    #!/usr/bin/perl use strict; use warnings; use Tk; use Tk::IO; use Tk::ROText; my $sequence = 0; my $mw = MainWindow->new; $mw->geometry( '+349+440' ); $mw->Button(-text => 'Exit', -command => sub {$mw->destroy}, )->pack(-fill => 'x'); my $entry = $mw->Entry( -validate => 'key', -validatecommand => \&lookup, )->pack(-fill => 'x'); my $text = $mw->ROText( )->pack; MainLoop; sub lookup { my $newword = shift; my $want = ++$sequence; $text->delete('1.0', 'end'); if( $newword =~ /^\w+\z/ ) # avoid shell error { Tk::IO->new( -linecommand => sub { $want == $sequence and $text->insert(end => shift) }, )->exec("sleep 1 ; look '$newword' | head -20"); } return 1; }
Re: Tk progressive search
by IB2017 (Pilgrim) on Jun 29, 2018 at 16:16 UTC

    Probably breaking out of the subrutines is the best solution. However I am not an expert in this. You could solve it delaying a little bit the search operations, so to say "waiting" to see if the user is typing the next character or not. For example:

    $EntryQuery->bind("<Key>", sub { if ($Timer){$Timer->cancel();} $Timer = $EntryQuery->after(100, sub { my $ResultsFinal=Query($QueryInput); DeleteAllRowsInTable(); PrintingResultsInTable($ResultsFinal); }); }

    Monks with more experience may tell you if this is a reasonable approach.

Re: Tk progressive search
by Anonymous Monk on Jun 29, 2018 at 15:54 UTC

    This kind of search is called - I think - "incremental search"

A reply falls below the community's threshold of quality. You may see it by logging in.