Windows服务模式下C#截图黑屏问题及解决需求
Windows服务模式下截图黑屏的解决方案
问题背景
用C#编写的控制台截图功能在调试模式下可正常保存屏幕画面,但以Windows服务模式运行时仅能捕获到黑屏。推测该问题由会话隔离导致,需修改实现以捕获PC上活跃用户的屏幕画面。此前尝试将截图功能封装为外部EXE并通过新进程调用,但未成功。相关原代码如下:
原截图类代码
internal class ScreenCapture { /// Creates an Image object containing a screen shot of the entire desktop public Image CaptureScreen() { return CaptureWindow(User32.GetDesktopWindow()); } /// Creates an Image object containing a screen shot of a specific window public Image CaptureWindow(IntPtr handle) { // get te hDC of the target window IntPtr hdcSrc = User32.GetWindowDC(handle); // get the size User32.RECT windowRect = new User32.RECT(); User32.GetWindowRect(handle, ref windowRect); int width = windowRect.right - windowRect.left; int height = windowRect.bottom - windowRect.top; // create a device context we can copy to IntPtr hdcDest = GDI32.CreateCompatibleDC(hdcSrc); // create a bitmap we can copy it to, // using GetDeviceCaps to get the width/height IntPtr hBitmap = GDI32.CreateCompatibleBitmap(hdcSrc, width, height); // select the bitmap object IntPtr hOld = GDI32.SelectObject(hdcDest, hBitmap); // bitblt over GDI32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, GDI32.SRCCOPY); // restore selection GDI32.SelectObject(hdcDest, hOld); // clean up GDI32.DeleteDC(hdcDest); User32.ReleaseDC(handle, hdcSrc); // get a .NET image object for it Image img = Image.FromHbitmap(hBitmap); // free up the Bitmap object GDI32.DeleteObject(hBitmap); return img; } /// <summary> /// Captures a screen shot of a specific window, and saves it to a file /// </summary> /// <param name="handle"></param> /// <param name="filename"></param> /// <param name="format"></param> public void CaptureWindowToFile(IntPtr handle, string filename, ImageFormat format) { Image img = CaptureWindow(handle); img.Save(filename, format); } /// <summary> /// Captures a screen shot of the entire desktop, and saves it to a file /// </summary> /// <param name="filename"></param> /// <param name="format"></param> public void CaptureScreenToFile(string filename, ImageFormat format) { Image img = CaptureScreen(); img.Save(filename, format); } /// <summary> /// Helper class containing Gdi32 API functions /// </summary> private class GDI32 { public const int SRCCOPY = 0x00CC0020; // BitBlt dwRop parameter [DllImport("gdi32.dll")] public static extern bool BitBlt(IntPtr hObject, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hObjectSource, int nXSrc, int nYSrc, int dwRop); [DllImport("gdi32.dll")] public static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int nWidth, int nHeight); [DllImport("gdi32.dll")] public static extern IntPtr CreateCompatibleDC(IntPtr hDC); [DllImport("gdi32.dll")] public static extern bool DeleteDC(IntPtr hDC); [DllImport("gdi32.dll")] public static extern bool DeleteObject(IntPtr hObject); [DllImport("gdi32.dll")] public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject); } /// <summary> /// Helper class containing User32 API functions /// </summary> private class User32 { [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left; public int top; public int right; public int bottom; } [DllImport("user32.dll")] public static extern IntPtr GetDesktopWindow(); [DllImport("user32.dll")] public static extern IntPtr GetWindowDC(IntPtr hWnd); [DllImport("user32.dll")] public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC); [DllImport("user32.dll")] public static extern IntPtr GetWindowRect(IntPtr hWnd, ref RECT rect); } }
原截图调用代码
// Screenshot function, capture entire screen, and save it to a file ScreenCapture sc = new ScreenCapture(); Image img = sc.CaptureScreen(); string pathOfScreenshot = "C:\\ProgramData\\war\\screenshots"; if (!Directory.Exists(pathOfScreenshot)) { Directory.CreateDirectory(pathOfScreenshot); } var date = DateTime.UtcNow; string image = pathOfScreenshot + "\\screenshot_" + date.Day + date.Month + date.Year + "_" + date.Hour + date.Minute + date.Second + ".jpeg"; img.Save(image, ImageFormat.Jpeg);
核心原因
Windows服务默认运行在会话0,而用户登录后的桌面会话在会话1及以上,系统的会话隔离机制会阻止服务直接访问用户会话的桌面资源,因此只能截到黑屏。
可行解决方案
1. 配置服务与用户会话交互(仅适用于Windows 7/Server 2008及更早系统)
- 打开服务属性面板,切换到「登录」选项卡
- 勾选「允许服务与桌面交互」
- 注意:Windows 8及以后版本已限制该功能,即使勾选也无法正常访问用户会话桌面
2. 注入进程到活跃用户会话(推荐,适配全系统)
通过Windows API获取活跃用户的会话ID,再创建运行在该会话下的进程执行截图操作,具体步骤如下:
步骤1:添加WTSAPI32 API声明
public class WTSApi32 { [DllImport("wtsapi32.dll")] public static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token); [DllImport("wtsapi32.dll")] public static extern void WTSFreeMemory(IntPtr memory); [DllImport("wtsapi32.dll")] public static extern bool WTSEnumerateSessions(IntPtr hServer, int reserved, int version, ref IntPtr ppSessionInfo, ref int pCount); [StructLayout(LayoutKind.Sequential)] public struct WTS_SESSION_INFO { public uint SessionId; [MarshalAs(UnmanagedType.LPStr)] public string pWinStationName; public WTS_CONNECTSTATE_CLASS State; } public enum WTS_CONNECTSTATE_CLASS { WTSActive, WTSConnected, WTSConnectQuery, WTSShadow, WTSDisconnected, WTSIdle, WTSListen, WTSReset, WTSDown, WTSInit } }
步骤2:获取活跃用户会话ID
private static uint GetActiveSessionId() { IntPtr serverHandle = IntPtr.Zero; IntPtr sessionInfoPtr = IntPtr.Zero; int sessionCount = 0; try { if (WTSApi32.WTSEnumerateSessions(serverHandle, 0, 1, ref sessionInfoPtr, ref sessionCount)) { for (int i = 0; i < sessionCount; i++) { IntPtr currentSessionPtr = IntPtr.Add(sessionInfoPtr, i * Marshal.SizeOf(typeof(WTSApi32.WTS_SESSION_INFO))); WTSApi32.WTS_SESSION_INFO sessionInfo = (WTSApi32.WTS_SESSION_INFO)Marshal.PtrToStructure(currentSessionPtr, typeof(WTSApi32.WTS_SESSION_INFO)); // 筛选活跃的用户会话 if (sessionInfo.State == WTSApi32.WTS_CONNECTSTATE_CLASS.WTSActive) { return sessionInfo.SessionId; } } } return 0; } finally { if (sessionInfoPtr != IntPtr.Zero) { WTSApi32.WTSFreeMemory(sessionInfoPtr); } } }
步骤3:在目标会话中启动截图进程
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] private static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [StructLayout(LayoutKind.Sequential)] private struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] private struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } [DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr hObject); public static void RunScreenshotInUserSession() { uint sessionId = GetActiveSessionId(); if (sessionId == 0) { // 无活跃用户会话,无法截图 return; } if (!WTSApi32.WTSQueryUserToken(sessionId, out IntPtr userToken)) { // 获取用户Token失败,需处理错误 return; } try { STARTUPINFO startupInfo = new STARTUPINFO(); startupInfo.cb = Marshal.SizeOf(startupInfo); startupInfo.lpDesktop = @"winsta0\default"; // 指定用户桌面会话 PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION(); // 替换为你的外部截图EXE路径 string exePath = @"C:\ProgramData\war\ScreenshotTool.exe"; bool success = CreateProcessAsUser(userToken, exePath, null, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref startupInfo, out processInfo); if (success) { // 关闭进程和线程句柄释放资源 CloseHandle(processInfo.hProcess); CloseHandle(processInfo.hThread); } } finally { CloseHandle(userToken); } }
步骤4:配置外部截图EXE
将原截图代码封装为独立的控制台EXE,注意:
- 保存路径使用
C:\ProgramData这类全用户可访问的目录 - 确保EXE能独立完成截图和保存操作,不依赖服务上下文
3. 使用Windows任务计划替代服务(简单方案)
若无需严格的服务运行逻辑,可将截图程序改为控制台应用,通过任务计划触发:
- 创建任务,设置触发条件(定时、事件触发等)
- 在「安全选项」中选择「不管用户是否登录都要运行」,并勾选「使用最高权限运行」
- 任务会在用户会话中执行,可正常截图
关键注意事项
- 服务运行账号需具备足够权限,推荐使用LocalSystem账号,或拥有
SeAssignPrimaryTokenPrivilege权限的账号 - Windows 10/11及Server 2012+系统会话隔离更严格,必须使用进程注入到用户会话的方案
- 测试时需以服务身份运行,避免直接调试导致的会话环境混淆
内容的提问来源于stack exchange,提问作者Marko
相关产品推荐
相关产品推荐

