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:
C#-Code: |
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();
this.FormBorderStyle = FormBorderStyle.FixedSingle;
this.Text = "Tray Application";
this.Visible = false;
this.MinimizeBox = false;
this.MaximizeBox = false;
this.FormClosing += new FormClosingEventHandler( HideMainFormHandler );
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 );
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 );
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 );
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;
}
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 )
{
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 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;
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 );
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