Flutter桌面端开发——系统托盘

注意:查看本文章前请先查看更新日志,以至于知晓该文章是否适合插件的最新版本

更新日志
详情日期
更新了system_tray2.0.2的用法,修正了tray_manager部分用法2022-12-10
更新了tray_manager0.1.5后MenuItem的用法2022-05-10

在我们日常使用的桌面软件中,有大部分都会在系统托盘中占一席地。当然,通过第三方插件,我们使用 Flutter 也能实现这种效果。

image

tray_manager

安装🛠

点击tray_manager获取最新版本。以下是在编写本文章时的最新版本:

tray_manager: ^0.2.0

👻注意:在开发 Linux 端的程序时,还需要额外的操作,具体可以查看这里

使用🥩

先实例化 TrayManager 对象:

final TrayManager _trayManager = TrayManager.instance;

设置图标🐱‍💻

在系统托盘显示,当然是先设置个图标了。

实例化 TrayManager 后,我们可以使用 setIcon来设置图标。

在这里,我们需要准备两种格式的图片:.png 和 .ico。png格式的图片用来在其他平台的托盘区显示,ico 用在 windows 平台显示。

final String _iconPathWin = 'assets/images/tray_manager.ico';
final String _iconPathOther = 'assets/images/tray_manager.png';

要是我们在 windows 平台用 png 格式显示会怎么样🤔?

await _trayManager.setIcon(_iconPathOther);

image

😲虽然占了位置,但是完全不会显示呢!我们还是改为正确的格式:

await _trayManager.setIcon(Platform.isWindows ? _iconPathWin : _iconPathOther);

image

这样就能正确显示我们设置好的图标了😀

既然这样,那我们就可以模拟QQ和微信来消息时的闪烁效果了!

先准备好两个不同格式的完全透明的图片。

final String _iconNullWin = 'assets/images/null.ico';
final String _iconNullOther = 'assets/images/null.png';

要想知道是否已经设置了图标,还需要一个布尔值判断:

bool _hasIcon = false;

更改一下设置图标的代码:

void _generateIcon() async {
  await _trayManager.setIcon(Platform.isWindows ? _iconPathWin : _iconPathOther);
  _hasIcon = true;
}

现在来写一个闪烁图片的方法,每过300毫秒换一次图片,这里需要用到 Timer 对象:

Timer? _timer;

别忘了在 dispose 方法中取消:

@override
void dispose() {
  // TODO: implement dispose
  _timer?.cancel();
  super.dispose();
}
void _iconFlash() {
  _timer = Timer.periodic(const Duration(milliseconds: 300), (timer) async {
    if (_hasIcon) {
      await _trayManager.setIcon(Platform.isWindows ? _iconNullWin : _iconNullOther);
    } else {
      await _trayManager.setIcon(Platform.isWindows ? _iconPathWin : _iconPathOther);
    }
    _hasIcon = !_hasIcon;
    setState(() {});
  });
}

有了开启闪烁动画的方法,还需要一个关闭闪烁效果的方法:

void _closeIconFlash() {
  _timer?.cancel();
  _generateIcon();
}

一切准备妥当,让我们来看看效果。

image

完美😎

设置提示信息🐾

当我们把鼠标移到托盘区的软件图标上,一般会有一个提示信息。这里我们也可以设置:

void _generateToolTip() async {
  await _trayManager.setToolTip('你想干嘛😒');
}

image

设置菜单项🕸

托盘区图标是有了,但是除了图标好像什么也没有,这不是纯粹恶心人吗?站着茅坑不拉💩。现在,我们就来帮它一把。

我们可以使用 setContextMenu方法来设置菜单,该方法需要传递一个 MenuItem 的数组对象,接下来我们就扒扒这个对象到底要传入些什么。MenuItem 中一共有以下多个参数可以传递:

  • String? key:组件唯一标识
  • String type:菜单项的类型,主要有normal、checkbox、separator、submenu几个值。默认为normal
  • String? label:菜单项显示的文本
  • String? sublabel:菜单项显示的二级文本
  • String? toolTip:菜单项的提示
  • String? icon:菜单项的图标(猜测,目前该插件版本还有待更新,文档并没有对该值进行描述)
  • bool? checked:是否被选择。仅在type为checkbox时生效
  • bool disabled = false:是否禁用选项。默认为false
  • Menu? submenu:子菜单项
  • void Function(MenuItem)? onClick:菜单项被点击后的方法

好了,摸清了 MenuItem 的家长里短,我们现在就来使用它:

Menu _menu = Menu(items: [
  MenuItem(label: '语文'),
  MenuItem(label: '数学', toolTip: '躲不掉的'),
  MenuItem.checkbox(
    label: '英语',
    checked: true,
    onClick: (menuItem) {
      menuItem.checked = !(menuItem.checked == true);
    },
  ),
  MenuItem.separator(),
  MenuItem.submenu(
    key: 'science',
    label: '理科',
    submenu: Menu(items: [
      MenuItem(label: '物理'),
      MenuItem(label: '化学'),
      MenuItem(label: '生物'),
    ]),
  ),
  MenuItem.separator(),
  MenuItem.submenu(
    key: 'arts',
    label: '文科',
    submenu: Menu(items: [
      MenuItem(label: '政治'),
      MenuItem(label: '历史'),
      MenuItem(label: '地理'),
    ]),
  ),
]);

把它们装填上去:

await trayManager.setContextMenu(_menu);

image

嗯😶怎么没用???看看了控制台,也没有错误。

其实菜单已经被我们设置好了,只是还没有写让它显示出来的方法。

注册鼠标事件🐹

一般什么情况下我们会让它显示出来呢🤔?鼠标右键单击图标时!要想监听鼠标右键单击图标的事件,我们先修改一下代码:

class _UseTrayManagerPageState extends State<UseTrayManagerPage> with TrayListener {
  
  @override
  void initState() {
    super.initState();
    _trayManager.addListener(this);
  }

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

通过 with 关键字混入 TrayListener 类,我们就可以对图标进行更多的事件操作了。

  • onTrayMenuItemClick:菜单选项点击事件
  • onTrayIconRightMouseUp:按理说右键单击后抬起事件,没试出来😕
  • onTrayIconRightMouseDown:图标右键单击事件
  • onTrayIconMouseUp:按理说左键单击后抬起事件,没试出来😕
  • onTrayIconMouseDown:图标左键单击事件

设置右键单击事件:

@override
void onTrayIconRightMouseDown() async {
  await _trayManager.popUpContextMenu();
}

设置菜单选项点击事件:

@override
void onTrayMenuItemClick(MenuItem menuItem) {
  BotToast.showText(text: '你选择了${menuItem.title}');
}

设置图标鼠标左键按下事件(显示程序):

@override
void onTrayIconMouseDown() {
  windowManager.show(); // 该方法来自window_manager插件
}

image

获取图标所在位置🐣

该方法将会返回图标在屏幕中的位置(Rect对象)

await _trayManager.getBounds();

移除托盘区程序🦝

除了关闭程序将程序从托盘区移除以外,我们也可以主动移除:

await _trayManager.destroy();

image

system_tray

安装🛠

点击system_tray获取最新版本。以下是在编写本文章时的最新版本:

system_tray: ^2.0.2

👻注意:在开发 Linux 端的程序时,还需要安装以下内容:

sudo apt-get install appindicator3-0.1 libappindicator3-dev

使用🥩

我们先准备好要使用的常量资源:

final String _title = '第一个Desktop应用';
final String _toolTip = '看什么看';
final String _iconPathWin = 'assets/images/system_tray.ico';
final String _iconPathOther = 'assets/images/system_tray.png';

然后实例化一个SystemTray对象:

final SystemTray _systemTray = SystemTray();

初始化🐱‍💻

有了SystemTray对象,我们就要使用它的initSystemTray方法来进行初始化。该方法有3个参数:

  • required String iconPath:程序在托盘显示的图标
  • String? title:程序名称(仅macos)
  • String? toolTip:鼠标移动到托盘图标时显示的提示信息(仅win、macos)
void _initTray() async {
  String _iconPath = Platform.isWindows ? _iconPathWin : _iconPathOther;
  await _systemTray.initSystemTray(title: _title, iconPath: _iconPath, toolTip: _toolTip);
}

image

这些内容都在初始化时设置了,那我们后期要修改怎么办?别着急,可以使用setSystemTrayInfo,参数和initSystemTray一样:

先准备好要换的内容:

final String _newToolTip = '怎么肥四啊';
final String _anotherIconWin = 'assets/images/another.ico';
final String _anotherIconOther = 'assets/images/another.png';

开始动手:

void _modifySystemTrayInfo() {
  String iconPath = Platform.isWindows ? _anotherIconWin : _anotherIconOther;
  _systemTray.setSystemTrayInfo(iconPath: iconPath, toolTip: _newToolTip);
}

image

设置菜单项🕸

初始化Menu对象:

final Menu _menu = Menu();

设置菜单项需要使用buildFrom方法:

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

该方法中传入一个MenuItem对象数组。该插件中一共有以下几种菜单项:

  • 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:菜单分隔符
void _setMenu() async {
  await _menu.buildFrom([
    MenuItemLabel(label: '五楼'),
    MenuItemLabel(label: '四楼'),
    SubMenu(label: '三楼', children: []),
    MenuItemCheckbox(label: '二楼'),
    MenuItemCheckbox(label: '一楼'),
    MenuSeparator(),
    MenuItemLabel(label: '地下室'),
  ]);
  await _systemTray.setContextMenu(_menu);
}

当然,和 tray_manager 一样,这样只设置了,但是没有让它什么时候显示。所以我们需要一个鼠标右键图标的事件。

注册鼠标事件🐹

注册事件需要用到registerSystemTrayEventHandler方法,注册好事件后一共有以下几种情况(Linux不适用):

  • kSystemTrayEventClick:鼠标左击
  • kSystemTrayEventRightClick:鼠标右击
  • kSystemTrayEventDoubleClick:鼠标双击(仅win)

需要注意的是,该插件中有一个AppWindow对象,可以用来控制Windows端的窗口显示、隐藏和关闭:

final AppWindow _appWindow = AppWindow();
void _setMenu() async {
  ...
  _systemTray.registerSystemTrayEventHandler((eventName) {
    if (eventName == kSystemTrayEventClick) {
      Platform.isWindows ? _appWindow.show() : _systemTray.popUpContextMenu();
    } else if (eventName == kSystemTrayEventRightClick) {
      Platform.isWindows ? _systemTray.popUpContextMenu() : _appWindow.show();
    } else if (eventName == kSystemTrayEventDoubleClick) {
      BotToast.showText(text: '点一次就够了!');
    }
  });
}

image

菜单项类型

我们可以通过上文得知,菜单项有不同样式和属性,我们修改代码如下:

await _menu.buildFrom([
  MenuItemLabel(label: '五楼'),
  MenuItemLabel(label: '四楼', image: getImagePath('another')),
  SubMenu(
    label: '三楼',
    children: [MenuItemLabel(label: '教室'), MenuItemLabel(label: '自习室')],
  ),
  MenuItemCheckbox(label: '二楼', checked: true),
  MenuItemCheckbox(label: '一楼'),
  MenuSeparator(),
  MenuItemLabel(label: '地下室'),
]);

image

注意:要想显示图片必须使用bmp格式,且最好制作bmp格式的软件为Windows自带的画图软件(不是3D画图)。win10及以上系统可以通过搜索打开该软件。(不要搜索那些在线转换bmp格式的网站,我认为它们只是单纯改了后缀名,害得我在这被坑了很久🤕)。

在菜单选项的onClicked方法中,会返回一个MenuItem对象,我们可以通过该对象对菜单项进行修改:

  • setCheck:是否选中
  • setEnable:是否启用
  • setImage:设置选项前的图片
  • setLabel:设置标签

这里只讲setCheck,剩余的几个很容易。设置是否选中有以下两种方法:

// 方法一
MenuItemCheckbox(
  label: '一楼',
  onClicked: (menuItem) async {
    await menuItem.setCheck(!menuItem.checked);
  },
),

// 方法二
MenuItemCheckbox(
  label: '二楼',
  name: 'second',
  checked: true,
  onClicked: (menuItem) async {
    MenuItemCheckbox? second = _menu.findItemByName<MenuItemCheckbox>('second');
    await second?.setCheck(!second.checked);
  },
),

image

根据方法二,我们也可以设置其他菜单项的属性。还有托盘区图标闪烁(使用该插件可以直接将图标设置为"",而不需要另外一张空白图片)、最小化显示应用移除图标(使用await _systemTray.destroy();)的功能这里就不演示了,可以参考tray_manager的使用方法。

🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。

最后,感谢 lijy91antler119 对以上插件的维护和开发😁。本应用代码已上传至 githubgitee,有需要的可以下载下来查看学习。

posted @ 2022-03-14 11:10  菠萝橙子丶  阅读(1584)  评论(5编辑  收藏  举报