MyTray 是 xly_flutter_package 中的系统托盘功能模块,作为全局服务提供完整的桌面应用托盘解决方案。
- 完全可选: 不需要托盘功能时完全不涉及
- 简洁强制: 只有iconPath是必需的,其他都可选
- 全局服务: 继承GetxService,享受全局生命周期管理
- 统一访问: 通过MyTray.to进行所有操作
- 职责分离: MyApp专注应用框架,MyTray专注托盘功能
class MyTray extends GetxService with TrayListener, WindowListener {
final String iconPath; // 必需:托盘图标路径
final String? tooltip; // 可选:悬停提示,默认不显示
final List<MyTrayMenuItem>? menuItems; // 可选:右键菜单,默认无菜单
MyTray({
required this.iconPath,
this.tooltip,
this.menuItems,
});
static MyTray get to => Get.find();
}void main() async {
await MyApp.initialize(
designSize: const Size(800, 600),
routes: [...],
// 推荐:使用tray参数(简化配置)
tray: MyTray(
iconPath: "assets/icon.png", // 可选,为空时自动使用默认图标
tooltip: "My App",
menuItems: [
MyTrayMenuItem(key: 'show', label: '显示', onTap: () => MyTray.to.pop()),
MyTrayMenuItem.separator(),
MyTrayMenuItem(key: 'settings', label: '设置', enabled: false), // 禁用项
MyTrayMenuItem(key: 'exit', label: '退出', onTap: () => exit(0)),
],
),
);
}void main() async {
await MyApp.initialize(
designSize: const Size(800, 600),
routes: [...],
services: [
// 最简使用
MyService<MyTray>(
service: () => MyTray(iconPath: "assets/icon.png"),
permanent: true,
),
// 完整配置
MyService<MyTray>(
service: () => MyTray(
iconPath: "assets/icon.png",
tooltip: "My App",
menuItems: [
MyTrayMenuItem(label: '显示', onTap: () => MyTray.to.pop()),
MyTrayMenuItem.separator(),
MyTrayMenuItem(label: '退出', onTap: () => exit(0)),
],
),
permanent: true,
),
],
);
}final tray = MyTray.to;
// 基础操作
tray.hide(); // 隐藏窗口到托盘
tray.pop(); // 从托盘恢复窗口
tray.notify("标题", "消息"); // 显示通知
// 动态配置
tray.setTooltip("新提示"); // 更新提示文本
tray.setContextMenu([...]); // 更新右键菜单
tray.setIcon("new_icon.png"); // 动态设置图标
// 菜单项禁用状态控制
tray.setMenuItemEnabled('settings', true); // 启用设置菜单
bool isEnabled = tray.getMenuItemEnabled('settings'); // 查询状态
tray.toggleMenuItemEnabled('settings'); // 切换状态
// 托盘点击行为控制
tray.setToggleOnClick(true); // 开启切换语义
bool isToggleMode = tray.getToggleOnClick(); // 查询当前状态
tray.toggleToggleOnClick(); // 切换开关状态- tooltip: 不显示(
null) - menuItems: 无菜单(
null) - hideTaskBarIcon: 隐藏任务栏图标(
true) - toggleOnClick: 开启切换语义(
true) - 图标验证: 构造时检查文件存在性,不存在则抛异常
- 平台检查: 非桌面平台自动跳过初始化
- 生命周期: 随应用启动/关闭自动管理
- MyApp.initialize: 完全不涉及托盘逻辑,只负责服务注册
- 职责分离: MyApp专注应用框架,MyTray专注托盘功能
- 可选性: 不注册MyTray服务,应用完全正常运行
- 托盘图标显示和管理
- 窗口最小化到托盘
- 从托盘恢复窗口
- 托盘右键菜单
- 托盘通知消息
- 动态图标切换
- 自定义托盘菜单
- 动态图标和提示更新
- 菜单项启用/禁用状态控制(原生灰色样式)
- 平台特定行为适配
MyTray (GetxService) ← 改为继承GetxService
├── TrayListener (mixin)
├── WindowListener (mixin)
└── 状态管理 (Rx变量)
MyTray: 主要的托盘管理器类(GetxService)MyTrayMenuItem: 托盘菜单项配置(支持key、enabled等属性)MyTrayNotification: 通知消息处理(已移除,简化设计)MyTrayIconConfig: 图标配置选项(已移除,不再需要)MyTrayWrapper: 托盘包装器组件
重要变更:已完全移除 MyApp.initialize 中的托盘相关参数:
(已移除)enableTray(已移除)trayIcon(已移除)trayTooltip(已删除文件)MyTrayWrapper
唯一初始化方式:现在托盘功能完全通过 MyService<MyTray> 管理,避免了架构重复和参数冲突。
enum MyTrayNotificationType {
info,
warning,
error,
success,
}- 隐式操作:系统自动触发的操作(如窗口事件监听)绝不显示消息
- 显式操作:用户明确点击按钮触发的操作,由调用方决定是否显示消息
- 示例演示:在 example 中演示如何在用户操作时显示适当的反馈消息
- 显式指定:用户必须明确指定图标路径,不提供自动检测
- 平台适配:Windows 使用 .ico 格式,其他平台使用 .png 格式
- 动态切换:支持运行时通过setIcon方法动态更换图标
MyTray.hide()方法本身不显示任何消息- 消息显示完全由调用方控制
- 图标路径必须由用户显式指定
- 在 example 中展示最佳实践:用户明确操作时显示托盘气泡通知
// 在 main.dart 中 - 最简单的配置
await MyApp.initialize(
appName: "示例App",
services: [
MyService<MyTray>(
service: () => MyTray(iconPath: "assets/tray_icon.png"),
),
],
);
// 在页面中使用
final myTray = MyTray.to;
// 隐藏到托盘按钮 - 明确显示消息
MyButton(
text: "隐藏到托盘",
onPressed: () {
myTray.hide();
// 用户明确操作时显示托盘气泡通知
myTray.notify("已隐藏到托盘", "点击托盘图标可恢复窗口");
},
);
// 静默隐藏(不显示消息)
MyButton(
text: "静默隐藏",
onPressed: () => myTray.hide(), // 不显示任何消息
);// 带tooltip和菜单的完整配置
await MyApp.initialize(
appName: "示例App",
services: [
MyService<MyTray>(
service: () => MyTray(
iconPath: "assets/tray_icon.png",
tooltip: "我的应用",
menuItems: [
MyTrayMenuItem(key: 'show', label: '显示主窗口', onTap: () => MyTray.to.pop()),
MyTrayMenuItem.separator(),
MyTrayMenuItem(key: 'settings', label: '设置', enabled: false), // 禁用项示例
MyTrayMenuItem(key: 'exit', label: '退出应用', onTap: () => exit(0)),
],
),
),
],
);
// 动态更换图标
final myTray = MyTray.to;
myTray.setIcon("assets/tray_busy.png"); // 切换到忙碌图标
myTray.setIcon("assets/tray_normal.png"); // 切换回正常图标MyTray 支持菜单项的启用/禁用状态控制,使用系统原生的禁用样式和行为。
- 原生支持:通过
tray_manager重导出的menu_base包,使用MenuItem.disabled属性 - 系统样式:禁用项显示为系统标准的灰色样式,在系统层面不可点击
- 跨平台:Windows/macOS 完全支持,Linux 依桌面环境而定
MyTrayMenuItem(
key: 'settings', // 推荐提供稳定的key
label: '设置',
enabled: false, // 设置为禁用状态
onTap: () => openSettings(),
),final myTray = MyTray.to;
// 查询状态
bool isEnabled = myTray.getMenuItemEnabled('settings');
// 设置状态
await myTray.setMenuItemEnabled('settings', true); // 启用
await myTray.setMenuItemEnabled('settings', false); // 禁用
// 切换状态
await myTray.toggleMenuItemEnabled('settings');// 初始化时设置菜单
MyTray(
menuItems: [
MyTrayMenuItem(key: 'show', label: '显示窗口', onTap: () => MyTray.to.pop()),
MyTrayMenuItem.separator(),
MyTrayMenuItem(key: 'sync', label: '同步数据', enabled: false), // 初始禁用
MyTrayMenuItem(key: 'settings', label: '设置', onTap: () => openSettings()),
MyTrayMenuItem.separator(),
MyTrayMenuItem(key: 'exit', label: '退出', onTap: () => exit(0)),
],
),
// 运行时根据状态动态启用/禁用
void onSyncStatusChanged(bool canSync) async {
await MyTray.to.setMenuItemEnabled('sync', canSync);
}- key 的重要性:推荐为每个菜单项提供唯一的
key,便于后续查找和修改 - 子菜单支持:子菜单项同样支持禁用功能
- 平台差异:Linux 下的视觉效果可能因桌面环境而异
- 性能考虑:动态修改会重建整个菜单,频繁操作时需注意性能
🆕 最佳实践:使用 XLY 图标生成工具确保托盘图标与应用图标完全一致:
# 一键生成所有平台图标,包括托盘图标资产
# 注:无论在什么操作系统上运行,都会为项目中存在的所有平台生成图标
dart run xly:generate icon="assets/app_icon.png"自动化优势:
- ✅ 完美一致:托盘图标与应用窗口图标使用相同源文件
- ✅ 跨启动方式一致:VSCode F5 调试和从应用目录运行表现完全相同
- ✅ 自动资产管理:自动复制图标到 Flutter assets 并更新 pubspec.yaml
- ✅ 零配置使用:MyTray 不传 iconPath 参数即可自动使用一致图标
工作原理:
- 图标生成工具为每个平台生成标准应用图标(如
windows/runner/resources/app_icon.ico) - 同时自动复制到 Flutter assets 统一路径(
assets/_auto_tray_icon_gen/) - MyTray 默认解析逻辑会优先使用这些资产,确保运行时一致性
📁 生成的图标文件说明:
assets/_auto_tray_icon_gen/app_icon.ico:Windows 专用,多尺寸支持assets/_auto_tray_icon_gen/app_icon.png:macOS/Linux 专用,系统推荐格式- MyTray 根据运行平台自动选择正确格式,两种文件并存无冗余问题
- 托盘图标:重新构建后立即更新 ✅
- 应用图标(任务栏/文件管理器):因系统缓存显示旧图标 ❌
- 解决方案:重启系统清除图标缓存
- Windows: 推荐使用
.ico格式,支持多尺寸 - macOS: 推荐使用
.png格式,系统会自动处理模板图标 - Linux: 推荐使用
.png格式 - 路径: 必须是相对于项目根目录的有效路径
- 尺寸: 32x32像素
// 显式指定图标路径(覆盖默认行为)
MyTray(
iconPath: "assets/custom_tray_icon.png",
// ...
)final myTray = MyTray.to;
// 根据应用状态动态切换图标
myTray.setIcon("assets/tray_normal.png"); // 正常状态
myTray.setIcon("assets/tray_warning.png"); // 警告状态
myTray.setIcon("assets/tray_error.png"); // 错误状态
myTray.setIcon("assets/tray_busy.png"); // 忙碌状态- Windows: 完整支持所有功能,使用 .ico 格式图标
- macOS: 支持基本功能,遵循 macOS 设计规范,使用 .png 格式图标
- Linux: 支持基本功能,使用 .png 格式图标
- 移动端: 优雅降级(不支持托盘时不报错)
- 问题描述:在Windows平台上,右键菜单在点击菜单项后可能不会自动关闭
- 相关Issue:tray_manager#63
- 临时解决方案:MyTray组件实现了一个workaround,通过重置菜单来强制关闭
- 影响:可能会有轻微的视觉闪烁,但确保菜单能正确关闭
- 状态:等待tray_manager官方修复
- Windows:推荐使用
.ico格式,支持多尺寸 - macOS:推荐使用
.png格式,系统会自动处理模板图标 - Linux:推荐使用
.png格式
- 当前实现使用
setToolTip方式显示通知 - 真正的系统通知需要额外的通知权限和API
- 添加
tray_manager依赖 - 创建基础文件结构
- 实现
MyTray核心功能(继承GetxService)
- 实现托盘菜单系统
- 实现通知功能
- 集成窗口管理
- 添加动态图标切换
- 集成到
MyApp初始化流程 - 创建 example 使用示例
- 添加文档和测试
- 更新 README 和 CHANGELOG
- 继承GetxService: MyTray改为继承GetxService而非GetxController,确保全局生命周期管理
- 简化构造函数: 只保留iconPath(可选)、tooltip(可选)、menuItems(可选)三个参数
- 移除复杂配置: 删除MyTrayIconConfig和MyTrayWrapper,简化API设计
- 职责分离: MyApp.initialize只负责服务注册,不涉及托盘具体逻辑
- 架构清理: 完全移除MyApp.initialize中的enableTray、trayIcon、trayTooltip参数,避免重复配置
- 完全可选: 不需要托盘时完全不涉及,零影响
- 架构清晰: 唯一初始化方式,避免参数重复和配置冲突
- 简洁易用: iconPath可选(自动使用默认图标),其他参数都有合理默认值
- 使用体验: 一行注册,MyTray.to全功能访问
- 生命周期: 作为GetxService自动管理,永不被意外释放
// 初始化(唯一方式)
MyService<MyTray>(
service: () => MyTray(
// iconPath: "assets/icon.png", // 可选:为空时自动使用默认应用图标
tooltip: "我的应用", // 可选:悬停提示
menuItems: [ // 可选:右键菜单
MyTrayMenuItem(label: '显示', onTap: () => MyTray.to.pop()),
MyTrayMenuItem.separator(),
MyTrayMenuItem(label: '退出', onTap: () => exit(0)),
],
),
);
// 使用
MyTray.to.hide();
MyTray.to.pop();
MyTray.to.notify("标题", "消息");
MyTray.to.setIcon("new_icon.png");
MyTray.to.setMenuItemEnabled("settings", true);架构优势:
- ✅ 唯一初始化方式,避免配置冲突
- ✅ 完全可选,不需要时零影响
- ✅ 参数简洁,智能默认值
- ✅ 架构清晰,职责单一
这个设计达到了最佳平衡:简洁易用、功能强大、架构清晰、生态一致。