Flutter系统托盘交互插件system_tray的使用

发布于 1周前 作者 h691938207 来自 Flutter

Flutter系统托盘交互插件system_tray的使用

system_tray 插件简介

Pub

system_tray 是一个用于Flutter桌面应用的插件,支持Windows、macOS和Linux平台的系统托盘菜单功能。

安装

pubspec.yaml 文件中添加依赖:

dependencies:
  ...
  system_tray: ^2.0.3

在代码文件中导入:

import 'package:system_tray/system_tray.dart';

Linux平台前置条件

对于Linux平台,需要安装以下依赖项:

sudo apt-get install appindicator3-0.1 libappindicator3-dev

或针对Ubuntu 22.04及以上版本:

sudo apt-get install libayatana-appindicator3-dev

示例应用

初始化系统托盘

以下是初始化系统托盘并设置上下文菜单的完整示例:

import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:system_tray/system_tray.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());

  doWhenWindowReady(() {
    final win = appWindow;
    const initialSize = Size(600, 450);
    win.minSize = initialSize;
    win.size = initialSize;
    win.alignment = Alignment.center;
    win.title = "How to use system tray with Flutter";
    win.show();
  });
}

String getTrayImagePath(String imageName) {
  return Platform.isWindows ? 'assets/$imageName.ico' : 'assets/$imageName.png';
}

String getImagePath(String imageName) {
  return Platform.isWindows ? 'assets/$imageName.bmp' : 'assets/$imageName.png';
}

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

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

class _MyAppState extends State<MyApp> {
  final AppWindow _appWindow = AppWindow();
  final SystemTray _systemTray = SystemTray();
  final Menu _menuMain = Menu();
  final Menu _menuSimple = Menu();

  Timer? _timer;
  bool _toogleTrayIcon = true;
  bool _toogleMenu = true;

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

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

  Future<void> initSystemTray() async {
    List<String> iconList = ['darts_icon', 'gift_icon'];

    // 初始化系统托盘
    await _systemTray.initSystemTray(iconPath: getTrayImagePath('app_icon'));
    _systemTray.setTitle("system tray");
    _systemTray.setToolTip("How to use system tray with Flutter");

    // 处理系统托盘事件
    _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();
      }
    });

    // 构建主菜单
    await _menuMain.buildFrom([
      MenuItemLabel(
        label: 'Change Context Menu',
        image: getImagePath('darts_icon'),
        onClicked: (menuItem) {
          debugPrint("Change Context Menu");
          _toogleMenu = !_toogleMenu;
          _systemTray.setContextMenu(_toogleMenu ? _menuMain : _menuSimple);
        },
      ),
      MenuSeparator(),
      MenuItemLabel(
          label: 'Show',
          image: getImagePath('darts_icon'),
          onClicked: (menuItem) => _appWindow.show()),
      MenuItemLabel(
          label: 'Hide',
          image: getImagePath('darts_icon'),
          onClicked: (menuItem) => _appWindow.hide()),
      MenuItemLabel(
        label: 'Start flash tray icon',
        image: getImagePath('darts_icon'),
        onClicked: (menuItem) {
          debugPrint("Start flash tray icon");
          _timer ??= Timer.periodic(
            const Duration(milliseconds: 500),
            (timer) {
              _toogleTrayIcon = !_toogleTrayIcon;
              _systemTray.setImage(
                  _toogleTrayIcon ? "" : getTrayImagePath('app_icon'));
            },
          );
        },
      ),
      MenuItemLabel(
        label: 'Stop flash tray icon',
        image: getImagePath('darts_icon'),
        onClicked: (menuItem) {
          debugPrint("Stop flash tray icon");
          _timer?.cancel();
          _timer = null;
          _systemTray.setImage(getTrayImagePath('app_icon'));
        },
      ),
      MenuSeparator(),
      SubMenu(
        label: "Test API",
        image: getImagePath('gift_icon'),
        children: [
          SubMenu(
            label: "setSystemTrayInfo",
            image: getImagePath('darts_icon'),
            children: [
              MenuItemLabel(
                label: 'setTitle',
                image: getImagePath('darts_icon'),
                onClicked: (menuItem) {
                  final String text = WordPair.random().asPascalCase;
                  debugPrint("click 'setTitle' : $text");
                  _systemTray.setTitle(text);
                },
              ),
              MenuItemLabel(
                label: 'setImage',
                image: getImagePath('gift_icon'),
                onClicked: (menuItem) {
                  String iconName = iconList[Random().nextInt(iconList.length)];
                  String path = getTrayImagePath(iconName);
                  debugPrint("click 'setImage' : $path");
                  _systemTray.setImage(path);
                },
              ),
              MenuItemLabel(
                label: 'setToolTip',
                image: getImagePath('darts_icon'),
                onClicked: (menuItem) {
                  final String text = WordPair.random().asPascalCase;
                  debugPrint("click 'setToolTip' : $text");
                  _systemTray.setToolTip(text);
                },
              ),
              MenuItemLabel(
                label: 'getTitle',
                image: getImagePath('gift_icon'),
                onClicked: (menuItem) async {
                  String title = await _systemTray.getTitle();
                  debugPrint("click 'getTitle' : $title");
                },
              ),
            ],
          ),
          MenuItemLabel(
              label: 'disabled Item',
              name: 'disableItem',
              image: getImagePath('gift_icon'),
              enabled: false),
        ],
      ),
      MenuSeparator(),
      MenuItemLabel(
        label: 'Set Item Image',
        onClicked: (menuItem) async {
          debugPrint("click 'SetItemImage'");
          String iconName = iconList[Random().nextInt(iconList.length)];
          String path = getImagePath(iconName);
          await menuItem.setImage(path);
          debugPrint(
              "click name: ${menuItem.name} menuItemId: ${menuItem.menuItemId} label: ${menuItem.label} image: ${menuItem.image}");
        },
      ),
      MenuItemCheckbox(
        label: 'Checkbox 1',
        name: 'checkbox1',
        checked: true,
        onClicked: (menuItem) async {
          debugPrint("click 'Checkbox 1'");
          MenuItemCheckbox? checkbox1 =
              _menuMain.findItemByName<MenuItemCheckbox>("checkbox1");
          await checkbox1?.setCheck(!checkbox1.checked);

          MenuItemCheckbox? checkbox2 =
              _menuMain.findItemByName<MenuItemCheckbox>("checkbox2");
          await checkbox2?.setEnable(checkbox1?.checked ?? true);

          debugPrint(
              "click name: ${checkbox1?.name} menuItemId: ${checkbox1?.menuItemId} label: ${checkbox1?.label} checked: ${checkbox1?.checked}");
        },
      ),
      MenuItemCheckbox(
        label: 'Checkbox 2',
        name: 'checkbox2',
        onClicked: (menuItem) async {
          debugPrint("click 'Checkbox 2'");
          await menuItem.setCheck(!menuItem.checked);
          await menuItem.setLabel(WordPair.random().asPascalCase);
          debugPrint(
              "click name: ${menuItem.name} menuItemId: ${menuItem.menuItemId} label: ${menuItem.label} checked: ${menuItem.checked}");
        },
      ),
      MenuItemCheckbox(
        label: 'Checkbox 3',
        name: 'checkbox3',
        checked: true,
        onClicked: (menuItem) async {
          debugPrint("click 'Checkbox 3'");
          await menuItem.setCheck(!menuItem.checked);
          debugPrint(
              "click name: ${menuItem.name} menuItemId: ${menuItem.menuItemId} label: ${menuItem.label} checked: ${menuItem.checked}");
        },
      ),
      MenuSeparator(),
      MenuItemLabel(
          label: 'Exit', onClicked: (menuItem) => _appWindow.close()),
    ]);

    // 构建简化菜单
    await _menuSimple.buildFrom([
      MenuItemLabel(
        label: 'Change Context Menu',
        image: getImagePath('app_icon'),
        onClicked: (menuItem) {
          debugPrint("Change Context Menu");
          _toogleMenu = !_toogleMenu;
          _systemTray.setContextMenu(_toogleMenu ? _menuMain : _menuSimple);
        },
      ),
      MenuSeparator(),
      MenuItemLabel(
          label: 'Show',
          image: getImagePath('app_icon'),
          onClicked: (menuItem) => _appWindow.show()),
      MenuItemLabel(
          label: 'Hide',
          image: getImagePath('app_icon'),
          onClicked: (menuItem) => _appWindow.hide()),
      MenuItemLabel(
        label: 'Exit',
        image: getImagePath('app_icon'),
        onClicked: (menuItem) => _appWindow.close(),
      ),
    ]);

    _systemTray.setContextMenu(_menuMain);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: WindowBorder(
          color: const Color(0xFF805306),
          width: 1,
          child: Column(
            children: [
              const TitleBar(),
              ContentBody(
                systemTray: _systemTray,
                menu: _menuMain,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

const backgroundStartColor = Color(0xFFFFD500);
const backgroundEndColor = Color(0xFFF6A00C);

class TitleBar extends StatelessWidget {
  const TitleBar({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return WindowTitleBarBox(
      child: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [backgroundStartColor, backgroundEndColor],
              stops: [0.0, 1.0]),
        ),
        child: Row(
          children: [
            Expanded(
              child: MoveWindow(),
            ),
            const WindowButtons()
          ],
        ),
      ),
    );
  }
}

class ContentBody extends StatelessWidget {
  final SystemTray systemTray;
  final Menu menu;

  const ContentBody({
    Key? key,
    required this.systemTray,
    required this.menu,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
        color: const Color(0xFFFFFFFF),
        child: ListView(
          padding: const EdgeInsets.symmetric(vertical: 4.0),
          children: [
            Card(
              elevation: 2.0,
              margin: const EdgeInsets.symmetric(
                horizontal: 16.0,
                vertical: 8.0,
              ),
              child: Padding(
                padding: const EdgeInsets.all(12.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'systemTray.initSystemTray',
                      style: TextStyle(
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                    const Text(
                      'Create system tray.',
                    ),
                    const SizedBox(
                      height: 12.0,
                    ),
                    ElevatedButton(
                      child: const Text("initSystemTray"),
                      onPressed: () async {
                        if (await systemTray.initSystemTray(
                            iconPath: getTrayImagePath('app_icon'))) {
                          systemTray.setTitle("new system tray");
                          systemTray.setToolTip(
                              "How to use system tray with Flutter");
                          systemTray.setContextMenu(menu);
                        }
                      },
                    ),
                  ],
                ),
              ),
            ),
            Card(
              elevation: 2.0,
              margin: const EdgeInsets.symmetric(
                horizontal: 16.0,
                vertical: 8.0,
              ),
              child: Padding(
                padding: const EdgeInsets.all(12.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'systemTray.destroy',
                      style: TextStyle(
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                    const Text(
                      'Destroy system tray.',
                    ),
                    const SizedBox(
                      height: 12.0,
                    ),
                    ElevatedButton(
                      child: const Text("destroy"),
                      onPressed: () async {
                        await systemTray.destroy();
                      },
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

final buttonColors = WindowButtonColors(
    iconNormal: const Color(0xFF805306),
    mouseOver: const Color(0xFFF6A00C),
    mouseDown: const Color(0xFF805306),
    iconMouseOver: const Color(0xFF805306),
    iconMouseDown: const Color(0xFFFFD500));

final closeButtonColors = WindowButtonColors(
    mouseOver: const Color(0xFFD32F2F),
    mouseDown: const Color(0xFFB71C1C),
    iconNormal: const Color(0xFF805306),
    iconMouseOver: Colors.white);

class WindowButtons extends StatelessWidget {
  const WindowButtons({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        MinimizeWindowButton(colors: buttonColors),
        MaximizeWindowButton(colors: buttonColors),
        CloseWindowButton(
          colors: closeButtonColors,
          onPressed: () {
            showDialog<void>(
              context: context,
              barrierDismissible: false,
              builder: (BuildContext context) {
                return AlertDialog(
                  title: const Text('Exit Program?'),
                  content: const Text(
                      ('The window will be hidden, to exit the program you can use the system menu.')),
                  actions: <Widget>[
                    TextButton(
                      child: const Text('OK'),
                      onPressed: () {
                        Navigator.of(context).pop();
                        appWindow.hide();
                      },
                    ),
                  ],
                );
              },
            );
          },
        ),
      ],
    );
  }
}

额外资源

推荐使用以下库来控制窗口:

常见问题解答 (Q&A)

  1. 问:如果遇到以下编译错误
    Undefined symbols for architecture x86_64:
      "___gxx_personality_v0", referenced from:
          ...
    
    答:添加 libc++.tbd
    1. 打开 example/macos/Runner.xcodeproj
    2. 添加 'libc++.tbd' 到 TARGET runner 'Link Binary With Libraries'
    

通过以上内容,您可以快速上手使用 system_tray 插件为您的Flutter桌面应用程序添加系统托盘功能。希望这些信息对您有所帮助!


更多关于Flutter系统托盘交互插件system_tray的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter系统托盘交互插件system_tray的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter中使用system_tray插件来实现系统托盘交互的代码示例。这个插件允许你在桌面平台(如Windows、Linux和macOS)上与系统托盘进行交互,比如显示图标、菜单项以及处理托盘点击事件。

首先,确保你已经在你的Flutter项目中添加了system_tray依赖。你可以在你的pubspec.yaml文件中添加以下依赖:

dependencies:
  flutter:
    sdk: flutter
  system_tray: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

接下来,我们编写一个示例应用,展示如何使用system_tray插件。

main.dart

import 'package:flutter/material.dart';
import 'package:system_tray/system_tray.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'System Tray Example',
      home: Scaffold(
        appBar: AppBar(
          title: Text('System Tray Example'),
        ),
        body: Center(
          child: Text('Check the system tray for interactions!'),
        ),
      ),
    );
  }
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  late SystemTray? _systemTray;
  late bool _isTrayIconSet = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addObserver(this);
    _initSystemTray();
  }

  @override
  void dispose() {
    WidgetsBinding.instance!.removeObserver(this);
    _systemTray?.destroy();
    super.dispose();
  }

  void _initSystemTray() async {
    if (!kIsDesktop) {
      return;
    }

    _systemTray = await SystemTray.instance;

    // 设置托盘图标
    final iconPath = 'assets/icon.png'; // 请确保你有这个图标文件
    _systemTray?.setIconFromPath(iconPath);

    // 设置托盘菜单项
    _systemTray?.setContextMenu([
      SystemTrayMenuItem(
        title: 'Item 1',
        onClick: () {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Item 1 clicked')),
          );
        },
      ),
      SystemTrayMenuItem(
        title: 'Quit',
        onClick: () {
          exit(0);
        },
      ),
    ]);

    // 处理托盘图标点击事件
    _systemTray?.onTrayIconClicked.listen((_) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Tray icon clicked')),
      );
    });

    setState(() {
      _isTrayIconSet = true;
    });
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.detached && _isTrayIconSet) {
      // 当应用进入后台时,可以添加一些清理代码
      // 例如:_systemTray?.destroy();
    }
  }
}

注意事项

  1. 图标文件:确保在assets文件夹中有一个名为icon.png的图标文件,并且已经在pubspec.yaml中声明了资源文件。

    flutter:
      assets:
        - assets/icon.png
    
  2. 平台支持:这个插件目前只支持桌面平台(Windows、Linux、macOS)。在移动平台上运行这段代码将不会有任何效果。

  3. 权限:在某些平台上(特别是Linux和macOS),你可能需要额外的权限或配置来显示系统托盘图标。确保按照插件的文档进行必要的配置。

这个示例展示了如何使用system_tray插件来设置托盘图标、菜单项以及处理托盘点击事件。你可以根据自己的需求进一步扩展和定制这些功能。

回到顶部