Flutter桌面端开发——系统托盘
注意:查看本文章前请先查看更新日志,以至于知晓该文章是否适合插件的最新版本
更新日志
详情 | 日期 |
---|---|
更新了system_tray2.0.2的用法,修正了tray_manager部分用法 | 2022-12-10 |
更新了tray_manager0.1.5后MenuItem的用法 | 2022-05-10 |
在我们日常使用的桌面软件中,有大部分都会在系统托盘中占一席地。当然,通过第三方插件,我们使用 Flutter 也能实现这种效果。
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);
😲虽然占了位置,但是完全不会显示呢!我们还是改为正确的格式:
await _trayManager.setIcon(Platform.isWindows ? _iconPathWin : _iconPathOther);
这样就能正确显示我们设置好的图标了😀
既然这样,那我们就可以模拟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();
}
一切准备妥当,让我们来看看效果。
完美😎
设置提示信息🐾
当我们把鼠标移到托盘区的软件图标上,一般会有一个提示信息。这里我们也可以设置:
void _generateToolTip() async {
await _trayManager.setToolTip('你想干嘛😒');
}
设置菜单项🕸
托盘区图标是有了,但是除了图标好像什么也没有,这不是纯粹恶心人吗?站着茅坑不拉💩。现在,我们就来帮它一把。
我们可以使用 setContextMenu
方法来设置菜单,该方法需要传递一个 MenuItem 的数组对象,接下来我们就扒扒这个对象到底要传入些什么。MenuItem 中一共有以下多个参数可以传递:
String? key
:组件唯一标识String type
:菜单项的类型,主要有normal、checkbox、separator、submenu几个值。默认为normalString? label
:菜单项显示的文本String? sublabel
:菜单项显示的二级文本String? toolTip
:菜单项的提示String? icon
:菜单项的图标(猜测,目前该插件版本还有待更新,文档并没有对该值进行描述)bool? checked
:是否被选择。仅在type为checkbox时生效bool disabled = false
:是否禁用选项。默认为falseMenu? 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);
嗯😶怎么没用???看看了控制台,也没有错误。
其实菜单已经被我们设置好了,只是还没有写让它显示出来的方法。
注册鼠标事件🐹
一般什么情况下我们会让它显示出来呢🤔?鼠标右键单击图标时!要想监听鼠标右键单击图标的事件,我们先修改一下代码:
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插件
}
获取图标所在位置🐣
该方法将会返回图标在屏幕中的位置(Rect对象)
await _trayManager.getBounds();
移除托盘区程序🦝
除了关闭程序将程序从托盘区移除以外,我们也可以主动移除:
await _trayManager.destroy();
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);
}
这些内容都在初始化时设置了,那我们后期要修改怎么办?别着急,可以使用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);
}
设置菜单项🕸
初始化Menu对象:
final Menu _menu = Menu();
设置菜单项需要使用buildFrom方法:
await _menu.buildFrom([
...
]);
该方法中传入一个MenuItem对象数组。该插件中一共有以下几种菜单项:
- MenuItemLabel:文本菜单
required String label
:显示文本标签String? image
:选项前的图片String? name
:用来区分的名称bool enabled
:是否启用,默认为truevoid Function(MenuItem)? onClicked
:菜单被点击事件,返回一个MenuItem对象,可以获取设置image、name等属性
- MenuItemCheckbox:复选框菜单
required String label
:显示文本标签String? image
:选项前的图片String? name
:用来区分的名称bool enabled
:是否启用,默认为truebool checked
:是否选中,默认为falsevoid 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: '点一次就够了!');
}
});
}
菜单项类型
我们可以通过上文得知,菜单项有不同样式和属性,我们修改代码如下:
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: '地下室'),
]);
注意:要想显示图片必须使用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);
},
),
根据方法二,我们也可以设置其他菜单项的属性。还有托盘区图标闪烁(使用该插件可以直接将图标设置为""
,而不需要另外一张空白图片)、最小化、显示应用和移除图标(使用await _systemTray.destroy();
)的功能这里就不演示了,可以参考tray_manager的使用方法。
🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。
最后,感谢 lijy91 和 antler119 对以上插件的维护和开发😁。本应用代码已上传至 github 和 gitee,有需要的可以下载下来查看学习。