none
Setting the desktop background image via Powershell RRS feed

  • Question

  • UPDATE: 

    I found the solution that addressed my problem, which I posted in a post below. Keeping the original post for Internet historical reasons as I the hope this helps someone else later on in their search for a solution. 

    ===============

    I have Bing'ed and I have Google'd and I haven't found a solid answer to setting the desktop wallpaper programmically.  I am trying to do this via PowerShell, but I would be happy for any other language flavor or application to work. 

    Below is the code I have tried to set the desktop background image. This has been scrounged from a few sources across the web. This solution works 25% of the time and I haven't figured out what the cause for success or failure of the execution. Or maybe it's neither, but waiting for Explorer to refresh. The only way to see the change 100% is to log out and log back in.

    I am running Windows 8.1 btw. 

    Thank you for looking.

    Function Get-WallPaper()
    {
     $wp=Get-ItemProperty -path 'HKCU:\Control Panel\Desktop\' -name wallpaper
     if(!$wp.WallPaper) 
       { "Wall paper is not set" }
     Else
      {"Wall paper is set to $($wp.WallPaper)" }
    }
    
    Function Refresh-Explorer { 
        $code = @' 
    private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff);  
    private const int WM_SETTINGCHANGE = 0x1a;  
    private const int SMTO_ABORTIFHUNG = 0x0002;  
      
     
    [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)] 
    static extern bool SendNotifyMessage(IntPtr hWnd, uint Msg, UIntPtr wParam, 
       IntPtr lParam); 
     
    [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]  
      private static extern IntPtr SendMessageTimeout ( IntPtr hWnd, int Msg, IntPtr wParam, string lParam, uint fuFlags, uint uTimeout, IntPtr lpdwResult );  
      
      
    [System.Runtime.InteropServices.DllImport("Shell32.dll")]  
    private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2); 
     
     
    public static void Refresh()  { 
        SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero); 
        SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, null, SMTO_ABORTIFHUNG, 100, IntPtr.Zero);  
    } 
    '@ 
     
        Add-Type -MemberDefinition $code -Namespace MyWinAPI -Name Explorer
        [MyWinAPI.Explorer]::Refresh() 
    } 
    
    Function Set-WallPaper($Value)
    {
     echo "Setting background to: $value"
     Refresh-Explorer
     Set-ItemProperty -path 'HKCU:\Control Panel\Desktop\' -name wallpaper -value $value
     rundll32.exe user32.dll, UpdatePerUserSystemParameters
     #sleep 1
     rundll32.exe user32.dll, UpdatePerUserSystemParameters
     #sleep 1
     rundll32.exe user32.dll, UpdatePerUserSystemParameters
     #sleep 1
     Refresh-Explorer
     
     #Stop-Process -ProcessName explorer
    }

    NOTE: I left in a few commented out statements. I found that they do not offer any help in formulating a solution.

    Thank you.


    Tuesday, December 2, 2014 3:19 PM

Answers

  • Great news! I found the solution and all props and credit goes to jsd1982 of the XKCD comic forum from a post he made in 2007 and some deep search kung-fu Googling on my part. Here is the direct link to the post.

    This solution has worked for me 100% of the time on Windows 8. The only gotcha I found (minor detail) was when setting this process to run as a scheduled task, I had to run it as elevated privileges for the code to compile in memory. Running this function from PowerShell at the command prompt was fine. 

    NOTE: The code below is a slight trimmed down version posted by jsd1982 to just set the desktop wallpaper. I removed the image fetching from the Internet and the color inversion.

    # Credit to jsd1982
    
    function Compile-Csharp ([string] $code, $FrameworkVersion="v2.0.50727",
    [Array]$References)
    {
        #
        # Get an instance of the CSharp code provider
        #
        $cp = new-object Microsoft.CSharp.CSharpCodeProvider
    
        #
        # Build up a compiler params object...
        $framework = [System.IO.Path]::Combine($env:windir, "Microsoft.NET\Framework\$FrameWorkVersion")
        $refs = new-object Collections.ArrayList
        $refs.AddRange( @("${framework}\System.dll",
    #        "${mshhome}\System.Management.Automation.dll",
    #        "${mshhome}\System.Management.Automation.ConsoleHost.dll",
            "${framework}\system.windows.forms.dll",
            "${framework}\System.data.dll",
            "${framework}\System.Drawing.dll",
            "${framework}\System.Xml.dll"))
        if ($references.Count -ge 1)
        {
            $refs.AddRange($References)
        }
    
        $cpar = New-Object System.CodeDom.Compiler.CompilerParameters
        $cpar.GenerateInMemory = $true
        $cpar.GenerateExecutable = $false
       $cpar.CompilerOptions = "/unsafe";
        $cpar.OutputAssembly = "custom"
        $cpar.ReferencedAssemblies.AddRange($refs)
        $cr = $cp.CompileAssemblyFromSource($cpar, $code)
    
        if ( $cr.Errors.Count)
        {
            $codeLines = $code.Split("`n");
            foreach ($ce in $cr.Errors)
            {
                write-host "Error: $($codeLines[$($ce.Line - 1)])"
                $ce |out-default
            }
            Throw "INVALID DATA: Errors encountered while compiling code"
        }
    }
    
    [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") > $null
    [System.Reflection.Assembly]::LoadWithPartialName("System.Runtime") > $null
    
    # C# code to post to wallpaper
    $code = @'
    using System;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Runtime.InteropServices;
    using System.IO;
    using System.Net;
    using Microsoft.Win32;
    
    namespace test
    {
        public class Wallpaper
        {
          const int SPI_SETDESKWALLPAPER = 20  ;
          const int SPIF_UPDATEINIFILE = 0x01;
          const int SPIF_SENDWININICHANGE = 0x02;
    
          [DllImport("user32.dll", CharSet=CharSet.Auto)]
          static  extern int SystemParametersInfo (int uAction , int uParam , string lpvParam , int fuWinIni) ;
    
          public static void SetWallpaper(string uri)
          {
             System.IO.Stream s = new WebClient().OpenRead(uri);
    
             Image img = System.Drawing.Image.FromStream(s);
             Bitmap copy = new Bitmap(img.Width, img.Height);
             Graphics g = Graphics.FromImage(copy);
             Rectangle rect = new Rectangle(0, 0, img.Width, img.Height);
             g.DrawImage(img, rect, 0, 0, img.Width, img.Height, GraphicsUnit.Pixel);
             g.Dispose();
             img.Dispose();
    
             // Save to a temp file:
             string tempPath = Path.Combine(Path.GetTempPath(), "wallpaper.bmp");
             copy.Save(tempPath, System.Drawing.Imaging.ImageFormat.Bmp);
    
             RegistryKey key = Registry.CurrentUser.OpenSubKey( @"Control Panel\Desktop", true ) ;
             key.SetValue(@"WallpaperStyle", 1.ToString( ) ) ;
             key.SetValue(@"TileWallpaper", 0.ToString( ) ) ;
    
             SystemParametersInfo( SPI_SETDESKWALLPAPER, 
                0, 
                tempPath,  
                SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE );
          }
       }
    }
    '@
    
    function Set-Wallpaper([string] $imgurl)
    {
    compile-CSharp $code
    [test.Wallpaper]::SetWallpaper($imgurl)
    }



    Wednesday, December 3, 2014 1:15 PM

All replies

  • Everything you have said is correct.  To change the theme you must recycle the login.  On WS2003 and earlier the parameters update works.  It does not work on Vista and later.

    Your Update-Explorer is what this "rundll32.exe user32.dll, UpdatePerUserSystemParameters" does.  You are just doing the same thing many times.  If it works it only takes once.  50 times won't help on the systems it doesn't work on.

    On any system that has a hung UI thread it will also fail.  I don't know why but that has been one issue in the past.  UpdateUI was not intended to be used to rebuild the desktop.  It was intended to be a way for the system to notify programs that something in the system had changed and that the program should take appropriate action such as re-reading the path.


    ¯\_(ツ)_/¯


    • Edited by jrv Tuesday, December 2, 2014 4:13 PM
    Tuesday, December 2, 2014 4:11 PM
  • I think if you kill explorer and start it again you can restart the UI, but I wouldn't use that in a prod solution.
    Tuesday, December 2, 2014 7:51 PM
  • Great news! I found the solution and all props and credit goes to jsd1982 of the XKCD comic forum from a post he made in 2007 and some deep search kung-fu Googling on my part. Here is the direct link to the post.

    This solution has worked for me 100% of the time on Windows 8. The only gotcha I found (minor detail) was when setting this process to run as a scheduled task, I had to run it as elevated privileges for the code to compile in memory. Running this function from PowerShell at the command prompt was fine. 

    NOTE: The code below is a slight trimmed down version posted by jsd1982 to just set the desktop wallpaper. I removed the image fetching from the Internet and the color inversion.

    # Credit to jsd1982
    
    function Compile-Csharp ([string] $code, $FrameworkVersion="v2.0.50727",
    [Array]$References)
    {
        #
        # Get an instance of the CSharp code provider
        #
        $cp = new-object Microsoft.CSharp.CSharpCodeProvider
    
        #
        # Build up a compiler params object...
        $framework = [System.IO.Path]::Combine($env:windir, "Microsoft.NET\Framework\$FrameWorkVersion")
        $refs = new-object Collections.ArrayList
        $refs.AddRange( @("${framework}\System.dll",
    #        "${mshhome}\System.Management.Automation.dll",
    #        "${mshhome}\System.Management.Automation.ConsoleHost.dll",
            "${framework}\system.windows.forms.dll",
            "${framework}\System.data.dll",
            "${framework}\System.Drawing.dll",
            "${framework}\System.Xml.dll"))
        if ($references.Count -ge 1)
        {
            $refs.AddRange($References)
        }
    
        $cpar = New-Object System.CodeDom.Compiler.CompilerParameters
        $cpar.GenerateInMemory = $true
        $cpar.GenerateExecutable = $false
       $cpar.CompilerOptions = "/unsafe";
        $cpar.OutputAssembly = "custom"
        $cpar.ReferencedAssemblies.AddRange($refs)
        $cr = $cp.CompileAssemblyFromSource($cpar, $code)
    
        if ( $cr.Errors.Count)
        {
            $codeLines = $code.Split("`n");
            foreach ($ce in $cr.Errors)
            {
                write-host "Error: $($codeLines[$($ce.Line - 1)])"
                $ce |out-default
            }
            Throw "INVALID DATA: Errors encountered while compiling code"
        }
    }
    
    [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") > $null
    [System.Reflection.Assembly]::LoadWithPartialName("System.Runtime") > $null
    
    # C# code to post to wallpaper
    $code = @'
    using System;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Runtime.InteropServices;
    using System.IO;
    using System.Net;
    using Microsoft.Win32;
    
    namespace test
    {
        public class Wallpaper
        {
          const int SPI_SETDESKWALLPAPER = 20  ;
          const int SPIF_UPDATEINIFILE = 0x01;
          const int SPIF_SENDWININICHANGE = 0x02;
    
          [DllImport("user32.dll", CharSet=CharSet.Auto)]
          static  extern int SystemParametersInfo (int uAction , int uParam , string lpvParam , int fuWinIni) ;
    
          public static void SetWallpaper(string uri)
          {
             System.IO.Stream s = new WebClient().OpenRead(uri);
    
             Image img = System.Drawing.Image.FromStream(s);
             Bitmap copy = new Bitmap(img.Width, img.Height);
             Graphics g = Graphics.FromImage(copy);
             Rectangle rect = new Rectangle(0, 0, img.Width, img.Height);
             g.DrawImage(img, rect, 0, 0, img.Width, img.Height, GraphicsUnit.Pixel);
             g.Dispose();
             img.Dispose();
    
             // Save to a temp file:
             string tempPath = Path.Combine(Path.GetTempPath(), "wallpaper.bmp");
             copy.Save(tempPath, System.Drawing.Imaging.ImageFormat.Bmp);
    
             RegistryKey key = Registry.CurrentUser.OpenSubKey( @"Control Panel\Desktop", true ) ;
             key.SetValue(@"WallpaperStyle", 1.ToString( ) ) ;
             key.SetValue(@"TileWallpaper", 0.ToString( ) ) ;
    
             SystemParametersInfo( SPI_SETDESKWALLPAPER, 
                0, 
                tempPath,  
                SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE );
          }
       }
    }
    '@
    
    function Set-Wallpaper([string] $imgurl)
    {
    compile-CSharp $code
    [test.Wallpaper]::SetWallpaper($imgurl)
    }



    Wednesday, December 3, 2014 1:15 PM
  • Way  too much code for the 21st century.

    Try this:

    $code = @'
    using System;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Runtime.InteropServices;
    using System.IO;
    using System.Net;
    using Microsoft.Win32;
    
    namespace Win32{
        
        public class Wallpaper{
    
          const int SPI_SETDESKWALLPAPER = 20  ;
          const int SPIF_UPDATEINIFILE = 0x01;
          const int SPIF_SENDWININICHANGE = 0x02;
    
          [DllImport("user32.dll", CharSet=CharSet.Auto)]
          static  extern int SystemParametersInfo (int uAction , int uParam , string lpvParam , int fuWinIni) ;
    
          public static void SetWallpaper(string uri){
             System.IO.Stream s = new WebClient().OpenRead(uri);
    
             Image img = System.Drawing.Image.FromStream(s);
             Bitmap copy = new Bitmap(img.Width, img.Height);
             Graphics g = Graphics.FromImage(copy);
             Rectangle rect = new Rectangle(0, 0, img.Width, img.Height);
             g.DrawImage(img, rect, 0, 0, img.Width, img.Height, GraphicsUnit.Pixel);
             g.Dispose();
             img.Dispose();
    
             // Save to a temp file:
             string tempPath = Path.Combine(Path.GetTempPath(), "wallpaper.bmp");
             copy.Save(tempPath, System.Drawing.Imaging.ImageFormat.Bmp);
    
             RegistryKey key = Registry.CurrentUser.OpenSubKey( @"Control Panel\Desktop", true ) ;
             key.SetValue(@"WallpaperStyle", 1.ToString( ) ) ;
             key.SetValue(@"TileWallpaper", 0.ToString( ) ) ;
    
             SystemParametersInfo( SPI_SETDESKWALLPAPER, 
                                    0, 
                                    tempPath,  
                                    SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE );
            }
        }
    }
    '@
    add-type $code -ReferencedAssemblies System.Drawing
    
    [Win32.Wallpaper]::SetWallpaper($imgurl)
    

    As I noted earlier.  This is unreliable on Windows 8 and later.  It is identical to what yuo did before,  It is just a differnt way to call the same APIs. If you look in the Gallery you will see this posted along with other methods that are similar.  I am pretty sure the code is from the P/Invoke site.


    ¯\_(ツ)_/¯

    Wednesday, December 3, 2014 11:08 PM
  • In the end this is all you need:

    $code = @'
    using System.Runtime.InteropServices;
    
    namespace Win32{
        
        public class Wallpaper{
    
          [DllImport("user32.dll", CharSet=CharSet.Auto)]
          static  extern int SystemParametersInfo (int uAction , int uParam , string lpvParam , int fuWinIni) ;
    
          public static void SetWallpaper(string thePath){
             SystemParametersInfo(20,0,thePath,3);
          }
        }
    }
    '@
    add-type $code
    
    [Win32.Wallpaper]::SetWallpaper($filepath)


    ¯\_(ツ)_/¯


    • Edited by jrv Wednesday, December 3, 2014 11:20 PM
    Wednesday, December 3, 2014 11:19 PM
  • Here are the docs: http://msdn.microsoft.com/en-us/library/windows/desktop/ms724947(v=vs.85).aspx#Desktop

    SPIF_UPDATEINIFILE - writes the new values to the registry
    SPIF_SENDWININICHANGE - tells desktop to reload with new profile settings.

    The other parts of the user code are redundant.  The default is to adjust the bitmap to the current desktop.  In earier versions of Windows this had to bedone manually.  If the bitmap was wrong it would not display.  Windows Vista and later can decide how to render the desktop and can use the same bitmap on different monitors and adapters.  Just place the file in your profile and send the message. Windows does the rest.  It still fails occasionally for some unknown (to me) reason.

    G.L.


    ¯\_(ツ)_/¯

    Wednesday, December 3, 2014 11:40 PM