Customize Windows 10+ default theme
Content
- Purpose
- Study
- PowerShell script
- Sources / usefull resources
Purpose
One day, I wanted to change the default wallpaper and lockscreen of Windows computers for all the user profiles and let the users change these settings as they want. I find different ways by browsing Internet but they did not answere all my needs. My needs were the folowing:
- Set custom wallpaper
- Customize wallpaper screen
- Set lockscreen
- Allow users to change lockscreen
- Apply this to existing and/or new profiles
Study
C# code integration (wallpaper)
Technical basis
This solution relies on the Win32 API function SystemParametersInfo from user32.dll. This script must be executed in User context or ran using ServiceUI.exe from Microsoft Deployment Toolkit
Limitations
- Must be executed in user context
- Apply only on current user
Code snippet
# Define wallpaper function
function Set-Wallpaper($MyWallpaper){
# Define C# code as string
$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);
}
}
}
'@
# Import C# class
Add-Type $code
# Call C# function
[Win32.Wallpaper]::SetWallpaper($MyWallpaper)
}
## Call your function
Set-WallPaper("C:\Wallpapers\nature.jpg")
Registry keys (lockscreen)
Technical basis
We can use the following registry value to customize lockscreen:
Name: LockScreenImage
Path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization\LockScreenImage
Type: String
Value: <path to lockscreen image>
Limitations
- No limitation found yet
Code snippet
$regKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization'
# Create the key if it doesn't already exist
if (!(Test-Path -Path $regKey)) {
$null = New-Item -Path $regKey
}
# Now set the registry entry
Set-ItemProperty -Path $regKey -Force -Name LockScreenImage -value "C:\Custom-Folder\wallpaper.jpg"
Registry keys (theme)
Technical basis
I found that we can simply customize the default theme using the following registry values:
| Name | Path | Type | Value |
|---|---|---|---|
InstallTheme | HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes | String | <path to theme file> |
DesktopBackground | HKLM\SOFTWARE\\Microsoft\Windows\CurrentVersion\Themes | String | <path to background file> |
BrandIcon | HKLM\SOFTWARE\\Microsoft\Windows\CurrentVersion\Themes | String | <path to icon file> |
ThemeName | HK\SOFTWARE\LM\Microsoft\Windows\CurrentVersion\Themes | String | <THEME NAME> |
CurrentTheme | HKU\<SID>\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes | String | <path to theme file> |
Theme file:
Default theme path: C:\Windows\resources\Themes\aero.theme
; Copyright © Microsoft Corp.
[Theme]
; Windows - IDS_THEME_DISPLAYNAME_AERO
DisplayName=@%SystemRoot%\System32\themeui.dll,-2013
SetLogonBackground=0
; Computer - SHIDI_SERVER
[CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D}\DefaultIcon]
DefaultValue=%SystemRoot%\System32\imageres.dll,-109
; UsersFiles - SHIDI_USERFILES
[CLSID\{59031A47-3F72-44A7-89C5-5595FE6B30EE}\DefaultIcon]
DefaultValue=%SystemRoot%\System32\imageres.dll,-123
; Network - SHIDI_MYNETWORK
[CLSID\{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}\DefaultIcon]
DefaultValue=%SystemRoot%\System32\imageres.dll,-25
; Recycle Bin - SHIDI_RECYCLERFULL SHIDI_RECYCLER
[CLSID\{645FF040-5081-101B-9F08-00AA002F954E}\DefaultIcon]
Full=%SystemRoot%\System32\imageres.dll,-54
Empty=%SystemRoot%\System32\imageres.dll,-55
[Control Panel\Cursors]
AppStarting=%SystemRoot%\cursors\aero_working.ani
Arrow=%SystemRoot%\cursors\aero_arrow.cur
Crosshair=
Hand=%SystemRoot%\cursors\aero_link.cur
Help=%SystemRoot%\cursors\aero_helpsel.cur
IBeam=
No=%SystemRoot%\cursors\aero_unavail.cur
NWPen=%SystemRoot%\cursors\aero_pen.cur
SizeAll=%SystemRoot%\cursors\aero_move.cur
SizeNESW=%SystemRoot%\cursors\aero_nesw.cur
SizeNS=%SystemRoot%\cursors\aero_ns.cur
SizeNWSE=%SystemRoot%\cursors\aero_nwse.cur
SizeWE=%SystemRoot%\cursors\aero_ew.cur
UpArrow=%SystemRoot%\cursors\aero_up.cur
Wait=%SystemRoot%\cursors\aero_busy.ani
DefaultValue=Windows Default
DefaultValue.MUI=@main.cpl,-1020
[Control Panel\Desktop]
Wallpaper=%SystemRoot%\web\wallpaper\Windows\img0.jpg
TileWallpaper=0
WallpaperStyle=10
Pattern=
[VisualStyles]
Path=%ResourceDir%\Themes\Aero\Aero.msstyles
ColorStyle=NormalColor
Size=NormalSize
AutoColorization=0
ColorizationColor=0XC40078D7
SystemMode=Dark
[boot]
SCRNSAVE.EXE=
[MasterThemeSelector]
MTSM=RJSPBS
[Sounds]
; IDS_SCHEME_DEFAULT
SchemeName=@%SystemRoot%\System32\mmres.dll,-800
In order to prevent misconfiguration, I suggest to either create a new theme file from the default file or customize the default theme file.
Limitations
- No limitation found yet
Code snippet
################################# Global FUNCTIONS ################################################
###################################################################################################
# Define function to update theme file
Function Set-INIValue(){
param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$INICOntent,
[Parameter(Mandatory=$true, Position=1)]
[ValidateNotNullOrEmpty()]
[string]$Category,
[Parameter(Mandatory=$true, Position=2)]
[ValidateNotNullOrEmpty()]
[string]$Key,
[Parameter(Mandatory=$true, Position=3)]
[ValidateNotNullOrEmpty()]
[string]$Value
)
If($INICOntent -match "(?m)^$Key=.*"){
$INICOntent -Replace "(?m)^$Key=.*","$Key=$Value"
}Else{
$INICOntent -Replace "(?m)^\[$Category\]","[$Category]`n$Key=$Value"
}
}
################################# Customization ####################################################
####################################################################################################
$THEME_NAME = "CORPORATE"
$THEME_FILENAME = "corporate.theme"
# Download theme or copy it from app package
New-Item "C:\Windows\Web\Wallpaper\Corporate" -ItemType Directory -Force | Out-Null
$WALLPAPER_PATH = "C:\Windows\Web\Wallpaper\Corporate\wallpaper.png"
Invoke-WebRequest -Uri "https://wallpaper-house.com/data/out/10/wallpaper2you_388644.png" -OutFile "$WALLPAPER_PATH"
#--------------------------------------------------------------------------------------------------------------#
$THEME=@{
"Theme:DisplayName" = "CORPORATE"
"Control Panel\Desktop:Wallpaper" = "$WALLPAPER_PATH"
"Control Panel\Desktop:WallpaperStyle" = "10"
"VisualStyles:SystemMode" = "Dark"
"VisualStyles:ColorizationColor" = "0XC40078D7"
"VisualStyles:AppMode" = "Dark"
}
# Doc: https://docs.microsoft.com/en-us/windows/win32/controls/themesfileformat-overview
#--------------------------------------------------------------------------------------------------------------#
################################# Logic ############################################################
####################################################################################################
$THEME_REGISTRY_KEY = "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes"
$THEME_FOLDER = "C:\Windows\resources\Themes"
# Copy Current theme
$currentThemePath = "$(Get-ItemPropertyValue "Registry::$THEME_REGISTRY_KEY" "InstallTheme")"
If("$currentThemePath" -ne "$THEME_FOLDER\$THEME_FILENAME"){
Copy-Item "$currentThemePath" -Destination "$THEME_FOLDER\$THEME_FILENAME"
}
# Edit theme file
## Get file content
$themeFileContent = Get-Content -Path "$THEME_FOLDER\$THEME_FILENAME" | Out-String
## Customize theme
## Customize theme
ForEach($item in $THEME.Keys){
$themeFileContent = Set-INIValue "$themeFileContent" "$(($item -Split ':')[0])" "$(($item -Split ':')[1])" "$($THEME[$item])"
}
## Set file content
$themeFileContent | Out-File -FilePath "$THEME_FOLDER\$THEME_FILENAME"
# Set as new theme
Set-ItemProperty -Path "Registry::$THEME_REGISTRY_KEY" -Name "ThemeName" -Value "$THEME_NAME"
Set-ItemProperty -Path "Registry::$THEME_REGISTRY_KEY" -Name "InstallTheme" -Value "$THEME_FOLDER\$THEME_FILENAME"
Applying theme to user already logged-in (optional)
Technical basis
In order to apply the theme to the current user, you can use the following code:
rundll32.exe themecpl.dll,OpenThemeAction "C:\path\to\mytheme.theme"
# Source: https://stackoverflow.com/questions/546818/how-do-i-change-the-current-windows-theme-programmatically
This code must be run in the user context, you can use KelvinTegelaar/RunAsUser or create your own system (this is what I choose)
Code snippet
# Define function to execute code as logged-on user
# Define function to execute code as logged-on user
Function Invoke-AsCurrentUser(){
param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[String]$Script
)
# Create random name for task
$taskName="Invoke-AsCurrentUser-$(Get-Random -Maximum 1000)"
# Put code in temp file
$Script | Out-File -FilePath "C:\Users\Public\$taskName.ps1"
# Define action
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -Command `"&{&('C:\Users\Public\$taskName.ps1') }`""
# Define trigger to run the task in 1 minute
$trigger = New-ScheduledTaskTrigger -AtLogon
# Define targeted user
$principal = New-ScheduledTaskPrincipal -UserId (Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object -expand UserName)
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries
# Create task
$task = New-ScheduledTask -Action $action -Trigger $trigger -Principal $principal -Settings $settings
# Register task
Register-ScheduledTask $taskName -InputObject $task
# Start Task
Start-ScheduledTask -TaskName $taskName
# Launch process to remove scheduled task in 5 minutes
Start-Process "powershell.exe" "Invoke-Command -ScriptBlock {Start-Sleep -Seconds 300;Unregister-ScheduledTask -TaskName `"$taskName`" -Confirm:`$false}" -WindowStyle Hidden
}
# Set new theme to current user (run in user context)
Invoke-AsCurrentUser -Script (
"`$THEME_FOLDER='$THEME_FOLDER';`$THEME_FILENAME='$THEME_FILENAME';"+
({
## Set theme file using themecpl.dll 'OpenThemeAction' function (https://windows10dll.nirsoft.net/themecpl_dll.html)
rundll32.exe themecpl.dll,OpenThemeAction "$THEME_FOLDER\$THEME_FILENAME"
## Wait for process to start
$loop=0
While("$(Get-Process SystemSettings -ErrorAction SilentlyContinue)" -eq ""){
Start-Sleep -Milliseconds 100
$loop=+1
If($loop -ge 50){
Break
}
}
Start-Sleep -Seconds 1
## Close process
$loop=0
While("$(Get-Process SystemSettings -ErrorAction SilentlyContinue)" -ne ""){
Start-Sleep -Milliseconds 100
$loop=+1
If($loop -ge 50){
Break
}
Get-Process SystemSettings -ErrorAction SilentlyContinue | Stop-Process -force
}
}).toString())
Final solution
Full code here (lockscreen left as default)

Sources / usefull resources
- https://docs.microsoft.com/en-us/answers/questions/619056/powershell-to-set-second-monitor-wallpaper.html
- https://stackoverflow.com/questions/69776800/how-to-set-lock-screen-photo-using-powershell
- https://stackoverflow.com/questions/546818/how-do-i-change-the-current-windows-theme-programmatically
- https://windows10dll.nirsoft.net/themecpl_dll.html