Overview
Administrators deploying VDI environments frequently need to enforce a consistent Windows visual theme — including dark mode — across all user sessions. The expected approach of setting the relevant registry values via ProfileUnity’s Registry module does not work reliably because Windows requires a running Explorer shell to process and apply the theme change.
Simply writing the registry keys at logon is insufficient. Even when the values are correctly set, the desktop session continues to display light theme because no broadcast message is sent to notify the shell of the change.
While the method outlined below is specific to Windows Dark theme its possible you can extrapolate this process\workflow for other items where the Root Cause listed below is similar to the shell (Explorer) must be notified via a WM_SETTINGCHANGE broadcast message before it will apply a requested change.
Root Cause Windows dark mode is controlled by two registry DWORD values under HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize. While these values determine the theme, the shell (Explorer) must be notified via a WM_SETTINGCHANGE broadcast message before it will visually apply the change. Writing the registry keys without sending this broadcast results in no visible effect. |
Why the Registry Module Alone Does Not Work
The two registry values that control dark mode are:
| Key: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize |
| Value: AppsUseLightTheme |
| Type: DWORD |
| Data: 0 (0 = Dark, 1 = Light) |
| Key: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize |
| Value: SystemUsesLightTheme |
| Type: DWORD |
| Data: 0 (0 = Dark, 1 = Light) |
Setting these values via the ProfileUnity Registry module — or via user-defined scripts run during configuration — will write the correct data, but querying the values after logon will still return 0x1 (light theme). The values appear unchanged because:
- Windows Explorer must be running before the theme change is processed.
- Registry writes that occur before Explorer has started are overwritten or ignored by the shell initialization sequence.
- The change requires a WM_SETTINGCHANGE broadcast message with the ImmersiveColorSet parameter to take effect — this is a Windows API call that cannot be made through a registry write alone.
Timing Matters Running the registry write during ProfileUnity's During Configuration Execution phase fails because Explorer has not yet started at that point in the logon sequence. The operation must run After Configuration Execution, when Explorer is already active and able to receive the broadcast. |
Solution: Application Launcher with PowerShell
The reliable solution is to use the ProfileUnity Application Launcher module to run a PowerShell script After Configuration Execution. The script writes the two registry values and then sends the required WM_SETTINGCHANGE broadcast to notify Explorer, which immediately applies the dark theme to the session.
Step 1: Create the PowerShell Script
Create the following PowerShell script and save it to a network share accessible by all users at logon. Example path: \\SERVER\Home\Scripts\Set-DarkTheme.ps1
Example:
Also see attached Set-DarkTheme.ps1.rename as reference.
| function Set-WindowsDarkTheme { |
| [CmdletBinding()] |
| param() |
| $RegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' |
| if (-not (Test-Path -LiteralPath $RegistryPath)) { |
| New-Item -Path $RegistryPath -Force | Out-Null |
| } |
| New-ItemProperty -Path $RegistryPath -Name 'AppsUseLightTheme' -Value 0 -PropertyType DWord -Force | Out-Null |
| New-ItemProperty -Path $RegistryPath -Name 'SystemUsesLightTheme' -Value 0 -PropertyType DWord -Force | Out-Null |
| $TypeDefinition = @' |
| using System; |
| using System.Runtime.InteropServices; |
| public static class NativeMethods |
| { |
| [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] |
| public static extern IntPtr SendMessageTimeout( |
| IntPtr hWnd, |
| uint Msg, |
| UIntPtr wParam, |
| string lParam, |
| uint fuFlags, |
| uint uTimeout, |
| out UIntPtr lpdwResult); |
| } |
| '@ |
| if ($null -eq ('NativeMethods' -as [type])) { |
| Add-Type -TypeDefinition $TypeDefinition |
| } |
| $HwndBroadcast = [IntPtr]0xffff |
| $WmSettingChange = 0x001A |
| $SmtoAbortIfHung = 0x0002 |
| $TimeoutMilliseconds = 5000 |
| $Result = [UIntPtr]::Zero |
| [void][NativeMethods]::SendMessageTimeout( |
| $HwndBroadcast, |
| $WmSettingChange, |
| [UIntPtr]::Zero, |
| 'ImmersiveColorSet', |
| $SmtoAbortIfHung, |
| $TimeoutMilliseconds, |
| [ref]$Result |
| ) |
| } |
| Set-WindowsDarkTheme |
Step 2: Configure the Application Launcher Rule
In the ProfileUnity Console, create an Application Launcher rule with the following settings:
| Field | Value |
| Filespec | C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe |
| Arguments | -NoLogo -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "\\SERVER\Home\Scripts\Set-DarkTheme.ps1" |
| Timing | After Configuration Execution |
| Elevation | No |
Timing — After Configuration Execution Is Required The script must be set to After Configuration Execution. Running it During Configuration Execution will fail silently because Windows Explorer has not yet started and cannot receive the WM_SETTINGCHANGE broadcast. The dark theme will appear to apply on the next logon but not the current one if timing is set incorrectly. |
How the Script Works
The script performs two distinct operations:
- Registry write: Sets both AppsUseLightTheme and SystemUsesLightTheme to 0 under HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize. Creates the key if it does not exist.
- Shell broadcast: Uses P/Invoke to call SendMessageTimeout from user32.dll, broadcasting a WM_SETTINGCHANGE message with the ImmersiveColorSet parameter to all windows (HWND_BROADCAST = 0xFFFF). This signals Explorer and all running applications to re-read and apply the current theme settings immediately.
| Product | Liquidware ProfileUnity with FlexApp |
| Component | Application Launcher / Registry Module |
| Applies To | ProfileUnity 6.8.7 and later |