为 Windows 服务创建系统托盘图标和对话框






4.84/5 (17投票s)
本文展示了如何为 Windows 服务创建一个系统托盘图标和一个对话框。
引言
一般来说,为 Windows 服务实现带上下文菜单的系统托盘图标的最简单方法是实现一个单独的 Shell 程序。但在这里,我正在创建一个 Windows 服务,它将拥有自己的系统托盘图标和对话框。
背景
Windows 服务在 Windows 登录之前启动,因此第一个挑战是如何在 Windows 服务中检测到 Shell 已准备好。 第二个挑战是如何与 Windows 服务交互。 我使用系统事件通知服务 (SENS) 解决第一个问题。 对于第二个问题,我使用 P/Invoke 将线程与默认桌面关联起来。
步骤
步骤 1:打开 VS2005,创建一个 Windows 服务项目,并为 Windows 服务添加一个安装程序。
[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 the service is not active, start it
if (controller.Status != ServiceControllerStatus.Running)
{
string[] args = { "-install" };
controller.Start(args);
}
}
}
注意:Windows 服务应依赖“SENS”和“COMSysApp”,因为我们需要确保 Windows 服务在依赖项准备就绪后启动。
步骤 2:SENS 订阅。 向项目添加对“COM+ 1.0 管理类型库”的引用。 向项目添加对“SENS 事件类型库”的引用。 添加 SensAdvisor
类,该类用于对 SENS 接口进行订阅。 我根据 MSDN 文章 “使用 SENS 访问系统电源和网络状态”编写了 SensAdvisor
类。 在这里,我们对 ISensLogon2 接口感兴趣(您也可以订阅 ISensLogon
)。
/// <summary>
/// ISensLogon2 Event Args
/// </summary>
public class SensLogon2EventArgs : EventArgs
{
public string Username;
public uint SessionId;
}
/// <summary>
/// subscribe SENS notification
/// Ref MSDN:Accessing System Power and Network Status Using SENS
/// </summary>
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();
// Specify the parameters of the desired subscription.
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);
// This setting allows subscriptions to work for non-Administrator users.
catalogObject.set_Value("PerUser", true);
// Save the changes made to the transient subscription collection.
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);
}
}
}
步骤 3:为 User32.dll 中所需的 API 添加一个包装类。
/// <summary>
/// The wrapper class for User32.dll
/// </summary>
public static class User32DLL
{
/// <summary>
/// The GetDesktopWindow function returns a handle to the desktop window.
/// The desktop window covers the entire screen.
/// The desktop window is the area on top of which other windows are painted.
/// </summary>
/// <returns>The return value is a handle to the desktop window. </returns>
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetDesktopWindow();
/// <summary>
/// Retrieves a handle to the current window station for the calling process.
/// </summary>
/// <returns>If the function succeeds,
/// the return value is a handle to the window station.
/// If the function fails, the return value is NULL.</returns>
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetProcessWindowStation();
/// <summary>
/// Retrieves a handle to the desktop assigned to the specified thread.
/// </summary>
/// <param name="dwThread">[in] Handle to the thread
/// for which to return the desktop handle.</param>
/// <returns>If the function succeeds, the return value is a handle to the
/// desktop associated with the specified thread.
/// If the function fails, the return value is NULL.</returns>
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetThreadDesktop(uint dwThread);
/// <summary>
/// Opens the specified window station.
/// </summary>
/// <param name="lpszWinSta">Pointer to a null-terminated
/// string specifying the name of the window station
/// to be opened. Window station names are case-insensitive.
/// This window station must belong to the current session.
/// </param>
/// <param name="fInherit">[in] If this value
/// is TRUE, processes created by this process
/// will inherit the handle. Otherwise,
/// the processes do not inherit this handle.
/// </param>
/// <param name="dwDesiredAccess">[in] Access to the window station</param>
/// <returns>If the function succeeds, the return value
/// is the handle to the specified window station.
/// If the function fails, the return value is NULL.</returns>
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr OpenWindowStation(string lpszWinSta
, bool fInherit
, WindowStationAccessRight dwDesiredAccess
);
/// <summary>
/// Assigns the specified window station to the calling process.
/// This enables the process to access objects in the window
/// station such as desktops, the clipboard, and global atoms.
/// All subsequent operations on the window station
/// use the access rights granted to hWinSta.
/// </summary>
/// <param name="hWinSta">[in] Handle to the window
/// station to be assigned to the calling process</param>
/// <returns>If the function succeeds, the return value is nonzero.
/// If the function fails, the return value is zero. </returns>
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr SetProcessWindowStation(IntPtr hWinSta);
/// <summary>
/// Closes an open window station handle.
/// </summary>
/// <param name="hWinSta">[in] Handle
/// to the window station to be closed.</param>
/// <returns>If the function succeeds, the return value is nonzero.
/// If the function fails, the return value is zero. </returns>
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr CloseWindowStation(IntPtr hWinSta);
/// <summary>
/// Opens the specified desktop object.
/// </summary>
/// <param name="lpszDesktop">[in] Pointer to null-terminated string
/// specifying the name of the desktop to be opened.
/// Desktop names are case-insensitive.
/// This desktop must belong to the current window station.</param>
/// <param name="dwFlags">[in] This parameter can
/// be zero or DF_ALLOWOTHERACCOUNTHOOK=0x0001</param>
/// <param name="fInherit">[in] If this value is TRUE, processes created by
/// this process will inherit the handle.
/// Otherwise, the processes do not inherit this handle. </param>
/// <param name="dwDesiredAccess">[in] Access
/// to the desktop. For a list of access rights</param>
/// <returns>If the function succeeds, the return value is a handle to the opened desktop.
/// When you are finished using the handle, call the CloseDesktop function to close it.
/// If the function fails, the return value is NULL.
/// </returns>
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr OpenDesktop(string lpszDesktop
, OpenDesktopFlag dwFlags
, bool fInherit
, DesktopAccessRight dwDesiredAccess
);
/// <summary>
/// Closes an open handle to a desktop object.
/// </summary>
/// <param name="hDesktop">[in] Handle to the desktop to be closed.</param>
/// <returns>If the function succeeds, the return value is nonzero.
/// If the function fails, the return value is zero. </returns>
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr CloseDesktop(IntPtr hDesktop);
/// <summary>
/// Assigns the specified desktop to the calling thread.
/// All subsequent operations on the desktop use the access rights granted to the desktop.
/// </summary>
/// <param name="hDesktop">[in] Handle to the desktop
/// to be assigned to the calling thread.</param>
/// <returns>If the function succeeds, the return value is nonzero.
/// If the function fails, the return value is zero. </returns>
[DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetThreadDesktop(IntPtr hDesktop);
}
/// <summary>
/// REF MSDN:Window Station Security and Access Rights
/// ms-help://MS.MSDN.vAug06.en/dllproc/base/window_station_security_and_access_rights.htm
/// </summary>
[FlagsAttribute]
public enum WindowStationAccessRight : uint
{
/// <summary>All possible access rights for the window station.</summary>
WINSTA_ALL_ACCESS = 0x37F,
/// <summary>Required to use the clipboard.</summary>
WINSTA_ACCESSCLIPBOARD = 0x0004,
/// <summary>Required to manipulate global atoms.</summary>
WINSTA_ACCESSGLOBALATOMS = 0x0020,
/// <summary>Required to create new desktop
/// objects on the window station.</summary>
WINSTA_CREATEDESKTOP = 0x0008,
/// <summary>Required to enumerate existing desktop objects.</summary>
WINSTA_ENUMDESKTOPS = 0x0001,
/// <summary>Required for the window station to be enumerated.</summary>
WINSTA_ENUMERATE = 0x0100,
/// <summary>Required to successfully call the ExitWindows or ExitWindowsEx function.
/// Window stations can be shared by users and this access type can prevent other users
/// of a window station from logging off the window station owner.</summary>
WINSTA_EXITWINDOWS = 0x0040,
/// <summary>Required to read the attributes of a window station object.
/// This attribute includes color settings
/// and other global window station properties.</summary>
WINSTA_READATTRIBUTES = 0x0002,
/// <summary>Required to access screen contents.</summary>
WINSTA_READSCREEN = 0x0200,
/// <summary>Required to modify the attributes of
/// a window station object.
/// The attributes include color settings
/// and other global window station properties.</summary>
WINSTA_WRITEATTRIBUTES = 0x0010,
}
/// <summary>
/// OpenDesktop 2nd param
/// </summary>
public enum OpenDesktopFlag : uint
{
/// <summary>
/// Default value
/// </summary>
DF_NONE = 0x0000,
/// <summary>
/// Allows processes running in other accounts on the desktop
/// to set hooks in this process.
/// </summary>
DF_ALLOWOTHERACCOUNTHOOK = 0x0001,
}
/// <summary>
/// REF MSDN:Desktop Security and Access Rights
/// ms-help://MS.MSDN.vAug06.en/dllproc/base/desktop_security_and_access_rights.htm
/// </summary>
[FlagsAttribute]
public enum DesktopAccessRight : uint
{
/// <summary>Required to create a menu on the desktop. </summary>
DESKTOP_CREATEMENU = 0x0004,
/// <summary>Required to create a window on the desktop. </summary>
DESKTOP_CREATEWINDOW = 0x0002,
/// <summary>Required for the desktop to be enumerated. </summary>
DESKTOP_ENUMERATE = 0x0040,
/// <summary>Required to establish any of the window hooks. </summary>
DESKTOP_HOOKCONTROL = 0x0008,
/// <summary>Required to perform journal playback on a desktop. </summary>
DESKTOP_JOURNALPLAYBACK = 0x0020,
/// <summary>Required to perform journal recording on a desktop. </summary>
DESKTOP_JOURNALRECORD = 0x0010,
/// <summary>Required to read objects on the desktop. </summary>
DESKTOP_READOBJECTS = 0x0001,
/// <summary>Required to activate the desktop
/// using the SwitchDesktop function. </summary>
DESKTOP_SWITCHDESKTOP = 0x0100,
/// <summary>Required to write objects on the desktop. </summary>
DESKTOP_WRITEOBJECTS = 0x0080,
}
步骤 4:添加 Desktop
类,它将帮助我们设置线程站。
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;
/// <summary>
/// associate the current thread to the default desktop
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// restore
/// </summary>
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);
}
}
步骤 5:添加 Form 和系统托盘图标类。 注意 UI 在另一个线程中运行。
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;
/// <summary>
/// Start the UI thread
/// </summary>
public static SettingDlg StartUIThread()
{
SettingDlg dlg = new SettingDlg();
Thread thread = new Thread(new ThreadStart(dlg.UIThread));
thread.Start();
return dlg;
}
/// <summary>
/// UI thread
/// </summary>
public void UIThread()
{
if( !m_Desktop.BeginInteraction() )
return;
Application.Run(this);
}
protected SettingDlg()
{
InitializeComponent();
}
/// <summary>
/// Move the window to the right-bottom corner
/// </summary>
/// <param name="e"></param>
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;
}
}
步骤 6:将以上所有内容集成在一起。
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)
{
// if the service started after the windows logon
m_Dlgs.Add(SettingDlg.StartUIThread());
}
protected override void OnStop()
{
foreach (SettingDlg dlg in m_Dlgs)
{
try
{
dlg.Close();
dlg.Dispose();
}
catch { }
}
m_Dlgs.Clear();
}
/// <summary>
/// Called when the shell is ready
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void PostShell(object sender, SensLogon2EventArgs e)
{
m_Dlgs.Add(SettingDlg.StartUIThread());
}
}
Points
我还没有从远程桌面客户端测试过它,因为该示例只是将 UI 与默认桌面相关联,因此它可能无法从远程桌面工作。如果是这样,您可以在连接到远程桌面时添加一个“/console”参数,这将使远程客户端使用默认桌面。注意,“/console”参数仅在 Windows 5.2 及更高版本下受支持。