Core Dependencies and Modules
The implementation relies on `PyQt5` for the graphical user interface, `cv2` (OpenCV) for video stream processing, and `win32api` to manipulate the Windows desktop hierarchy. Below is the essential import structure required for the application to function.
import os
import sys
import time
import win32gui
import win32con
from threading import Thread
import cv2
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel, QFileDialog,
QGroupBox, QSystemTrayIcon, QMenu, QAction, QMessageBox)
from PyQt5.QtCore import Qt, QTimer, QPoint
from PyQt5.QtGui import QImage, QPixmap, QIcon, QColor
UI Layout Design
The interface is constructed programmatically to create a frameless window with a translucent background. The layout consists of a central display area for the video preview and a control panel for playback management.
class WallpaperWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Dynamic Wallpaper Tool")
self.setFixedSize(480, 360)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setStyleSheet("background-color: rgba(30, 30, 30, 200); border-radius: 10px;")
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.main_layout = QVBoxLayout(self.central_widget)
# Header Bar
self.header_layout = QHBoxLayout()
self.title_label = QLabel(" Live Wallpaper ")
self.title_label.setStyleSheet("color: white; font-weight: bold;")
self.close_btn = QPushButton("X")
self.close_btn.setFixedSize(30, 30)
self.close_btn.setStyleSheet("background-color: #ff5555; color: white; border-radius: 15px;")
self.close_btn.clicked.connect(self.close)
self.header_layout.addWidget(self.title_label)
self.header_layout.addStretch()
self.header_layout.addWidget(self.close_btn)
self.main_layout.addLayout(self.header_layout)
# Video Preview Area
self.preview_group = QGroupBox("Preview")
self.preview_group.setStyleSheet("color: white; border: 1px solid gray;")
self.preview_layout = QVBoxLayout()
self.video_label = QLabel("No video selected")
self.video_label.setAlignment(Qt.AlignCenter)
self.video_label.setStyleSheet("background-color: black; color: gray;")
self.preview_layout.addWidget(self.video_label)
self.preview_group.setLayout(self.preview_layout)
self.main_layout.addWidget(self.preview_group)
# Control Buttons
self.controls_layout = QHBoxLayout()
self.btn_select = QPushButton("Select Video")
self.btn_select.setStyleSheet("background-color: #4CAF50; color: white; padding: 5px;")
self.btn_select.clicked.connect(self.load_video)
self.btn_apply = QPushButton("Set Wallpaper")
self.btn_apply.setStyleSheet("background-color: #2196F3; color: white; padding: 5px;")
self.btn_apply.clicked.connect(self.apply_wallpaper)
self.btn_stop = QPushButton("Stop")
self.btn_stop.setStyleSheet("background-color: #f44336; color: white; padding: 5px;")
self.btn_stop.clicked.connect(self.stop_wallpaper)
self.controls_layout.addWidget(self.btn_select)
self.controls_layout.addWidget(self.btn_apply)
self.controls_layout.addWidget(self.btn_stop)
self.main_layout.addLayout(self.controls_layout)
# Timer for video playback
self.timer = QTimer()
self.timer.setInterval(30)
self.timer.timeout.connect(self.update_frame)
self.cap = None
self.video_path = ""
Windows Desktop Manipulation
To render video behind desktop icons, the application must locate the specific WorkerW window generated by Windows. The following function sends specific messages to the Program Manager window to reveal the target handle.
def get_desktop_hwnd():
"""
Finds the HWND of the desktop WorkerW window to parent the wallpaper.
"""
def callback(hwnd, extra):
if win32gui.GetWindowText(hwnd) == "Program Manager":
win32gui.SendMessageTimeout(hwnd, 0x052C, 0, None, 0, 0x03E8)
win32gui.EnumWindows(callback, None)
hwnd_worker = None
while True:
hwnd_worker = win32gui.FindWindowEx(None, hwnd_worker, "WorkerW", None)
if not hwnd_worker:
continue
# Check if this WorkerW hosts the SHELLDLL_DefView
h_view = win32gui.FindWindowEx(hwnd_worker, None, "SHELLDLL_DefView", None)
if h_view:
# We found the parent of the desktop icons, we need the WorkerW *behind* it
hwnd_target = win32gui.FindWindowEx(None, hwnd_worker, "WorkerW", None)
if hwnd_target:
return hwnd_target
Video Playback Logic
The video stream is captured using OpenCV. Each frame is converted from BGR to RGB format, transformed into a QImage, and displayed either in the preview window or on the desktop background.
def load_video(self):
file_path, _ = QFileDialog.getOpenFileName(self, "Open Video File", "", "Video Files (*.mp4 *.mkv *.avi *.mov)")
if file_path:
self.video_path = file_path
self.cap = cv2.VideoCapture(file_path)
self.timer.start()
def update_frame(self):
if self.cap and self.cap.isOpened():
ret, frame = self.cap.read()
if ret:
# Convert BGR (OpenCV) to RGB (Qt)
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
bytes_per_line = ch * w
# Create QImage
qt_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
# Scale to label size
pixmap = QPixmap.fromImage(qt_image).scaled(self.video_label.size(), Qt.KeepAspectRatio)
self.video_label.setPixmap(pixmap)
else:
# Loop video or stop
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
def apply_wallpaper(self):
if not self.cap or not self.cap.isOpened():
QMessageBox.warning(self, "Error", "Please select a video first.")
return
# Spawn a separate windowless process or window for the desktop background
# For this example, we assume a secondary window class WallpaperRenderer is defined
try:
desktop_id = get_desktop_hwnd()
self.renderer = WallpaperRenderer(desktop_id, self.video_path)
self.renderer.show()
except Exception as e:
print(f"Error setting wallpaper: {e}")
def stop_wallpaper(self):
if hasattr(self, 'renderer'):
self.renderer.close()
self.timer.stop()
if self.cap:
self.cap.release()
self.video_label.clear()
self.video_label.setText("No video selected")
Dedicated Renderer Class
A separate window class is instantiated and parented to the WorkerW handle found previously. This ensures the window sits behind desktop icons but above the actual desktop background.
class WallpaperRenderer(QMainWindow):
def __init__(self, parent_hwnd, video_source):
super().__init__()
self.setParent(None)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnBottomHint)
# Parent the window to the desktop worker
win32gui.SetParent(int(self.winId()), parent_hwnd)
self.video_label = QLabel()
self.setCentralWidget(self.video_label)
self.showMaximized() # Fill the screen
self.cap = cv2.VideoCapture(video_source)
self.timer = QTimer()
self.timer.timeout.connect(self.render_frame)
self.timer.start(30) # ~30 FPS
def render_frame(self):
if self.cap.isOpened():
ret, frame = self.cap.read()
if ret:
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
qt_image = QImage(rgb_image.data, w, h, w * ch, QImage.Format_RGB888)
self.video_label.setPixmap(QPixmap.fromImage(qt_image).scaled(self.size(), Qt.IgnoreAspectRatio))
else:
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
Inter-Process Communication
To manage the dynamic wallpaper lifecycle without keeping the main GUI open, a temporary text file or local socket can be used to pass the video path to the background rendering process. Below is a simplified method of writing the configuration to a shared location.
def launch_background_process(self):
config_path = "wallpaper_config.txt"
if not self.video_path:
return
with open(config_path, "w") as f:
f.write(self.video_path)
# Execute the background renderer script
# Assuming the renderer is packaged as 'bg_renderer.exe'
os.system("start bg_renderer.exe")