Win11 动态壁纸引擎:WebView2 + systray + 和风天气 - WebGL 极光背景动画 - 实时天气(24h/7d预报) - 星座运势(托盘切换) - 暂停/继续控制 - 单实例互斥锁防双开 - vendor systray 修复 ClickedCh 静默丢弃
730 lines
18 KiB
Go
730 lines
18 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"embed"
|
||
"encoding/binary"
|
||
"encoding/json"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/png"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"runtime"
|
||
"strconv"
|
||
"sync/atomic"
|
||
"strings"
|
||
"time"
|
||
"unsafe"
|
||
|
||
"github.com/getlantern/systray"
|
||
"github.com/jchv/go-webview2"
|
||
"golang.org/x/sys/windows"
|
||
)
|
||
|
||
//go:embed wallpaper.html
|
||
var fs embed.FS
|
||
|
||
var (
|
||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||
procFindWindowW = user32.NewProc("FindWindowW")
|
||
procFindWindowExW = user32.NewProc("FindWindowExW")
|
||
procSendMessageTimeoutW = user32.NewProc("SendMessageTimeoutW")
|
||
procSetParent = user32.NewProc("SetParent")
|
||
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
|
||
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||
procMoveWindow = user32.NewProc("MoveWindow")
|
||
procShowWindow = user32.NewProc("ShowWindow")
|
||
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
|
||
procGetMessageW = user32.NewProc("GetMessageW")
|
||
procPostMessageW = user32.NewProc("PostMessageW")
|
||
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||
procDispatchMessageW = user32.NewProc("DispatchMessageW")
|
||
)
|
||
|
||
var wv webview2.WebView
|
||
var configPath string
|
||
var wvHwnd uintptr
|
||
var jsQueue = make(chan string, 64)
|
||
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||
var paused int32 // atomic: 0=running, 1=paused
|
||
|
||
const wmEvalJS = 0x0401 // WM_USER + 1
|
||
|
||
func evalJS(js string) {
|
||
select {
|
||
case jsQueue <- js:
|
||
log.Println("📤 evalJS queued:", js[:min(60, len(js))])
|
||
default:
|
||
log.Println("⚠️ jsQueue full, dropping eval")
|
||
}
|
||
if wvHwnd != 0 {
|
||
ret, _, _ := procPostMessageW.Call(wvHwnd, wmEvalJS, 0, 0)
|
||
log.Printf("📤 PostMessageW ret=%d hwnd=0x%x", ret, wvHwnd)
|
||
} else {
|
||
log.Println("⚠️ wvHwnd is 0, skip PostMessage")
|
||
}
|
||
}
|
||
|
||
func min(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|
||
|
||
// ── Config ──
|
||
|
||
type Config struct {
|
||
Zodiac string `json:"zodiac"`
|
||
City string `json:"city"` // QWeather city ID, 空=自动定位
|
||
}
|
||
|
||
const defaultZodiac = "射手座"
|
||
|
||
func main() {
|
||
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
||
|
||
// 单实例互斥锁,防止双托盘
|
||
mutexName, _ := windows.UTF16PtrFromString("Global\\u-desktop-single-instance")
|
||
mutex, err := windows.CreateMutex(nil, false, mutexName)
|
||
if err != nil {
|
||
log.Fatal("创建互斥锁失败:", err)
|
||
}
|
||
if windows.GetLastError() == windows.ERROR_ALREADY_EXISTS {
|
||
log.Println("⚠️ 已有实例运行,退出")
|
||
windows.CloseHandle(mutex)
|
||
return
|
||
}
|
||
defer windows.CloseHandle(mutex)
|
||
|
||
exePath, _ := os.Executable()
|
||
configDir := filepath.Join(filepath.Dir(exePath), "config")
|
||
os.MkdirAll(configDir, 0755)
|
||
configPath = filepath.Join(configDir, "settings.json")
|
||
procSetProcessDPIAware.Call()
|
||
systray.Run(onSystrayReady, nil)
|
||
}
|
||
|
||
func loadConfig() *Config {
|
||
data, err := os.ReadFile(configPath)
|
||
if err != nil {
|
||
return &Config{Zodiac: defaultZodiac}
|
||
}
|
||
var cfg Config
|
||
if json.Unmarshal(data, &cfg) != nil {
|
||
return &Config{Zodiac: defaultZodiac}
|
||
}
|
||
if cfg.Zodiac == "" {
|
||
cfg.Zodiac = defaultZodiac
|
||
}
|
||
return &cfg
|
||
}
|
||
|
||
func saveConfig(cfg *Config) error {
|
||
data, _ := json.MarshalIndent(cfg, "", " ")
|
||
return os.WriteFile(configPath, data, 0644)
|
||
}
|
||
|
||
// ── Icon ──
|
||
|
||
func generateIcon() []byte {
|
||
img := image.NewRGBA(image.Rect(0, 0, 16, 16))
|
||
c := color.RGBA{R: 88, G: 101, B: 242, A: 255}
|
||
for y := 0; y < 16; y++ {
|
||
for x := 0; x < 16; x++ {
|
||
img.Set(x, y, c)
|
||
}
|
||
}
|
||
var buf bytes.Buffer
|
||
png.Encode(&buf, img)
|
||
pngData := buf.Bytes()
|
||
ico := make([]byte, 22+len(pngData))
|
||
binary.LittleEndian.PutUint16(ico[0:], 0)
|
||
binary.LittleEndian.PutUint16(ico[2:], 1)
|
||
binary.LittleEndian.PutUint16(ico[4:], 1)
|
||
ico[6], ico[7], ico[8], ico[9] = 16, 16, 0, 0
|
||
binary.LittleEndian.PutUint16(ico[10:], 1)
|
||
binary.LittleEndian.PutUint16(ico[12:], 32)
|
||
binary.LittleEndian.PutUint32(ico[14:], uint32(len(pngData)))
|
||
binary.LittleEndian.PutUint32(ico[18:], 22)
|
||
copy(ico[22:], pngData)
|
||
return ico
|
||
}
|
||
|
||
// ── Weather ──
|
||
|
||
const qweatherKey = "3b67b65a53c04170b602d2d1a7e6096f"
|
||
const qweatherHost = "https://pb4nmv4qnu.re.qweatherapi.com"
|
||
|
||
type City struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Adm2 string `json:"adm2"`
|
||
Adm1 string `json:"adm1"`
|
||
}
|
||
|
||
var cities = []City{
|
||
{"101010100", "北京", "北京", "北京"},
|
||
{"101020100", "上海", "上海", "上海"},
|
||
{"101280101", "广州", "广州", "广东"},
|
||
{"101280601", "深圳", "深圳", "广东"},
|
||
{"101200101", "武汉", "武汉", "湖北"},
|
||
{"101110101", "西安", "西安", "陕西"},
|
||
{"101210101", "杭州", "杭州", "浙江"},
|
||
{"101190101", "南京", "南京", "江苏"},
|
||
{"101070101", "沈阳", "沈阳", "辽宁"},
|
||
{"101050101", "哈尔滨", "哈尔滨", "黑龙江"},
|
||
{"101250101", "长沙", "长沙", "湖南"},
|
||
{"101270101", "成都", "成都", "四川"},
|
||
}
|
||
|
||
var defaultCity = City{"101200101", "武汉", "武汉", "湖北"}
|
||
|
||
type hourlyItem struct {
|
||
Time string `json:"time"`
|
||
Temp string `json:"temp"`
|
||
Icon string `json:"icon"`
|
||
}
|
||
|
||
type dailyItem struct {
|
||
Date string `json:"date"`
|
||
Icon string `json:"icon"`
|
||
TempMin string `json:"tempMin"`
|
||
TempMax string `json:"tempMax"`
|
||
}
|
||
|
||
var weatherIcons = map[string]string{
|
||
"晴": "☀️", "多云": "⛅", "阴": "☁️",
|
||
"雨": "🌧️", "雪": "❄️", "雷": "⛈️",
|
||
"雾": "🌫️", "霾": "😷", "风": "💨",
|
||
}
|
||
|
||
func getWeatherIcon(text string) string {
|
||
for k, v := range weatherIcons {
|
||
if strings.Contains(text, k) {
|
||
return v
|
||
}
|
||
}
|
||
return "🌤️"
|
||
}
|
||
|
||
func httpGet(url string) ([]byte, error) {
|
||
resp, err := httpClient.Get(url)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
return io.ReadAll(resp.Body)
|
||
}
|
||
|
||
// getLocation 自动定位,返回城市
|
||
func getLocation() City {
|
||
// 1. 尝试 ipip.net
|
||
if city := locateByIPIP(); city != nil {
|
||
return *city
|
||
}
|
||
// 2. 尝试 QWeather GeoAPI
|
||
if city := locateByQWeather(); city != nil {
|
||
return *city
|
||
}
|
||
// 3. fallback
|
||
log.Println("⚠️ 所有定位失败,使用默认城市:", defaultCity.Name)
|
||
return defaultCity
|
||
}
|
||
|
||
func locateByIPIP() *City {
|
||
data, err := httpGet("https://myip.ipip.net")
|
||
if err != nil {
|
||
log.Println("ipip.net 不可用:", err)
|
||
return nil
|
||
}
|
||
text := string(data)
|
||
log.Println("ipip.net 响应:", text)
|
||
re := regexp.MustCompile(`来自于[::]\s*(.+?)\s*$`)
|
||
m := re.FindStringSubmatch(text)
|
||
if m == nil {
|
||
return nil
|
||
}
|
||
parts := strings.Fields(m[1])
|
||
cityName := parts[0]
|
||
if len(parts) >= 3 {
|
||
cityName = parts[2]
|
||
} else if len(parts) >= 2 {
|
||
cityName = parts[1]
|
||
}
|
||
for _, c := range cities {
|
||
if c.Name == cityName {
|
||
log.Println("✅ ipip.net 匹配:", c.Name)
|
||
return &c
|
||
}
|
||
}
|
||
log.Println("ipip.net 城市", cityName, "不在预置列表中")
|
||
return nil
|
||
}
|
||
|
||
func locateByQWeather() *City {
|
||
data, err := httpGet("https://myip.ipip.net")
|
||
if err == nil {
|
||
re := regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+)`)
|
||
if m := re.FindStringSubmatch(string(data)); m != nil {
|
||
return geoLookup(m[1])
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func geoLookup(ip string) *City {
|
||
url := fmt.Sprintf(qweatherHost + "/v2/city/lookup?location=%s&key=%s", ip, qweatherKey)
|
||
data, err := httpGet(url)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
var resp struct {
|
||
Code string `json:"code"`
|
||
Location []struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Adm2 string `json:"adm2"`
|
||
Adm1 string `json:"adm1"`
|
||
} `json:"location"`
|
||
}
|
||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" || len(resp.Location) == 0 {
|
||
return nil
|
||
}
|
||
loc := resp.Location[0]
|
||
// 优先匹配预置列表
|
||
for _, c := range cities {
|
||
if c.Name == loc.Name {
|
||
return &c
|
||
}
|
||
}
|
||
return &City{loc.ID, loc.Name, loc.Adm2, loc.Adm1}
|
||
}
|
||
|
||
// getCurrentCity 获取当前城市(手动优先 > 自动定位)
|
||
func getCurrentCity() City {
|
||
cfg := loadConfig()
|
||
if cfg.City != "" {
|
||
for _, c := range cities {
|
||
if c.ID == cfg.City {
|
||
return c
|
||
}
|
||
}
|
||
}
|
||
return getLocation()
|
||
}
|
||
|
||
// fetchAndPushWeather 获取天气并推送给 JS
|
||
func fetchAndPushWeather(city City) {
|
||
type weatherData struct {
|
||
Current string `json:"current"`
|
||
Hourly []hourlyItem `json:"hourly"`
|
||
Daily []dailyItem `json:"daily"`
|
||
City string `json:"city"`
|
||
}
|
||
|
||
wd := weatherData{}
|
||
cityName := city.Name
|
||
if city.Adm2 != city.Name {
|
||
cityName = city.Adm2 + " " + city.Name
|
||
}
|
||
wd.City = cityName
|
||
|
||
// 当前天气
|
||
if now := fetchCurrentWeather(city.ID); now != nil {
|
||
icon := getWeatherIcon(now.Text)
|
||
wd.Current = fmt.Sprintf("📍 %s %s %s %s°C", cityName, icon, now.Text, now.Temp)
|
||
} else {
|
||
wd.Current = fmt.Sprintf("📍 %s ☀️ 晴 24°C", cityName)
|
||
}
|
||
|
||
// 24h 预报
|
||
wd.Hourly = fetchHourlyForecast(city.ID)
|
||
|
||
// 7d 预报
|
||
wd.Daily = fetchDailyForecast(city.ID)
|
||
|
||
jsonData, _ := json.Marshal(wd)
|
||
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, string(jsonData)))
|
||
log.Println("✅ 天气已推送:", wd.Current)
|
||
}
|
||
|
||
type currentWeather struct {
|
||
Text string `json:"text"`
|
||
Temp string `json:"temp"`
|
||
}
|
||
|
||
func fetchCurrentWeather(cityID string) *currentWeather {
|
||
url := fmt.Sprintf(qweatherHost + "/v7/weather/now?location=%s&key=%s", cityID, qweatherKey)
|
||
data, err := httpGet(url)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
var resp struct {
|
||
Code string `json:"code"`
|
||
Now struct {
|
||
Text string `json:"text"`
|
||
Temp string `json:"temp"`
|
||
} `json:"now"`
|
||
}
|
||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
|
||
return nil
|
||
}
|
||
return ¤tWeather{resp.Now.Text, resp.Now.Temp}
|
||
}
|
||
|
||
func fetchHourlyForecast(cityID string) []hourlyItem {
|
||
url := fmt.Sprintf(qweatherHost + "/v7/weather/24h?location=%s&key=%s", cityID, qweatherKey)
|
||
data, err := httpGet(url)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
var resp struct {
|
||
Code string `json:"code"`
|
||
Hourly []struct {
|
||
FxTime string `json:"fxTime"`
|
||
Temp string `json:"temp"`
|
||
Text string `json:"text"`
|
||
} `json:"hourly"`
|
||
}
|
||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
|
||
return nil
|
||
}
|
||
var result []hourlyItem
|
||
for i, h := range resp.Hourly {
|
||
if i >= 8 {
|
||
break
|
||
}
|
||
// fxTime: "2026-05-25T14:00+08:00" -> "14:00"
|
||
t := h.FxTime
|
||
if idx := strings.Index(t, "T"); idx >= 0 {
|
||
t = t[idx+1:]
|
||
if len(t) >= 5 {
|
||
t = t[:5]
|
||
}
|
||
}
|
||
result = append(result, hourlyItem{t, h.Temp + "°", getWeatherIcon(h.Text)})
|
||
}
|
||
return result
|
||
}
|
||
|
||
func fetchDailyForecast(cityID string) []dailyItem {
|
||
url := fmt.Sprintf(qweatherHost + "/v7/weather/7d?location=%s&key=%s", cityID, qweatherKey)
|
||
data, err := httpGet(url)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
var resp struct {
|
||
Code string `json:"code"`
|
||
Daily []struct {
|
||
FxDate string `json:"fxDate"`
|
||
TextDay string `json:"textDay"`
|
||
TempMin string `json:"tempMin"`
|
||
TempMax string `json:"tempMax"`
|
||
} `json:"daily"`
|
||
}
|
||
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
|
||
return nil
|
||
}
|
||
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
|
||
today := time.Now().Format("2006-01-02")
|
||
var result []dailyItem
|
||
for _, d := range resp.Daily {
|
||
date := d.FxDate
|
||
if date == today {
|
||
date = "今天"
|
||
} else {
|
||
if t, err := time.Parse("2006-01-02", date); err == nil {
|
||
date = weekdays[t.Weekday()]
|
||
}
|
||
}
|
||
result = append(result, dailyItem{date, getWeatherIcon(d.TextDay), d.TempMin, d.TempMax})
|
||
}
|
||
return result
|
||
}
|
||
|
||
func weatherLoop() {
|
||
// 首次延迟等 WebView 初始化
|
||
time.Sleep(3 * time.Second)
|
||
city := getCurrentCity()
|
||
log.Println("🌤️ 初始城市:", city.Name)
|
||
fetchAndPushWeather(city)
|
||
|
||
ticker := time.NewTicker(10 * time.Minute)
|
||
for range ticker.C {
|
||
city := getCurrentCity()
|
||
fetchAndPushWeather(city)
|
||
}
|
||
}
|
||
|
||
// ── Systray ──
|
||
|
||
var zodiacItems []*systray.MenuItem
|
||
var cityItems []*systray.MenuItem
|
||
|
||
func onSystrayReady() {
|
||
systray.SetIcon(generateIcon())
|
||
systray.SetTooltip("动态壁纸引擎")
|
||
|
||
mPause := systray.AddMenuItem("暂停", "暂停/继续")
|
||
systray.AddSeparator()
|
||
|
||
// 星座子菜单
|
||
mZodiac := systray.AddMenuItem("星座设置", "")
|
||
zodiacs := []string{
|
||
"白羊座", "金牛座", "双子座",
|
||
"巨蟹座", "狮子座", "处女座",
|
||
"天秤座", "天蝎座", "射手座",
|
||
"摩羯座", "水瓶座", "双鱼座",
|
||
}
|
||
cfg := loadConfig()
|
||
for _, z := range zodiacs {
|
||
item := mZodiac.AddSubMenuItem(z, z)
|
||
if z == cfg.Zodiac {
|
||
item.Check()
|
||
}
|
||
zodiacItems = append(zodiacItems, item)
|
||
}
|
||
|
||
systray.AddSeparator()
|
||
|
||
// 城市子菜单
|
||
mCity := systray.AddMenuItem("城市设置", "")
|
||
for _, c := range cities {
|
||
item := mCity.AddSubMenuItem(c.Name, c.Adm1+" "+c.Name)
|
||
if cfg.City == c.ID {
|
||
item.Check()
|
||
}
|
||
cityItems = append(cityItems, item)
|
||
}
|
||
|
||
systray.AddSeparator()
|
||
mQuit := systray.AddMenuItem("退出", "退出程序")
|
||
|
||
// 星座选择监听
|
||
for i, item := range zodiacItems {
|
||
go func(idx int, mi *systray.MenuItem) {
|
||
name := zodiacs[idx]
|
||
log.Printf("⭐ 星座监听启动: %s", name)
|
||
for {
|
||
<-mi.ClickedCh
|
||
log.Printf("⭐ 星座点击: %s", name)
|
||
cfg := loadConfig()
|
||
cfg.Zodiac = name
|
||
saveConfig(cfg)
|
||
for _, it := range zodiacItems {
|
||
it.Uncheck()
|
||
}
|
||
mi.Check()
|
||
evalJS(fmt.Sprintf(`window.userZodiac = %q; if(window.updateTime) updateTime();`, name))
|
||
}
|
||
}(i, item)
|
||
}
|
||
|
||
// 城市选择监听
|
||
for i, item := range cityItems {
|
||
go func(idx int, mi *systray.MenuItem) {
|
||
log.Printf("🏙️ 城市监听启动: %s", cities[idx].Name)
|
||
for {
|
||
<-mi.ClickedCh
|
||
city := cities[idx]
|
||
log.Printf("🏙️ 城市点击: %s", city.Name)
|
||
cfg := loadConfig()
|
||
cfg.City = city.ID
|
||
saveConfig(cfg)
|
||
for _, it := range cityItems {
|
||
it.Uncheck()
|
||
}
|
||
mi.Check()
|
||
go fetchAndPushWeather(city)
|
||
}
|
||
}(i, item)
|
||
}
|
||
|
||
// 暂停
|
||
go func() {
|
||
log.Println("暂停监听启动")
|
||
for {
|
||
<-mPause.ClickedCh
|
||
newVal := 1 - atomic.LoadInt32(&paused)
|
||
atomic.StoreInt32(&paused, newVal)
|
||
isPaused := newVal == 1
|
||
log.Printf("暂停切换: paused=%v", isPaused)
|
||
if isPaused {
|
||
mPause.SetTitle("继续")
|
||
} else {
|
||
mPause.SetTitle("暂停")
|
||
}
|
||
evalJS("if(window.setPaused) setPaused(" + strconv.FormatBool(isPaused) + ")")
|
||
}
|
||
}()
|
||
|
||
|
||
// 退出
|
||
go func() {
|
||
<-mQuit.ClickedCh
|
||
os.Exit(0)
|
||
}()
|
||
|
||
go startWebView()
|
||
go weatherLoop()
|
||
}
|
||
|
||
// ── WebView ──
|
||
|
||
func startWebView() {
|
||
runtime.LockOSThread()
|
||
|
||
workerw := findWorkerW()
|
||
if workerw == 0 {
|
||
log.Fatal("WorkerW not found")
|
||
}
|
||
|
||
screenW, screenH := getScreenSize()
|
||
log.Printf("Screen: %dx%d", screenW, screenH)
|
||
|
||
wv = webview2.NewWithOptions(webview2.WebViewOptions{
|
||
AutoFocus: false,
|
||
WindowOptions: webview2.WindowOptions{
|
||
Title: "动态壁纸",
|
||
Width: uint(screenW),
|
||
Height: uint(screenH),
|
||
},
|
||
})
|
||
if wv == nil {
|
||
log.Fatal("WebView2 create failed")
|
||
}
|
||
|
||
htmlData, err := fs.ReadFile("wallpaper.html")
|
||
if err != nil {
|
||
log.Fatal("读取 wallpaper.html 失败:", err)
|
||
}
|
||
|
||
wv.Bind("setZodiacFromGo", func(zodiac string) error {
|
||
cfg := loadConfig()
|
||
cfg.Zodiac = zodiac
|
||
return saveConfig(cfg)
|
||
})
|
||
|
||
wv.SetHtml(string(htmlData))
|
||
time.Sleep(1 * time.Second)
|
||
|
||
wvHwnd = uintptr(wv.Window())
|
||
procSetWindowLongPtrW.Call(wvHwnd, uintptr(0xFFFFFFF0), uintptr(0x80000000|0x10000000|0x02000000))
|
||
procShowWindow.Call(wvHwnd, 5)
|
||
procMoveWindow.Call(wvHwnd, uintptr(^uint(0)), uintptr(^uint(0)), uintptr(screenW+2), uintptr(screenH+2), 1)
|
||
log.Printf("✅ 壁纸已嵌入: HWND=0x%x, %dx%d", wvHwnd, screenW, screenH)
|
||
|
||
// 注入配置
|
||
go func() {
|
||
time.Sleep(500 * time.Millisecond)
|
||
cfg := loadConfig()
|
||
evalJS(fmt.Sprintf(`window.userZodiac = %q;`, cfg.Zodiac))
|
||
log.Printf("✅ 配置已注入: zodiac=%s", cfg.Zodiac)
|
||
}()
|
||
|
||
go fullscreenMonitor()
|
||
|
||
// 延迟嵌入 WorkerW
|
||
go func() {
|
||
time.Sleep(3 * time.Second)
|
||
workerw := findWorkerW()
|
||
if workerw != 0 {
|
||
oldParent, _, _ := procSetParent.Call(wvHwnd, workerw)
|
||
log.Printf("✅ SetParent: 0x%x -> 0x%x (old=0x%x)", wvHwnd, workerw, oldParent)
|
||
procMoveWindow.Call(wvHwnd, uintptr(^uint(0)), uintptr(^uint(0)), uintptr(screenW+2), uintptr(screenH+2), 1)
|
||
}
|
||
}()
|
||
|
||
// 消息循环
|
||
type msg struct {
|
||
hwnd uintptr
|
||
message uint32
|
||
wParam uintptr
|
||
lParam uintptr
|
||
time uint32
|
||
pt struct{ x, y int32 }
|
||
}
|
||
var m msg
|
||
log.Println("启动自定义消息循环...")
|
||
for {
|
||
ret, _, _ := procGetMessageW.Call(
|
||
uintptr(unsafe.Pointer(&m)),
|
||
0, 0, 0,
|
||
)
|
||
if ret == 0 {
|
||
break
|
||
}
|
||
if m.message == wmEvalJS {
|
||
log.Println("收到 wmEvalJS, drain queue...")
|
||
for {
|
||
select {
|
||
case js := <-jsQueue:
|
||
wv.Eval(js)
|
||
default:
|
||
goto nextMsg
|
||
}
|
||
}
|
||
}
|
||
nextMsg:
|
||
procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
|
||
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
|
||
}
|
||
}
|
||
|
||
func findWorkerW() uintptr {
|
||
progman, _, _ := procFindWindowW.Call(
|
||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Progman"))), 0)
|
||
if progman == 0 {
|
||
return 0
|
||
}
|
||
var result uintptr
|
||
procSendMessageTimeoutW.Call(progman, 0x052C, 0, 0, 0x0000, 1000, uintptr(unsafe.Pointer(&result)))
|
||
shellDefView, _, _ := procFindWindowExW.Call(progman, 0,
|
||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("SHELLDLL_DefView"))), 0)
|
||
workerwAfterShell, _, _ := procFindWindowExW.Call(progman, shellDefView,
|
||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW"))), 0)
|
||
if workerwAfterShell != 0 {
|
||
return workerwAfterShell
|
||
}
|
||
ww, _, _ := procFindWindowExW.Call(progman, 0,
|
||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW"))), 0)
|
||
return ww
|
||
}
|
||
|
||
func getScreenSize() (int32, int32) {
|
||
w, _, _ := procGetSystemMetrics.Call(0)
|
||
h, _, _ := procGetSystemMetrics.Call(1)
|
||
return int32(w), int32(h)
|
||
}
|
||
|
||
func fullscreenMonitor() {
|
||
type rect struct{ Left, Top, Right, Bottom int32 }
|
||
var lastState string
|
||
for {
|
||
if atomic.LoadInt32(&paused) == 0 && wv != nil {
|
||
fg, _, _ := procGetForegroundWindow.Call()
|
||
if fg != 0 {
|
||
var r rect
|
||
procGetWindowRect.Call(fg, uintptr(unsafe.Pointer(&r)))
|
||
screenW, screenH := getScreenSize()
|
||
isFull := (r.Right-r.Left >= screenW) && (r.Bottom-r.Top >= screenH)
|
||
state := strconv.FormatBool(isFull)
|
||
if state != lastState {
|
||
lastState = state
|
||
evalJS("if(window.setFullscreen) setFullscreen(" + state + ")")
|
||
}
|
||
}
|
||
}
|
||
time.Sleep(2 * time.Second)
|
||
}
|
||
}
|