之前做了一个网页天气可视化项目 web-weather,用 Canvas 2D + CSS 实现了雨、雪、雾、晴、阴、冰雹、沙尘暴七种天气效果,接了 Open-Meteo 的免费 API 做自动天气,还加了声音系统、世界地图选点、沉浸模式。做完之后我把它挂在副屏上当动态壁纸用了好一阵子——浏览器全屏,切到第二块屏幕,效果确实不错。
但"浏览器全屏"这个方案有几个很实际的问题:
• 浏览器窗口始终在最前面,Alt+Tab 会切到它
• 不小心点到桌面,壁纸就被盖住了
• 每次开机要手动打开浏览器、输地址、全屏,繁琐
• 任务栏上多一个浏览器窗口,看着碍眼
既然这个网页本身就是为"长期挂着"设计的,为什么不干脆做一个桌面应用,把它真正嵌到 Windows 桌面壁纸层?于是就有了这个项目——Weather Wallpaper。 本项目基于 Lively 进行二次开发,聚焦网页天气桌面场景。
在线体验网页版:https://weather.anhejin.cn
演示视频:https://files.anhejin.cn/videos/demo.mp4
网页天气可视化做了什么
先简单回顾一下网页端的实现。详细的技术细节写过两篇文章:
• 做了一个网页天气可视化
• 做了一个网页天气可视化 2
第一篇主要解决"怎么让天气看起来像那么回事"。Canvas 2D 画粒子——雨滴是上窄下宽的梯形,风力通过 windOffset 让它斜着落;雪花飘落带正弦波摇摆,落到导航栏上会堆积,温度高了会融化;雾天是 Canvas 雾团 + CSS 烟雾纹理 + backdrop-filter 三层叠出来的;晴天有镜头光斑,位置跟太阳成镜像关系;闪电用递归算法做分叉。数据结构用 SoA + Float32Array 做连续内存访问,绘制按透明度分档批量 fill 减少 draw call,雾气纹理预渲染到离屏 Canvas。接了 Open-Meteo 的 API,浏览器定位拿经纬度,WMO 天气代码映射到视觉效果,每十分钟刷新一次。
第二篇是后续更新。声音系统全面重做——雷声用 Web Audio API 合成了七层(crack、sub-bass boom、re-strike、rolling rumble、延迟重击、远方余震),每层都是独立的音频节点链路,预生成多个变体缓存在内存里,每次打雷听起来都不完全一样。雨声、风声、雪声也不是 MP3 循环,而是多层滤波噪声合成,音量和滤波器频率跟天气参数实时联动。风向还会影响左右声道——风从右边吹,雨声就偏右耳。
新增了冰雹和沙尘暴两种极端天气。冰雹有加速下落、旋转、落地弹跳碎片、地面碎冰堆积和融化。沙尘暴分三层——350 颗不规则多边形沙粒、5 种手绘碎屑(树枝、树叶、石子、塑料片、泥块)、60 个贴地滚动的沙团——再盖一层全屏沙色蒙版。
世界地图让你点地球上任意一个点就能看那里的天气,带 24 小时和 7 天预报时间线。沉浸模式把所有 UI 藏起来,全屏只剩天气本身。
这些东西加在一起,网页端已经是一个可以打开、选地方、听天气、挂一整天的"天气体验器"了。但它始终跑在浏览器里。
为什么要做桌面应用
原因只有一个:我想让它真的变成壁纸。
不是"看起来像壁纸",是嵌到桌面图标后面那一层,和 Windows 原生壁纸占同一个位置。桌面图标在上面,网页天气在下面,任务栏正常显示,Alt+Tab 看不到它。开机自动启动,托盘常驻,不占任务栏位置。
这种需求在动态壁纸领域并不新鲜。Wallpaper Engine 早就做了,Lively Wallpaper 也是开源方案。但我不需要一个通用的动态壁纸引擎——我只需要把一个特定的网页嵌到桌面上。所以我参考了 Lively 的核心思路,用 .NET 9 + WPF + WinForms + WebView2 写了一个极简版本。
核心原理:WorkerW 窗口层
Windows 的桌面壁纸机制其实很有意思。桌面本身是一个叫 Progman(Program Manager)的窗口,桌面图标放在它的子窗口 SHELLDLL_DefView 里的 SysListView32 中。
要把自己的窗口塞到壁纸层,关键是让 Windows 创建一个 WorkerW 窗口。方法是向 Progman 发送一个未公开的消息 0x052C:- NativeMethods.SendMessageTimeout(
- _progman,
- 0x052C,
- new IntPtr(0xD),
- new IntPtr(0x1),
- NativeMethods.SendMessageTimeoutFlags.SMTO_NORMAL,
- 1000,
- out _);
复制代码 发完之后,Windows 会在 Progman 下面创建一个 WorkerW 窗口。这时候窗口层级变成了:- Progman
- ├── SHELLDLL_DefView (桌面图标)
- └── WorkerW (壁纸层)
复制代码 把你的窗口 SetParent 到 WorkerW 里,它就在图标后面了。这就是 Lively 和大多数动态壁纸软件用的方案。
但 Windows 11 和较新的 Windows 10 有一个区别——Progman 带了 WS_EX_NOREDIRECTIONBITMAP 标志,这意味着桌面使用了"raised desktop"模式。这种模式下 WorkerW 是 Progman 的子窗口,需要用不同的策略来 attach:- if (_isRaisedDesktop)
- {
- // 设置 WS_CHILD 样式
- style |= NativeMethods.WS_CHILD;
- // 设置 WS_EX_LAYERED 并设满不透明度
- NativeMethods.SetLayeredWindowAttributes(hwnd, 0, 255, NativeMethods.LWA_ALPHA);
- // 父窗口设为 Progman
- NativeMethods.SetParent(hwnd, _progman);
- // Z 序:壁纸在 SHELLDLL_DefView 下面
- NativeMethods.SetWindowPos(hwnd, _shellDLL_DefView, 0, 0, 0, 0, flags);
- }
- else
- {
- // 经典模式:直接挂到 WorkerW
- NativeMethods.SetParent(hwnd, _workerW);
- }
复制代码 多显示器的情况也要处理。壁纸窗口需要定位到目标显示器的坐标上,reparent 之前用 MapWindowPoints 把屏幕坐标转成父窗口内的相对坐标,不然多屏环境下位置会算错。
WebView2:把网页塞进桌面
有了壁纸层,下一步是把网页塞进去。这里用的是 Microsoft 的 WebView2——基于 Chromium 的嵌入式浏览器控件。
架构上是这样的:一个隐藏的 WinForms 窗口(WallpaperHostForm)里放一个 WebView2 控件,WebView2 加载网页,然后把整个 WinForms 窗口 SetParent 到桌面的 WorkerW 层。- _hostForm = new WallpaperHostForm();
- _hostForm.Size = new System.Drawing.Size((int)monitor.Bounds.Width, (int)monitor.Bounds.Height);
- _hostForm.Show();
- // 初始化 WebView2
- await InitializeWebView2(audioEnabled);
- // 把宿主窗口嵌到桌面
- _desktopWorker.SetWallpaper(_hostForm.Handle, monitor.Bounds);
复制代码 WallpaperHostForm 做了几个特殊处理:
- • FormBorderStyle.None:无边框
- • ShowInTaskbar = false:不在任务栏显示
- • WS_EX_TOOLWINDOW:Alt+Tab 里隐藏
- • WS_EX_NOACTIVATE:不抢焦点
- • ShowWithoutActivation = true:显示时不激活
- • 初始位置 (-32000, -32000):先放到屏幕外避免闪烁
WebView2 初始化时有一个关键参数——--autoplay-policy=no-user-gesture-required。网页天气的声音系统依赖 Web Audio API,而浏览器默认的 autoplay policy 要求用户先交互才能播放声音。桌面壁纸没有"用户点一下"这个动作,所以必须绕过这个限制:- var options = new CoreWebView2EnvironmentOptions("--autoplay-policy=no-user-gesture-required");
- var env = await CoreWebView2Environment.CreateAsync(null, userDataPath, options);
- await _webView.EnsureCoreWebView2Async(env);
复制代码 同时还关掉了右键菜单、状态栏、缩放控制、新窗口弹出、下载——这些在壁纸场景里都是不需要的。
鼠标事件转发:让壁纸可以交互
壁纸嵌到桌面之后,有一个问题:你点桌面,鼠标消息会被 SHELLDLL_DefView(桌面图标层)吃掉,WebView2 收不到。
但网页天气是有交互的——世界地图可以点,控制面板可以拖,沉浸模式可以切换。如果壁纸完全不能交互,就少了很多有意思的玩法。
所以我用了一个全局低级鼠标钩子(WH_MOUSE_LL),在鼠标消息到达任何窗口之前拦截它,判断当前前台窗口是不是桌面(Progman、WorkerW 或 SHELLDLL_DefView),如果是,就把鼠标消息转发给 WebView2:- _hookId = NativeMethods.SetWindowsHookEx(
- NativeMethods.WH_MOUSE_LL,
- _hookProc,
- NativeMethods.GetModuleHandle(null),
- 0);
复制代码 转发的时候不能简单地 PostMessage 给 WebView2 的顶层窗口——WebView2 内部有多层子窗口(Chrome 的渲染进程窗口),鼠标消息需要送到最深层的子窗口才能被正确处理。所以用了一个递归查找最深层可见子窗口的方法:- private static IntPtr GetDeepestChild(IntPtr parent, NativeMethods.POINT screenPt)
- {
- var clientPt = screenPt;
- NativeMethods.ScreenToClient(parent, ref clientPt);
- var child = NativeMethods.ChildWindowFromPointEx(parent, clientPt, CWP_SKIPINVISIBLE);
- if (child == IntPtr.Zero || child == parent)
- return parent;
- return GetDeepestChild(child, screenPt);
- }
复制代码 目前支持鼠标移动、左键、右键和滚轮。键盘输入没做转发——壁纸场景下基本用不到。
应用层面的完整度
技术原理之外,作为一个"真的会拿来用"的应用,还需要处理很多日常细节:
系统托盘:应用没有主窗口,常驻系统托盘。双击打开设置,右键菜单支持设置、停止、重启和退出。关闭设置窗口只是隐藏,不会退出程序。
设置持久化:网页地址、目标显示器、声音开关、开机启动这些配置保存在 %LOCALAPPDATA%\WeatherWallpaper\settings.json,启动时自动读取并恢复上次的壁纸。
多显示器支持:通过 EnumDisplayMonitors 枚举所有显示器,设置界面可以选择把壁纸显示在哪块屏幕上。
开机启动:写入注册表 HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run,标准的 Windows 启动项方案。
单实例控制:用 Mutex 防止重复启动,多开会弹提示。
默认网页地址就是 https://weather.anhejin.cn——也就是之前做的那个网页天气可视化。打开应用,点"应用壁纸",桌面就变成了实时天气。它会自动获取你的地理位置,显示你所在城市的当前天气,每十分钟刷新一次。雨天桌面上飘雨,雪天看见雪花堆积,雾天整个桌面朦胧一片,打雷的时候闪电劈下来还有声音。
当然,因为本质上是嵌入一个网页,所以理论上任何网页都能当壁纸用——只是这个项目本身就是为天气可视化设计的。
技术栈
• .NET 9(net9.0-windows10.0.18362.0)
• WPF:主程序入口和设置窗口
• WinForms:WebView2 的宿主窗体(WebView2 的 WinForms 版本更适合做底层窗口嵌入)
• Microsoft.Web.WebView2:嵌入式 Chromium 浏览器
• Win32 API:WorkerW/Progman 操作、显示器枚举、鼠标钩子、输入转发
• Newtonsoft.Json:设置文件读写
整个项目代码量不大,核心逻辑集中在三个文件:
- • DesktopWorker.cs:桌面层管理,处理 WorkerW/Progman 窗口层级和 raised desktop 兼容
- • InputForwarder.cs:全局鼠标钩子和消息转发
- • WallpaperEngine.cs:WebView2 初始化、壁纸启停、总调度
从"做出来"到"用起来"
回看整个链路:网页端用 Canvas + CSS + Web Audio API 做出了有雨声有风声有闪电有积雪的天气可视化,桌面端用 WebView2 + Win32 API 把它嵌到了 Windows 壁纸层。两个项目各管各的——网页端只管视觉和声音效果,桌面端只管"怎么把一个网页变成壁纸"。
这种分层的好处是:网页端可以继续迭代新天气、新效果、新交互,桌面端完全不用改。只要网页更新了,壁纸自动就变了。
现在我的家里电脑就是这么跑的——Weather Wallpaper 开机自启,副屏一直显示着当前天气。上海下雨的时候桌面也在下雨,晴天的时候有镜头光斑从太阳对侧漂过来。偶尔切到东京看看梅雨季,或者切到迪拜感受 45°C 的酷热。
开源地址:
- • 网页天气可视化:https://github.com/greywen/web-weather
- • 桌面壁纸应用:https://github.com/greywen/weather-wallpaper
- 本项目基于 Lively 进行二次开发,聚焦网页天气桌面场景。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |