初始提交
Win11 动态壁纸引擎:WebView2 + systray + 和风天气 - WebGL 极光背景动画 - 实时天气(24h/7d预报) - 星座运势(托盘切换) - 暂停/继续控制 - 单实例互斥锁防双开 - vendor systray 修复 ClickedCh 静默丢弃
This commit is contained in:
5
docs/backup-opengl/go_opengl.mod.txt
Normal file
5
docs/backup-opengl/go_opengl.mod.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
module u-desktop
|
||||
|
||||
go 1.26.3
|
||||
|
||||
require golang.org/x/sys v0.45.0
|
||||
530
docs/backup-opengl/main_opengl.go.txt
Normal file
530
docs/backup-opengl/main_opengl.go.txt
Normal file
@@ -0,0 +1,530 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
WM_DESTROY = 0x0002
|
||||
WM_CLOSE = 0x0010
|
||||
WM_QUIT = 0x0012
|
||||
WM_MOUSEMOVE = 0x0200
|
||||
WM_LBUTTONDOWN = 0x0201
|
||||
|
||||
WS_POPUP = 0x80000000
|
||||
WS_VISIBLE = 0x10000000
|
||||
WS_CHILD = 0x40000000
|
||||
WS_CLIPCHILDREN = 0x02000000
|
||||
WS_CLIPSIBLINGS = 0x04000000
|
||||
|
||||
GWL_STYLE = ^uintptr(0) - 15
|
||||
|
||||
PFD_DRAW_TO_WINDOW = 0x04
|
||||
PFD_SUPPORT_OPENGL = 0x20
|
||||
PFD_DOUBLEBUFFER = 0x01
|
||||
|
||||
GL_COLOR_BUFFER_BIT = 0x4060
|
||||
GL_TRIANGLE_STRIP = 0x0005
|
||||
GL_ARRAY_BUFFER = 0x8892
|
||||
GL_STATIC_DRAW = 0x88E4
|
||||
GL_VERTEX_SHADER = 0x8B31
|
||||
GL_FRAGMENT_SHADER = 0x8B30
|
||||
GL_COMPILE_STATUS = 0x8B81
|
||||
GL_INFO_LOG_LENGTH = 0x8B84
|
||||
GL_RGBA = 0x1908
|
||||
GL_UNSIGNED_BYTE = 0x1401
|
||||
GL_FLOAT = 0x1406
|
||||
GL_VERSION = 0x1F02
|
||||
|
||||
DIB_RGB_COLORS = 0
|
||||
SWP_NOSIZE = 0x0001
|
||||
SWP_NOMOVE = 0x0002
|
||||
SWP_NOZORDER = 0x0004
|
||||
SWP_FRAMECHANGED = 0x0020
|
||||
HWND_BOTTOM = 1
|
||||
)
|
||||
|
||||
type bitmapInfoHeader struct {
|
||||
Size uint32
|
||||
Width int32
|
||||
Height int32
|
||||
Planes uint16
|
||||
BitCount uint16
|
||||
Compression uint32
|
||||
SizeImage uint32
|
||||
XPelsPerMeter int32
|
||||
YPelsPerMeter int32
|
||||
ClrUsed uint32
|
||||
ClrImportant uint32
|
||||
}
|
||||
|
||||
var (
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
gdi32 = windows.NewLazySystemDLL("gdi32.dll")
|
||||
opengl32 = windows.NewLazySystemDLL("opengl32.dll")
|
||||
procFindWindowW = user32.NewProc("FindWindowW")
|
||||
procFindWindowExW = user32.NewProc("FindWindowExW")
|
||||
procSendMsgTimeout = user32.NewProc("SendMessageTimeoutW")
|
||||
procSetParent = user32.NewProc("SetParent")
|
||||
procMoveWindow = user32.NewProc("MoveWindow")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
|
||||
procSetWindowPos = user32.NewProc("SetWindowPos")
|
||||
procCreateWindowExW = user32.NewProc("CreateWindowExW")
|
||||
procDefWindowProcW = user32.NewProc("DefWindowProcW")
|
||||
procRegisterClassExW = user32.NewProc("RegisterClassExW")
|
||||
procEnumDisplayMonitors = user32.NewProc("EnumDisplayMonitors")
|
||||
procEnumWindows = user32.NewProc("EnumWindows")
|
||||
procGetClassNameW = user32.NewProc("GetClassNameW")
|
||||
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||||
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||||
procPeekMessageW = user32.NewProc("PeekMessageW")
|
||||
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||
procDispatchMessageW = user32.NewProc("DispatchMessageW")
|
||||
procPostQuitMessage = user32.NewProc("PostQuitMessage")
|
||||
procGetDC = user32.NewProc("GetDC")
|
||||
procReleaseDC = user32.NewProc("ReleaseDC")
|
||||
procSetDIBitsToDevice = gdi32.NewProc("SetDIBitsToDevice")
|
||||
procChoosePixelFormat = gdi32.NewProc("ChoosePixelFormat")
|
||||
procSetPixelFormat = gdi32.NewProc("SetPixelFormat")
|
||||
procSwapBuffers = gdi32.NewProc("SwapBuffers")
|
||||
glFinish = opengl32.NewProc("glFinish")
|
||||
procWglCreateContext = opengl32.NewProc("wglCreateContext")
|
||||
procWglMakeCurrent = opengl32.NewProc("wglMakeCurrent")
|
||||
procWglDeleteContext = opengl32.NewProc("wglDeleteContext")
|
||||
procWglGetProcAddress = opengl32.NewProc("wglGetProcAddress")
|
||||
glViewport = opengl32.NewProc("glViewport")
|
||||
glDrawArrays = opengl32.NewProc("glDrawArrays")
|
||||
glGetString = opengl32.NewProc("glGetString")
|
||||
glReadPixels = opengl32.NewProc("glReadPixels")
|
||||
glClear = opengl32.NewProc("glClear")
|
||||
)
|
||||
|
||||
type rect struct{ Left, Top, Right, Bottom int32 }
|
||||
type wndClassEx struct {
|
||||
CbSize, Style uint32
|
||||
LpfnWndProc uintptr
|
||||
CbClsExtra, CbWndExtra int32
|
||||
HInstance, HIcon, HCursor, HbrBackground uintptr
|
||||
LpszMenuName, LpszClassName *uint16
|
||||
HIconSm uintptr
|
||||
}
|
||||
type pfd struct {
|
||||
Size, Version uint16
|
||||
Flags uint32
|
||||
PixelType, ColorBits, RedBits, RedShift, GreenBits, GreenShift,
|
||||
BlueBits, BlueShift, AlphaBits, AlphaShift, AccumBits, AccumRed,
|
||||
AccumGreen, AccumBlue, AccumAlpha, DepthBits, StencilBits,
|
||||
AuxBuffers, LayerType, Reserved byte
|
||||
LayerMask, VisibleMask, DamageMask uint32
|
||||
}
|
||||
|
||||
var (
|
||||
wallpaperHwnd uintptr
|
||||
glDC uintptr
|
||||
glCtx uintptr
|
||||
mouseX, mouseY float32
|
||||
clickX, clickY float32
|
||||
clickTime float64
|
||||
screenW, screenH int32
|
||||
startTime = float64(time.Now().UnixNano()) / 1e9
|
||||
frameCount int
|
||||
pixelBuf []byte
|
||||
bmi bitmapInfoHeader
|
||||
glUseProgram, glUniform1fv, glUniform2fv uintptr
|
||||
useSwap bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
runtime.LockOSThread()
|
||||
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
||||
procSetProcessDPIAware.Call()
|
||||
|
||||
if len(os.Args) > 1 && os.Args[1] == "probe" {
|
||||
probe()
|
||||
return
|
||||
}
|
||||
screenW, screenH = getDesktopSize()
|
||||
log.Printf("Desktop: %dx%d", screenW, screenH)
|
||||
|
||||
// Allocate pixel buffer for GDI blit
|
||||
pixels := screenW * screenH * 4
|
||||
pixelBuf = make([]byte, pixels)
|
||||
log.Printf("Pixel buffer: %d bytes (%.1fMB)", pixels, float64(pixels)/1024/1024)
|
||||
|
||||
bmi = bitmapInfoHeader{
|
||||
Size: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
|
||||
Width: screenW,
|
||||
Height: screenH, // positive = bottom-up (matches GL)
|
||||
Planes: 1,
|
||||
BitCount: 32,
|
||||
Compression: 0, // BI_RGB
|
||||
}
|
||||
|
||||
mode := ""
|
||||
if len(os.Args) > 1 {
|
||||
mode = os.Args[1]
|
||||
}
|
||||
useSwap = len(os.Args) > 2 && os.Args[2] == "swap"
|
||||
|
||||
createWindow()
|
||||
|
||||
switch mode {
|
||||
case "windowed":
|
||||
// Diagnostic: skip embedding, show as normal fullscreen window
|
||||
procMoveWindow.Call(wallpaperHwnd, 0, 0, uintptr(screenW), uintptr(screenH), 1)
|
||||
log.Println("WINDOWED mode (no embed)")
|
||||
default:
|
||||
// 选择嵌入目标:progman 模式直接挂到 Progman(Win11 新内核),否则找 WorkerW
|
||||
var target uintptr
|
||||
switch mode {
|
||||
case "progman", "progman-top":
|
||||
target, _, _ = procFindWindowW.Call(uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Progman"))), 0)
|
||||
var discard uintptr
|
||||
procSendMsgTimeout.Call(target, 0x052C, 0xD, 0, 0x0000, 1000, uintptr(unsafe.Pointer(&discard)))
|
||||
case "childww", "childww-top":
|
||||
// Progman 的直接子 WorkerW —— 可能才是 DWM 合成的壁纸层
|
||||
progman, _, _ := procFindWindowW.Call(uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Progman"))), 0)
|
||||
var discard uintptr
|
||||
procSendMsgTimeout.Call(progman, 0x052C, 0xD, 0, 0x0000, 1000, uintptr(unsafe.Pointer(&discard)))
|
||||
target, _, _ = procFindWindowExW.Call(progman, 0, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW"))), 0)
|
||||
default:
|
||||
target = findWorkerW()
|
||||
}
|
||||
if target == 0 {
|
||||
log.Fatal("embed target not found")
|
||||
}
|
||||
procSetParent.Call(wallpaperHwnd, target)
|
||||
procSetWindowLongPtrW.Call(wallpaperHwnd, GWL_STYLE, uintptr(WS_CHILD|WS_VISIBLE|WS_CLIPCHILDREN|WS_CLIPSIBLINGS))
|
||||
procMoveWindow.Call(wallpaperHwnd, 0, 0, uintptr(screenW), uintptr(screenH), 1)
|
||||
// *-top: 不置底(默认 z-order,盖住图标也认)用于验证子窗口能否上屏
|
||||
// 其它: 置底到图标之下
|
||||
if mode != "progman-top" && mode != "childww-top" {
|
||||
procSetWindowPos.Call(wallpaperHwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED)
|
||||
}
|
||||
log.Printf("Embedded into target=%x mode=%s", target, mode)
|
||||
}
|
||||
|
||||
initGL()
|
||||
loadExtensions()
|
||||
progID := setupShaders()
|
||||
|
||||
log.Println("Rendering...")
|
||||
targetFPS := 30.0
|
||||
frameInterval := 1.0 / targetFPS
|
||||
lastFrame := 0.0
|
||||
lastMove := 0.0
|
||||
|
||||
type msg struct {
|
||||
HWnd uintptr
|
||||
Message uint32
|
||||
WParam uintptr
|
||||
LParam uintptr
|
||||
Time uint32
|
||||
Pt struct{ X, Y int32 }
|
||||
}
|
||||
var m msg
|
||||
for {
|
||||
for {
|
||||
r, _, _ := procPeekMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0, 1)
|
||||
if r == 0 {
|
||||
break
|
||||
}
|
||||
if m.Message == WM_QUIT {
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
switch m.Message {
|
||||
case WM_MOUSEMOVE:
|
||||
mouseX = float32(int16(m.LParam&0xFFFF)) / float32(screenW)
|
||||
mouseY = 1.0 - float32(int16(m.LParam>>16)) / float32(screenH)
|
||||
lastMove = now()
|
||||
case WM_LBUTTONDOWN:
|
||||
clickX = float32(int16(m.LParam&0xFFFF)) / float32(screenW)
|
||||
clickY = 1.0 - float32(int16(m.LParam>>16)) / float32(screenH)
|
||||
clickTime = now()
|
||||
}
|
||||
procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
|
||||
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
|
||||
}
|
||||
|
||||
t := now()
|
||||
if t-lastFrame < frameInterval {
|
||||
time.Sleep(time.Duration((frameInterval - (t - lastFrame)) * 0.8 * float64(time.Second)))
|
||||
continue
|
||||
}
|
||||
lastFrame = t
|
||||
if t-lastMove > 5.0 {
|
||||
targetFPS = 10.0
|
||||
} else {
|
||||
targetFPS = 30.0
|
||||
}
|
||||
frameInterval = 1.0 / targetFPS
|
||||
|
||||
// GL render
|
||||
glViewport.Call(0, 0, uintptr(screenW), uintptr(screenH))
|
||||
glClear.Call(uintptr(GL_COLOR_BUFFER_BIT))
|
||||
syscall.SyscallN(glUseProgram, uintptr(progID))
|
||||
|
||||
elapsed := float32(0)
|
||||
if clickTime > 0 {
|
||||
e := t - clickTime
|
||||
if e < 3.0 {
|
||||
elapsed = float32(e)
|
||||
} else {
|
||||
clickTime = 0
|
||||
}
|
||||
}
|
||||
|
||||
fTime := float32(t)
|
||||
fScreen := [2]float32{float32(screenW), float32(screenH)}
|
||||
syscall.SyscallN(glUniform1fv, uintptr(0), uintptr(1), uintptr(unsafe.Pointer(&fTime)))
|
||||
syscall.SyscallN(glUniform2fv, uintptr(1), uintptr(1), uintptr(unsafe.Pointer(&fScreen[0])))
|
||||
syscall.SyscallN(glUniform2fv, uintptr(2), uintptr(1), uintptr(unsafe.Pointer(&mouseX)))
|
||||
syscall.SyscallN(glUniform1fv, uintptr(3), uintptr(1), uintptr(unsafe.Pointer(&elapsed)))
|
||||
syscall.SyscallN(glUniform2fv, uintptr(4), uintptr(1), uintptr(unsafe.Pointer(&clickX)))
|
||||
|
||||
glDrawArrays.Call(GL_TRIANGLE_STRIP, 0, 4)
|
||||
|
||||
if useSwap {
|
||||
// 双缓冲呈现:走 DWM flip,嵌入子窗口也能合成上屏
|
||||
procSwapBuffers.Call(glDC)
|
||||
} else {
|
||||
glFinish.Call()
|
||||
// Read framebuffer → GDI blit to window
|
||||
glReadPixels.Call(0, 0, uintptr(screenW), uintptr(screenH), GL_RGBA, GL_UNSIGNED_BYTE, uintptr(unsafe.Pointer(&pixelBuf[0])))
|
||||
winDC, _, _ := procGetDC.Call(wallpaperHwnd)
|
||||
procSetDIBitsToDevice.Call(
|
||||
winDC,
|
||||
0, 0,
|
||||
uintptr(screenW), uintptr(screenH),
|
||||
0, 0,
|
||||
0, uintptr(screenH),
|
||||
uintptr(unsafe.Pointer(&pixelBuf[0])),
|
||||
uintptr(unsafe.Pointer(&bmi)),
|
||||
DIB_RGB_COLORS,
|
||||
)
|
||||
procReleaseDC.Call(wallpaperHwnd, winDC)
|
||||
}
|
||||
|
||||
frameCount++
|
||||
if frameCount <= 3 {
|
||||
log.Printf("FRAME#%d swap=%v", frameCount, useSwap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadExt(name string) uintptr {
|
||||
cname, _ := windows.BytePtrFromString(name)
|
||||
ptr, _, _ := procWglGetProcAddress.Call(uintptr(unsafe.Pointer(cname)))
|
||||
if ptr == 0 {
|
||||
log.Fatalf("ext not found: %s", name)
|
||||
}
|
||||
return ptr
|
||||
}
|
||||
|
||||
func loadExtensions() {
|
||||
glUseProgram = loadExt("glUseProgram")
|
||||
glUniform1fv = loadExt("glUniform1fv")
|
||||
glUniform2fv = loadExt("glUniform2fv")
|
||||
}
|
||||
|
||||
func setupShaders() uint32 {
|
||||
glCreateShader := loadExt("glCreateShader")
|
||||
glShaderSource := loadExt("glShaderSource")
|
||||
glCompileShader := loadExt("glCompileShader")
|
||||
glGetShaderiv := loadExt("glGetShaderiv")
|
||||
glGetShaderInfoLog := loadExt("glGetShaderInfoLog")
|
||||
|
||||
compile := func(st uint32, src string) uint32 {
|
||||
s, _, _ := syscall.SyscallN(glCreateShader, uintptr(st))
|
||||
cstr, _ := windows.BytePtrFromString(src)
|
||||
p := uintptr(unsafe.Pointer(cstr))
|
||||
syscall.SyscallN(glShaderSource, s, 1, uintptr(unsafe.Pointer(&p)), 0)
|
||||
syscall.SyscallN(glCompileShader, s)
|
||||
var status int32
|
||||
syscall.SyscallN(glGetShaderiv, s, uintptr(GL_COMPILE_STATUS), uintptr(unsafe.Pointer(&status)))
|
||||
if status == 0 {
|
||||
var logLen int32
|
||||
syscall.SyscallN(glGetShaderiv, s, uintptr(GL_INFO_LOG_LENGTH), uintptr(unsafe.Pointer(&logLen)))
|
||||
if logLen > 0 {
|
||||
logBuf := make([]byte, logLen)
|
||||
syscall.SyscallN(glGetShaderInfoLog, s, uintptr(logLen), 0, uintptr(unsafe.Pointer(&logBuf[0])))
|
||||
log.Fatalf("shader compile failed:\n%s", string(logBuf[:logLen]))
|
||||
}
|
||||
log.Fatal("shader compile failed (no info log)")
|
||||
}
|
||||
return uint32(s)
|
||||
}
|
||||
vs := compile(GL_VERTEX_SHADER, vertexSrc)
|
||||
fs := compile(GL_FRAGMENT_SHADER, fragmentSrc)
|
||||
glCreateProgram := loadExt("glCreateProgram")
|
||||
prog, _, _ := syscall.SyscallN(glCreateProgram)
|
||||
syscall.SyscallN(loadExt("glAttachShader"), prog, uintptr(vs))
|
||||
syscall.SyscallN(loadExt("glAttachShader"), prog, uintptr(fs))
|
||||
syscall.SyscallN(loadExt("glLinkProgram"), prog)
|
||||
log.Println("Program:", prog)
|
||||
var vao, vbo uint32
|
||||
syscall.SyscallN(loadExt("glGenVertexArrays"), 1, uintptr(unsafe.Pointer(&vao)))
|
||||
syscall.SyscallN(loadExt("glGenBuffers"), 1, uintptr(unsafe.Pointer(&vbo)))
|
||||
quad := [8]float32{-1, -1, 1, -1, -1, 1, 1, 1}
|
||||
syscall.SyscallN(loadExt("glBindVertexArray"), uintptr(vao))
|
||||
syscall.SyscallN(loadExt("glBindBuffer"), uintptr(GL_ARRAY_BUFFER), uintptr(vbo))
|
||||
syscall.SyscallN(loadExt("glBufferData"), uintptr(GL_ARRAY_BUFFER), uintptr(len(quad)*4), uintptr(unsafe.Pointer(&quad[0])), uintptr(GL_STATIC_DRAW))
|
||||
syscall.SyscallN(loadExt("glEnableVertexAttribArray"), 0)
|
||||
syscall.SyscallN(loadExt("glVertexAttribPointer"), 0, 2, uintptr(GL_FLOAT), 0, 0, 0)
|
||||
return uint32(prog)
|
||||
}
|
||||
|
||||
func createWindow() {
|
||||
var hinstance windows.Handle
|
||||
windows.GetModuleHandleEx(0, nil, &hinstance)
|
||||
className, _ := windows.UTF16PtrFromString("GLWallpaper")
|
||||
wndProc := windows.NewCallback(func(hwnd uintptr, msg uint32, wp, lp uintptr) uintptr {
|
||||
if msg == WM_DESTROY || msg == WM_CLOSE {
|
||||
procPostQuitMessage.Call(0)
|
||||
return 0
|
||||
}
|
||||
ret, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wp, lp)
|
||||
return ret
|
||||
})
|
||||
wc := wndClassEx{
|
||||
CbSize: uint32(unsafe.Sizeof(wndClassEx{})),
|
||||
LpfnWndProc: wndProc,
|
||||
HInstance: uintptr(hinstance),
|
||||
LpszClassName: className,
|
||||
}
|
||||
procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
|
||||
windowName, _ := windows.UTF16PtrFromString("GLWallpaper")
|
||||
wallpaperHwnd, _, _ = procCreateWindowExW.Call(
|
||||
0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(windowName)),
|
||||
WS_POPUP|WS_VISIBLE, 0, 0, uintptr(screenW), uintptr(screenH),
|
||||
0, 0, uintptr(hinstance), 0,
|
||||
)
|
||||
if wallpaperHwnd == 0 {
|
||||
log.Fatal("CreateWindow failed")
|
||||
}
|
||||
procShowWindow.Call(wallpaperHwnd, 5)
|
||||
log.Println("Window:", wallpaperHwnd)
|
||||
}
|
||||
|
||||
func initGL() {
|
||||
glDC, _, _ = procGetDC.Call(wallpaperHwnd)
|
||||
if glDC == 0 {
|
||||
log.Fatal("GetDC failed")
|
||||
}
|
||||
flags := uint32(PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL)
|
||||
if useSwap {
|
||||
flags |= PFD_DOUBLEBUFFER
|
||||
}
|
||||
desc := pfd{Size: uint16(unsafe.Sizeof(pfd{})), Version: 1, Flags: flags, PixelType: 0, ColorBits: 32, DepthBits: 24}
|
||||
pf, _, _ := procChoosePixelFormat.Call(glDC, uintptr(unsafe.Pointer(&desc)))
|
||||
if pf == 0 {
|
||||
log.Fatal("ChoosePixelFormat failed")
|
||||
}
|
||||
ok, _, _ := procSetPixelFormat.Call(glDC, pf, uintptr(unsafe.Pointer(&desc)))
|
||||
if ok == 0 {
|
||||
log.Fatal("SetPixelFormat failed")
|
||||
}
|
||||
glCtx, _, _ = procWglCreateContext.Call(glDC)
|
||||
if glCtx == 0 {
|
||||
log.Fatal("wglCreateContext failed")
|
||||
}
|
||||
mcR, _, _ := procWglMakeCurrent.Call(glDC, glCtx)
|
||||
ver, _, _ := glGetString.Call(GL_VERSION)
|
||||
log.Printf("MakeCurrent=%d GL=%s", mcR, windows.BytePtrToString((*byte)(unsafe.Pointer(ver))))
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
if glCtx != 0 {
|
||||
procWglMakeCurrent.Call(0, 0)
|
||||
procWglDeleteContext.Call(glCtx)
|
||||
}
|
||||
if glDC != 0 {
|
||||
procReleaseDC.Call(wallpaperHwnd, glDC)
|
||||
}
|
||||
}
|
||||
|
||||
func classOf(hwnd uintptr) string {
|
||||
buf := make([]uint16, 256)
|
||||
procGetClassNameW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), 256)
|
||||
return windows.UTF16ToString(buf)
|
||||
}
|
||||
|
||||
func enumTops(tag string) {
|
||||
cb := windows.NewCallback(func(hwnd, _ uintptr) uintptr {
|
||||
cls := classOf(hwnd)
|
||||
if cls == "WorkerW" || cls == "Progman" {
|
||||
dv, _, _ := procFindWindowExW.Call(hwnd, 0, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("SHELLDLL_DefView"))), 0)
|
||||
var r rect
|
||||
procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&r)))
|
||||
full := ""
|
||||
if r.Right-r.Left == screenW && r.Bottom-r.Top == screenH {
|
||||
full = " <FULLSCREEN>"
|
||||
}
|
||||
log.Printf("[%s] hwnd=%x class=%-8s DefView=%x rect=(%d,%d,%d,%d)%s", tag, hwnd, cls, dv, r.Left, r.Top, r.Right, r.Bottom, full)
|
||||
}
|
||||
return 1
|
||||
})
|
||||
procEnumWindows.Call(cb, 0)
|
||||
}
|
||||
|
||||
// probe 用权威参数 WPARAM=0xD 发消息,多轮观察 WorkerW 是否分离出来
|
||||
func probe() {
|
||||
screenW, screenH = getDesktopSize()
|
||||
progman, _, _ := procFindWindowW.Call(uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Progman"))), 0)
|
||||
log.Printf("Progman=%x screen=%dx%d", progman, screenW, screenH)
|
||||
var discard uintptr
|
||||
procSendMsgTimeout.Call(progman, 0x052C, 0xD, 0, 0x0000, 1000, uintptr(unsafe.Pointer(&discard)))
|
||||
procSendMsgTimeout.Call(progman, 0x052C, 0xD, 1, 0x0000, 1000, uintptr(unsafe.Pointer(&discard)))
|
||||
log.Println("sent 0x052C WPARAM=0xD (LPARAM 0 and 1)")
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
log.Printf("--- round %d ---", i)
|
||||
enumTops("top")
|
||||
}
|
||||
}
|
||||
|
||||
func findWorkerW() uintptr {
|
||||
progman, _, _ := procFindWindowW.Call(uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Progman"))), 0)
|
||||
if progman == 0 {
|
||||
return 0
|
||||
}
|
||||
var discard uintptr
|
||||
procSendMsgTimeout.Call(progman, 0x052C, 0, 0, 0x0000, 1000, uintptr(unsafe.Pointer(&discard)))
|
||||
sdv, _, _ := procFindWindowExW.Call(progman, 0, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("SHELLDLL_DefView"))), 0)
|
||||
if sdv != 0 {
|
||||
ww, _, _ := procFindWindowExW.Call(progman, sdv, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW"))), 0)
|
||||
if ww != 0 {
|
||||
return ww
|
||||
}
|
||||
}
|
||||
ww, _, _ := procFindWindowExW.Call(progman, 0, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW"))), 0)
|
||||
return ww
|
||||
}
|
||||
|
||||
func getDesktopSize() (int32, int32) {
|
||||
var mr, mb int32
|
||||
cb := windows.NewCallback(func(_, _, lprc, _ uintptr) uintptr {
|
||||
r := (*rect)(unsafe.Pointer(lprc))
|
||||
if r.Right > mr {
|
||||
mr = r.Right
|
||||
}
|
||||
if r.Bottom > mb {
|
||||
mb = r.Bottom
|
||||
}
|
||||
return 1
|
||||
})
|
||||
procEnumDisplayMonitors.Call(0, 0, cb, 0)
|
||||
return mr, mb
|
||||
}
|
||||
|
||||
func now() float64 { return float64(time.Now().UnixNano())/1e9 - startTime }
|
||||
13
docs/backup-opengl/shader.go.bak
Normal file
13
docs/backup-opengl/shader.go.bak
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
const vertexSrc = `#version 430 core
|
||||
in vec2 a_pos;
|
||||
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
|
||||
`
|
||||
|
||||
const fragmentSrc = `#version 430 core
|
||||
out vec4 FragColor;
|
||||
void main() {
|
||||
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
13
docs/backup-opengl/shader_opengl.go.txt
Normal file
13
docs/backup-opengl/shader_opengl.go.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
const vertexSrc = `#version 430 core
|
||||
in vec2 a_pos;
|
||||
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
|
||||
`
|
||||
|
||||
const fragmentSrc = `#version 430 core
|
||||
out vec4 FragColor;
|
||||
void main() {
|
||||
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
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 的子窗口不在图标层之上但仍可点击)。
|
||||
96
docs/wallpaper-test.html
Normal file
96
docs/wallpaper-test.html
Normal file
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>测试</title>
|
||||
<style>
|
||||
body { margin: 0; overflow: hidden; background: #000; }
|
||||
#test {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
font-family: sans-serif;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
button {
|
||||
background: #5865f2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
font-size: 18px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
}
|
||||
#result {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test">
|
||||
<div>🧪 WebView2 测试页面</div>
|
||||
<button id="btn1">测试按钮1</button>
|
||||
<button id="btn2">测试按钮2</button>
|
||||
<div id="result">等待点击...</div>
|
||||
<div id="time"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('=== 页面已加载 ===');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('DOM loaded');
|
||||
|
||||
// 方法1:addEventListener(推荐)
|
||||
const btn1 = document.getElementById('btn1');
|
||||
btn1.addEventListener('click', function() {
|
||||
console.log('按钮1被点击');
|
||||
document.getElementById('result').innerHTML = '✅ 按钮1点击成功!<br>时间: ' + new Date().toLocaleTimeString();
|
||||
document.getElementById('result').style.background = 'rgba(76, 175, 80, 0.6)';
|
||||
});
|
||||
|
||||
// 方法2:addEventListener
|
||||
const btn2 = document.getElementById('btn2');
|
||||
btn2.addEventListener('click', function() {
|
||||
console.log('按钮2被点击');
|
||||
document.getElementById('result').innerHTML = '✅ 按钮2点击成功!<br>时间: ' + new Date().toLocaleTimeString();
|
||||
document.getElementById('result').style.background = 'rgba(33, 150, 243, 0.6)';
|
||||
});
|
||||
|
||||
// 全局点击测试
|
||||
document.addEventListener('click', function(e) {
|
||||
console.log('全局点击事件:', e.target.tagName, e.target.id);
|
||||
});
|
||||
|
||||
console.log('事件监听器已设置');
|
||||
});
|
||||
|
||||
// 更新时间
|
||||
setInterval(() => {
|
||||
const timeEl = document.getElementById('time');
|
||||
if (timeEl) {
|
||||
timeEl.textContent = '时间: ' + new Date().toLocaleTimeString();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 鼠标移动测试
|
||||
let moveCount = 0;
|
||||
document.addEventListener('mousemove', function() {
|
||||
moveCount++;
|
||||
if (moveCount % 60 === 0) {
|
||||
console.log('鼠标移动计数:', moveCount);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('=== 初始化完成 ===');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user