IronPython / PythonNET - NotifyIcon



Introducción: ¿Qué es un NotifyIcon?


Un NotifyIcon es un icono en el área de notificación (en la barra de tareas, al lado de la fecha y hora).

NotifyIcons

Un NotifyIcon es un componente que muestra un icono. Puede, además, mostrar un mensaje o tener asociado un menú contextual que puede abrirse haciendo click derecho en el icono.

Normalmente se utiliza para mostrar notificaciones al usuario o para acceder a un menú adicional de la aplicación.


Crear un NotifyIcon básico

El siguiente código muestra un NotifyIcon, pero no muestra ningún mensaje ni tiene asociado un menú contextual:

# -*- coding: utf-8 -*-

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import NotifyIcon, Application
from System.Drawing import Icon


notify_icon = NotifyIcon()              # Instanciar NotifyIcon
notify_icon.Icon = Icon('icon.ico')     # Asignar Icono
notify_icon.Visible = True              # Hacerlo visible

Application.Run()                       # Iniciar bucle de eventos

Para este código, en el mismo directorio en el que se encuentra el script debe existir un archivo icon.ico.

Al ejecutar este script se muestra un NotifyIcon que no hace absolutamente nada. Puede verse así:

NotifyIcon 1

O puede verse junto a otros NotifyIcons en el menú desplegable:

NotifyIcon 1

A continuación vamos a ver como mostrar un BalloonTip, es decir, un mensaje asociado al NotifyIcon.



NotifyIcon con BalloonTip


Vamos a ampliar el código anterior:

# -*- coding: utf-8 -*-

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import *

# Crear NotifyIcon visible
notify_icon = NotifyIcon()
notify_icon.Icon = Icon('icon.ico')
notify_icon.Visible = True

# Mostrar BallonTip
notify_icon.ShowBalloonTip(0, 'Hey!', 'Que tengas un buen día! :D', ToolTipIcon.None)
# En PythonNET no puede usarse ToolTipIcon.None, en su lugar: getattr(ToolTipIcon, 'None')

Application.Run()

Al ejecutar este código, además del NotifyIcon se muestra un mensaje: notify_icon.ShowBalloonTip(0, 'Hey!', 'Que tengas un buen día! :D', ToolTipIcon.None)

BalloonTip

El método ShowBalloonTip recibe los siguientes parámetros:

  1. Tiempo de duración - Es el tiempo que permanece visible el BalloonTip (el mensaje). Realmente, el máximo y mínimo de este valor depende del sistema operativo, y he asignado el valor 0 porque en Windows 10 este parámetro parece ignorarse (el BalloonTip permanece visible el mismo tiempo independientemente del valor pasado para el tiempo).

  2. Título del mensaje

  3. Texto del mensaje

  4. Icono - Es un logo que se muestra a la izquierda del texto del mensaje. Puede tomar los siguientes valores:

    • ToolTipIcon.Error
    • ToolTipIcon.Info
    • ToolTipIcon.None
    • ToolTipIcon.Warning

    En pythonnet no puede utilizarse ToolTipIcon.None directamente. En su lugar utiliza getattr(ToolTipIcon, 'None'). En ironpython puede utilizarse ToolTipIcon.None directamente.

Para cerrar el mensaje puedes hacer click en el botón X. Fíjate en que aunque cierres el BalloonTip, la aplicación sigue abierta gracias a Application.Run().

Podemos dotar a nuestros NotifyIcon y BalloonTip de algo mas de funcionalidad. Continuemos añadiendo código a nuestro script:

# -*- coding: utf-8 -*-

import clr, sys

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import *

notify_icon = NotifyIcon()
notify_icon.Icon = Icon('icon.ico')
notify_icon.Visible = True

# Al hacer click en el Icono (si el BalloonTip está abierto y se hace click en el icono, este evento se ignora)
notify_icon.Click += lambda *args: sys.stdout.write('Has hecho Click en el NotifyIcon\n')

# Al hacer click en el BallonTip
notify_icon.BalloonTipClicked += lambda *args: sys.stdout.write('Has hecho Click en el BalloonTip\n')

# Al cerrar el BallonTip (pulsando X o al cumplirse el tiempo indicado en ShowBalloonTip)
notify_icon.BalloonTipClosed += lambda *args: sys.stdout.write('El BalloonTip se ha cerrado\n')

ToolTipIcon.NONE = getattr(ToolTipIcon, 'None')
notify_icon.ShowBalloonTip(0, 'Hey!', 'Que tengas un buen día! :D', ToolTipIcon.NONE)

Application.Run()

En este código estamos manejando 3 eventos posibles:

  1. NotifyIcon.Click - Tiene lugar cuando se hace click en el icono (click izquierdo o derecho). Solamente se dispara si el BalloonTip ya ha sido cerrado. Si el BalloonTip está abierto y se hace click en el NotifyIcon cuenta como si se hubiese hecho click en este último.

  2. NotifyIcon.BalloonTipClicked - Tiene lugar cuando se hace click en el BalloonTip o, si este aun permanece abierto, en el NotifyIcon.

  3. NotifyIcon.BalloonTipClosed - Tiene lugar cuando se cierra el BalloonTip, bien haciendo click en el botón X (si está presente, depende del sistema operativo) o bien cuando se cierra automáticamente tras pasar un tiempo.

Hagamos lo que hagamos, la aplicación permanece abierta. Vamos a modificar este código y ver un ejemplo de uso un poco mas realista:

# -*- coding: utf-8 -*-

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import *


def close(*args):
    notify_icon.Dispose()
    Application.Exit()


notify_icon = NotifyIcon()
notify_icon.Icon = Icon('icon.ico')
notify_icon.Visible = True

notify_icon.BalloonTipClicked += close
notify_icon.BalloonTipClosed += close

ToolTipIcon.NONE = getattr(ToolTipIcon, 'None')
notify_icon.ShowBalloonTip(0, 'Hey!', 'Que tengas un buen día! :D', ToolTipIcon.NONE)

Application.Run()

En este ejemplo hemos definido una función close que cierra el NotifyIcon y finalmente la Application. Esta función va a ser el manejador de los eventos notify_icon.BalloonTipClicked y notify_icon.BalloonTipClosed.

No he utilizado el evento notify_icon.Click porque solamente se dispara cuando el BalloonTip se ha cerrado, momento en el cual ya hemos definido que tiene que cerrarse la aplicación: notify_icon.BalloonTipClosed += close.



NotifyIcon con ContextMenu


Un NotifyIcon no tiene por qué llevar asociado un BalloonTip. Otra opción es añadirle un ContextMenu. Seguramente has utilizado aplicaciones con un NotifyIcon en el que si haces click derecho aparece un menú con opciones. Vamos a implementarlo a continuación:

# -*- coding: utf-8 -*-

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import Icon

notify_icon = NotifyIcon()
notify_icon.Icon = Icon('icon.ico')
notify_icon.ContextMenu = ContextMenu()
notify_icon.Visible = True

Application.Run()

No ejecutes este código, ya que solamente mostrará el icono, y aunque hemos añadido un ContextMenu, como este todavía no contiene nada, si haces click derecho en el icono no sucede nada.

Vamos a añadir dos MenuItem al ContextMenu. Uno de ellos será un botón con el texto "Open" y otro con el texto "Close":

open_option = MenuItem()
open_option.Text = 'Open'

close_option = MenuItem()
close_option.Text = 'Close'

Pero todavía tienes que añadir estas opciones al ContextMenu:

notify_icon.ContextMenu.MenuItems.Add(open_option)
notify_icon.ContextMenu.MenuItems.Add(close_option)

Si ahora ejecutas el script y haces click derecho en el NotifyIcon verás el siguiente ContextMenu:

ContextMenu

Sin embargo, al hacer click en cualquiera de los dos botones todavía no sucede nada. Para que suceda algo al hacer click simplemente tienes que manejar el evento Click de cada una de las opciones:

# Manejamos el evento Click de open_option
open_option.Click += lambda *args: notify_icon.ShowBalloonTip(0,
    'Mensaje',
    'Has hecho click en [ Open ]',
    ToolTipIcon.Info
    )

# Manejamos el evento Click de close_option
close_option.Click += lambda *args: (
    notify_icon.Dispose(),
    Application.Exit()
    )

En este caso, si haces click en [Open] se muestra un BalloonTip, y si haces click en [Close] se cierra primero el NotifyIcon y a continuación la aplicación.

El código completo de este ejemplo es el siguiente:

# -*- coding: utf-8 -*-

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import Icon

notify_icon = NotifyIcon()
notify_icon.Icon = Icon('icon.ico')
notify_icon.ContextMenu = ContextMenu()
notify_icon.Visible = True

open_option = MenuItem()
open_option.Text = 'Open'

close_option = MenuItem()
close_option.Text = 'Close'

# Manejamos el evento Click de open_option
open_option.Click += lambda *args: notify_icon.ShowBalloonTip(0,
    'Mensaje',
    'Has hecho click en [ Open ]',
    ToolTipIcon.Info
    )

# Manejamos el evento Click de close_option
close_option.Click += lambda *args: (
    notify_icon.Dispose(),
    Application.Exit()
    )

notify_icon.ContextMenu.MenuItems.Add(open_option)
notify_icon.ContextMenu.MenuItems.Add(close_option)

Application.Run()



NotifyIcon con ContextMenu al hacer click en X


Por defecto, cuando se hace click en el botón X de una aplicación, esta se cierra. Pero el programador de la aplicación puede cambiar este comportamiento. Algunas aplicaciones en vez de cerrarse, se ocultan al usuario cuando se hace click en el botón X, pero permanecen abiertas en segundo plano.

Para ver como implementar este comportamiento crearemos una sencilla aplicación WinForms. Empecemos:

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import *


class MainForm(Form):
    def __init__(self):
        self.Text = 'App'
        self.Icon = Icon('icon.ico')


form = MainForm()
Application.EnableVisualStyles()
Application.Run(form)

Si ejecutas este código, se muetra un formulario vacío con el título "App", y el icono icon.ico. Si haces click en el botón X la aplicación se cierra.

MainForm

Vamos a definir un método en el que crearemos un NotifyIcon con un ContextMenu. Será el método MainForm.make_notify_icon:

def make_notify_icon(self):

    # Crear un NotifyIcon con un Menú contextual

    self.notify_icon = NotifyIcon()
    self.notify_icon.Icon = self.Icon
    self.notify_icon.ContextMenu = ContextMenu()
    self.notify_icon.Visible = False

    # Crear las opciones del Menú contextual

    open_option = MenuItem()
    open_option.Text = 'Open'
    open_option.Click += self.show_me

    close_option = MenuItem()
    close_option.Text = 'Close'
    close_option.Click += self.on_close

    # Añadir las opciones al Menú contextual

    self.notify_icon.ContextMenu.MenuItems.Add(open_option)
    self.notify_icon.ContextMenu.MenuItems.Add(close_option)

Fíjate en la línea self.notify_icon.Visible = False: Al crear el NotifyIcon este será invisible. Tenemos que crear dos métodos que serán los manejadores del evento Click para las opciones del ContextMenu: Para open_option crearemos MainForm.show_me y para close_option crearemos MainForm.on_close.

Por otra parte, como ya hemos utilizado el archivo icon.ico para que sea el icono de MainForm, en lugar de crear un nuevo System.Drawing.Icon, lo reutilizaremos: self.notify_icon.Icon = self.Icon

En estos momentos, el código de la clase MainForm se ve así:

class MainForm(Form):

    def __init__(self):

        self.Text = 'App'
        self.Icon = Icon('icon.ico')

        self.make_notify_icon()

    def make_notify_icon(self):

        # Crear un NotifyIcon con un Menú contextual

        self.notify_icon = NotifyIcon()
        self.notify_icon.Icon = self.Icon
        self.notify_icon.ContextMenu = ContextMenu()
        self.notify_icon.Visible = False

        # Crear las opciones del Menú contextual

        open_option = MenuItem()
        open_option.Text = 'Open'
        open_option.Click += self.show_me

        close_option = MenuItem()
        close_option.Text = 'Close'
        close_option.Click += self.on_close

        # Añadir las opciones al Menú contextual

        self.notify_icon.ContextMenu.MenuItems.Add(open_option)
        self.notify_icon.ContextMenu.MenuItems.Add(close_option)

    def show_me(self, *args):
        pass

    def on_close(self, *args):
        pass

De momento los métodos show_me y on_close no hacen nada. En el método __init__ hemos añadido la sentencia self.make_notify_icon() para crear el NotifyIcon de la aplicación en el momento de instanciar a MainForm. Recuerda que, en principio, el notify-icon es invisible: self.notify_icon.Visible = False.

Definamos pues los métodos show_me y on_close:

Método show_me

# Se ejecuta cuando se hace click en el botón [Open] del ContextMenu
# Hace visible a MainForm y oculta al NotifyIcon

def show_me(self, *args):
    self.Visible = True
    self.notify_icon.Visible = False

Método on_close

# Se ejecuta cuando se hace click en el botón [Close] del ContextMenu
# Cierra el NotifyIcon, el MainForm y finalmente la Application

def on_close(self, *args):
    self.notify_icon.Dispose()
    self.Dispose()
    Application.Exit()

Ya sólo nos falta modificar lo que sucede al hacer click en el botón X, que por defecto cierra la aplicación. Podemos modificar este comportamiento manejando el evento Closing del formulario. Añade lo siguiente al método __init__:

self.Closing += self.cancel_close_and_notify_icon

Ahora tenemos que definir el método MainForm.cancel_close_and_notify_icon, que contendrá el código que debe ejecutarse cuando se haga click en el botón X:

def cancel_close_and_notify_icon(self, sender, e):
    e.Cancel = True
    self.Visible = False
    self.notify_icon.Visible = True

Para cancelar lo acción por defecto, es decir, para cancelar el cierre de la aplicación, utilizamos e.Cancel = True. A continuación hacemos invisible a MainForm y visible al NotifyIcon. Este método hace lo contrario que el método show_me.

El código de esta aplicación que no se cierra al hacer click en el botón X queda finalmente así:

# -*- coding: utf-8 -*-

# Al hacer click en el botón X de la caja de control de MainForm, este se hace invisible
# y un NotifyIcon que tiene un ContextMenu se hace visible. El ContextMenu permite
# volver al estado anterior o cerrar la aplicación completamente.

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import *


class MainForm(Form):

    def __init__(self):

        self.Text = 'App'
        self.Icon = Icon('icon.ico')

        self.make_notify_icon()

        self.Closing += self.cancel_close_and_notify_icon

    def make_notify_icon(self):

        # Crear un NotifyIcon con un Menú contextual

        self.notify_icon = NotifyIcon()
        self.notify_icon.Icon = self.Icon
        self.notify_icon.ContextMenu = ContextMenu()
        self.notify_icon.Visible = False

        # Crear las opciones del Menú contextual

        open_option = MenuItem()
        open_option.Text = 'Open'
        open_option.Click += self.show_me

        close_option = MenuItem()
        close_option.Text = 'Close'
        close_option.Click += self.on_close

        # Añadir las opciones al Menú contextual

        self.notify_icon.ContextMenu.MenuItems.Add(open_option)
        self.notify_icon.ContextMenu.MenuItems.Add(close_option)

    def cancel_close_and_notify_icon(self, sender, e):
        e.Cancel = True
        self.Visible = False
        self.notify_icon.Visible = True

    def show_me(self, *args):
        self.Visible = True
        self.notify_icon.Visible = False

    def on_close(self, *args):
        self.notify_icon.Dispose()
        self.Dispose()
        Application.Exit()



form = MainForm()

Application.EnableVisualStyles()
Application.Run(form)



NotifyIcon con ContextMenu al minimizar


Puedes hacer que el formulario de la aplicación "desaparezca" al hacer click en el botón de minimizar de la caja de control, y que entonces se muestre el NotifyIcon con opciones en su ContextMenu.

Para hacerlo, toma el código que hemos creado anteriormente (el de mostrar el NotifyIcon y ocultar el formulario al presionar el botón X) y aplica las siguientes modificaciones:

  1. Elimina el método MainForm.cancel_close_and_notify_icon y en el método __init__ sustituye self.Closing += self.cancel_close_and_notify_icon por self.Closing += self.on_close para que al hacer click en el botón X se cierre correctamente la aplicación.

  2. Crea un método llamado on_resize y haz que sea el manejador del evento Resize del formulario:

    def __init__(self):
        self.Text = 'App'
        self.Icon = Icon('icon.ico')
    
        self.make_notify_icon()
    
        self.Resize += self.on_resize # <-- Atención
        self.Closing += self.on_close
    
    def on_resize(self, *args):
        pass
    
  3. Modifica el método show_me así:

    def show_me(self, *args):
        self.Visible = True
        self.WindowState = FormWindowState.Normal # <-- Atención
        self.notify_icon.Visible = False
    

    De esta manera, cuando hagamos visible al formulario, este recuperará su estado normal. Si no asignamos self.WindowState = FormWindowState.Normal después de hacer visible al formulario, este aparecerá lo mas pequeño posible.


Una vez aplicadas estas modificaciones, vamos a definir el método on_resize. El evento Resize tiene lugar siempre que se redimensiona el formulario, y cuando se hace click en el botón de minimizar de la caja de control lo que sucede es una redimensión del formulario.

Lo que queremos conseguir es cuando se minimice, el formulario se haga invisible y aparezca el NotifyIcon:

def on_resize(self, *args):
    if self.WindowState == FormWindowState.Minimized:
        self.Visible = False
        self.notify_icon.Visible = True

Con este código el formulario se hace invisible (tampoco aparece en la barra de tareas), y el NotifyIcon se hace visible cuando se dispara el evento Resize, que invoca a on_resize y verifica que el estado de la ventana es minimizado.

El código completo de este ejemplo de aplicación es el siguiente:

# -*- coding: utf-8 -*-

# Al minimizar el MainForm, este se hace invisible y un NotifyIcon
# que tiene un ContextMenu se hace visible. El ContextMenu permite
# volver al estado anterior o cerrar la aplicación completamente.

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import *


class MainForm(Form):

    def __init__(self):

        self.Text = 'App'
        self.Icon = Icon('icon.ico')

        self.make_notify_icon()

        self.Resize += self.on_resize
        self.Closing += self.on_close

    def make_notify_icon(self):

        # Crear un NotifyIcon con un Menú contextual

        self.notify_icon = NotifyIcon()
        self.notify_icon.Icon = self.Icon
        self.notify_icon.ContextMenu = ContextMenu()
        self.notify_icon.Visible = False

        # Crear las opciones del Menú contextual

        open_option = MenuItem()
        open_option.Text = 'Open'
        open_option.Click += self.show_me

        close_option = MenuItem()
        close_option.Text = 'Close'
        close_option.Click += self.on_close

        # Añadir las opciones al Menú contextual

        self.notify_icon.ContextMenu.MenuItems.Add(open_option)
        self.notify_icon.ContextMenu.MenuItems.Add(close_option)

    def on_resize(self, *args):
        if self.WindowState == FormWindowState.Minimized:
            self.Visible = False
            self.notify_icon.Visible = True

    def show_me(self, *args):
        self.Visible = True
        self.WindowState = FormWindowState.Normal
        self.notify_icon.Visible = False

    def on_close(self, *args):
        self.notify_icon.Dispose()
        self.Dispose()
        Application.Exit()



form = MainForm()

Application.EnableVisualStyles()
Application.Run(form)