likes
comments
collection
share

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

作者站长头像
站长
· 阅读数 10

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

 系统托盘功能对于桌面程序是很重要的,我们可以在系统托盘对程序进行简单控制,也可以在托盘向用户展示一些信息。我们这次基于system_tray插件实现windows系统下的可爱小太阳系统托盘功能,并使它像QQ来新消息时那样进行图标闪烁。效果图如下所示:

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

system_tray插件简介

system_tray支持在Windows、macOS 和 Linux 上实现系统托盘功能,与之相似的还有一个tray_manager插件,但是这个最近更新时间比较久远,所以我们选择system_tray插件进行实现。下边是官方展示的例子:

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

开始使用system_tray

 首先实例化一个SystemTray对象:final SystemTray systemTray = SystemTray();,为了方便后边使用,我们直接定义全局变量,后边都只针对这个对象进行操作。

 接着我们定义initSystemTray函数,在这个函数中进行托盘功能的设置。 由于windwos与linux支持的图标格式不同,我们要根据操作系统类型选择对应的图标String path = Platform.isWindows ? 'assets/sun.ico' : 'assets/app_icon.png';,如果操作系统是windwos的话,就选icon格式图标,其他操作系统的情况就使用png格式就可以了。

system_tray初始化

 上边实例化的SystemTray对象,接着我们就要使用它的initSystemTray方法来进行初始化。传入的参数分别是图标路径,macos下显示的title,以及鼠标悬浮时的提示文字。

required String iconPath:托盘显示的图标路径
String? title:名称(仅macos)
String? toolTip:鼠标悬停到托盘图标时的提示信息(仅win、macos)
  // We first init the systray menu
  await systemTray.initSystemTray(
    title: "system tray",
    toolTip: "system tray toolTip",
    iconPath: path,
  );

 这样就可以实现一个简单的系统托盘图标啦:

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

右键菜单栏

 只显示图标肯定是不够的,我们还可以实现右键点击图标展示菜单栏,实现一些简单的功能交互。实现也是实例化菜单对象final Menu _menu = Menu();,接着使用菜单对象的buildFrom方法进行设置。

await _menu.buildFrom([
  ...
]);

 buildFrom中放置的是以下几种菜单项:

MenuItemLabel:文本菜单

required String label:文本标签
String? image:选项前的图片
String? name:名称
bool enabled:是否启用,默认为true
void Function(MenuItem)? onClicked:菜单被点击事件,返回一个MenuItem对象,可以获取设置image、name等属性


MenuItemCheckbox:复选框菜单

required String label:显示文本标签
String? image:选项前的图片
String? name:名称
bool enabled:是否启用,默认为true
bool checked:是否选中,默认为false
void Function(MenuItem)? onClicked:菜单被点击事件,返回一个MenuItem对象,可以获取设置image、name等属性


SubMenu:二级菜单

required String label:文本标签
required List<MenuItem> children:子菜单列表
String? image:选项前的图片


MenuSeparator:菜单分隔符

 我们实现两级的菜单,功能包括设置标题、设置提示文字,闪烁图标、复选框菜单、显示/隐藏窗口等功能:

  // create context menu
  final Menu menu = Menu();
  await menu.buildFrom([
    SubMenu(
      label: "submenu",
      children: [
        MenuItemLabel(
          label: 'setTitle',
          onClicked: (menuItem) {
            systemTray.setTitle("setTitle");
          },
        ),
        。。。
        MenuItemLabel(
          label: 'Start flash tray icon',
          image: "assets/icons8-sun-30.BMP",
          onClicked: (menuItem) {
            debugPrint("Start flash tray icon");
            _timer ??= Timer.periodic(
              const Duration(milliseconds: 500),
              (timer) {
                _toogleTrayIcon = !_toogleTrayIcon;
                systemTray.setImage(_toogleTrayIcon ? "" : "assets/sun.ico");
              },
            );
          },
        ),
      ],
    ),
    MenuItemCheckbox(
      label: "MenuItemCheckbox",
      name: "MenuItemCheckbox name",
      checked: true,
      onClicked: (menuItem) async {
        await menuItem.setCheck(!menuItem.checked);
      },
    ),
    MenuSeparator(),
    。。。
    MenuItemLabel(
      label: 'Exit',
      onClicked: (menuItem) => appWindow.close(),
    ),
  ]);

 别忘了使用await systemTray.setContextMenu(menu);函数使之生效。效果图如下:

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

右键菜单栏实现图标闪烁功能

 我们在右键菜单中实现一个点击开始/停止图标闪烁的功能,开始菜单中的onClicked函数中添加一个计时器,每隔一段时间触发systemTray.setImage函数重新设置托盘图标路径,路径在空白和正确的icons路径之间切换,实现图标闪烁的效果。


     MenuItemLabel(
          label: 'Stop flash tray icon',
          image: "assets/sun.ico",
          onClicked: (menuItem) {
            debugPrint("Stop flash tray icon");
            _timer?.cancel();
            _timer = null;
            systemTray.setImage("assets/sun.ico");
          },
        ),
        MenuItemLabel(
          label: 'Start flash tray icon',
          image: "assets/icons8-sun-30.BMP",
          onClicked: (menuItem) {
            debugPrint("Start flash tray icon");
            _timer ??= Timer.periodic(
              const Duration(milliseconds: 500),
              (timer) {
                _toogleTrayIcon = !_toogleTrayIcon;
                systemTray.setImage(_toogleTrayIcon ? "" : "assets/sun.ico");
              },
            );
          },
        ),

 效果图:

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

窗口操作和鼠标事件监控

 实例化final AppWindow appWindow = AppWindow();对象,windows窗口的一些操作依赖这个对象,我们可以利用它实现窗口显示和隐藏。结合鼠标事件使用。appWindow.show()可以显示窗口,appWindow.hide()隐藏窗口,appWindow.close()关闭窗口。

 鼠标注册事件需要用到registerSystemTrayEventHandler方法,有以下几种鼠标情况可以检测(Linux不适用),默认设置点击图标显示窗口。此事件是针对托盘图标而言,右键菜单鼠标事件与这个鼠标事件无关、菜单有自己的事件监控。

kSystemTrayEventClick:左击
kSystemTrayEventRightClick:右击
kSystemTrayEventDoubleClick:双击(仅win)
 // handle system tray event
  systemTray.registerSystemTrayEventHandler((eventName) {
    debugPrint("eventName: $eventName");
    if (eventName == kSystemTrayEventClick) {
      Platform.isWindows ? appWindow.show() : systemTray.popUpContextMenu();
    } else if (eventName == kSystemTrayEventRightClick) {
      Platform.isWindows ? systemTray.popUpContextMenu() : appWindow.show();
    }
  });

添加动画,控制按钮

 为了美观,我们选择一个lottie动画放到程序中,让我们的编程不那么枯燥,增添几分乐趣。同时我们添加两个文字按钮,在主界面控制图标是否闪烁。

     TextButton(
      onPressed: () {
        iconFlash();
      },
      child: const Text("Start Desktop Tray Icon Spalsh"),
    ),
    TextButton(
      onPressed: () {
        closeIconFlash();
      },
      child: const Text("Close Desktop Tray Icon Spalsh"),
    ),

 iconFlash()函数与closeIconFlash()函数分别控制了图标闪烁的开始和停止,与右键菜单控制图标闪烁的函数内容和原理基本相同,具体实现可以在完整代码中查看。  效果图:

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

完整代码

 最终效果图:

flutter desktop tray 桌面端实现可爱的小太阳系统托盘

import 'package:system_tray/system_tray.dart';

final SystemTray systemTray = SystemTray();

Future<void> initSystemTray() async {
  Timer? _timer;
  bool _toogleTrayIcon = true;

  String path = Platform.isWindows ? 'assets/sun.ico' : 'assets/app_icon.png';
  final AppWindow appWindow = AppWindow();

  await systemTray.initSystemTray(
    title: "system tray",
    toolTip: "system tray toolTip",
    iconPath: path,
  );

  // create context menu
  final Menu menu = Menu();
  await menu.buildFrom([
    SubMenu(
      label: "submenu",
      children: [
        MenuItemLabel(
          label: 'setTitle',
          onClicked: (menuItem) {
            systemTray.setTitle("setTitle");
          },
        ),
        MenuItemLabel(
          label: 'setImage null',
          onClicked: (menuItem) {
            systemTray.setImage("");
          },
        ),
        MenuItemLabel(
          label: 'setToolTip',
          onClicked: (menuItem) {
            systemTray.setToolTip("setToolTip");
          },
        ),
        MenuItemLabel(
          label: 'Stop flash tray icon',
          image: "assets/sun.ico",
          onClicked: (menuItem) {
            debugPrint("Stop flash tray icon");
            _timer?.cancel();
            _timer = null;
            systemTray.setImage("assets/sun.ico");
          },
        ),
        MenuItemLabel(
          label: 'Start flash tray icon',
          image: "assets/icons8-sun-30.BMP",
          onClicked: (menuItem) {
            debugPrint("Start flash tray icon");
            _timer ??= Timer.periodic(
              const Duration(milliseconds: 500),
              (timer) {
                _toogleTrayIcon = !_toogleTrayIcon;
                systemTray.setImage(_toogleTrayIcon ? "" : "assets/sun.ico");
              },
            );
          },
        ),
      ],
    ),
    MenuItemCheckbox(
      label: "MenuItemCheckbox",
      name: "MenuItemCheckbox name",
      checked: true,
      onClicked: (menuItem) async {
        await menuItem.setCheck(!menuItem.checked);
      },
    ),
    MenuSeparator(),
    MenuItemLabel(
      label: 'Show',
      image: "assets/icons8-sun-30.BMP",
      onClicked: (menuItem) => appWindow.show(),
    ),
    MenuItemLabel(
      label: 'Hide',
      onClicked: (menuItem) => appWindow.hide(),
    ),
    MenuItemLabel(
      label: 'Exit',
      onClicked: (menuItem) => appWindow.close(),
    ),
  ]);

  // set context menu
  await systemTray.setContextMenu(menu);

  // handle system tray event
  systemTray.registerSystemTrayEventHandler((eventName) {
    debugPrint("eventName: $eventName");
    if (eventName == kSystemTrayEventClick) {
      Platform.isWindows ? appWindow.show() : systemTray.popUpContextMenu();
    } else if (eventName == kSystemTrayEventRightClick) {
      Platform.isWindows ? systemTray.popUpContextMenu() : appWindow.show();
    }
  });
}

void main() {
  runApp(
    const MyApp(),
  );
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Timer? timer;
  bool _hasIcon = true;
  void closeIconFlash() {
    timer?.cancel();
    systemTray
        .setImage(Platform.isWindows ? "assets/sun.ico" : "assets/sun.JPG");
    _hasIcon = true;
  }

  void iconFlash() {
    timer = Timer.periodic(
      const Duration(milliseconds: 300),
      (timer) async {
        if (_hasIcon) {
          await systemTray.setImage(
            Platform.isWindows ? "assets/ .ico" : "assets/sun.JPG",
          );
        } else {
          await systemTray.setImage(
            Platform.isWindows ? "assets/sun.ico" : "assets/sun.JPG",
          );
        }
        _hasIcon = !_hasIcon;
        setState(() {});
      },
    );
  }

  @override
  void initState() {
    super.initState();
    initSystemTray();
  }

  @override
  void dispose() {
    timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Container(
          child: Stack(
            children: [
                  Lottie.asset(
                    'assets/147963-high-five.json',
                    width: 100,
                    height: 100,
                  ),
              Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Padding(
                    padding: EdgeInsets.all(10),
                  ),
                  const Text("Desktop tray"),
                  const Padding(
                    padding: EdgeInsets.all(10),
                  ),
                  TextButton(
                    onPressed: () {
                      iconFlash();
                    },
                    child: const Text("Start Desktop Tray Icon Spalsh"),
                  ),
                  TextButton(
                    onPressed: () {
                      closeIconFlash();
                    },
                    child: const Text("Close Desktop Tray Icon Spalsh"),
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}