commit 971764c47d3fc4be1ec907a24c89eec0d4c68dc7 Author: chun_qiu Date: Fri Jul 31 12:51:28 2026 +0800 初始提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88ecac4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +dist/ +build/ +*.spec +logs/ +cache/ +assets/ +config.json +__pycache__/ +*.pyc +*.pyo +.vscode/ +.idea/ +*.egg-info/ diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..23e7480 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,49 @@ +#!/usr/bin/env powershell +# -*- coding: utf-8 -*- +<# +.SYNOPSIS + 壁纸更换器 - 编译脚本 +.DESCRIPTION + 使用 PyInstaller 将 Python 源码打包为单个 .exe 文件。 + 输出: .\dist\WallpaperChanger.exe +#> + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " 壁纸更换器 - 编译打包" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +Write-Host "[1/4] 激活 conda 环境 ..." -ForegroundColor Yellow +conda activate 312 +if ($LASTEXITCODE -ne 0) { throw "激活 conda 环境失败 (312)" } + +Write-Host "[2/4] 安装编译依赖 ..." -ForegroundColor Yellow +python -m pip install pyinstaller --quiet +if ($LASTEXITCODE -ne 0) { throw "安装 PyInstaller 失败" } + +python -m pip install -r "$ScriptDir\requirements.txt" --quiet + +Write-Host "[3/4] 清理旧产物 ..." -ForegroundColor Yellow +Remove-Item -Recurse -Force "$ScriptDir\dist" -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force "$ScriptDir\build" -ErrorAction SilentlyContinue +Remove-Item -Force "$ScriptDir\*.spec" -ErrorAction SilentlyContinue + +Write-Host "[4/4] PyInstaller 打包 ..." -ForegroundColor Yellow +Set-Location -LiteralPath $ScriptDir +python -m PyInstaller ` + --onefile ` + --noconsole ` + --name "WallpaperChanger" ` + --clean ` + "$ScriptDir\main.py" + +if ($LASTEXITCODE -ne 0) { throw "PyInstaller 打包失败" } + +Write-Host "" +Write-Host "========================================" -ForegroundColor Green +Write-Host " 编译完成!" -ForegroundColor Green +Write-Host " 输出: $ScriptDir\dist\WallpaperChanger.exe" -ForegroundColor Green +Write-Host "========================================" -ForegroundColor Green \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..b53c67b --- /dev/null +++ b/config.py @@ -0,0 +1,103 @@ +import json +import os +import sys + + +def _get_app_dir(): + if getattr(sys, 'frozen', False): + return os.path.dirname(sys.executable) + return os.path.dirname(os.path.abspath(__file__)) + + +def _get_config_path(): + return os.path.join(_get_app_dir(), "config.json") + + +DEFAULT_CONFIG = { + "api": { + "url": "http://localhost:8080/wallpaper", + "mode": "direct", + "json_image_field": "url", + "timeout": 30, + }, + "wallpaper": { + "fill_style": "fill", + "cache_dir": "./cache", + "max_cache_files": 10, + "show_notification": True, + }, + "auto_refresh": { + "enabled": False, + "interval_minutes": 10, + }, +} + +_config = None + + +def _resolve_path(path: str) -> str: + if os.path.isabs(path): + return path + return os.path.normpath(os.path.join(_get_app_dir(), path)) + + +def load() -> dict: + global _config + if _config is not None: + return _config + config_path = _get_config_path() + if os.path.exists(config_path): + with open(config_path, "r", encoding="utf-8") as f: + _config = json.load(f) + else: + _config = DEFAULT_CONFIG + merged = dict(DEFAULT_CONFIG) + _deep_merge(merged, _config) + _config = merged + return _config + + +def save(cfg: dict = None) -> None: + global _config + if cfg is not None: + _config = cfg + with open(_get_config_path(), "w", encoding="utf-8") as f: + json.dump(_config, f, indent=4, ensure_ascii=False) + + +def reload() -> dict: + global _config + _config = None + return load() + + +def get(path: str, default=None): + cfg = load() + keys = path.split(".") + for k in keys: + if isinstance(cfg, dict): + cfg = cfg.get(k) + if cfg is None: + return default + else: + return default + return cfg + + +def set_(path: str, value): + global _config + cfg = load() + keys = path.split(".") + d = cfg + for k in keys[:-1]: + d = d.setdefault(k, {}) + d[keys[-1]] = value + save(cfg) + + +def _deep_merge(base, override): + for key, value in override.items(): + if key in base and isinstance(base[key], dict) and isinstance(value, dict): + _deep_merge(base[key], value) + else: + base[key] = value diff --git a/fetcher.py b/fetcher.py new file mode 100644 index 0000000..e8f13c7 --- /dev/null +++ b/fetcher.py @@ -0,0 +1,107 @@ +import os +import uuid +import logging +import mimetypes + +import requests + +import config + +logger = logging.getLogger(__name__) + +IMAGE_MIME_TYPES = { + "image/jpeg", + "image/png", + "image/bmp", + "image/gif", + "image/webp", + "image/tiff", +} + +EXT_MAP = { + "image/jpeg": ".jpg", + "image/png": ".png", + "image/bmp": ".bmp", + "image/gif": ".gif", + "image/webp": ".webp", + "image/tiff": ".tiff", +} + + +def _detect_ext(response) -> str: + content_type = response.headers.get("Content-Type", "").split(";")[0].strip().lower() + if content_type in EXT_MAP: + return EXT_MAP[content_type] + + url_path = response.url.split("?")[0] + guessed, _ = mimetypes.guess_type(url_path) + if guessed and guessed in EXT_MAP: + return EXT_MAP[guessed] + + return ".jpg" + + +def fetch_wallpaper() -> str: + cfg = config.load() + api = cfg["api"] + wallpaper_cfg = cfg["wallpaper"] + + url = api["url"] + mode = api.get("mode", "direct") + timeout = api.get("timeout", 30) + + cache_dir = wallpaper_cfg.get("cache_dir", "./cache") + cache_dir = _resolve_cache(cache_dir) + os.makedirs(cache_dir, exist_ok=True) + + if mode == "json": + image_url = _fetch_json(url, api.get("json_image_field", "url"), timeout) + response = _download(image_url, timeout) + else: + response = _download(url, timeout) + + ext = _detect_ext(response) + filename = f"{uuid.uuid4().hex}{ext}" + filepath = os.path.join(cache_dir, filename) + + with open(filepath, "wb") as f: + f.write(response.content) + + logger.info("Wallpaper saved to: %s", filepath) + max_cache = cfg["wallpaper"].get("max_cache_files", 10) + _cleanup_cache(cache_dir, keep=max_cache) + return filepath + + +def _fetch_json(url: str, field: str, timeout: int) -> str: + r = requests.get(url, timeout=timeout) + r.raise_for_status() + data = r.json() + image_url = data.get(field) + if not image_url: + raise ValueError(f"Field '{field}' not found in JSON response") + return image_url + + +def _download(url: str, timeout: int) -> requests.Response: + r = requests.get(url, timeout=timeout, stream=True) + r.raise_for_status() + return r + + +def _resolve_cache(cache_dir: str) -> str: + if os.path.isabs(cache_dir): + return cache_dir + base = os.path.dirname(os.path.abspath(__file__)) + return os.path.normpath(os.path.join(base, cache_dir)) + + +def _cleanup_cache(cache_dir: str, keep: int) -> None: + try: + files = [os.path.join(cache_dir, f) for f in os.listdir(cache_dir)] + files = [f for f in files if os.path.isfile(f)] + files.sort(key=os.path.getmtime, reverse=True) + for old in files[keep:]: + os.remove(old) + except Exception: + pass diff --git a/install_menu.py b/install_menu.py new file mode 100644 index 0000000..8c9a35d --- /dev/null +++ b/install_menu.py @@ -0,0 +1,88 @@ +import os +import sys +import logging +import winreg + +logger = logging.getLogger(__name__) + +REG_KEY = r"Software\Classes\Directory\Background\shell\RefreshWallpaper" +COMMAND_KEY = REG_KEY + r"\command" + +MENU_NAME = "刷新壁纸(&R)" + + +def _get_app_dir() -> str: + if getattr(sys, 'frozen', False): + return os.path.dirname(sys.executable) + return os.path.dirname(os.path.abspath(__file__)) + + +def _get_command() -> str: + if getattr(sys, 'frozen', False): + return f'"{sys.executable}" --refresh' + else: + base = os.path.dirname(sys.executable) + pythonw = os.path.join(base, "pythonw.exe") + if not os.path.exists(pythonw): + pythonw = sys.executable + script = os.path.join(_get_app_dir(), "refresh.py") + return f'"{pythonw}" "{script}"' + + +def _get_icon_path() -> str: + return os.path.join(_get_app_dir(), "assets", "icon.ico") + + +def install() -> bool: + command = _get_command() + icon = _get_icon_path() + + try: + key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, REG_KEY) + winreg.SetValueEx(key, "", 0, winreg.REG_SZ, MENU_NAME) + if os.path.exists(icon): + winreg.SetValueEx(key, "Icon", 0, winreg.REG_SZ, icon) + winreg.CloseKey(key) + + key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, COMMAND_KEY) + winreg.SetValueEx(key, "", 0, winreg.REG_SZ, command) + winreg.CloseKey(key) + + logger.info("Desktop right-click menu installed: %s", MENU_NAME) + return True + except Exception as e: + logger.error("Failed to install menu: %s", e) + return False + + +def uninstall() -> bool: + try: + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_KEY) + winreg.CloseKey(key) + except FileNotFoundError: + logger.info("Menu not installed, nothing to uninstall") + return True + + try: + winreg.DeleteKey(winreg.HKEY_CURRENT_USER, COMMAND_KEY) + logger.info("Deleted registry key: %s", COMMAND_KEY) + except Exception as e: + logger.warning("Failed to delete command key: %s", e) + + try: + winreg.DeleteKey(winreg.HKEY_CURRENT_USER, REG_KEY) + logger.info("Deleted registry key: %s", REG_KEY) + except Exception as e: + logger.error("Failed to delete menu key: %s", e) + return False + + logger.info("Desktop right-click menu uninstalled") + return True + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + if len(sys.argv) > 1 and sys.argv[1] == "uninstall": + uninstall() + else: + install() diff --git a/main.py b/main.py new file mode 100644 index 0000000..c024fdd --- /dev/null +++ b/main.py @@ -0,0 +1,113 @@ +import os +import sys +import json +import ctypes +import logging + +LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" +LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + +MUTEX_NAME = r"Global\WallpaperChanger_SingleInstance" +ERROR_ALREADY_EXISTS = 183 + + +def _bootstrap(): + import config + + app_dir = config._get_app_dir() + for d in ["assets", "cache", "logs"]: + os.makedirs(os.path.join(app_dir, d), exist_ok=True) + + config_path = config._get_config_path() + if not os.path.exists(config_path): + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config.DEFAULT_CONFIG, f, indent=4, ensure_ascii=False) + + +def _do_refresh(): + logging.basicConfig( + level=logging.INFO, + format=LOG_FORMAT, + datefmt=LOG_DATE_FORMAT, + ) + logger = logging.getLogger("refresh") + + import config + import fetcher + import wallpaper + + try: + image_path = fetcher.fetch_wallpaper() + fill_style = config.get("wallpaper.fill_style", "fill") + wallpaper.set_wallpaper(image_path, fill_style) + logger.info("Wallpaper updated successfully") + except Exception as e: + logger.error("Refresh failed: %s", e) + sys.exit(1) + + +def _setup_logging(): + import config + + log_dir = os.path.join(config._get_app_dir(), "logs") + os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, "app.log") + logging.basicConfig( + level=logging.INFO, + format=LOG_FORMAT, + datefmt=LOG_DATE_FORMAT, + handlers=[ + logging.FileHandler(log_file, encoding="utf-8"), + logging.StreamHandler(sys.stdout), + ], + ) + + +def _ensure_icon(): + import config + + icon_path = os.path.join(config._get_app_dir(), "assets", "icon.ico") + if os.path.exists(icon_path): + return + from PIL import Image + + img = Image.new("RGB", (64, 64), color=(66, 133, 244)) + sizes = [(16, 16), (32, 32), (48, 48), (64, 64)] + img.save(icon_path, format="ICO", sizes=sizes) + + +def _check_single_instance(): + handle = ctypes.windll.kernel32.CreateMutexW(None, False, MUTEX_NAME) + if ctypes.windll.kernel32.GetLastError() == ERROR_ALREADY_EXISTS: + ctypes.windll.user32.MessageBoxW(0, "程序已运行", "壁纸更换器", 0x40) + return False + return True + + +def main(): + _bootstrap() + + if "--refresh" in sys.argv: + _do_refresh() + return + + if not _check_single_instance(): + return + + _setup_logging() + logger = logging.getLogger("main") + + try: + _ensure_icon() + except Exception as e: + logger.warning("Failed to generate icon: %s", e) + + from tray_app import TrayApp + + app = TrayApp() + logger.info("Starting Wallpaper Changer...") + app.run() + + +if __name__ == "__main__": + main() diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..71fcf9a --- /dev/null +++ b/readme.md @@ -0,0 +1,61 @@ +# 壁纸更换器 + +从 HTTP API 自动拉取壁纸并设置为 Windows 桌面背景,支持托盘常驻和桌面右键菜单。 + +## 功能 + +- 托盘程序常驻,右键菜单手动/自动刷新壁纸 +- 桌面右键菜单「刷新壁纸」一键更换 +- 支持 API 直接返回图片二进制或 JSON 含图片 URL 两种模式 +- 可配置填充方式(填充/适应/拉伸/平铺/居中) +- 可配置缓存目录及最大缓存数量 +- 可开关换壁纸后气泡通知 +- 单实例运行,重复启动弹窗提示 +- 首次运行自动创建配置和目录 + +## 使用 + +```powershell +# 运行托盘程序 +python main.py + +# 右键菜单刷新 +python main.py --refresh + +# 安装桌面右键菜单(首次需要,从托盘菜单操作) +右键托盘 → 安装右键菜单 +``` + +## 编译为 .exe + +```powershell +powershell -ExecutionPolicy Bypass -File build.ps1 +``` + +产物:`dist/WallpaperChanger.exe` + +- 双击运行 → 启动托盘 +- `WallpaperChanger.exe --refresh` → 拉取并设置壁纸后退出(右键菜单用) +- 首次运行在同目录下自动创建 `config.json`、`cache/`、`logs/`、`assets/` + +## 配置 + +`config.json` 字段说明: + +| 路径 | 类型 | 默认值 | 说明 | +|---|---|---|---| +| `api.url` | string | — | 壁纸 API 地址 | +| `api.mode` | string | `direct` | `direct` 返回图片二进制 / `json` 返回 JSON | +| `api.json_image_field` | string | `url` | JSON 模式下图片 URL 的字段名 | +| `api.timeout` | int | `30` | 请求超时秒数 | +| `wallpaper.fill_style` | string | `fill` | `fill`/`fit`/`stretch`/`tile`/`center` | +| `wallpaper.cache_dir` | string | `./cache` | 壁纸缓存目录 | +| `wallpaper.max_cache_files` | int | `10` | 最大缓存文件数 | +| `wallpaper.show_notification` | bool | `true` | 换壁纸后是否弹气泡通知 | +| `auto_refresh.enabled` | bool | `false` | 是否开启定时自动刷新 | +| `auto_refresh.interval_minutes` | int | `10` | 自动刷新间隔分钟数 | + +## 依赖 + +- Python 3.12+ +- requests, pystray, Pillow diff --git a/refresh.py b/refresh.py new file mode 100644 index 0000000..481df90 --- /dev/null +++ b/refresh.py @@ -0,0 +1,33 @@ +import sys +import os +import logging + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import config +import fetcher +import wallpaper + + +def main(): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + logger = logging.getLogger("refresh") + + try: + logger.info("Fetching wallpaper...") + image_path = fetcher.fetch_wallpaper() + fill_style = config.get("wallpaper.fill_style", "fill") + wallpaper.set_wallpaper(image_path, fill_style) + logger.info("Wallpaper updated successfully") + except Exception as e: + logger.error("Refresh failed: %s", e) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bc17988 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests>=2.28.0 +pystray>=0.19.0 +Pillow>=9.0.0 diff --git a/tray_app.py b/tray_app.py new file mode 100644 index 0000000..e45367d --- /dev/null +++ b/tray_app.py @@ -0,0 +1,176 @@ +import os +import threading +import logging + +from PIL import Image +import pystray + +import config +import fetcher +import wallpaper + +logger = logging.getLogger(__name__) + +STYLES = ["fill", "fit", "stretch", "tile", "center"] +STYLE_LABELS = {"fill": "填充", "fit": "适应", "stretch": "拉伸", "tile": "平铺", "center": "居中"} + + +def _get_icon_path(): + return os.path.join(config._get_app_dir(), "assets", "icon.ico") + + +class TrayApp: + def __init__(self): + self._icon = None + self._auto_timer = None + self._running = False + + def run(self): + self._running = True + image = Image.open(_get_icon_path()) + self._icon = pystray.Icon( + "WallpaperChanger", + image, + "壁纸更换器", + menu=self._build_menu(), + ) + self._icon.title = "壁纸更换器" + if config.get("auto_refresh.enabled"): + self._start_auto_refresh() + self._icon.run() + + def stop(self): + self._running = False + self._stop_auto_refresh() + if self._icon: + self._icon.stop() + + def _build_menu(self): + show_notify = config.get("wallpaper.show_notification", True) + notify_label = "弹出通知: 开" if show_notify else "弹出通知: 关" + return pystray.Menu( + pystray.MenuItem("刷新壁纸", self._on_refresh, default=True), + pystray.MenuItem("自动刷新", self._auto_refresh_submenu()), + pystray.MenuItem("填充方式", self._fill_style_submenu()), + pystray.Menu.SEPARATOR, + pystray.MenuItem(notify_label, self._on_toggle_notification), + pystray.Menu.SEPARATOR, + pystray.MenuItem("安装右键菜单", self._on_install_menu), + pystray.MenuItem("卸载右键菜单", self._on_uninstall_menu), + pystray.Menu.SEPARATOR, + pystray.MenuItem("退出", self._on_exit), + ) + + def _auto_refresh_submenu(self): + enabled = config.get("auto_refresh.enabled", False) + label = "状态: 开启" if enabled else "状态: 关闭" + return pystray.Menu( + pystray.MenuItem(label, None), + pystray.Menu.SEPARATOR, + pystray.MenuItem("开启", self._on_auto_on), + pystray.MenuItem("关闭", self._on_auto_off), + ) + + def _fill_style_submenu(self): + items = [] + for s in STYLES: + items.append( + pystray.MenuItem( + STYLE_LABELS[s], + self._on_set_fill_style, + checked=lambda item, _s=s: _s == config.get("wallpaper.fill_style", "fill"), + ) + ) + return pystray.Menu(*items) + + def _on_refresh(self, icon, item=None): + threading.Thread(target=self._do_refresh, daemon=True).start() + + def _do_refresh(self): + try: + image_path = fetcher.fetch_wallpaper() + fill_style = config.get("wallpaper.fill_style", "fill") + ok = wallpaper.set_wallpaper(image_path, fill_style) + if ok and self._icon: + if config.get("wallpaper.show_notification", True): + self._icon.notify("壁纸已更新") + except Exception as e: + logger.exception("Refresh failed") + if self._icon and config.get("wallpaper.show_notification", True): + self._icon.notify(f"刷新失败: {e}") + + def _on_auto_on(self, icon, item): + config.set_("auto_refresh.enabled", True) + self._start_auto_refresh() + icon.update_menu() + + def _on_auto_off(self, icon, item): + config.set_("auto_refresh.enabled", False) + self._stop_auto_refresh() + icon.update_menu() + + def _on_set_fill_style(self, icon, item): + for s in STYLES: + if STYLE_LABELS[s] == str(item): + config.set_("wallpaper.fill_style", s) + break + icon.update_menu() + + def _on_toggle_notification(self, icon, item): + current = config.get("wallpaper.show_notification", True) + config.set_("wallpaper.show_notification", not current) + icon.update_menu() + + def _on_install_menu(self, icon, item): + threading.Thread(target=self._do_install_menu, daemon=True).start() + + def _do_install_menu(self): + try: + from install_menu import install + install() + if self._icon: + self._icon.notify("右键菜单已安装") + except Exception as e: + logger.exception("Install menu failed") + if self._icon: + self._icon.notify(f"安装失败: {e}") + + def _on_uninstall_menu(self, icon, item): + threading.Thread(target=self._do_uninstall_menu, daemon=True).start() + + def _do_uninstall_menu(self): + try: + from install_menu import uninstall + uninstall() + if self._icon: + self._icon.notify("右键菜单已卸载") + except Exception as e: + logger.exception("Uninstall menu failed") + if self._icon: + self._icon.notify(f"卸载失败: {e}") + + def _on_exit(self, icon, item): + self.stop() + + def _start_auto_refresh(self): + self._stop_auto_refresh() + interval = config.get("auto_refresh.interval_minutes", 10) * 60 + self._auto_timer = threading.Timer(interval, self._auto_refresh_tick) + self._auto_timer.daemon = True + self._auto_timer.start() + logger.info("Auto refresh started, interval=%ds", interval) + + def _stop_auto_refresh(self): + if self._auto_timer: + self._auto_timer.cancel() + self._auto_timer = None + + def _auto_refresh_tick(self): + if not self._running: + return + self._do_refresh() + if config.get("auto_refresh.enabled"): + interval = config.get("auto_refresh.interval_minutes", 10) * 60 + self._auto_timer = threading.Timer(interval, self._auto_refresh_tick) + self._auto_timer.daemon = True + self._auto_timer.start() diff --git a/wallpaper.py b/wallpaper.py new file mode 100644 index 0000000..0c3f247 --- /dev/null +++ b/wallpaper.py @@ -0,0 +1,63 @@ +import ctypes +import os +import logging +from ctypes import wintypes + +logger = logging.getLogger(__name__) + +SPI_SETDESKWALLPAPER = 0x0014 +SPIF_UPDATEINIFILE = 0x01 +SPIF_SENDWININICHANGE = 0x02 + +FILL_STYLES = { + "fill": {"WallpaperStyle": "10", "TileWallpaper": "0"}, + "fit": {"WallpaperStyle": "6", "TileWallpaper": "0"}, + "stretch": {"WallpaperStyle": "2", "TileWallpaper": "0"}, + "tile": {"WallpaperStyle": "0", "TileWallpaper": "1"}, + "center": {"WallpaperStyle": "0", "TileWallpaper": "0"}, +} + + +def set_fill_style(style: str) -> None: + import winreg + + values = FILL_STYLES.get(style) + if not values: + values = FILL_STYLES["fill"] + + try: + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Control Panel\Desktop", + 0, + winreg.KEY_SET_VALUE, + ) + winreg.SetValueEx(key, "WallpaperStyle", 0, winreg.REG_SZ, values["WallpaperStyle"]) + winreg.SetValueEx(key, "TileWallpaper", 0, winreg.REG_SZ, values["TileWallpaper"]) + winreg.CloseKey(key) + logger.info("Fill style set to: %s", style) + except Exception as e: + logger.error("Failed to set fill style: %s", e) + + +def set_wallpaper(image_path: str, fill_style: str = "fill") -> bool: + if not os.path.exists(image_path): + logger.error("Image not found: %s", image_path) + return False + + set_fill_style(fill_style) + + abs_path = os.path.abspath(image_path) + result = ctypes.windll.user32.SystemParametersInfoW( + SPI_SETDESKWALLPAPER, + 0, + abs_path, + SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE, + ) + + if result: + logger.info("Wallpaper set: %s", abs_path) + else: + logger.error("Failed to set wallpaper: %s", abs_path) + + return bool(result)