Beefy Boxes and Bandwidth Generously Provided by pair Networks
Clear questions and runnable code
get the best and fastest answer
 
PerlMonks  

How to display Tk window without waiting for user input

by Special_K (Monk)
on Jan 15, 2021 at 20:56 UTC ( #11126974=perlquestion: print w/replies, xml ) Need Help??

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

I am working on a script whose logic is essentially as follows:

#!/usr/bin/perl -w use strict; use Tk; my $status = 0; my $prev_status = 0; my $mw = MainWindow->new(); $mw->withdraw(); while (1) { my $status = check_for_status(); if ($status != $prev_status) { # need to notify user but don't wait for click $mw->messageBox( -title => 'status changed', -message => 'status changed', -type => 'OK', -icon => 'info', ); } $prev_status = $status; }

I would like the window to display and also have the program keep executing, but currently the program waits for the user to click "OK". The windows are only intended to notify the user that the status variable has changed and no action is taken based on clicking OK to close the windows. I was not able to find a Tk window type that does not have some sort of dialog button that causes execution to wait for the user to click them.

I also considered somehow using fork() to spawn off each dialog window as a separate process, but exec() expects a system call. Is there an equivalent to exec() that accepts a block of perl code that I can use to spawn off each dialog window and have execution continue within the main loop? Status changes are relatively infrequent so I don't expect the computer to be swamped with open dialog boxes.

Replies are listed 'Best First'.
Re: How to display Tk window without waiting for user input
by kcott (Bishop) on Jan 16, 2021 at 08:21 UTC

    G'day Special_K,

    Unfortunately, you're completely on the wrong track with an infinite loop constantly checking for a status change. A change of status, however that is effected, is an event: you should allow Tk to respond to such an event.

    I don't think popping up a window for the notification is the right way to go. You could perhaps use a Tk::Toplevel that holds the notification. That's going to be messy. If it obscures part of the main GUI, user intervention will be needed to move it or bring the main GUI back to the foreground. Then there's the question of how long the popup persists. You could use Tk::after to remove it automatically after a certain period of time: that needs to be long enough for the user to read it; but not so long that you that get a number of popups all piled on top of each other.

    I would implement what you're trying to do with a status bar that notifies individual changes, some sort of window with a log of status changes as they occur, or perhaps even both. I've put together a short script to demonstrate both methods; changing the selected radiobutton simulates a status change.

    #!/usr/bin/env perl use strict; use warnings; use Tk; my $mw = MainWindow::->new(-title => 'Status Notifications'); $mw->geometry('512x288+50+100'); my $status = 'A'; my $last_status = $status; my $status_msg; _set_status_msg($status, \$status_msg); my $status_bar_F = $mw->Frame( )->pack(-side => 'bottom', -fill => 'x'); $status_bar_F->Label( -textvariable => \$status_msg, -anchor => 'w', -relief => 'sunken', )->pack(-fill => 'x'); my $main_F = $mw->Frame( )->pack(-side => 'top', -fill => 'both', -expand => 1); my $text_F = $main_F->Frame( )->pack(-side => 'right', -fill => 'y'); my $status_text = $text_F->Text( -width => 50, )->pack(); $status_text->insert(end => "$status_msg\n"); my $radio_F = $main_F->Frame( )->pack(-side => 'left', -fill => 'both', -expand => 1); for my $status_letter ('A' .. 'F') { $radio_F->Radiobutton( -text => $status_letter, -variable => \$status, -value => $status_letter, -command => sub { if ($status ne $last_status) { $last_status = $status; _set_status_msg($status_letter, \$status_msg); $status_text->insert(end => "$status_msg\n"); } }, )->pack(-side => 'top', -anchor => 'w', -pady => 2); } MainLoop; sub _set_status_msg { my ($status, $msg_ref) = @_; $$msg_ref = 'Status: ' . $status . '; Changed at: ' . localtime(); return; }

    That code is fully functional and should work as is. Pay particular attention to references; for example, \$status_msg, \$status, and $$msg_ref in various places.

    — Ken

      > Unfortunately, you're completely on the wrong track with an infinite loop constantly checking for a status change. A change of status, however that is effected, is an event: you should allow Tk to respond to such an event.

      The issue is that within the call to check_for_status(); is a webpage scrape; there is no way to have the webpage "push" the status changes to me; I have to determine that myself. For that reason a while (1) loop seems necessary.

      The use case is that this script will just run silently in the background and alert me with a popup when/if a status change occurs.

        The issue is that within the call to check_for_status(); is a webpage scrape; there is no way to have the webpage "push" the status changes to me; I have to determine that myself. For that reason a while (1) loop seems necessary.

        Well I certainly hope that whoever is operating the webpage doesn't mind you polling the page that often, and that you're not voilating any TOS. You should definitely add a delay in there so that you're not hitting the webserver constantly.

        If this is a Tk GUI then you will need the MainLoop;, and everything needs to happen in there, and kcott's comment that you're on the wrong track by writing your own loop is correct. This means you need to use Tk's functionalities to achieve your "loop", which is usually done by setting a timer to go off in the future, which will then run a callback inside of the main loop, and the callback then starts a new timer. Though I am far from a Tk expert, Tk::after as suggested by kcott seems to be a way to achieve this (Update: tybalt89 and kcott have since posted examples). Either that, or spawn a separate thread/process, though then you've got the complication of two processes needing to be managed and needing to communicate.

        Of course, it's a different story if the only thing your code is doing is sending notifications, then most modern window managers have built-in notifications that you can use. For example, on Ubuntu, there's notify-send, which exits immediately and therefore wouldn't block your script (see Calling External Commands More Safely).

        "The issue is that within the call to check_for_status(); is a webpage scrape; ..."

        That's new information; however, it's fairly immaterial with respect to the Tk code.

        "... there is no way to have the webpage "push" the status changes to me; I have to determine that myself."

        Both points are obvious and accepted.

        "For that reason a while (1) loop seems necessary."

        It may seem necessary to you but, not only is it not necessary, it's still the wrong solution.

        Consider the following (fully functional) script which produces a notification similar in appearance to what I get from my email client when a new email arrives.

        #!/usr/bin/env perl use strict; use warnings; use constant { TIMER_INTERVAL => 1_000, NOTIFY_PERIOD => 10_000, }; use Tk; my $mw = MainWindow::->new(); $mw->configure(-title => 'Status Notification Manager'); $mw->geometry('320x60+50+100'); #$mw->withdraw(); my $status = _get_status(); my $last_status = $status; my $status_msg; _set_status_msg($status, \$status_msg); my $timer_id = $mw->repeat(TIMER_INTERVAL, sub { $status = _get_status(); if ($status ne $last_status) { $last_status = $status; _set_status_msg($status, \$status_msg); my $notify_panel = $mw->Toplevel(); $notify_panel->geometry('200x50-0-0'); $notify_panel->overrideredirect(1); my $msg_F = $notify_panel->Frame( )->pack( -side => 'left', -fill => 'both', -expand => 1, ); $msg_F->Label( -text => $status_msg, -anchor => 'w', )->pack( -side => 'left', -padx => 5, ); my $x_F = $notify_panel->Frame( )->pack( -side => 'left', -fill => 'y', ); my $x_B = $x_F->Button( -text => 'X', -bg => '#ff0000', -bd => 1, -relief => 'flat', -anchor => 'ne', -command => sub { if (Exists($notify_panel)) { $notify_panel->destroy(); } }, )->pack(); $notify_panel->raise(); $notify_panel->bell(); $mw->after(NOTIFY_PERIOD, sub { if (Exists($notify_panel)) { $notify_panel->destroy(); } }); } return; }); $mw->Button( -text => 'Exit', -command => sub { $timer_id->cancel(); exit; }, )->pack(-pady => 5); my $status_bar_F = $mw->Frame( )->pack(-side => 'bottom', -fill => 'x'); $status_bar_F->Label( -textvariable => \$status_msg, -anchor => 'w', -relief => 'sunken', )->pack(-fill => 'x'); MainLoop; sub _get_status { join ':', map sprintf('%02d', $_), (localtime())[2,1]; } sub _set_status_msg { my ($status, $msg_ref) = @_; $$msg_ref = 'Status: ' . $status; return; }

        I've commented out the $mw->withdraw();. You had it in your original (OP) code. It does work with my code; however, it makes shutting down the application problematic. Here's an example of running my code with it left in, and then shutting it down:

        ken@titan ~/tmp $ ./pm_11126974_tk_status_change_2.pl & [1] 618 ken@titan ~/tmp $ kill -HUP 618 ken@titan ~/tmp $ [1]+ Hangup ./pm_11126974_tk_status_change_2.pl ken@titan ~/tmp $

        If you didn't leave the PID in plain sight, as I did for this example, you have additional work determining what the PID is. Alternatively, you can run it without the '&' at the end; minimise the console you used — effectively tying it up for whatever amount of time you want to monitor status changes (hours?) — and then end the application as needed.

        I suggest leaving withdraw() out and just minimise the GUI, which is clearly labelled "Status Notification Manager". You can use this to query the last status between notifications; and just click on the "Exit" button when you're done.

        Notice that I've used three methods related to Tk::after: repeat() handles checking for status changes; the ID from repeat() is used to cancel() that checking when the "Exit" button is used; and, after() is used (within the repeat() callback) to get rid if the notification panel after a period of time. See that documentation for more on those methods and additional information.

        You may want to change the NOTIFY_PERIOD constant. You should almost definitely change the TIMER_INTERVAL constant; every five or ten minutes (300_000 or 600_000) would probably be better for accessing a webpage. Note that the values are in milliseconds.

        My &_get_status just returns a time (hh:mm) for demonstration purposes. This is where your webpage status retrieval code should go.

        I find all of the system sounds (bells, beeps, and so on) really annoying, so I've turned all of them off on my computer. The bell() method is untested by me, beyond the fact that its presence causes no problems: it's non-essential; you choose whether to leave or remove it.

        See also: Tk; Tk::Widget; Tk::Wm.

        Tests were run using: Win10; Cygwin; Cygwin XWin Server; Perl 5.32.0.

        — Ken

Re: How to display Tk window without waiting for user input
by davies (Prior) on Jan 15, 2021 at 21:13 UTC
Re: How to display Tk window without waiting for user input
by tybalt89 (Prior) on Jan 16, 2021 at 20:31 UTC

    Here's an example that uses a Toplevel to fake a messagebox, and ->after for the timing.
    Fill in the toplevel with whatever message you want. I sort of tried to simulate what you say you want to do.
    If the scraper is a long running process, take a look at Tk::IO which would run it in a sub-process and just fetch the results.

    #!/usr/bin/perl use strict; # https://perlmonks.org/?node_id=11126974 use warnings; use Tk; my $fakebox; my $oldstatus = ''; my $vertical = 0; my $mw = MainWindow->new; $mw->geometry( '+500+600' ); $mw->Button( -text => 'Exit', -command => sub {$mw->destroy}, -font => 'courierbold 24')->pack; $mw->after( 1, \&checker ); MainLoop; sub checker { my $new = localtime(); # replace localtime with your scraper results if( $new ne $oldstatus ) { $fakebox and $fakebox->destroy; $fakebox = $mw->Toplevel(); $fakebox->title( 'Message Box' ); # really fake it :) $fakebox->geometry( '+300+' . (300, 400)[$vertical ^= 1] ); $fakebox->Label( -text => ' Notice ', -font => 'times 24', -fg => 'white', -bg => 'red3' )->pack; $fakebox->Label( -text => $new, -font => 'times 24' )->pack; $oldstatus = $new; } $mw->after( 3000, \&checker ); }
Re: How to display Tk window without waiting for user input
by jcb (Parson) on Jan 16, 2021 at 00:50 UTC

    If you just need to spawn another process for the dialog box and you have KDE installed, try fork/exec and running the kdialog tool. At a quick test here, kdialog --msgbox "status changed" --title "status changed" seems to do what you want.

Re: How to display Tk window without waiting for user input
by Anonymous Monk on Jan 16, 2021 at 13:07 UTC
Re: How to display Tk window without waiting for user input
by nikosv (Chaplain) on Jan 20, 2021 at 21:07 UTC
    Typicaly you spawn a new thread that does the scraping and communicates with the main loop through a Thread::Queue. So you have the main loop decoupled while the scraping is taking place,therefore the OK button would not block.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://11126974]
Approved by davies
Front-paged by kcott
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others scrutinizing the Monastery: (5)
As of 2021-02-25 08:39 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found

    Notices?