You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 16:10:26