I combined Xdynix's answer with some others, this should cover most use cases:
import os
import ctypes
from ctypes import HRESULT, POINTER, pointer
from ctypes.wintypes import LPCWSTR, UINT, LPWSTR
from re import L
from typing import Literal
import winreg
import comtypes
from comtypes import IUnknown, GUID, COMMETHOD
WallpaperMode = Literal["FILL", "FIT", "STRETCH", "CENTER", "SPAN", "TILE"]
wallpaper_mode_map = {
"FILL": (0, 10), # Fills the screen, may crop to maintain aspect ratio
"FIT": (0, 6), # Fits entire image into screen, may add padding
"STRETCH": (0, 2), # Stretches image to fit screen, may distort image
"CENTER": (0, 0), # Centers image on the screen with background color if smaller
"SPAN": (0, 22), # Fill, but across all monitors with one image
"TILE": (1, 0), # Tiles the image across all monitors. Meant for tiny images
}
# https://stackoverflow.com/a/74203777/3620725
class IDesktopWallpaper(IUnknown):
# Ref: https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-idesktopwallpaper
# Search `IDesktopWallpaper` in `\HKEY_CLASSES_ROOT\Interface` to obtain the magic string
_iid_ = GUID("{B92B56A9-8B55-4E14-9A89-0199BBB6F93B}")
@classmethod
def CoCreateInstance(cls):
# Search `Desktop Wallpaper` in `\HKEY_CLASSES_ROOT\CLSID` to obtain the magic string
class_id = GUID("{C2CF3110-460E-4fc1-B9D0-8A1C0C9CC4BD}")
return comtypes.CoCreateInstance(class_id, interface=cls)
_methods_ = [
COMMETHOD(
[],
HRESULT,
"SetWallpaper",
(["in"], LPCWSTR, "monitorID"),
(["in"], LPCWSTR, "wallpaper"),
),
COMMETHOD(
[],
HRESULT,
"GetWallpaper",
(["in"], LPCWSTR, "monitorID"),
(["out"], POINTER(LPWSTR), "wallpaper"),
),
COMMETHOD(
[],
HRESULT,
"GetMonitorDevicePathAt",
(["in"], UINT, "monitorIndex"),
(["out"], POINTER(LPWSTR), "monitorID"),
),
COMMETHOD(
[],
HRESULT,
"GetMonitorDevicePathCount",
(["out"], POINTER(UINT), "count"),
),
]
def SetWallpaper(self, monitorId: str, image_path: str):
self.__com_SetWallpaper(LPCWSTR(monitorId), LPCWSTR(image_path))
def GetWallpaper(self, monitorId: str) -> str:
wallpaper = LPWSTR()
self.__com_GetWallpaper(LPCWSTR(monitorId), pointer(wallpaper))
return wallpaper.value
def GetMonitorDevicePathAt(self, monitorIndex: int) -> str:
monitorId = LPWSTR()
self.__com_GetMonitorDevicePathAt(UINT(monitorIndex), pointer(monitorId))
return monitorId.value
def GetMonitorDevicePathCount(self) -> int:
count = UINT()
self.__com_GetMonitorDevicePathCount(pointer(count))
return count.value
def set_wallpaper(image_path: str, monitor_ix: int = 0, mode: WallpaperMode = "FILL"):
if mode not in wallpaper_mode_map:
raise ValueError(f"Invalid mode: {mode}. Options: {wallpaper_mode_map.keys()}")
desktop_wallpaper = IDesktopWallpaper.CoCreateInstance()
mon_count = desktop_wallpaper.GetMonitorDevicePathCount()
if monitor_ix > mon_count:
raise ValueError(f"Invalid index: {monitor_ix}. Found {mon_count} monitors.")
if not os.path.exists(image_path):
raise FileNotFoundError(f"Wallpaper not found: {image_path}")
image_path = os.path.abspath(image_path)
set_wallpaper_mode(mode)
# Multi-monitor modes
if mode in ["SPAN", "TILE"]:
# IDesktopWallpaper is for setting wallpaper on a specific monitor
# So we use SystemParametersInfoW here instead
# Note - Use SystemParametersInfoW over SystemParametersInfoA (https://stackoverflow.com/a/44875514/3620725)
ctypes.windll.user32.SystemParametersInfoW(20, 0, image_path, 0)
else:
# Single monitor modes
monitor_id = desktop_wallpaper.GetMonitorDevicePathAt(monitor_ix)
desktop_wallpaper.SetWallpaper(monitor_id, str(image_path))
def get_wallpaper(monitor_ix: int):
desktop_wallpaper = IDesktopWallpaper.CoCreateInstance()
monitor_count = desktop_wallpaper.GetMonitorDevicePathCount()
if monitor_ix > monitor_count:
raise ValueError(f"Invalid index: {monitor_ix}. Only {monitor_count} monitors.")
monitor_id = desktop_wallpaper.GetMonitorDevicePathAt(monitor_ix)
return desktop_wallpaper.GetWallpaper(monitor_id)
def set_wallpaper_mode(mode: WallpaperMode):
# https://stackoverflow.com/a/71784961/3620725
value1, value2 = wallpaper_mode_map[mode]
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, r"Control Panel\Desktop", 0, winreg.KEY_WRITE
)
winreg.SetValueEx(key, "TileWallpaper", 0, winreg.REG_SZ, str(value1))
winreg.SetValueEx(key, "WallpaperStyle", 0, winreg.REG_SZ, str(value2))
winreg.CloseKey(key)
def get_wallpaper_mode() -> WallpaperMode:
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, r"Control Panel\Desktop", 0, winreg.KEY_READ
)
value1 = int(winreg.QueryValueEx(key, "TileWallpaper")[0])
value2 = int(winreg.QueryValueEx(key, "WallpaperStyle")[0])
winreg.CloseKey(key)
for mode, values in wallpaper_mode_map.items():
if values == (value1, value2):
return mode
raise ValueError(f"Invalid wallpaper mode: {value1, value2}")
if __name__ == "__main__":
import requests
# Set wallpaper on each monitor using random sample images from picsum
monitor_count = IDesktopWallpaper.CoCreateInstance().GetMonitorDevicePathCount()
for i in range(monitor_count):
image_link = "https://picsum.photos/1920/1080"
image_path = f"./wallpaper_{i}.jpg"
with open(image_path, "wb") as f:
f.write(requests.get(image_link).content)
set_wallpaper(image_path, i, "FILL")