From bb1574641fe5b501ad0afa60e4e48064f1ff6b3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com>
Date: Mon, 25 May 2026 19:54:32 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E5=A3=81=E7=BA=B8?=
=?UTF-8?q?=E5=88=87=E6=8D=A2=EF=BC=88=E4=B8=BB=E9=A2=98/=E6=9C=AC?=
=?UTF-8?q?=E5=9C=B0=E5=9B=BE=E7=89=87/Bing/=E7=BA=AF=E8=89=B2=E6=B8=90?=
=?UTF-8?q?=E5=8F=98=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
bing.go | 82 +++++++++
config.go | 56 +++++-
dialog.go | 119 +++++++++++++
main.go | 6 +-
systray.go | 162 +++++++++++++++--
wallpaper.go | 113 ++++++++++++
weather.go | 4 +-
web/overlay.html | 133 ++++++++++++++
web/themes/aurora.html | 77 +++++++++
web/themes/gradient.html | 17 ++
web/themes/particles.html | 60 +++++++
web/themes/starfield.html | 64 +++++++
web/wallpaper.html | 356 --------------------------------------
win32.go | 14 +-
14 files changed, 868 insertions(+), 395 deletions(-)
create mode 100644 bing.go
create mode 100644 dialog.go
create mode 100644 wallpaper.go
create mode 100644 web/overlay.html
create mode 100644 web/themes/aurora.html
create mode 100644 web/themes/gradient.html
create mode 100644 web/themes/particles.html
create mode 100644 web/themes/starfield.html
delete mode 100644 web/wallpaper.html
diff --git a/bing.go b/bing.go
new file mode 100644
index 0000000..b1ba3a4
--- /dev/null
+++ b/bing.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "encoding/json"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+const bingAPI = "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN"
+
+type bingResponse struct {
+ Images []struct {
+ URL string `json:"url"`
+ URLBase string `json:"urlbase"`
+ Copyright string `json:"copyright"`
+ } `json:"images"`
+}
+
+func fetchBingWallpaper() {
+ resp, err := httpClient.Get(bingAPI)
+ if err != nil {
+ log.Println("Bing API 请求失败:", err)
+ return
+ }
+ defer resp.Body.Close()
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return
+ }
+ var br bingResponse
+ if json.Unmarshal(data, &br) != nil || len(br.Images) == 0 {
+ log.Println("Bing API 解析失败")
+ return
+ }
+
+ imgURL := br.Images[0].URL
+ if !strings.HasPrefix(imgURL, "http") {
+ imgURL = "https://www.bing.com" + imgURL
+ }
+
+ imgResp, err := httpClient.Get(imgURL)
+ if err != nil {
+ log.Println("Bing 图片下载失败:", err)
+ return
+ }
+ defer imgResp.Body.Close()
+ imgData, err := io.ReadAll(imgResp.Body)
+ if err != nil {
+ return
+ }
+
+ bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg")
+ if err := os.WriteFile(bingPath, imgData, 0644); err != nil {
+ log.Println("Bing 壁纸缓存失败:", err)
+ return
+ }
+ log.Printf("Bing 壁纸已缓存: %s (%d bytes)", bingPath, len(imgData))
+
+ reloadWallpaper()
+}
+
+func bingWallpaperLoop() {
+ cfg := loadConfig()
+ if cfg.WallpaperType == WPBing {
+ bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg")
+ if _, err := os.Stat(bingPath); err != nil {
+ fetchBingWallpaper()
+ }
+ }
+
+ ticker := time.NewTicker(4 * time.Hour)
+ for range ticker.C {
+ cfg := loadConfig()
+ if cfg.WallpaperType == WPBing {
+ fetchBingWallpaper()
+ }
+ }
+}
diff --git a/config.go b/config.go
index 15c68c6..79024ad 100644
--- a/config.go
+++ b/config.go
@@ -8,32 +8,80 @@ import (
"image/color"
"image/png"
"os"
+ "path/filepath"
+)
+
+type WallpaperType string
+
+const (
+ WPTheme WallpaperType = "theme"
+ WPImage WallpaperType = "image"
+ WPBing WallpaperType = "bing"
+ WPColor WallpaperType = "color"
+)
+
+type ThemeName string
+
+const (
+ ThemeAurora ThemeName = "aurora"
+ ThemeStar ThemeName = "starfield"
+ ThemeGradient ThemeName = "gradient"
+ ThemeParticle ThemeName = "particles"
)
type Config struct {
- Zodiac string `json:"zodiac"`
- City string `json:"city"`
+ Zodiac string `json:"zodiac"`
+ City string `json:"city"`
+ WallpaperType WallpaperType `json:"wallpaperType"`
+ Theme ThemeName `json:"theme"`
+ ImagePath string `json:"imagePath"`
+ Color1 string `json:"color1"`
+ Color2 string `json:"color2"`
+ ColorGradient bool `json:"colorGradient"`
}
const defaultZodiac = "射手座"
var configPath string
+func configDir() string {
+ return filepath.Dir(configPath)
+}
+
func loadConfig() *Config {
data, err := os.ReadFile(configPath)
if err != nil {
- return &Config{Zodiac: defaultZodiac}
+ return defaultConfig()
}
var cfg Config
if json.Unmarshal(data, &cfg) != nil {
- return &Config{Zodiac: defaultZodiac}
+ return defaultConfig()
}
if cfg.Zodiac == "" {
cfg.Zodiac = defaultZodiac
}
+ if cfg.WallpaperType == "" {
+ cfg.WallpaperType = WPTheme
+ }
+ if cfg.Theme == "" {
+ cfg.Theme = ThemeAurora
+ }
+ if cfg.Color1 == "" {
+ cfg.Color1 = "#1a1a2e"
+ }
return &cfg
}
+func defaultConfig() *Config {
+ return &Config{
+ Zodiac: defaultZodiac,
+ WallpaperType: WPTheme,
+ Theme: ThemeAurora,
+ Color1: "#1a1a2e",
+ Color2: "#16213e",
+ }
+}
+
func saveConfig(cfg *Config) error {
data, _ := json.MarshalIndent(cfg, "", " ")
return os.WriteFile(configPath, data, 0644)
diff --git a/dialog.go b/dialog.go
new file mode 100644
index 0000000..138f099
--- /dev/null
+++ b/dialog.go
@@ -0,0 +1,119 @@
+package main
+
+import (
+ "fmt"
+ "unicode/utf16"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+)
+
+var (
+ comdlg32 = windows.NewLazySystemDLL("comdlg32.dll")
+ procGetOpenFileNameW = comdlg32.NewProc("GetOpenFileNameW")
+ procChooseColorW = comdlg32.NewProc("ChooseColorW")
+)
+
+func slicePtr(s interface{}) uintptr {
+ switch v := s.(type) {
+ case []uint16:
+ if len(v) == 0 { return 0 }
+ return uintptr(unsafe.Pointer(&v[0]))
+ case []uint32:
+ if len(v) == 0 { return 0 }
+ return uintptr(unsafe.Pointer(&v[0]))
+ }
+ return 0
+}
+
+func openFileDialog(owner uintptr) string {
+ type openFileName struct {
+ lStructSize uint32
+ hwndOwner uintptr
+ hInstance uintptr
+ lpstrFilter uintptr
+ lpstrCustomFilter uintptr
+ nMaxCustFilter uint32
+ nFilterIndex uint32
+ lpstrFile uintptr
+ nMaxFile uint32
+ lpstrFileTitle uintptr
+ nMaxFileTitle uint32
+ lpstrInitialDir uintptr
+ lpstrTitle uintptr
+ Flags uint32
+ nFileOffset uint16
+ nFileExtension uint16
+ lpstrDefExt uintptr
+ lCustData uintptr
+ lpfnHook uintptr
+ lpTemplateName uintptr
+ pvReserved uintptr
+ dwReserved uint32
+ FlagsEx uint32
+ }
+
+ // Filter: "Images\0*.jpg;*.png\0All\0*.*\0\0"
+ filterStr := "图片文件\x00*.jpg;*.jpeg;*.png;*.bmp;*.webp;*.gif\x00所有文件\x00*.*\x00\x00"
+ filterUTF16 := utf16.Encode([]rune(filterStr))
+
+ titleUTF16 := utf16.Encode([]rune("选择壁纸图片"))
+ fileBuf := make([]uint16, 260)
+
+ ofn := openFileName{
+ lStructSize: uint32(unsafe.Sizeof(openFileName{})),
+ hwndOwner: owner,
+ lpstrFilter: slicePtr(filterUTF16),
+ lpstrFile: slicePtr(fileBuf),
+ nMaxFile: 260,
+ lpstrTitle: slicePtr(titleUTF16),
+ Flags: 0x00001000 | 0x00000800 | 0x00000004,
+ }
+
+ ret, _, _ := procGetOpenFileNameW.Call(uintptr(unsafe.Pointer(&ofn)))
+ if ret == 0 {
+ return ""
+ }
+ return windows.UTF16ToString(fileBuf)
+}
+
+func colorPickerDialog(owner uintptr, initialColor string) string {
+ type chooseColor struct {
+ lStructSize uint32
+ hwndOwner uintptr
+ hInstance uintptr
+ rgbResult uint32
+ lpCustColors uintptr
+ Flags uint32
+ lCustData uintptr
+ lpfnHook uintptr
+ lpTemplateName uintptr
+ }
+
+ custColors := make([]uint32, 16)
+
+ var initRGB uint32
+ if len(initialColor) > 4 && initialColor[0] == '#' {
+ var r, g, b uint32
+ fmt.Sscanf(initialColor[1:], "%02x%02x%02x", &r, &g, &b)
+ initRGB = r | (g << 8) | (b << 16)
+ }
+
+ cc := chooseColor{
+ lStructSize: uint32(unsafe.Sizeof(chooseColor{})),
+ hwndOwner: owner,
+ rgbResult: initRGB,
+ lpCustColors: slicePtr(custColors),
+ Flags: 0x00000002,
+ }
+
+ ret, _, _ := procChooseColorW.Call(uintptr(unsafe.Pointer(&cc)))
+ if ret == 0 {
+ return ""
+ }
+
+ r := cc.rgbResult & 0xFF
+ g := (cc.rgbResult >> 8) & 0xFF
+ b := (cc.rgbResult >> 16) & 0xFF
+ return fmt.Sprintf("#%02x%02x%02x", r, g, b)
+}
diff --git a/main.go b/main.go
index d819ddf..adfc7af 100644
--- a/main.go
+++ b/main.go
@@ -25,9 +25,9 @@ func main() {
defer windows.CloseHandle(mutex)
exePath, _ := os.Executable()
- configDir := filepath.Join(filepath.Dir(exePath), "config")
- os.MkdirAll(configDir, 0755)
- configPath = filepath.Join(configDir, "settings.json")
+ cfgDir := filepath.Join(filepath.Dir(exePath), "config")
+ os.MkdirAll(cfgDir, 0755)
+ configPath = filepath.Join(cfgDir, "settings.json")
procSetProcessDPIAware.Call()
systray.Run(onSystrayReady, nil)
}
diff --git a/systray.go b/systray.go
index c9c3756..c8b39cc 100644
--- a/systray.go
+++ b/systray.go
@@ -16,14 +16,44 @@ import (
var zodiacItems []*systray.MenuItem
var cityItems []*systray.MenuItem
+var themeItems []*systray.MenuItem
+
+var themeNames = []struct {
+ Name ThemeName
+ Label string
+}{
+ {ThemeAurora, "极光"},
+ {ThemeStar, "星空"},
+ {ThemeGradient, "渐变"},
+ {ThemeParticle, "粒子"},
+}
func onSystrayReady() {
systray.SetIcon(generateIcon())
systray.SetTooltip("动态壁纸引擎")
+ cfg := loadConfig()
+
mPause := systray.AddMenuItem("暂停", "暂停/继续")
systray.AddSeparator()
+ // 壁纸主题
+ mTheme := systray.AddMenuItem("壁纸主题", "")
+ for _, t := range themeNames {
+ item := mTheme.AddSubMenuItem(t.Label, t.Label)
+ if cfg.WallpaperType == WPTheme && cfg.Theme == t.Name {
+ item.Check()
+ }
+ themeItems = append(themeItems, item)
+ }
+ mLocalImage := systray.AddMenuItem("本地图片", "选择本地图片作为壁纸")
+ mBingDaily := systray.AddMenuItem("Bing 每日壁纸", "使用 Bing 每日壁纸")
+ mSolidColor := systray.AddMenuItem("纯色壁纸", "选择纯色壁纸")
+ mGradientColor := systray.AddMenuItem("渐变壁纸", "选择渐变壁纸")
+
+ systray.AddSeparator()
+
+ // 星座
mZodiac := systray.AddMenuItem("星座设置", "")
zodiacs := []string{
"白羊座", "金牛座", "双子座",
@@ -31,7 +61,6 @@ func onSystrayReady() {
"天秤座", "天蝎座", "射手座",
"摩羯座", "水瓶座", "双鱼座",
}
- cfg := loadConfig()
for _, z := range zodiacs {
item := mZodiac.AddSubMenuItem(z, z)
if z == cfg.Zodiac {
@@ -42,6 +71,7 @@ func onSystrayReady() {
systray.AddSeparator()
+ // 城市
mCity := systray.AddMenuItem("城市设置", "")
for _, c := range cities {
item := mCity.AddSubMenuItem(c.Name, c.Adm1+" "+c.Name)
@@ -54,14 +84,113 @@ func onSystrayReady() {
systray.AddSeparator()
mQuit := systray.AddMenuItem("退出", "退出程序")
+ // 主题切换监听
+ for i, item := range themeItems {
+ go func(idx int, mi *systray.MenuItem) {
+ for {
+ <-mi.ClickedCh
+ cfg := loadConfig()
+ cfg.WallpaperType = WPTheme
+ cfg.Theme = themeNames[idx].Name
+ saveConfig(cfg)
+ for _, it := range themeItems {
+ it.Uncheck()
+ }
+ mi.Check()
+ log.Printf("主题切换: %s", themeNames[idx].Label)
+ reloadWallpaper()
+ }
+ }(i, item)
+ }
+
+ // 本地图片
+ go func() {
+ for {
+ <-mLocalImage.ClickedCh
+ path := openFileDialog(wvHwnd)
+ if path == "" {
+ continue
+ }
+ cfg := loadConfig()
+ cfg.WallpaperType = WPImage
+ cfg.ImagePath = path
+ saveConfig(cfg)
+ for _, it := range themeItems {
+ it.Uncheck()
+ }
+ log.Printf("本地图片: %s", path)
+ reloadWallpaper()
+ }
+ }()
+
+ // Bing 每日
+ go func() {
+ for {
+ <-mBingDaily.ClickedCh
+ cfg := loadConfig()
+ cfg.WallpaperType = WPBing
+ saveConfig(cfg)
+ for _, it := range themeItems {
+ it.Uncheck()
+ }
+ log.Println("切换 Bing 壁纸")
+ go fetchBingWallpaper()
+ }
+ }()
+
+ // 纯色壁纸
+ go func() {
+ for {
+ <-mSolidColor.ClickedCh
+ color := colorPickerDialog(wvHwnd, "")
+ if color == "" {
+ continue
+ }
+ cfg := loadConfig()
+ cfg.WallpaperType = WPColor
+ cfg.Color1 = color
+ cfg.ColorGradient = false
+ saveConfig(cfg)
+ for _, it := range themeItems {
+ it.Uncheck()
+ }
+ log.Printf("纯色壁纸: %s", color)
+ reloadWallpaper()
+ }
+ }()
+
+ // 渐变壁纸
+ go func() {
+ for {
+ <-mGradientColor.ClickedCh
+ c1 := colorPickerDialog(wvHwnd, "")
+ if c1 == "" {
+ continue
+ }
+ c2 := colorPickerDialog(wvHwnd, "")
+ if c2 == "" {
+ c2 = "#16213e"
+ }
+ cfg := loadConfig()
+ cfg.WallpaperType = WPColor
+ cfg.Color1 = c1
+ cfg.Color2 = c2
+ cfg.ColorGradient = true
+ saveConfig(cfg)
+ for _, it := range themeItems {
+ it.Uncheck()
+ }
+ log.Printf("渐变壁纸: %s -> %s", c1, c2)
+ reloadWallpaper()
+ }
+ }()
+
// 星座选择监听
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)
@@ -77,11 +206,9 @@ func onSystrayReady() {
// 城市选择监听
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)
@@ -96,13 +223,11 @@ func onSystrayReady() {
// 暂停
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 {
@@ -120,6 +245,7 @@ func onSystrayReady() {
go startWebView()
go weatherLoop()
+ go bingWallpaperLoop()
}
func startWebView() {
@@ -145,18 +271,13 @@ func startWebView() {
log.Fatal("WebView2 create failed")
}
- htmlData, err := fs.ReadFile("web/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))
+ wv.SetHtml(buildWallpaperHTML(loadConfig()))
time.Sleep(1 * time.Second)
wvHwnd = uintptr(wv.Window())
@@ -165,17 +286,14 @@ func startWebView() {
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()
@@ -186,7 +304,6 @@ func startWebView() {
}
}()
- // 消息循环
type msg struct {
hwnd uintptr
message uint32
@@ -196,7 +313,6 @@ func startWebView() {
pt struct{ x, y int32 }
}
var m msg
- log.Println("启动自定义消息循环...")
for {
ret, _, _ := procGetMessageW.Call(
uintptr(unsafe.Pointer(&m)),
@@ -215,7 +331,15 @@ func startWebView() {
}
}
}
- nextMsg:
+ if m.message == wmSetHtml {
+ select {
+ case html := <-htmlQueue:
+ wv.SetHtml(html)
+ default:
+ }
+ goto nextMsg
+ }
+nextMsg:
procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
}
diff --git a/wallpaper.go b/wallpaper.go
new file mode 100644
index 0000000..1dbc59d
--- /dev/null
+++ b/wallpaper.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+ _ "embed"
+ "encoding/base64"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+//go:embed web/overlay.html
+var overlayHTML string
+
+//go:embed web/themes/aurora.html
+var themeAurora string
+
+//go:embed web/themes/starfield.html
+var themeStarfield string
+
+//go:embed web/themes/gradient.html
+var themeGradient string
+
+//go:embed web/themes/particles.html
+var themeParticles string
+
+var themeMap = map[ThemeName]string{
+ ThemeAurora: themeAurora,
+ ThemeStar: themeStarfield,
+ ThemeGradient: themeGradient,
+ ThemeParticle: themeParticles,
+}
+
+func buildWallpaperHTML(cfg *Config) string {
+ var bg string
+
+ switch cfg.WallpaperType {
+ case WPTheme:
+ if t, ok := themeMap[cfg.Theme]; ok {
+ bg = t
+ } else {
+ bg = themeAurora
+ }
+ case WPImage:
+ if cfg.ImagePath != "" {
+ src := imageToDataURI(cfg.ImagePath)
+ if src != "" {
+ bg = fmt.Sprintf(``, src)
+ }
+ }
+ case WPBing:
+ bingPath := filepath.Join(configDir(), "bing_wallpaper.jpg")
+ if _, err := os.Stat(bingPath); err == nil {
+ src := imageToDataURI(bingPath)
+ if src != "" {
+ bg = fmt.Sprintf(`
`, src)
+ }
+ }
+ case WPColor:
+ if cfg.ColorGradient && cfg.Color2 != "" {
+ bg = fmt.Sprintf(`