近期需要開發一個檔案監控服務,查閱大量網路資料發現,大多數文章僅說明如何建立基本的 Windows 服務,卻鮮少提及如何與登入的使用者進行互動。根據 MSDN 文件說明,Windows 服務運行於非互動的視窗站與桌面環境中,若直接在服務中彈出視窗或對話框,將會不可見甚至導致程式停止回應。.NET Framework 內建的 ServiceBase 類別不支援與互動桌面溝通,若必須實現互動功能(如運行時參數設定、托盤圖示顯示),必須透過 P/Invoke 呼叫非受控的 Windows API。
要在服務中顯示 UI,關鍵在於將執行緒的桌面環境切換至互動使用者的 WinSta0\\Default 桌面。主要步驟如下:
OnStart 中啟動獨立執行緒(不可直接在主執行緒建立表單)。OpenWindowStation 開啟互動視窗站 WinSta0。SetProcessWindowStation 將程序關聯至該視窗站。OpenDesktop 取得 Default 桌面控制代碼。SetThreadDesktop 將當前執行緒繫結至互動桌面。Application.Run(new Form1()) 進入訊息迴圈。using System;
using System.ComponentModel;
using System.Diagnostics;
using System.ServiceProcess;
using System.Threading;
using System.Runtime.InteropServices;
namespace FileWatchService
{
public class FileWatchService : ServiceBase
{
private Thread _uiThread;
private IContainer components;
public FileWatchService()
{
InitializeComponent();
}
private void InitializeComponent()
{
this.ServiceName = "JadeWatchService";
}
[STAThread]
static void Main()
{
ServiceBase.Run(new FileWatchService());
}
protected override void Dispose(bool disposing)
{
if (disposing && components != null)
components.Dispose();
base.Dispose(disposing);
}
protected override void OnStart(string[] args)
{
_uiThread = new Thread(ShowInteractiveForm);
_uiThread.SetApartmentState(ApartmentState.STA);
_uiThread.Start();
}
protected override void OnStop()
{
if (_uiThread != null && _uiThread.IsAlive)
{
_uiThread.Abort();
_uiThread = null;
}
}
private void ShowInteractiveForm()
{
// 取得目前程序與執行緒的視窗站/桌面控制代碼,稍後需還原
IntPtr hWinStaSave = GetProcessWindowStation();
IntPtr hThreadId = GetCurrentThreadId();
IntPtr hDeskSave = GetThreadDesktop(hThreadId);
// 開啟互動視窗站 WinSta0
IntPtr hWinStaUser = OpenWindowStation("WinSta0", false, 0x033F); // MAXIMUM_ALLOWED
if (hWinStaUser == IntPtr.Zero)
{
RpcRevertToSelf();
return;
}
if (!SetProcessWindowStation(hWinStaUser))
{
CloseWindowStation(hWinStaUser);
return;
}
// 開啟互動桌面 Default
IntPtr hDeskUser = OpenDesktop("Default", 0, false, 0x033F);
RpcRevertToSelf(); // 還原模擬權限
if (hDeskUser == IntPtr.Zero)
{
SetProcessWindowStation(hWinStaSave);
CloseWindowStation(hWinStaUser);
return;
}
if (!SetThreadDesktop(hDeskUser))
{
CloseDesktop(hDeskUser);
SetProcessWindowStation(hWinStaSave);
CloseWindowStation(hWinStaUser);
return;
}
try
{
// 此處可建立帶有 NotifyIcon 的表單,實現托盤互動
using (var form = new Form1())
{
Application.Run(form);
}
}
finally
{
// 還原環境並釋放資源
SetThreadDesktop(hDeskSave);
SetProcessWindowStation(hWinStaSave);
CloseDesktop(hDeskUser);
CloseWindowStation(hWinStaUser);
}
}
#region P/Invoke 宣告
[DllImport("user32.dll")] private static extern int GetDesktopWindow();
[DllImport("user32.dll")] private static extern IntPtr GetProcessWindowStation();
[DllImport("kernel32.dll")] private static extern IntPtr GetCurrentThreadId();
[DllImport("user32.dll")] private static extern IntPtr GetThreadDesktop(IntPtr dwThread);
[DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr OpenWindowStation(string lpszWinSta, bool fInherit, int dwDesiredAccess);
[DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr OpenDesktop(string lpszDesktop, int dwFlags, bool fInherit, int dwDesiredAccess);
[DllImport("user32.dll")] private static extern bool CloseDesktop(IntPtr hDesktop);
[DllImport("rpcrt4.dll", SetLastError = true)] private static extern int RpcImpersonateClient(IntPtr bindingHandle);
[DllImport("rpcrt4.dll", SetLastError = true)] private static extern int RpcRevertToSelf();
[DllImport("user32.dll")] private static extern bool SetThreadDesktop(IntPtr hDesktop);
[DllImport("user32.dll")] private static extern bool SetProcessWindowStation(IntPtr hWinSta);
[DllImport("user32.dll")] private static extern bool CloseWindowStation(IntPtr hWinSta);
#endregion
}
}
[STAThread] 或 SetApartmentState),否則 Windows Forms 會拋出例外。finally 區塊務必還原視窗站與桌面,否則後續服務啟動可能失敗,甚至影響系統穩定。OpenWindowStation("WinSta0") 僅適用於舊版 Windows;新版建議改用 IPC 機制(Named Pipe、WCF、TCP)讓獨立 GUI 應用程式與服務通訊,較為穩健且符合現代架構。考慮到 Session 0 隔離與 UAC 限制,微軟官方建議採用「服務 + 獨立桌面應用程式」架構:服務專注背景邏輯,透過 Named Pipe / WCF / gRPC 與啟動於使用者會話的 WPF/WinForms 托盤程式雙向通訊。此法相容性最佳,亦便於單元測試與維護。