Skip to content

开发桌面壁纸 ​

在 Widget.js 中,桌面壁纸是组件包里的一个专用网页。你负责页面、动画和数据;宿主负责把页面放到 Windows 桌面图标下面,并管理启动恢复、关闭和异常重试。

目前只支持 Windows 主屏,一次运行一张壁纸。壁纸接收鼠标移动和左键事件,不接收右键、滚轮或键盘,不获得输入焦点。需要输入文字的功能应放到独立设置窗口。

准备项目 ​

可以在已有 Vite 组件项目中添加壁纸,也可以先按创建项目建立组件包。React、Vue 或普通 TypeScript 页面都可以,下面用 React 演示最小接入。

需要包含 WallpaperWidget、WallpaperApi 的 core SDK,以及支持壁纸的 Widget.js 宿主。不要仅根据 npm 的 latest 判断这些接口是否已发布:工作区开发可以使用 "@widget-js/core": "workspace:*",先构建本地 core;独立项目需使用实际包含这些导出的发布版本。

项目需要 @widget-js/vite-plugin-widget。在现有 React Vite 配置中加入插件:

ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import widget from '@widget-js/vite-plugin-widget'

export default defineConfig({
  base: './',
  plugins: [react(), widget()],
})

插件生成 widget.json,开发时向本地 Widget.js 注册组件包。先启动宿主,再启动开发服务,查看注册结果。相对资源路径有助于 ZIP 离线加载;不要把开发服务器绝对地址写进页面资源。

声明专用壁纸 ​

在项目根目录创建或扩展 widget.package.ts:

ts
import { WallpaperWidget, WidgetPackage } from '@widget-js/core'

export const wallpaper = new WallpaperWidget({
  name: 'cn.example.wallpaper.ripple',
  lang: 'zh-CN',
  title: { 'zh-CN': '鼠标互动壁纸' },
  description: { 'zh-CN': '随鼠标移动的桌面场景' },
  path: '/wallpaper',
  previewImage: 'preview.png',
  keywords: [],
  configPage: {
    path: '/settings',
    width: 720,
    height: 780,
    minWidth: 380,
    minHeight: 560,
    resizable: true,
    frame: true,
    titleBarStyle: 'hidden',
    transparent: false,
  },
})

export default new WidgetPackage({
  name: 'cn.example.wallpaper',
  author: '你的名字',
  title: { 'zh-CN': '互动壁纸包' },
  description: { 'zh-CN': '我的网页壁纸' },
  entry: 'index.html',
  hash: true,
  widgets: [wallpaper],
})

将预览图放到 public/preview.png。没有设置页面时省略 configPage。WallpaperWidget 自动指定专用部署模式,不需要网格宽高,也不会显示普通组件的“桌面”“悬浮”按钮。

页面路由与就绪 ​

声明中的 path 必须能被前端路由识别。使用 hash 路由,让离线包也能打开 index.html#/wallpaper 和 index.html#/settings。不要给壁纸页面套用依赖普通组件 IPC 的初始化逻辑。

下面是一个没有外部资源的最小页面。它安装鼠标监听器后才报告就绪,离开页面时清理监听器;普通浏览器也可预览。

tsx
// src/WallpaperPage.tsx
import { useEffect, useRef } from 'react'
import { ElectronUtils, WallpaperApi } from '@widget-js/core'

export function WallpaperPage() {
  const dot = useRef<HTMLDivElement>(null)
  const sent = useRef(false)

  useEffect(() => {
    const move = (event: MouseEvent) => {
      if (dot.current) {
        dot.current.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`
      }
    }
    window.addEventListener('mousemove', move)
    if (ElectronUtils.hasElectronApi() && !sent.current) {
      sent.current = true
      void WallpaperApi.ready().catch(console.error)
    }
    return () => window.removeEventListener('mousemove', move)
  }, [])

  return <main style={{ position: 'fixed', inset: 0, background: '#102532' }}>
    <div ref={dot} style={{ position: 'absolute', width: 24, height: 24,
      borderRadius: '50%', background: '#78e4d0', pointerEvents: 'none' }} />
  </main>
}

例如已有 react-router-dom 时,在入口使用:

tsx
<HashRouter>
  <Routes>
    <Route path="/wallpaper" element={<WallpaperPage />} />
    <Route path="/settings" element={<SettingsPage />} />
  </Routes>
</HashRouter>

HashRouter、Routes、Route 从 react-router-dom 导入,两个页面从项目自己的文件导入;SettingsPage 是你实现的设置表单。

有图片、视频或模型时,应等必要资源可用后再调用 ready(),不能仅以组件已挂载作为完成。资源失败时可以显示已加载的降级背景后报告就绪;无法展示内容则不要报告成功。页面 ready 加上原生桌面挂载成功后才算启用完成,整个准备阶段最多 30 秒,每次恢复都会重新计时。

设置页面 ​

壁纸里的按钮可以通过 WallpaperApi.openConfigPage() 打开已声明的设置页面。设置窗口可以正常输入、滚动,并与壁纸使用同一组件包的持久化分区。卡片在组件可配置时也会显示“设置”。

ts
// 壁纸里的设置按钮
await WallpaperApi.openConfigPage()

// 独立设置页保存
localStorage.setItem('my-wallpaper-settings', JSON.stringify({ color: '#78e4d0' }))

// 壁纸监听另一窗口的修改;卸载时移除监听器
const onStorage = (event: StorageEvent) => {
  if (event.key === 'my-wallpaper-settings') {
    // 校验 JSON 后更新画面;首次加载也要主动读取一次。
  }
}
window.addEventListener('storage', onStorage)
// window.removeEventListener('storage', onStorage)

相同分区仍要求相同网页来源才能共享 localStorage。浏览器预览与 Electron 不共享数据,localhost 与 127.0.0.1 也不是同一来源。

优先使用 configPage,不要同时依赖旧 configPagePath 查询参数。两者并存时优先 configPage.path,窗口选项直接来自 configPage。titleBarStyle: 'hidden' 用于隐藏原生标题栏;页面需要自行提供可拖动区域及窗口控制按钮。修改窗口尺寸、边框或标题栏后,等待组件包重新注册,关闭旧设置窗口再打开,已有窗口不会自动重建。

环境变化与性能 ​

宿主提供 locked、suspended、displayOff 和 fullscreen 通知,不强制暂停或静音。页面自己决定降低帧率、暂停视频或停止动画;恢复时重新读取当前时间,避免收入、时钟等内容停留在旧值。

ts
let receivedEvent = false
const apply = (state: { locked: boolean; suspended: boolean; displayOff: boolean; fullscreen: boolean }) => {
  const shouldPause = state.locked || state.suspended || state.displayOff || state.fullscreen
  // 根据 shouldPause 控制自己的动画和音频。
}
const unsubscribe = WallpaperApi.onEnvironmentChanged(state => {
  receivedEvent = true
  apply(state)
})
const initial = await WallpaperApi.getEnvironment()
if (!receivedEvent) apply(initial)
// 页面卸载时调用 unsubscribe();异步初始化也应检查页面是否已卸载。

使用 requestAnimationFrame 驱动动画,闲置时停止无用绘制。使用当前窗口尺寸布局,并处理 resize;不要硬编码主屏分辨率。主屏切换时宿主尽量调整同一窗口保留页面状态,异常恢复重建页面时则需要从持久化存储恢复业务数据。

调试、打包与启用 ​

  1. 启动支持壁纸的 Widget.js,再运行项目的开发脚本,确认插件报告组件包注册成功。
  2. 在组件卡片点击“壁纸”。加载完成后出现“关闭”,运行中可用 DevTools 检查控制台、网络和资源加载。可配置的组件显示“设置”。
  3. 使用项目构建脚本生成包含 widget.json、index.html、预览图和所有资源的离线包。标准插件支持 ZIP 生成;不要只打包 HTML,也不要把额外父目录包在 ZIP 根层。
  4. 通过管理端 WidgetPackageApi.install(zipPath) 安装 ZIP,再在卡片启用。壁纸页自身无权部署、关闭或打开 DevTools。

初次加载失败或超时会通知用户并露出系统壁纸,不自动重试初次启用。运行中异常最多尝试恢复 3 次,稳定运行 60 秒后清零计数。正常退出宿主保留恢复选择;主动关闭或进入失败终态则清除选择。替换失败不会回滚旧网页壁纸。

验收与排查 ​

  • 浏览器预览正常不等于桌面挂载成功:实际检查图标优先、左键与鼠标移动、桌面右键菜单、设置窗口输入和 DevTools。
  • 超过 30 秒:检查入口路由、资源路径、控制台异常,以及是否在必要资源就绪后调用了 ready()。
  • 修改设置窗口样式未生效:检查生成的 widget.json 和宿主注册结果,关闭旧设置窗口再打开。
  • 壁纸不响应输入框或滚轮:这是壁纸模式的输入限制,应改用设置窗口。
  • 测试主屏切换、分辨率变化、锁屏恢复和应用重启;系统桌面的正常点击、框选可能与壁纸点击同时发生。

完整接口见 WallpaperApi,Windows 宿主架构和历史验证边界见设计与实现记录。仓库内的 packages/@widget-js/wallpaper 是蟹堡王壁纸接入示例。