2

I have a simple code for changing wallpaper:

import ctypes
import random
import os

start_path = "D:/my_wallpapers"
list_images = os.listdir(start_path)
img_path = os.path.join(start_path, random.choice(list_images))

ctypes.windll.user32.SystemParametersInfoW(20, 0, img_path, 0)

I have two monitors. This code sets the same image to both monitors. I want to set different images.I discovered that there is IDesktopWallpaper (https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-idesktopwallpaper-setwallpaper). Is it possible to use IDesktopWallpaper in Python?

I ask for help, write code so that the IdesktopWallpaper can be launched through Python 3. With respect, Maxim.

p.s. I use windows 10 64-bit

p.s.2. Especially for Strive Sun - MSFT It seems to me that I was able to solve the first question. I was able to transfer id_monitour to DLL.

    void SetWallpaper(int id_mon)
{
    std::wstring s = L"D:\\images\\my_img.jpg";
    HRESULT  ad;
    UINT count;
    WCHAR* mid = new WCHAR[200];
    WCHAR* temp = mid;
    memset(mid, 0, 200 * 2);
    CoInitialize(NULL);

    IDesktopWallpaper* p;
    if (SUCCEEDED(CoCreateInstance(__uuidof(DesktopWallpaper), 0, CLSCTX_LOCAL_SERVER, __uuidof(IDesktopWallpaper), (void**)&p))) {
        p->GetMonitorDevicePathCount(&count); //count: the numbers of monitor
        if (id_mon!=0) {
            p->GetMonitorDevicePathAt(id_mon-1, &mid);  //1: first monitor  2: second monitor, etc...
            ad = p->SetWallpaper(mid, s.c_str());
        }
        else {
            ad = p->SetWallpaper(NULL, s.c_str()); // 0: both monitors
        }
        p->Release();
    }
    delete[] temp;
    CoUninitialize();
}

But transfer the path_to_my_img.jpg, I could not. I do not understand what is going on in C ++ with the String parameter. I tried to take advantage of char, but I could not convert char in wstring

2
  • 1
    Any COM interface can be made available to Python, although the Windows API bindings support in Python looks to be quite the mess. It's mostly hit and miss, and in this case it's a miss. You're going to have to write your own bindings. The PythonCOM module provides the building blocks. Commented Feb 26, 2021 at 11:31
  • 1
    Thanks, but alas without a ready-made example or DLL, I will not cope. Commented Feb 27, 2021 at 13:22

3 Answers 3

3

No need to compile a new DLL, you can use COM interface as a wrapper. This is what I'm using:

import random
from ctypes import HRESULT, POINTER, pointer
from ctypes.wintypes import LPCWSTR, UINT, LPWSTR

import comtypes
from comtypes import IUnknown, GUID, COMMETHOD


# noinspection PyPep8Naming
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, wallpaper: str):
        self.__com_SetWallpaper(LPCWSTR(monitorId), LPCWSTR(wallpaper))

    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 main():
    wallpapers = [*map(str, IMG_LIBRARY_DIR.iterdir())]

    desktop_wallpaper = IDesktopWallpaper.CoCreateInstance()
    monitor_count = desktop_wallpaper.GetMonitorDevicePathCount()
    for i in range(monitor_count):
        monitor_id = desktop_wallpaper.GetMonitorDevicePathAt(i)
        wallpaper = random.choice(wallpapers)
        desktop_wallpaper.SetWallpaper(monitor_id, str(wallpaper))


if __name__ == '__main__':
    main()

Dependency: comtypes

Sign up to request clarification or add additional context in comments.

Comments

2

The easiest way is to call the C++ DLL from python, there is no need to deal with some syntax problems of the com interface in python.

.py

import os
import sys
from ctypes import *

lib = cdll.LoadLibrary('D:\\Sample\\Debug\\Sample.dll')
lib.SetWallpaper(0,'C:\\Users\\xyz\\Desktop\\panda.png')

.dll

// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <Windows.h>
#include <string>
#include "Shobjidl.h"

extern "C" {
    __declspec(dllexport)
        void SetWallpaper(int id, const wchar_t* s)
    {          
        HRESULT  ad;
        UINT count;
        WCHAR* mid = new WCHAR[200];
        WCHAR* temp = mid;
        memset(mid, 0, 200 * 2);
        CoInitialize(NULL);

        IDesktopWallpaper* p;
        if (SUCCEEDED(CoCreateInstance(__uuidof(DesktopWallpaper), 0, CLSCTX_LOCAL_SERVER, __uuidof(IDesktopWallpaper), (void**)&p))) {
            p->GetMonitorDevicePathCount(&count); //count: the numbers of monitor
            p->GetMonitorDevicePathAt(id, &mid);  //0: the first monitor  1: the second monitor
            ad = p->SetWallpaper(mid, s);
            p->Release();
        }
        delete[] temp;
        CoUninitialize();
    }
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

If you don’t know how to create a simple dll, please refer: Walkthrough: Create and use your own Dynamic Link Library (C++)

Updated:

You need to install a vs compiler, such as VS2019, and then follow the steps below,

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

13 Comments

Sorry, but I do not understand why to do a new DLL? After all, it should already be in Windows, starting with version 8. I just need to connect to the DLL, in which IdesktopWallpaper is already lying. Sorry for my English. p.s. I do not have enough experience to make at least something on C ++.
@MaxBolovsky You can implement your needs in dll, without worrying about the syntax of winapi in python. Of course, you can also call the com interface in python, see some examples: How to use IFileOperation from ctypes
@MaxBolovsky Some obscure winapi examples in python are rarely found, which is why I cannot provide a sample of using IDesktopWallpaper in python. ps: Python is not my specialty.
Thank you, you did everything you could.
@MaxBolovsky As I described, creating your own dll is an efficient solution. And the method of creating dll is not cumbersome, if you need, I can provide you with some tutorial help.
|
-1

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")

4 Comments

The resources you used as a reference are questionable at best. There's no reason to mess with the registry or fall back to SystemParametersInfo(). Everything you meant to implement is exposed by the IDesktopWallpaper interface.
I'm not an expert with COM and ctypes and welcome anyone to edit my answer if there are problems. Not sure what you mean by questionable references, it's just links to docs or other SO answers with more discussion on the methods I used. Combining IDesktopWallpaper.SetWallpaper with the SPAN wallpaper mode simply didn't work for me, and I see no reference to modes in the docs. If there's a better way without using the registry or SystemParametersInfo then please share.
In fact, I just checked and it seems like those registry values are the ones that change when I switch my wallpaper mode in the Windows 11 Control Panel > Personalization > Background dialog, so this isn't a hack or anything. What do you mean "There's no reason to mess with the registry"? How else would you change the mode?
"How else would you change the mode?" - By using the public API (IDesktopWallpaper::SetPosition()).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.