Files
u-desktop/docs/wallpaper-embedding.md

9.2 KiB
Raw Blame History

Win11 桌面壁纸嵌入技术笔记

环境: Win11 Build 26200 (Germanium, 24H2+) | 3240x2160 @200% DPI | Go 1.26


核心结论

Germanium 平台上WorkerW 子窗口必须经 DirectComposition 合成才上屏。 传统 GDI blt / OpenGL SwapBuffers 一律不被 DWM 合成。WebView2 能显示是因其内部走了 DComp 管线。

方案 是否可行 资源占用
WebView2 (DComp) ~704MB (全屏 WebGL, GPU 子进程 425MB)
原生 OpenGL (SwapBuffers) 渲染正常但不可见
D3D11 + DirectComposition 理论可行,未实现 预估 ~30-60MB

成功配方 (WebView2, 6 步缺一不可)

1. SetProcessDPIAware

procSetProcessDPIAware.Call()

否则 200% DPI 下 GetSystemMetrics 返回逻辑像素 1620x1080壁纸只占左上 1/4。必须在最开头调用。

2. 找到正确 WorkerW

// 1. 发送 0x052C 消息让 Progman 创建新 WorkerW
procSendMessageTimeoutW.Call(progman, 0x052C, 0, 0, 0x0000, 1000, ...)

// 2. 找 SHELLDLL_DefView
shellDefView := FindWindowExW(progman, 0, "SHELLDLL_DefView", 0)

// 3. 找 SHELLDLL_DefView 之后的 WorkerW ← 这是关键
workerw := FindWindowExW(progman, shellDefView, "WorkerW", 0)

要点:必须是 SHELLDLL_DefView 之后的那个 WorkerW不是随便一个 WorkerW。

3. SetParent 嵌入

procSetParent.Call(wvHwnd, workerw)

4. 去边框 (GWL_STYLE)

const GWL_STYLE = ^uintptr(15) // -16, 不是 ^uintptr(0)>>1-19
procSetWindowLongPtrW.Call(wvHwnd, GWL_STYLE,
    uintptr(WS_POPUP|WS_VISIBLE|WS_CLIPCHILDREN))

GWL_STYLE = -16 的正确写法是 ^uintptr(15)。常见错误 ^uint32(0)>>1-19 = 0x7FFFFFEC(大正数),根本不是 -16SetWindowLongPtrW 不会报错但修改的是错误的属性。

切忌写成 -4(那是 GWL_WNDPROC会覆盖窗口过程指针导致 exit 127 崩溃。

5. 自定义消息循环替代 wv.Run()

// 不用 wv.Run(),用标准 GetMessage 循环
var m winMsg
for {
    r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
    if r == 0 { return }
    procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
    procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
}

为什么不能 wv.Run(): go-webview2 的 Run() 内部用 GetAncestor(GA_ROOT) 判断消息路由。SetParent 到 WorkerW 后,窗口根祖先变成 Progman(explorer)Run() 的消息循环异常结束 → WebView2 停止渲染,壁纸冻结。

6. 延迟嵌入 + 直接调用

go func() {
    time.Sleep(3 * time.Second) // 等 WebView2 初始化完成
    wvHwnd := uintptr(wv.Window())
    procSetParent.Call(wvHwnd, workerw)    // user32 调用,跨线程安全
    procSetWindowLongPtrW.Call(...)
    procMoveWindow.Call(...)
}()

不能用 wv.Dispatch 嵌入——它依赖 wv.Run() 驱动,自定义消息循环不驱动它 → Dispatch 回调永不执行。SetParent/MoveWindow/SetWindowLongPtr 是 Win32 API跨线程安全直接调即可。


踩坑记录

explorer 桌面层损坏

反复 SetParent / kill 进程会把桌面 WorkerW 层搞坏(连之前能跑的产物都不显示)。

症状: 枚举找不到全屏 WorkerW、SHELLDLL_DefView 直接挂在 Progman 下。

修复: 重启 explorer.exe 重建干净结构。

OpenGL 不可见

在干净环境下explorer 重启后 + 正确 WorkerW + SwapBuffers 双缓冲 + 渲染像素验证 RGBA=0,255,0,255 正确OpenGL 窗口仍不显示,透出系统壁纸。

根因: Germanium 上桌面壁纸层的子窗口必须经 DirectComposition 合成。OpenGL SwapBuffers 走传统 GDI blt 路径DWM 不合成。

尝试过的方案及结果

方案 结果 说明
SwapBuffers双缓冲 帧缓冲正确但不可见 glReadPixels 确认 RGBA=0,255,0,255
SetParent 前初始化 GL GL context 在 SetParent 后可能与 DC 断联
SetParent 后初始化 GL 调换顺序无改善
WS_CHILD 样式 + SetWindowPos 窗口样式调整不影响 DWM 合成
glReadPixels → GDI SetDIBitsToDevice GDI blit 也不被 DWM 合成
去掉 PFD_DOUBLEBUFFER 单缓冲也无改善

结论: WorkerW 子窗口的呈现链路被 DWM 接管,只有通过 DirectComposition 提交的图面才能上屏。GDI BitBlt/SetDIBitsToDevice、OpenGL SwapBuffers 全部走传统路径DWM 不处理。

Go + OpenGL 踩坑汇总

  1. Go syscall 不能传浮点参数: Windows x64 用 XMM 寄存器传 float但 Go 的 syscall.SyscallN 只设 GPR 寄存器 (RCX/RDX/R8/R9)。glClearColor(1,0,0,1) 实际传入 (0,0,0,0)必须用指针变体: glUniform1fv, glUniform2fv, glClearBufferfv

  2. glClearBufferfv 错误 1280 (GL_INVALID_ENUM): 第一个参数用了 GL_COLOR_BUFFER_BIT (0x4060),正确值是 GL_COLOR (0x1800)。改用 glClear(GL_COLOR_BUFFER_BIT) 更简单。

  3. wglGetProcAddress 不能加载 GL 1.1 函数: glGetError, glClear, glFlush 等是 GL 1.1,直接从 opengl32.dll 导出,wglGetProcAddress 返回 0。GL 1.2+ 才用 wglGetProcAddress

  4. PFD 结构体必须 40 字节: Go 结构体布局要与 C 的 PIXELFORMATDESCRIPTOR 完全一致。关键字段: SizeVersionuint16Flagsuint32,中间 20 个 byte,末尾 3 个 uint32

  5. shader 用 layout(location=N): glGetUniformLocation 在 Intel GPU 上崩溃 (0xC0000005)。用 GLSL 430 的 layout(location=N) 绑定 uniform 位置避免调用此函数。

  6. wglCreateContext vs wglCreateContextAttribsARB: 前者创建遗留 context后者创建 Core Profile。wglCreateContext 在 Intel 驱动上也能拿到 GL 4.6 context。

WebView2 降资源方案(未实现)

wallpaper.html 里把 RENDER_SCALE 从 1.0 降到 0.5canvas 渲染分辨率减半再拉伸。GPU 子进程内存可从 425MB 大幅降低,视觉略糊。比重写 DComp 省事得多。


项目结构

u-desktop/
├── main.go            # 入口: 单实例互斥锁 + 配置目录 + 托盘启动
├── win32.go           # Win32 API 声明
├── systray.go         # 系统托盘 + WebView2 壁纸嵌入 + 消息循环
├── wallpaper.go       # 壁纸 HTML 构建 + 主题注入
├── config.go          # 配置结构体 + JSON 持久化
├── settings.go        # 设置窗口 (独立 WebView2)
├── weather.go         # 天气 API + IP 定位 + 城市列表
├── horoscope.go       # 星座运势 API + 文件缓存
├── ainews.go          # AI 资讯 API + 文件缓存
├── knowledge.go       # 知识卡片 AI 生成
├── bing.go            # Bing 壁纸下载 + 历史导航 + 收藏
├── dialog.go          # Win32 对话框 (文件/颜色选择)
├── web/
│   ├── overlay.html   # 桌面覆盖层 (时间/天气/星座/资讯/知识)
│   ├── settings.html  # 设置窗口 UI
│   └── themes/        # 壁纸主题 HTML
├── config/            # 运行时配置 (settings.json + 缓存)
└── docs/
    └── wallpaper-embedding.md  # 本文档

依赖

github.com/jchv/go-webview2    # WebView2 绑定
github.com/getlantern/systray   # 系统托盘
golang.org/x/sys/windows        # Win32 API

开机自启

注册表 HKCU\Software\Microsoft\Windows\CurrentVersion\Run\UDesktopWallpaper = u-desktop.exe 路径

构建 & 运行

go build -o u-desktop.exe .
.\u-desktop.exe

托盘图标右键: 暂停/继续、星座设置(子菜单)、城市选择(点击天气文字)、退出。全屏应用自动暂停渲染。


2025-05-25 踩坑追加

IP 定位在国内不准 / 永远显示上海

症状: 天气城市始终显示上海IP 定位不准。

根因链路:

  1. ipify.org 在国内被墙 → 获取公网 IP 失败
  2. fallbackLocation() 按预置城市列表顺序逐个试天气 API
  3. 上海排在第一位,且天气 API 返回成功 → 固定为上海
  4. 即便 ipify 能用,和风天气 GeoAPI 对部分 IP 段(如移动 111.60.x.x)返回 404

修复: IP 定位改为 myip.ipip.net(国内可用,直接返回城市名文本),从文本提取城市名匹配预置列表。保留 ipify + QWeather GeoAPI 作为降级源。优先级:localStorage 手动选择 > ipip.net > QWeather GeoAPI > fallback 逐城尝试

WebGL requestAnimationFrame 导致电脑卡顿

症状: 启动后系统明显卡顿。

根因: WebGL 极光 shader 以 60fps 全屏渲染持续占 GPU。壁纸场景动画是慢波效果不需要高帧率。

修复: 帧率限制为 15fps视觉无差异GPU 占用降 ~75%。

壁纸层 WebView 无法弹出 Modal 交互

症状: 托盘菜单"星座设置"点击无反应。

根因链路:

  1. Go 调用的 JS 函数名 showZodiacSettings() 不存在,实际函数名是 openModal() → 修复后仍无效
  2. WebView 已 SetParent 到 WorkerW壁纸层被桌面图标层遮挡
  3. Modal 在壁纸层渲染,用户看不到也点不到

修复: 星座设置改为托盘子菜单直接选择(AddSubMenuItem),不依赖 WebView 交互。城市选择通过点击天气文字触发因壁纸层可接收鼠标事件WorkerW 的子窗口不在图标层之上但仍可点击)。