Introduction
Generally speaking, the easiest way to implement a system tray icon with a context menu for a Windows Service is to implement a separate Shell program. But here, I am creating a Windows Service which will have its own system tray icon and dialog.
Background
A Windows Service is started before the Windows logon, so the first challenge is how to detect the Shell is ready in the Windows Service. The second challenge is how to interact with the Windows service. I solve the first issue with the System Event Notification Service (SENS). For the second, I associate the thread to the default desktop by using P/Invoke.
Steps
Step 1: Open VS2005, create a Windows Service project, and add an installer for the Windows Service.
[RunInstaller(true)]
public class SvcInstaller : Installer
{
private static readonly string SVC_NAME = "SystemTrayIconInSvc";
private static readonly string SVC_DESC = "This is a test";
public SvcInstaller()
{
Installers.Clear();
ServiceInstaller serviceInstaller = new ServiceInstaller();
serviceInstaller.StartType = ServiceStartMode.Automatic;
serviceInstaller.ServiceName = SVC_NAME;
serviceInstaller.DisplayName = SVC_NAME;
serviceInstaller.Description = SVC_DESC;
serviceInstaller.ServicesDependedOn = new string[] { "SENS", "COMSysApp" };
Installers.Add(serviceInstaller);
ServiceProcessInstaller processInstaller = new ServiceProcessInstaller();
processInstaller.Account = ServiceAccount.LocalSystem;
processInstaller.Password = null;
processInstaller.Username = null;
Installers.Add(processInstaller);
}
protected override void OnAfterInstall(IDictionary savedState)
{
ServiceController controller = null;
ServiceController[] controllers = ServiceController.GetServices();
for (int i = 0; i < controllers.Length; i++)
{
if (controllers[i].ServiceName == SVC_NAME)
{
controller = controllers[i];
break;
}
}
if (controller == null)
{
return;
}
if (controller.Status != ServiceControllerStatus.Running)
{
string[] args = { "-install" };
controller.Start(args);
}
}
}
Note: The Windows service should depend on the "SENS" and "COMSysApp" because we need to make sure the Windows service is launched after the dependency is ready.
Step 2: SENS subscription. Add a reference to "COM+ 1.0 Admin Type Library" to the project. Add a reference to "SENS Events Type Library" to the project. Add the SensAdvisor
class which is used to do the subscription against the SENS interface. I wrote the SensAdvisor
class based on the MSDN article, "Accessing System Power and Network Status Using SENS". Here, we are interested in the ISensLogon2 interface (you can subscribe to the ISensLogon
as well).
public class SensLogon2EventArgs : EventArgs
{
public string Username;
public uint SessionId;
}
public sealed class SensAdvisor : ISensLogon2
{
public const string ISensLogon2_ID = "{d5978650-5b9f-11d1-8dd2-00aa004abd5e}";
public SensAdvisor()
{
COMAdminCatalogClass comAdmin = new COMAdminCatalogClass();
ICatalogCollection subCollection =
(ICatalogCollection)comAdmin.GetCollection("TransientSubscriptions");
SubscribeToEvent(subCollection, "PostShell", ISensLogon2_ID);
SubscribeToEvent(subCollection, "Logon", ISensLogon2_ID);
SubscribeToEvent(subCollection, "Logoff", ISensLogon2_ID);
SubscribeToEvent(subCollection, "SessionReconnect", ISensLogon2_ID);
SubscribeToEvent(subCollection, "SessionDisconnect", ISensLogon2_ID);
}
private void SubscribeToEvent(ICatalogCollection subCollection,
string methodName, string guidString)
{
ICatalogObject catalogObject = (ICatalogObject)subCollection.Add();
catalogObject.set_Value("EventCLSID", guidString);
catalogObject.set_Value("Name", "Subscription to " +
methodName + " event");
catalogObject.set_Value("MethodName", methodName);
catalogObject.set_Value("SubscriberInterface", this);
catalogObject.set_Value("Enabled", true);
catalogObject.set_Value("PerUser", true);
subCollection.SaveChanges();
}
public delegate void PostShellEventHandler(object sender, SensLogon2EventArgs e);
public delegate void SessionReconnectEventHandler(object sender, SensLogon2EventArgs e);
public delegate void SessionDisconnectEventHandler(object sender, SensLogon2EventArgs e);
public delegate void LogonEventHandler(object sender, SensLogon2EventArgs e);
public delegate void LogoffEventHandler(object sender, SensLogon2EventArgs e);
public event PostShellEventHandler OnShellStarted;
public event SessionReconnectEventHandler OnSessionReconnected;
public event SessionDisconnectEventHandler OnSessionDisconnected;
public event LogonEventHandler OnLogon;
public event LogoffEventHandler OnLogoff;
public void PostShell(string bstrUserName, uint dwSessionId)
{
if (OnShellStarted != null)
{
SensLogon2EventArgs args = new SensLogon2EventArgs();
args.Username = bstrUserName;
args.SessionId = dwSessionId;
OnShellStarted(this, args);
}
}
public void SessionReconnect(string bstrUserName, uint dwSessionId)
{
if (OnSessionReconnected != null)
{
SensLogon2EventArgs args = new SensLogon2EventArgs();
args.Username = bstrUserName;
args.SessionId = dwSessionId;
OnSessionReconnected(this, args);
}
}
public void SessionDisconnect(string bstrUserName, uint dwSessionId)
{
if (OnSessionDisconnected != null)
{
SensLogon2EventArgs args = new SensLogon2EventArgs();
args.Username = bstrUserName;
args.SessionId = dwSessionId;
OnSessionDisconnected(this, args);
}
}
public void Logoff(string bstrUserName, uint dwSessionId)
{
if (OnLogoff != null)
{
SensLogon2EventArgs args = new SensLogon2EventArgs();
args.Username = bstrUserName;
args.SessionId = dwSessionId;
OnLogoff(this, args);
}
}
public void Logon(string bstrUserName, uint dwSessionId)
{
if (OnLogon != null)
{
SensLogon2EventArgs args = new SensLogon2EventArgs();
args.Username = bstrUserName;
args.SessionId = dwSessionId;
OnLogon(this, args);
}
}
}
Step 3: Add a wrapper class for the required APIs in the User32.dll.
public static class User32DLL
{
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetDesktopWindow();
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetProcessWindowStation();
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetThreadDesktop(uint dwThread);
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr OpenWindowStation(string lpszWinSta
, bool fInherit
, WindowStationAccessRight dwDesiredAccess
);
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr SetProcessWindowStation(IntPtr hWinSta);
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr CloseWindowStation(IntPtr hWinSta);
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr OpenDesktop(string lpszDesktop
, OpenDesktopFlag dwFlags
, bool fInherit
, DesktopAccessRight dwDesiredAccess
);
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr CloseDesktop(IntPtr hDesktop);
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetThreadDesktop(IntPtr hDesktop);
}
[FlagsAttribute]
public enum WindowStationAccessRight : uint
{
WINSTA_ALL_ACCESS = 0x37F,
WINSTA_ACCESSCLIPBOARD = 0x0004,
WINSTA_ACCESSGLOBALATOMS = 0x0020,
WINSTA_CREATEDESKTOP = 0x0008,
WINSTA_ENUMDESKTOPS = 0x0001,
WINSTA_ENUMERATE = 0x0100,
WINSTA_EXITWINDOWS = 0x0040,
WINSTA_READATTRIBUTES = 0x0002,
WINSTA_READSCREEN = 0x0200,
WINSTA_WRITEATTRIBUTES = 0x0010,
}
public enum OpenDesktopFlag : uint
{
DF_NONE = 0x0000,
DF_ALLOWOTHERACCOUNTHOOK = 0x0001,
}
[FlagsAttribute]
public enum DesktopAccessRight : uint
{
DESKTOP_CREATEMENU = 0x0004,
DESKTOP_CREATEWINDOW = 0x0002,
DESKTOP_ENUMERATE = 0x0040,
DESKTOP_HOOKCONTROL = 0x0008,
DESKTOP_JOURNALPLAYBACK = 0x0020,
DESKTOP_JOURNALRECORD = 0x0010,
DESKTOP_READOBJECTS = 0x0001,
DESKTOP_SWITCHDESKTOP = 0x0100,
DESKTOP_WRITEOBJECTS = 0x0080,
}
Step 4: Add the Desktop
class, which will help us to set the thread station.
internal class Desktop
{
private IntPtr m_hCurWinsta = IntPtr.Zero;
private IntPtr m_hCurDesktop = IntPtr.Zero;
private IntPtr m_hWinsta = IntPtr.Zero;
private IntPtr m_hDesk = IntPtr.Zero;
internal bool BeginInteraction()
{
EndInteraction();
m_hCurWinsta = User32DLL.GetProcessWindowStation();
if (m_hCurWinsta == IntPtr.Zero)
return false;
m_hCurDesktop = User32DLL.GetDesktopWindow();
if (m_hCurDesktop == IntPtr.Zero)
return false;
m_hWinsta = User32DLL.OpenWindowStation("winsta0", false,
WindowStationAccessRight.WINSTA_ACCESSCLIPBOARD |
WindowStationAccessRight.WINSTA_ACCESSGLOBALATOMS |
WindowStationAccessRight.WINSTA_CREATEDESKTOP |
WindowStationAccessRight.WINSTA_ENUMDESKTOPS |
WindowStationAccessRight.WINSTA_ENUMERATE |
WindowStationAccessRight.WINSTA_EXITWINDOWS |
WindowStationAccessRight.WINSTA_READATTRIBUTES |
WindowStationAccessRight.WINSTA_READSCREEN |
WindowStationAccessRight.WINSTA_WRITEATTRIBUTES
);
if (m_hWinsta == IntPtr.Zero)
return false;
User32DLL.SetProcessWindowStation(m_hWinsta);
m_hDesk = User32DLL.OpenDesktop("default", OpenDesktopFlag.DF_NONE, false,
DesktopAccessRight.DESKTOP_CREATEMENU |
DesktopAccessRight.DESKTOP_CREATEWINDOW |
DesktopAccessRight.DESKTOP_ENUMERATE |
DesktopAccessRight.DESKTOP_HOOKCONTROL |
DesktopAccessRight.DESKTOP_JOURNALPLAYBACK |
DesktopAccessRight.DESKTOP_JOURNALRECORD |
DesktopAccessRight.DESKTOP_READOBJECTS |
DesktopAccessRight.DESKTOP_SWITCHDESKTOP |
DesktopAccessRight.DESKTOP_WRITEOBJECTS
);
if (m_hDesk == IntPtr.Zero)
return false;
User32DLL.SetThreadDesktop(m_hDesk);
return true;
}
internal void EndInteraction()
{
if (m_hCurWinsta != IntPtr.Zero)
User32DLL.SetProcessWindowStation(m_hCurWinsta);
if (m_hCurDesktop != IntPtr.Zero)
User32DLL.SetThreadDesktop(m_hCurDesktop);
if (m_hWinsta != IntPtr.Zero)
User32DLL.CloseWindowStation(m_hWinsta);
if (m_hDesk != IntPtr.Zero)
User32DLL.CloseDesktop(m_hDesk);
}
}
Step 5: Add the Form and the system tray icon class. Note that the UI is running in another thread.
public partial class SettingDlg : Form
{
private Desktop m_Desktop = new Desktop();
private IContainer m_Container = null;
private NotifyIcon m_NotifyIcon = null;
private Button btnHide;
private ContextMenu m_ContextMenu = null;
public static SettingDlg StartUIThread()
{
SettingDlg dlg = new SettingDlg();
Thread thread = new Thread(new ThreadStart(dlg.UIThread));
thread.Start();
return dlg;
}
public void UIThread()
{
if( !m_Desktop.BeginInteraction() )
return;
Application.Run(this);
}
protected SettingDlg()
{
InitializeComponent();
}
protected override void OnShown(EventArgs e)
{
this.Left = Screen.PrimaryScreen.WorkingArea.Left
+ Screen.PrimaryScreen.WorkingArea.Width
- this.Width
;
this.Top = Screen.PrimaryScreen.WorkingArea.Top
+ Screen.PrimaryScreen.WorkingArea.Height
- this.Height
;
}
private void SettingDlg_Load(object sender, EventArgs e)
{
m_ContextMenu = new ContextMenu();
m_ContextMenu.MenuItems.Add(new MenuItem("Open Dialog", this.OpenDialog));
Icon icon = new Icon(SystemIcons.Application, 16, 16);
m_Container = new Container();
m_NotifyIcon = new NotifyIcon(m_Container);
m_NotifyIcon.ContextMenu = m_ContextMenu;
m_NotifyIcon.Icon = icon;
m_NotifyIcon.Visible = true;
m_NotifyIcon.ShowBalloonTip( 200
, "SystemTrayIconInSvc"
, "The system tray icon is implemented in the windows service itself."
, ToolTipIcon.Info
);
}
public void OpenDialog(Object sender, EventArgs e)
{
this.Visible = true;
BringToFront();
}
protected override void OnClosed(EventArgs e)
{
m_NotifyIcon.Dispose();
m_ContextMenu.Dispose();
m_Container.Dispose();
}
private void btnHide_Click(object sender, EventArgs e)
{
this.Visible = false;
}
}
Step 6: Integrate all of the above together.
public partial class Svc : ServiceBase
{
private SensAdvisor m_Advisor = new SensAdvisor();
private List<SettingDlg> m_Dlgs = new List<SettingDlg>();
public Svc()
{
InitializeComponent();
m_Advisor.OnShellStarted += this.PostShell;
}
internal void DebugStart()
{
OnStart(null);
}
protected override void OnStart(string[] args)
{
m_Dlgs.Add(SettingDlg.StartUIThread());
}
protected override void OnStop()
{
foreach (SettingDlg dlg in m_Dlgs)
{
try
{
dlg.Close();
dlg.Dispose();
}
catch { }
}
m_Dlgs.Clear();
}
public void PostShell(object sender, SensLogon2EventArgs e)
{
m_Dlgs.Add(SettingDlg.StartUIThread());
}
}
Points
I haven't tested this from a remote desktop client because the sample just associates the UI to the default desktop, so it may not work from a remote desktop. If so, you can add a "/console" parameter when you connect to the remote desktop, and that will make the remote client use the default desktop. Note, the "/console" parameter is only supported under Windows 5.2 and above.