初始提交
Win11 动态壁纸引擎:WebView2 + systray + 和风天气 - WebGL 极光背景动画 - 实时天气(24h/7d预报) - 星座运势(托盘切换) - 暂停/继续控制 - 单实例互斥锁防双开 - vendor systray 修复 ClickedCh 静默丢弃
This commit is contained in:
208
docs/wallpaper-embedding.md
Normal file
208
docs/wallpaper-embedding.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# 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`(大正数),根本不是 -16,SetWindowLongPtrW 不会报错但修改的是错误的属性。
|
||||
|
||||
**切忌写成 -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.5,canvas 渲染分辨率减半再拉伸。GPU 子进程内存可从 425MB 大幅降低,视觉略糊。比重写 DComp 省事得多。
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
u-desktop/
|
||||
├── main.go # Go 层: WebView2 创建 + WorkerW 嵌入 + 系统托盘
|
||||
├── wallpaper.html # 渲染层: WebGL 极光 + 天气组件 + 星座运势
|
||||
├── backup-opengl/ # OpenGL 方案备份(已确证不可行)
|
||||
├── go.mod / go.sum
|
||||
└── 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 的子窗口不在图标层之上但仍可点击)。
|
||||
Reference in New Issue
Block a user