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

C#编写Windows服务修改桌面壁纸:服务运行失效问题排查

解决Windows服务无法修改桌面壁纸的问题

问题背景

这段C#代码作为控制台应用运行时能正常修改桌面壁纸,但安装为Windows服务后完全失效,运行环境是Windows 10。已尝试调整计时器、修改图片路径,问题仍未解决。

原代码:

using System.Runtime.InteropServices;
using System.Timers;

namespace WindowsServicesManager
{
    class WallpaperService
    {
        
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern int SystemParametersInfo(
        Int32 uAction,
        Int32 uParam,
        String lpvParam,
        Int32 fuWinIni);
        

        private readonly System.Timers.Timer _timer;

        private Random rand = new();

        public WallpaperService()
        {
            _timer = new System.Timers.Timer(100) {AutoReset = true};
            _timer.Elapsed += change;
        }

        void change(object? sender, EventArgs e)
        {
            SetWallpaper($"{Environment.CurrentDirectory}/Icon.png");
            _timer.Interval = TimeSpan.FromDays(rand.Next(6, 15)).TotalMilliseconds;
        }

        public void SetWallpaper(String path) => SystemParametersInfo(20, 0, path, 0x01 | 0x02);

        public void Start()
        {
            _timer.Start();
        }

        public void Stop()
        {
            _timer.Stop();
        }
    }
}

核心原因

Windows服务默认运行在会话0隔离环境,这是Windows Vista及以后系统的安全机制。用户的交互式桌面(登录后看到的桌面)运行在会话1或更高编号的会话中,服务在会话0调用SystemParametersInfo修改的是会话0的壁纸设置,用户完全看不到。另外,服务的运行上下文(权限、环境变量等)和当前登录用户不一致,无法直接访问用户桌面会话。

有效解决方案

方案一:配置服务允许与桌面交互(Windows 10+受限)

  1. 安装服务后,打开「服务」管理器,找到目标服务
  2. 右键→属性→登录选项卡,勾选「允许服务与桌面交互」
  3. 重启服务

注意:这个方法在Windows 10的高权限模式(UAC开启)下可能无法生效,仅适用于部分场景。

方案二:切换到用户交互式会话执行修改(推荐)

通过Windows API获取当前登录用户的会话令牌,以用户身份启动一个独立程序来修改壁纸,这是最可靠的方法。步骤如下:

1. 抽离壁纸修改逻辑为独立可执行程序

创建一个名为WallpaperChanger.exe的控制台程序,代码如下:

using System;
using System.Runtime.InteropServices;

namespace WallpaperChanger
{
    class Program
    {
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern int SystemParametersInfo(
            Int32 uAction,
            Int32 uParam,
            String lpvParam,
            Int32 fuWinIni);

        static void Main(string[] args)
        {
            if (args.Length > 0 && !string.IsNullOrEmpty(args[0]))
            {
                SystemParametersInfo(20, 0, args[0], 0x01 | 0x02);
            }
        }
    }
}
2. 修改Windows服务代码,添加会话切换逻辑

在原服务类中添加必要的API导入和会话切换方法:

using System;
using System.Runtime.InteropServices;
using System.Timers;
using System.IO;
using System.Reflection;

namespace WindowsServicesManager
{
    class WallpaperService
    {
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern int SystemParametersInfo(
            Int32 uAction,
            Int32 uParam,
            String lpvParam,
            Int32 fuWinIni);

        // 新增API导入
        [DllImport("wtsapi32.dll", SetLastError = true)]
        private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool GetShellWindow(out IntPtr hwnd);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool ProcessIdToSessionId(uint processId, out uint sessionId);

        [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);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(IntPtr hObject);

        [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;
        }

        private readonly System.Timers.Timer _timer;
        private Random rand = new();

        public WallpaperService()
        {
            _timer = new System.Timers.Timer(100) { AutoReset = true };
            _timer.Elapsed += change;
        }

        void change(object? sender, EventArgs e)
        {
            // 使用程序所在目录的绝对路径,避免服务当前目录问题
            string imagePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Icon.png");
            SetWallpaperInUserSession(imagePath);
            _timer.Interval = TimeSpan.FromDays(rand.Next(6, 15)).TotalMilliseconds;
        }

        // 新增:在用户会话中启动程序修改壁纸
        private void SetWallpaperInUserSession(string imagePath)
        {
            // 获取Explorer窗口句柄(代表当前用户桌面会话)
            if (!GetShellWindow(out IntPtr shellHwnd))
                return;

            // 获取窗口所属进程ID
            GetWindowThreadProcessId(shellHwnd, out uint processId);

            // 获取进程对应的会话ID
            if (!ProcessIdToSessionId(processId, out uint sessionId))
                return;

            // 获取该会话的用户令牌
            if (!WTSQueryUserToken(sessionId, out IntPtr userToken))
                return;

            try
            {
                // 拼接启动命令,调用独立的壁纸修改程序
                string changerPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "WallpaperChanger.exe");
                string commandLine = $"\"{changerPath}\" \"{imagePath}\"";

                STARTUPINFO si = new STARTUPINFO();
                si.cb = Marshal.SizeOf(si);
                si.lpDesktop = "WinSta0\\Default"; // 指定用户桌面会话

                PROCESS_INFORMATION pi = new PROCESS_INFORMATION();

                // 以用户身份创建进程
                if (CreateProcessAsUser(
                    userToken,
                    null,
                    commandLine,
                    IntPtr.Zero,
                    IntPtr.Zero,
                    false,
                    0,
                    IntPtr.Zero,
                    null,
                    ref si,
                    out pi))
                {
                    // 关闭不必要的句柄
                    CloseHandle(pi.hProcess);
                    CloseHandle(pi.hThread);
                }
            }
            finally
            {
                CloseHandle(userToken);
            }
        }

        public void Start()
        {
            _timer.Start();
        }

        public void Stop()
        {
            _timer.Stop();
        }
    }
}

额外注意事项

  • 服务需使用本地系统账户运行,否则可能没有权限获取用户令牌
  • 确保Icon.png和WallpaperChanger.exe都放在服务程序的同一目录下
  • Windows Defender或杀毒软件可能拦截服务创建进程的操作,需将程序目录添加到信任列表
  • 不要使用Environment.CurrentDirectory获取路径,服务的当前目录默认是C:\Windows\System32,改用程序集所在目录更可靠

内容的提问来源于stack exchange,提问作者Arnas Bies

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 22:20:27