http://www.perlmonks.org?node_id=342422
Category: Win32 Stuff
Author/Contact Info http://bbfu.perlmonk.org
Description:

Monitors another process, showing an icon in the system tray while the other process is running. This process can be an already active process (specified by PID), or can be a command to be launched by this program. Programs launched by traymon.pl do not have a console window, so it can be used to monitor them as background processes.

If you have some control over the program to be monitored, and can get it to set a named event, you can also include icon frames to animate the system tray icon while the event is set. This can be used to tell when the program is performing some special action, has received a connection from a user, etc.

You can also define a custom menu to be shown when the icon is right-clicked. The menu actions can be any valid Perl code, and a couple of convinience functions are predefined. The default menu consists of an 'Exit' option, that terminates the program being monitored (via kill 9).

#!/usr/bin/perl
use warnings;
use strict;

use Getopt::Long qw(GetOptions :config no_ignore_case no_auto_abbrev);
use Win32;
use Win32::GUI;
use Win32::GUI::Carp 'fatalsToDialog', 'windie';
use Win32::Event;
use Win32::Process;
use Config::IniFiles;

use constant ICON_NUM => 1;

# Program to monitor
our $command;
our $directory = '.';
our $child;

# Icon and animation
our $event;
our $tip = '';
our $icon;
our @frames;
our $anim_delay = 125;
our $rewind;

# Menu
our @menu = ('E&xit' => '\&terminate');

# GUI Controls
our $win;
our $menu;
our $niTray;

# Help
our $usage = <<"EOU";
Usage: $0 [ -e [PROGRAM] | -p [PID] ] -i [ICON FILE] [OPTIONS]
   or: $0 INIFILE
EOU
our $details = <<'EOD';
Monitor another program by showing a system tray icon while it is runn
+ing.
  -e, --execute COMMAND  Specify a command to execute and monitor
  -E, --event NAME       Specify the name of the event to watch for
  -f, --frame FILE       Include a frame for animating the icon
  -h, --help             Show this information screen
  -i, --icon FILE        Specify the .ico file to display
  -p, --pid PID          Specify the PID to monitor
  -r, --rewind           Play the icon animation backwards to loop
  -t, --tip TIP          Tip to display when hovering over icon

You must specify either a command or a PID to monitor, as well as an
icon file.

You may also specify a named event to listen for and one or more other
icon files as frames for the animation to play while the named event
is set.  Each frame of animation should be a single .ico file.  These
files are shown in the order they are specified.  If the --rewind
option is given, the animation will play backwards when it reaches the
last frame, instead of simply starting over from the first frame.

Alternatively, you may specify an INI file containing the above
information, and possibly a menu.  The format of the INI file should
be as follows:

  [ Program ]
  Command = String (may use $^X variable)
  Directory = String                             [optional]

  [ Icon ]
  Icon = Icon file
  Tip = String                                   [optional]
  Event = String                                 [optional]
  Rewind = True|False                            [optional]
  Delay = Number (ms)                            [optional]
  Frame = Icon file                              [optional]
  Frame = Icon file                              [optional]
  ...

  [ Menu ]                                       [optional]
  Label = Perl code
  - = undef # Separator
  Label = Perl code
  > Nested Label = Perl code
  ...

The value of each menu entry can be any Perl code that returns a
reference to a subroutine to be executed when the menu item is
selected, or undef.  There are two special functions that may be used
from the menu code: terminate(), and winexec().

terminate() forces the monitored program to quit (via signal 9), and
removes the tray icon.  You can use it as:

  Exit = \&terminate

winexec() launches another program in the background, without causing
the tray icon to freeze.  You can use it as:

  View Log = sub { winexec('notepad.exe logfile.log') }

The default menu consists of a single Exit item which calls terminate(
+).
EOD


GetOptions(
  'h|help'      => sub { print $usage, $details; exit; },
  'e|execute=s' => \$command,
  'p|pid=i'     => \$child,
  'i|icon=s'    => \$icon,
  'f|frame=s'   => \@frames,
  'd|delay=i'   => \$anim_delay,
  'E|event=s'   => \$event,
  'r|rewind!'   => \$rewind,
  't|tip=s'     => \$tip,
) or windie $usage;

read_ini(shift) if @ARGV;

windie $usage unless $child || $command and $icon;


$child = winexec($command) if $command;
start_gui();


# Startup Functions

sub read_ini {
  my $file = shift;
  my $cfg = Config::IniFiles->new(-file => $file)
    or windie("Unable to load $file\n");

  $command    = $cfg->val('Program', 'Command'  ) || $command;
  $directory  = $cfg->val('Program', 'Directory') || $directory;
  $event      = $cfg->val('Icon',    'Event'    ) || $event;
  $tip        = $cfg->val('Icon',    'Tip'      ) || $tip;
  $icon       = $cfg->val('Icon',    'Icon'     ) || $icon;
  @frames     = $cfg->val('Icon',    'Frame'    );
  $anim_delay = $cfg->val('Icon',    'Delay'    ) || $anim_delay;
  $rewind     = $cfg->val('Icon',    'Rewind'   ) || $rewind;

  $rewind = lc($rewind) ne 'false' || $rewind;

  # Load the menu
  if($cfg->SectionExists('Menu')) {
    @menu = map { $_ => scalar $cfg->val('Menu', $_) } $cfg->Parameter
+s('Menu');
  }
}

sub start_gui {
  $win  = Win32::GUI::Window->new();
  $icon = Win32::GUI::Icon->new($icon);

  if($event) {
    $event = Win32::Event->new(1, 0, $event);
    @frames = map { new Win32::GUI::Icon($_) } sort map { glob $_ } @f
+rames;
    push @frames, reverse @frames if $rewind;
  }

  $win->Text($tip);
  $win->AddTimer('Poll', 500);

  set_icon($icon);

  # Translate the menu
  for(my $i=0; $i<$#menu; $i+=2) {
    my $ref   = eval $menu[$i+1];
    windie("Error in menu item $menu[$i]: $@\n") if $@;

    no strict 'refs';
    *{"MenuItem$i\_Click"} = $ref if defined $ref;

    $menu[$i] = " > $menu[$i]";
    $menu[$i+1] = "MenuItem$i";
  }
  unshift @menu, ('Tray' => 'Tray');

  $menu = Win32::GUI::MakeMenu(@menu);

  Win32::GUI::Dialog();
}


# GUI Functions

sub set_icon {
  my $img = shift;

  unless($niTray) {
    $niTray = $win->AddNotifyIcon(
      -id   => ICON_NUM,
      -name => 'Tray',
      -icon => $img,
      -tip  => $tip,
    );
  } else {
    Win32::GUI::NotifyIcon::Modify(
      $win,
      -id   => ICON_NUM,
      -name => 'Tray',
      -icon => $img,
      -tip  => $tip,
    );
  }
}

sub clear_icon {
  if($niTray) {
    $win->AddNotifyIcon(
      -id   => ICON_NUM,
      -name => 'Tray',
    );
    undef $niTray;
  }
}

{
  my $frame;
  my $atimer;

  sub start_animation {
    return if defined $frame;
    $atimer = $win->AddTimer('Animate', $anim_delay);
    $frame = 0;
  }

  sub stop_animation {
    return unless defined $frame;
    undef $frame;
    $atimer->Kill();
    set_icon($icon);
  }

  sub Animate_Timer {
    $frame = ($frame + 1) % @frames;
    set_icon($frames[$frame]);
    return 1;
  }
}

sub Poll_Timer {
  if(not kill 0, $child) {
    clear_icon();
    return -1;
  }

  if($event and $event->wait(0) == 1) {
    start_animation();
  } else {
    stop_animation();
  }

  return 1;
}

sub Tray_RightClick {
  $win->TrackPopupMenu($menu->{Tray}, Win32::GUI::GetCursorPos());
  return 1;
}


# User Functions

sub terminate {
  clear_icon();
  kill 9, $child;
  return -1;
}

sub winexec {
  my $cmd = join ' ', @_;
  my $pgm = $#_ > 1 ? shift : (split /(?<!\\) /, $cmd)[0];

  $pgm =~ s/(\$\^X)/$1/eeg;

  Win32::Process::Create(
    my $obj,
    $pgm,
    $cmd,
    0,
    DETACHED_PROCESS,
    $directory,
  ) or windie "Unable to start `$pgm': " . Win32::FormatMessage(
    Win32::GetLastError()
  );

  return $obj->GetProcessID();
}
  • Comment on traymon.pl - Easily add a System Tray icon to an exisiting program
  • Download Code
Replies are listed 'Best First'.
Re: traymon.pl - Easily add a System Tray icon to an exisiting program
by Zero_Flop (Pilgrim) on Apr 04, 2004 at 07:36 UTC
    Where do you get use Win32::GUI::Carp. I am running Activestate, ppm'd win32::Gui but got no Carp. I then did a search on the web for Win32::GUI::Carp and ended up with nothing, which I think is strange, becouse if it was common it should popup in published scripts.

    Thanks would love to try this out

      It's something I wrote myself. I thought I had posted it, along with my Tk::Carp, but apparently I'd forgotten to. I went ahead and posted it now. You can find it at Win32::GUI::Carp. I need to get both of these on CPAN, but I never got around to it.

      bbfu
      Black flowers blossom
      Fearless on my breath

        I can't get it to work. I downloaded Win32::GUI from PPM, downloaded and installed carp. I created an icon for my program, and created an INI with but two entries, one for the program, and one for the Icon. When I run Traymon, it starts the target program and then dies with the "Please tell Microsoft about this problem" dialog. I thought perhaps my Icon was causing the problem, so I commented out the Set_Icon call as a test. Same result. Any clues for the clueless? Thanks
Re: traymon.pl - Easily add a System Tray icon to an exisiting program
by jsbruns (Initiate) on May 17, 2010 at 15:24 UTC
    Is there any way to set attributes for a menu item? For example, I was previously using PerlTray from the ActiveState Perl Dev Kit. I've been running into issues with the way their systray functionality works and am looking for an alternative. Within their PerlTray module, you can precede a menu item's label with a 'v' to indicate it as checked or '_' to indicate it as unchecked. There are also ways to make the menu item appear grayed out so that it's disabled. I'm more interested in making a menu item checked.