Laden...

[Artikel] Tray Applikationen (NotifyIcon)

Erstellt von egrath vor 17 Jahren Letzter Beitrag vor 16 Jahren 61.997 Views
egrath Themenstarter:in
871 Beiträge seit 2005
vor 17 Jahren
[Artikel] Tray Applikationen (NotifyIcon)

Windows Tray Applikationen

Grundlegendes

Unter einer Windows Tray Applikation (im folgenden nur mehr kurz TrayApp genannt) versteht man eine Applikation welche im Gegensatz zu "normalen" Applikationen in erster Linie kein permanent geöffnetes User Interface besitzt, sondern nur in der Windows Tray sichtbar ist und im Hintergrund seine Aufgaben erledigt. Im Gegensatz zu Windows Services welche auch Aufgaben im Hintergrund erledigen, wird eine TrayApp allerdings im Kontext des gerade Lokal angemeldeten Benutzers ausgeführt und bietet in der Regel die möglichkeit, dass diese über eine grafische Oberfläche direkt konfiguriert wird.

Beispiele für Applikationen die dazu prädisteniert sind als TrayApp zu laufen:
*Tools für die schnelle Rekonfiguration von Systemparametern (bsp. Lautstärkenregler, Ändern der Bildschirmauflösung, etc.) *Tools für die Überwachung von Systemparametern (bsp. Netzwerkverbindungen, Systemtemperatur, Systemlast, etc.) *Konfigurationsinterfaces für Systemdienste (bsp. Virenscanner, Personal Firewall, etc.) *Benachrichtigungsanzeigen für Applikationen (bsp. E-Mail, Instant Messenger, etc.)

Im folgenden Tutorial werden wir eine TrayApp entwickeln, welche zwar keinerlei relevante Funktionalität enthält, allerdings die grundlegenden Konzepte und möglichkeiten Aufzeigt welche für TrayApp's typisch sind:
*Icon in der Tray *Kontextmenü für das Icon in der Tray *Hauptfenster durch Auswahl im Kontextmenü oder Doppelklick auf das Icon *Benachrichtungsfenster beim Minimieren des Hauptfensters (ala Windows Messenger beim anmelden neuer Benutzer in der Kontaktliste)

1. Das Grundgerüst der TrayApp

Das erstellen einer TrayApp unterscheidet sich nur in sehr wenigen Punkten von denen, die man für das erstellen einer anderen Anwendung benötigt. Das erstellen des Icons in der Tray und das reagieren auf Ereignisse, welche von diesem ausgelöst werden werden über die Klasse "NotifyIcon" gesteuert. Um ein Icon in der Tray zu erstellen reicht es also, wenn wir uns ein Objekt vom Typ "NotifyIcon" instantiieren und die entsprechenden Eigenschaften setzen (Sourcecode ab Zeile 73)  

2. Das Hauptfenster

Da die Interaktion mit der Applikation über ein Windows Forms Fenster erfolgen soll müssen wir auch dieses erstellen und so mit dem Tray Icon verbinden, dass es in den folgenden Situationen geöffnet wird:  

*Der Benutzer wählt den entsprechenden Eintrag im Kontextmenü der TrayApp *Der Benuzter tätigt einen Doppelklick auf das Icon der TrayApp - in diesem Fall muss je nach zustand des Hauptfensters (angezeigt/nicht angezeigt) der inverse Zustand hergestellt werden.

Für unsere Demo TrayApp habe ich mich dazu entschieden die Hauptklasse welche alle Aktionen kontrolliert direkt von System.Windows.Forms.Form abzuleiten (Sourcecode ab Zeile 25). Im Konstruktor dieser Klasse werden alle Initialisierungsarbeiten geleistet die für die TrayApp notwendig sind (Erstellen des Layouts des Hauptfensters, Erstellen des Tray-Icons, Erstellen des Kontextmenüs für das Tray-Icon) .  

Den Ereignissen für Doppelklick auf das Tray-Icon und auswahl im Kontextmenü werden im gleichen Zug entsprechende Event-Handler zugewiesen damit wir die gewünschte Aktion durchführen können. Eine besonderheit nimmt dabei der Handler für das Event "FormClosing" ein. Da wir zu Demonstrationszwecken alle möglichkeiten zum Beenden der Applikation deaktiviert haben und dies nur über den entsprechenden Eintrag im Kontextmenü durchgeführt werden kann, haben wir das Event "FormClosing" auf den Handler für das Verstecken des Hauptfensters gesetzt. In diesem muss nun überprüft werden, ob es sich bei den übergebenen Objekt vom Typ "EventArgs" in wirklichkeit um ein "FormClosingEventArgs" handelt. Sollte dies der Fall sein, so wurden dieser durch einen Versuch die Applikation zu beenden aufgerufen - und diese muss abgebrochen werden. Leider beginnt hier aber eine andere Tatsache zum Tragen, nämlich dass dieser Handler auch dann aufgerufen wird, wenn wir unsere TrayApp regulär durch das Kontextmenü beenden wollen. Aus diesem Grund wird im EventHandler für den entsprechenden Menüeintrag zuerst eine Boolsche Variable auf "true" gesetzt und diese dann im Handler abgefragt. Das hört sich jetzt etwas kompliziert an, ein Blick auf den Sourcecode ab Zeile 94, respektive 117 verrät uns aber dass es gar nicht so wild ist.  

3. Die Benachrichtigung

Wie eingangs bereits erwähnt wollen wir eine Mitteilung sobald das Hauptfenster minimiert wurde. Wie es sich für eine TrayApp gehört werden wir diese in einem kleinen, transparenten Fenster überhalb der Tray darstellen. Die dazu notwendigen Schritte sind folgende:  

*Im EventHandler welcher sich um das Minimieren kümmert, ein neues Objekt vom Typ "NotifyBox" instantiieren *Einen Thread starten welcher sich um die Darstellung der Mitteilung kümmert

Das darstellen der Mitteilung beschränkt sich im Endeffekt auf das errechnen der Position überhalb des Tray Bereichs und das erstellen einer neuen, transparenten Form an der entsprechenden Position und das schliessen derselben nach einer gewissen Zeitspanne. Um die Position korrekt berechnen zu können, müssen wir auf native Win32-Api Funktionalität mittels P/Invoke zurückgreifen da .NET hier leider kein mir bekanntes Bordmittel bietet. (Sourcecode ab Zeile 128, P/Invoke ab Zeile 163)  

Quellcode der Applikation:


using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.InteropServices;

namespace TrayApp1
{
    class Program
    {
        public static void Main( string[] args )
        {
            NotifyApplication notifyApp = new NotifyApplication();
            notifyApp.FormClosed += new FormClosedEventHandler( TrayAppExitHandler );

            Application.Run();
        }

        protected static void TrayAppExitHandler( object sender, FormClosedEventArgs e )
        {
            Application.Exit();
        }
    }

    class NotifyApplication : Form
    {
        private NotifyIcon m_NotifyIcon;
        private ContextMenu m_ContextMenu;
        private bool m_ReallyClose;

        public NotifyApplication()
        {
            m_ReallyClose = false;

            this.SuspendLayout();

            // Allgemeine Form Parameter setzen
            this.FormBorderStyle = FormBorderStyle.FixedSingle;
            this.Text = "Tray Application";
            this.Visible = false;
            this.MinimizeBox = false;
            this.MaximizeBox = false;
            this.FormClosing += new FormClosingEventHandler( HideMainFormHandler );

            // Ein paar Controls zum Form hinzufügen
            // Hide-Button
            Button buttonHide = new Button();
            buttonHide.Text = "Verstecken";
            buttonHide.Location = new Point( 10, this.ClientRectangle.Height - 10 - buttonHide.Size.Height );
            buttonHide.Anchor = ( AnchorStyles.Bottom | AnchorStyles.Left );
            buttonHide.Click += new EventHandler( HideMainFormHandler );
            this.Controls.Add( buttonHide );

            // Label
            string labelText = "Hello Tray App!";
            Font drawFont = new Font( FontFamily.GenericSansSerif, 20.0f );
            SizeF textSize = CreateGraphics().MeasureString( labelText, drawFont );
            Label labelHello = new Label();
            labelHello.Text = labelText;
            labelHello.Anchor = ( AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right );
            labelHello.Location = new Point(( int ) (( this.ClientRectangle.Width - ( int ) textSize.Width ) / 2 ), 20 );
            labelHello.Font = drawFont;
            labelHello.Size = textSize.ToSize();
            this.Controls.Add( labelHello );

            // Erstellen des Menüs für das Tray Icon
            m_ContextMenu = new ContextMenu();
            MenuItem menuEntryExit = new MenuItem( "Beenden", new EventHandler( this.ExitApplicationHandler ));
            MenuItem menuEntryShow = new MenuItem( "Zeigen", new EventHandler( this.ShowMainFormHandler ));
            m_ContextMenu.MenuItems.Add( menuEntryShow );
            m_ContextMenu.MenuItems.Add( menuEntryExit );
            
            // Display the Tray Icon
            m_NotifyIcon = new NotifyIcon();
            m_NotifyIcon.Icon = new Icon( "trayicon.ico" );
            m_NotifyIcon.Text = "Tray Application";
            m_NotifyIcon.ContextMenu = m_ContextMenu;
            m_NotifyIcon.Visible = true;
            m_NotifyIcon.DoubleClick += new EventHandler( ToggleMainFormHandler );
        }

        public void ToggleMainFormHandler( object sender, EventArgs e )
        {
            if( this.Visible )
            {
                HideMainFormHandler( sender, e );
            }
            else
            {
                ShowMainFormHandler( sender, e );
            }
        }

        public void HideMainFormHandler( object sender, EventArgs e )
        {
            this.Visible = false;
            
            if( e is FormClosingEventArgs && ! m_ReallyClose )
            {
                (( FormClosingEventArgs ) e ).Cancel = true;
            }

            // Notify Form anzeigen
            if( ! m_ReallyClose )
            {
                NotifyBox notifyBox = new NotifyBox();
                Thread notifyBoxThread = new Thread( new ParameterizedThreadStart( notifyBox.ShowBox ));
                notifyBoxThread.Start( ( object ) "Minimized!" );
            }
        }

        public void ShowMainFormHandler( object sender, EventArgs e )
        {
            this.Visible = true;
        }

        public void ExitApplicationHandler( object sender, EventArgs e )
        {
            m_ReallyClose = true;
            this.Close();
        }
    }

    class NotifyBox
    {
        private Form m_Notify;

        public void ShowBox( object message )
        {
            // Erstellen der Form welche die Benachrichtigung einblendet
            m_Notify = new Form();
            m_Notify.FormBorderStyle = FormBorderStyle.None;
            m_Notify.AllowTransparency = true;
            m_Notify.BackColor = Color.Magenta;
            m_Notify.TransparencyKey = m_Notify.BackColor;
            m_Notify.ShowInTaskbar = false;

            // Label mit Mitteilung
            Label textLabel = new Label();
            textLabel.Text = ( string ) message;          
            textLabel.Font = new Font( FontFamily.GenericSansSerif, 20.0f, FontStyle.Regular );
            Graphics textGraphics = m_Notify.CreateGraphics();
            textLabel.Size = textGraphics.MeasureString( ( string ) message, textLabel.Font ).ToSize();
            textLabel.Location = new Point( 0, 0 );

            m_Notify.Controls.Add( textLabel );
            m_Notify.Size = textLabel.Size;

            // Herausfinden an welche Position wir die Form setzen müssen, damit diese überhalb der Tray eingeblendet wird
            m_Notify.StartPosition = FormStartPosition.Manual;
            m_Notify.DesktopLocation = new Point( Screen.PrimaryScreen.Bounds.Width - m_Notify.Size.Width,
                                           Screen.PrimaryScreen.Bounds.Height - WindowHelper.GetTaskbarHeight() - m_Notify.Size.Height );

            // Quick and Dirt: Da wir anschliessend den Thread anhalten, müssen wir noch alle Window Messages
            // abarbeiten, damit unsere NotifyBox richtig gezeichnet wird.
            m_Notify.Show();
            Application.DoEvents();
            Thread.Sleep( 1000 );            
            m_Notify.Close();
        }
    }

    class WindowHelper
    {
        [DllImport("user32.dll")]
        private static extern IntPtr FindWindowEx( IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow );

        [DllImport("user32.dll")]
        private static extern bool GetWindowRect( IntPtr hWnd, out Rectangle lpRect );

        public static int GetTaskbarHeight()
        {
            IntPtr handleTaskbar = FindWindowEx( IntPtr.Zero, IntPtr.Zero, "Shell_TrayWnd", "" );
            if( handleTaskbar == IntPtr.Zero ) return( 0 );

            Rectangle taskbarRect;
            GetWindowRect( handleTaskbar, out taskbarRect );

            return( taskbarRect.Height - taskbarRect.Y );
        }
    }
}

/edit:
Im anhang befindet sich nun die komplette Solution mit allen Icons die verwendet wurden.
/edit:
Das Tutorial wurde überarbeitet, ein Tipp von Herbivore bezüglich der Windows Messages wurde eingearbeitet.
/edit:
Ich möchte gerne auch noch auf das Tray/Notify Template von Herbivore verweisen: Vorlage für Tray-/NotifyIcon-Anwendung

B
1.529 Beiträge seit 2006
vor 17 Jahren

Sehr schöner Artikel.

Ich wollte dich nur darauf hinweisen, dass sich die Taskbar an jedem Bildschirmrand befinden kann. Daher funktioniert deine Methode GetTaskbarHeight() sowie die entsprechende Koordinatenberechnung nicht allgemeingültig.

egrath Themenstarter:in
871 Beiträge seit 2005
vor 17 Jahren

Hallo Borg,

danke für deine Anregung, ich werd bei gelegenheit das Tutorial erweitern um dies zu berücksichtigen.

Grüsse, Egon

A
3 Beiträge seit 2006
vor 17 Jahren
NotifyIcon

Vielen Dank für den Ansatz! Habe erst nach Taskbar-Applikation gesucht🙂

Übrigens gibt es in der Klasse NotifyIcon eine sehr schöne Möglichkeit eine Sprechblase zum Icon zu öffnen - inkl. Timer, Titel und Nachricht! Die Methode hierzu heisst "ShowBalloonTip" mit der das Problem der genauen Platzierung des Minimieren-Fenster dann erledigt wäre😉

Ciao und Danke noch mal
Antonio

Die Tragig der Welt liegt darin, dass die Dummen so voller Selbstsicherheit sind und die Weisen voller Selbstzweifel!

1.271 Beiträge seit 2005
vor 17 Jahren

Hallo antopa,

Du solltest mit den BalloonTips aber nichts wichtiges anzeigen, denn man kann das Anzeigen von BalloonTips systemweit unterbinden. Siehe dazu auch toolTip.IsBallon funktioniert nicht

Gruß,
progger

A wise man can learn more from a foolish question than a fool can learn from a wise answer!
Bruce Lee

Populanten von Domizilen mit fragiler, transparenter Außenstruktur sollten sich von der Translation von gegen Deformierung resistenter Materie distanzieren!
Wer im Glashaus sitzt, sollte nicht mit Steinen werfen.

A
38 Beiträge seit 2006
vor 17 Jahren

Hi,
Erstmal 👍! Echt super Artikel!!! Konnte die Sourcen einwandfrei in mein Programm integrieren! Nur eins hast du vergessen: Im

ExitApplicationHandler

muss vor der Zeile

this.Close();

noch die Zeile

m_NotifyIcon.Dispose();

eingefügt werden.
Ansonsten verschwindet das NotifyIcon nicht automtisch, nachdem die Anwendung über den Klick auf den Context-Menu-Eintrag "Beenden" geschlossen wurde. Erst wenn man nochmal mit der Maus über das Icon fährt verschwindet es.

Gruß, Andi!

M
302 Beiträge seit 2004
vor 16 Jahren

Ich hatte mit dem Code ein kleines Problem und zwar fährt Windows nicht runter solange die AW im Try ist da die Anwendung immer ein .Cancel zurück giebt.

Es müssen folgenden Sachen geändert werden um dieses Problem zu umgehen:

public void HideMainFormHandler( object sender, EventArgs e )
        {
            this.Visible = false;
            
            if( e is FormClosingEventArgs && ! m_ReallyClose )
            {
                (( FormClosingEventArgs ) e ).Cancel = true;
            }

            // Notify Form anzeigen
            if( ! m_ReallyClose )
            {
                NotifyBox notifyBox = new NotifyBox();
                Thread notifyBoxThread = new Thread( new ParameterizedThreadStart( notifyBox.ShowBox ));
                notifyBoxThread.Start( ( object ) "Minimized!" );
            }
        }

Ersetzen durch

public void HideMainFormHandler( object sender, EventArgs e )
        {
            this.Visible = false;
            
            if( e is FormClosingEventArgs && ! m_ReallyClose){
				if((( FormClosingEventArgs ) e ).CloseReason == System.Windows.Forms.CloseReason.UserClosing)
                	(( FormClosingEventArgs ) e ).Cancel = true;	
			}

            // Notify Form anzeigen
            if( ! m_ReallyClose )
            {
                NotifyBox notifyBox = new NotifyBox();
                Thread notifyBoxThread = new Thread( new ParameterizedThreadStart( notifyBox.ShowBox ));
                notifyBoxThread.Start( ( object ) "Minimized!" );
            }
        }

So hat es bei mir ohne Probleme funktioniert.