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

222 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```go
procSetProcessDPIAware.Call()
```
否则 200% DPI 下 `GetSystemMetrics` 返回逻辑像素 1620x1080壁纸只占左上 1/4。必须在最开头调用。
### 2. 找到正确 WorkerW
```go
// 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 嵌入
```go
procSetParent.Call(wvHwnd, workerw)
```
### 4. 去边框 (GWL_STYLE)
```go
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()
```go
// 不用 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
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` 完全一致。关键字段: `Size``Version``uint16``Flags``uint32`,中间 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 路径`
## 构建 & 运行
```bash
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 的子窗口不在图标层之上但仍可点击)。