前言
随着Flutter
快速迭代更新,越来越多的开发者倾向于使用flutter构建多平台跨端项目。今天给大家分享原创flutter3+dart3
实战桌面端OS系统FlutterMacOS。
Flutter3-MacOS原创开发的一款flutter3+getX+window_manager
仿MacOS桌面系统平台。
技术框架
- 编辑器:Vscode
- 框架技术:flutter3.19+dart3.3
- 窗口管理器:window_manager^0.3.8
- 路由/状态管理:get^4.6.6
- 缓存服务:get_storage^2.1.1
- 拖拽排序:reorderables^0.6.0
- 图表组件:fl_chart^0.67.0
- 系统托盘:system_tray^2.0.3
功能特点
- 桌面菜单支持一级/二级弹窗菜单
- 整体运用毛玻璃模糊背景效果
- 经典程序坞Dock动效菜单
- Dock菜单可拖拽式排序、支持二级弹窗式菜单
- 丰富视觉效果,自定义桌面主题换肤背景
- 可视化多窗口路由,支持弹窗方式打开新路由页面
项目结构目录
入口配置main.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:system_tray/system_tray.dart';
import 'package:window_manager/window_manager.dart';
import 'utils/index.dart';
// 引入布局模板
import 'layouts/index.dart';
// 引入路由管理
import 'router/index.dart';
void main() async {
// 初始化国际化语言
initializeDateFormatting();
// 初始化get_storage本地存储
await GetStorage.init();
// 初始化window_manager窗口
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
title: 'Flutter-MacOS',
size: Size(1000, 640),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.hidden, // 是否隐藏系统导航栏
windowButtonVisibility: false,
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
windowManager.setAsFrameless(); // 无边框
windowManager.setHasShadow(true); // 是否有阴影
// windowManager.setResizable(false); // 是否可缩放
// windowManager.setAlwaysOnTop(false); // 是否置顶窗口
await windowManager.show();
await windowManager.focus();
});
await initSystemTray();
runApp(const MyApp());
}
如上图:配置系统托盘图标。
// 初始化系统托盘图标
Future<void> initSystemTray() async {
String trayIco = 'assets/images/tray.ico';
SystemTray systemTray = SystemTray();
// 初始化系统托盘
await systemTray.initSystemTray(
title: 'Flutter-MacOS',
iconPath: trayIco,
);
// 右键菜单
final Menu menu = Menu();
await menu.buildFrom([
MenuItemLabel(label: '打开主界面', image: 'assets/images/tray_main.bmp', onClicked: (menuItem) async => await windowManager.show()),
MenuItemLabel(label: '隐藏窗口', image: 'assets/images/tray_hide.bmp', onClicked: (menuItem) async => await windowManager.hide()),
MenuItemLabel(label: '设置中心', image: 'assets/images/tray_setting.bmp', onClicked: (menuItem) => {}),
MenuItemLabel(label: '锁屏', image: 'assets/images/tray_lock.bmp', onClicked: (menuItem) => {}),
MenuItemLabel(label: '关闭程序并退出', image: 'assets/images/tray_logout.bmp', onClicked: (menuItem) async => await windowManager.destroy()),
]);
await systemTray.setContextMenu(menu);
// 右键事件
systemTray.registerSystemTrayEventHandler((eventName) async {
debugPrint('eventName: $eventName');
if (eventName == kSystemTrayEventClick) {
Platform.isWindows ? await windowManager.show() : systemTray.popUpContextMenu();
} else if (eventName == kSystemTrayEventRightClick) {
Platform.isWindows ? systemTray.popUpContextMenu() : await windowManager.show();
}
});
}
window_manager窗口管理
flutter_macos项目去掉了系统原生顶部导航条,使用window_manager
来进行系统窗口管理。
目前flutter桌面端比较不错的两款窗口管理器是bitsdojo_window
和window_manager
插件,其中前者功能简单一些,后者则提供了更为丰富的功能。
有下面几个组件是文档没有写的:
- DragToMoveArea 拖拽窗口
- DragToResizeArea 缩放窗口
- VirtualWindowFrame(仅Linux)
- WindowCaption 自定义顶部导航栏
- WindowCaptionButtonIcon 导航栏右上角按钮组
return Container(
height: widget.titlebarHeight,
decoration: BoxDecoration(
backgroundBlendMode: widget.backgroundBlendMode,
color: widget.backgroundColor,
gradient: widget.gradient,
),
child: Stack(
children: [
Row(
children: [
// 头部
Container(
child: widget.leading,
),
// 可拖动区(标题)
Expanded(
child: DragToMoveArea(
child: SizedBox(
height: double.infinity,
child: Row(
children: [
// 标题(不居中)
customTitle(!widget.centerTitle),
],
),
),
),
),
// 尾部
Container(
child: widget.trailing,
),
// 按钮组
WinBtns(theme: widget.actionButtonTheme, buttonSize: widget.actionButtonSize),
],
),
// 标题(居中)
customTitle(widget.centerTitle),
],
),
);
支持自定义标题(居中)、背景色、头部、尾部、导航条高度等功能。
// 标题
final Widget? title;
// 背景色
final Color? backgroundColor;
// 前景色
final Color? foregroundColor;
// 混合模式
final BlendMode? backgroundBlendMode;
// 渐变背景
final LinearGradient? gradient;
// 标题是否居中
final bool centerTitle;
// 头部
final Widget? leading;
// 尾部
final Widget? trailing;
// 标题栏高度
final double titlebarHeight;
// 右上角按钮组主题色(light/dark)
final String? actionButtonTheme;
// 右上角按钮组宽高
final Size actionButtonSize;
flutter3桌面布局模板
/*
* flutter桌面os布局 Q:282310962
*/
return Scaffold(
key: scaffoldKey,
body: Container(
// 背景图主题
decoration: skinTheme(),
// DragToResizeArea自定义缩放窗口
child: DragToResizeArea(
child: Flex(
direction: Axis.vertical,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 导航栏
WindowTitlebar(
onDrawer: () {
// 自定义打开右侧drawer
scaffoldKey.currentState?.openEndDrawer();
},
),
// 桌面区域
Expanded(
child: GestureDetector(
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: GestureDetector(
child: const WindowDesktop(),
onSecondaryTapDown: (TapDownDetails details) {
posDX = details.globalPosition.dx;
posDY = details.globalPosition.dy;
},
onSecondaryTap: () {
debugPrint('桌面图标右键');
showDeskIconContextmenu();
},
),
),
],
),
),
onSecondaryTapDown: (TapDownDetails details) {
posDX = details.globalPosition.dx;
posDY = details.globalPosition.dy;
},
onSecondaryTap: () {
debugPrint('桌面右键');
showDeskContextmenu();
},
),
),
// Dock菜单
settingController.settingData['dock'] == 'windows' ?
const WindowTabbar()
:
const WindowDock()
,
],
),
),
),
endDrawer: Drawer(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
width: 300,
child: const Settings(),
),
);
flutter实现dock菜单
支持macOS和windows11两种风格。采用毛玻璃虚化背景、支持拖拽排序和二级弹窗菜单。
MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (event) {
setState(() {
hoveredIndex = index;
});
controller.forward(from: 0.0);
},
onExit: (event) {
setState(() {
hoveredIndex = -1;
});
controller.stop();
},
child: GestureDetector(
onTapDown: (TapDownDetails details) {
anchorDx = details.globalPosition.dx;
},
onTap: () {
if(item!['children'] != null) {
showDockDialog(item!['children']);
}
},
// 缩放动画
child: ScaleTransition(
alignment: Alignment.bottomCenter,
scale: hoveredIndex == index ?
controller.drive(Tween(begin: 1.0, end: 1.5).chain(CurveTween(curve: Curves.easeOutCubic)))
:
Tween(begin: 1.0, end: 1.0).animate(controller)
,
child: UnconstrainedBox(
child: Stack(
alignment: AlignmentDirectional.topCenter,
children: [
// tooltip提示
Visibility(
visible: hoveredIndex == index && !draggable,
child: Positioned(
top: 0,
child: SizedOverflowBox(
size: Size.zero,
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 1.0),
margin: const EdgeInsets.only(bottom: 20.0),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(3.0),
),
child: Text('${item!['tooltip']}', style: const TextStyle(color: Colors.white, fontSize: 8.0, fontFamily: 'arial')),
),
),
),
),
// 图片/图标
item!['children'] != null ?
thumbDock(item!['children'])
:
SizedBox(
height: 35.0,
width: 35.0,
child: item!['type'] != null && item!['type'] == 'icon' ?
IconTheme(
data: const IconThemeData(color: Colors.white, size: 32.0),
child: item!['imgico'],
)
:
Image.asset('${item!['imgico']}')
,
),
// 圆点
Visibility(
visible: item!['active'] != null,
child: Positioned(
bottom: 0,
child: SizedOverflowBox(
size: Size.zero,
child: Container(
margin: const EdgeInsets.only(top: 2.0),
height: 4.0,
width: 4.0,
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(10.0),
),
),
),
),
),
],
),
),
),
),
)
菜单json格式配置。
List dockList = [
{'tooltip': 'Flutter3.19', 'imgico': 'assets/images/logo.png'},
{'tooltip': 'Safari', 'imgico': 'assets/images/mac/safari.png', 'active': true},
{
'tooltip': 'Launchpad',
'imgico': 'assets/images/mac/launchpad.png',
'children': [
{'tooltip': 'Podcasts', 'imgico': 'assets/images/mac/podcasts.png'},
{'tooltip': 'Quicktime', 'imgico': 'assets/images/mac/quicktime.png'},
{'tooltip': 'Notes', 'imgico': 'assets/images/mac/notes.png'},
{'tooltip': 'Reminder', 'imgico': 'assets/images/mac/reminders.png'},
{'tooltip': 'Calc', 'imgico': 'assets/images/mac/calculator.png'},
]
},
{'tooltip': 'Appstore', 'imgico': 'assets/images/mac/appstore.png',},
{'tooltip': 'Messages', 'imgico': 'assets/images/mac/messages.png', 'active': true},
{'type': 'divider'},
...
{'tooltip': 'Recycle Bin', 'imgico': 'assets/images/mac/bin.png'},
];
void showDockDialog(data) {
anchorDockOffset();
showDialog(
context: context,
barrierColor: Colors.transparent,
builder: (context) {
return Stack(
children: [
Positioned(
top: anchorDy - 210,
left: anchorDx - 120,
width: 240.0,
height: 210,
child: ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10.0),
decoration: const BoxDecoration(
backgroundBlendMode: BlendMode.overlay,
color: Colors.white,
),
child: ListView(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0,),
child: Wrap(
runSpacing: 5.0,
spacing: 5.0,
children: List.generate(data.length, (index) {
final item = data[index];
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: Column(
children: [
// 图片/图标
SizedBox(
height: 40.0,
width: 40.0,
child: item!['type'] != null && item!['type'] == 'icon' ?
IconTheme(
data: const IconThemeData(color: Colors.black87, size: 35.0),
child: item!['imgico'],
)
:
Image.asset('${item!['imgico']}')
,
),
SizedBox(
width: 70,
child: Text(item['tooltip'], style: const TextStyle(color: Colors.black87, fontSize: 12.0), maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center,),
)
],
),
onTap: () {
// ...
},
),
);
}),
),
),
],
),
),
),
),
),
],
);
},
);
}
flutter桌面菜单
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10.0),
child: Wrap(
direction: Axis.vertical,
spacing: 5.0,
runSpacing: 5.0,
children: List.generate(deskList.length, (index) {
final item = deskList[index];
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (event) {
setState(() {
hoveredIndex = index;
});
},
onExit: (event) {
setState(() {
hoveredIndex = -1;
});
},
child: GestureDetector(
onTapDown: (TapDownDetails details) {
anchorDx = details.globalPosition.dx;
anchorDy = details.globalPosition.dy;
},
onTap: () {
if(item!['children'] != null) {
showDeskDialog(item!['children']);
}else {
showRouteDialog(item);
}
},
child: Container(
...
),
),
);
}),
),
);
}
桌面菜单json配置。
List deskList = [
{'title': 'Flutter3.19', 'imgico': 'assets/images/logo.png', 'link': 'https://flutter.dev/'},
{
'title': '首页', 'imgico': const Icon(Icons.home_outlined), 'type': 'icon',
'component': const Home(),
'dialog': {
'fullscreen': true
}
},
{
'title': '工作台', 'imgico': const Icon(Icons.poll_outlined), 'type': 'icon',
'component': const Dashboard(),
},
{
'title': '组件',
'imgico': const Icon(Icons.apps),
'type': 'icon',
'children': [
{'title': 'Mail', 'imgico': 'assets/images/mac/mail.png'},
{'title': 'Info', 'imgico': 'assets/images/mac/info.png'},
{'title': 'Editor', 'imgico': 'assets/images/mac/scripteditor.png'},
{'title': '下载', 'imgico': const Icon(Icons.download_outlined), 'type': 'icon'},
{'title': 'Bug统计', 'imgico': const Icon(Icons.bug_report_outlined), 'type': 'icon'},
{'title': '计算器', 'imgico': const Icon(Icons.calculate), 'type': 'icon'},
{'title': '图表', 'imgico': const Icon(Icons.bar_chart), 'type': 'icon'},
{'title': '打印', 'imgico': const Icon(Icons.print), 'type': 'icon'},
{'title': '站内信', 'imgico': const Icon(Icons.campaign), 'type': 'icon'},
{'title': '云存储', 'imgico': const Icon(Icons.cloud_outlined), 'type': 'icon'},
{'title': '裁剪', 'imgico': const Icon(Icons.crop_outlined), 'type': 'icon'},
]
},
{
'title': '私密空间', 'imgico': const Icon(Icons.camera_outlined), 'type': 'icon',
'component': const Uzone(),
},
...
{
'title': '公众号', 'imgico': const Icon(Icons.qr_code), 'type': 'icon',
'dialog': {
'title': const Text('QRcode', style: TextStyle(color: Colors.white60, fontSize: 14.0, fontFamily: 'arial')),
'content': Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset('assets/images/qrcode_white.png', height: 120.0, fit: BoxFit.contain,),
const Spacer(),
const Text('扫一扫,关注公众号', style: TextStyle(color: Colors.white60, fontSize: 12.0,),),
],
),
),
'backgroundColor': const Color(0xff07c160),
'actionColor': Colors.white54,
'width': 300,
'height': 220,
'maximizable': false,
'closable': true,
'draggable': true,
}
},
];
其中component
参数用于弹窗打开页面,dialog
用于配置弹窗参数。
void showRouteDialog(item) async {
// 链接
if(item!['link'] != null) {
await launchUrl(Uri.parse(item!['link']));
return;
}
// 图标
Widget dialogIcon() {
if(item!['type'] != null && item!['type'] == 'icon') {
return IconTheme(
data: const IconThemeData(size: 16.0),
child: item!['imgico'],
);
}else {
return Image.asset('${item!['imgico']}', height: 16.0, width: 16.0, fit: BoxFit.cover);
}
}
// Fdialog参数
dynamic dialog = item!['dialog'] ?? {};
navigator?.push(FdialogRoute(
child: Fdialog(
// 标题
title: dialog!['title'] ?? Row(
children: [
dialogIcon(),
const SizedBox(width: 5.0,),
Text('${item!['title']}',),
],
),
// 内容
content: dialog!['content'] ?? ListView(
padding: const EdgeInsets.all(10.0),
children: [
item!['component'] ?? const Center(child: Column(children: [Icon(Icons.layers,), Text('Empty~'),],)),
],
),
titlePadding: dialog!['titlePadding'], // 标题内间距
backgroundColor: dialog!['backgroundColor'] ?? Colors.white.withOpacity(.85), // 弹窗背景色
barrierColor: dialog!['barrierColor'], // 弹窗遮罩层颜色
offset: dialog!['offset'], // 弹窗位置(坐标点)
width: dialog!['width'] ?? 800, // 宽度
height: dialog!['height'] ?? 500, // 高度
radius: dialog!['radius'], // 圆角
fullscreen: dialog!['fullscreen'] ?? false, // 是否全屏
maximizable: dialog!['maximizable'] ?? true, // 是否显示最大化按钮
closable: dialog!['closable'] ?? true, // 是否显示关闭按钮
customClose: dialog!['customClose'], // 自定义关闭按钮
closeIcon: dialog!['closeIcon'], // 自定义关闭图标
actionColor: dialog!['actionColor'], // 右上角按钮组颜色
actionSize: dialog!['actionSize'], // 右上角按钮组大小
draggable: dialog!['draggable'] ?? true, // 是否可拖拽
destroyOnExit: dialog!['destroyOnExit'] ?? false, // 鼠标滑出弹窗是否销毁关闭
),
));
}
flutter开发桌面os涉及到的知识还是蛮多的,限于篇幅,就先分享到这里。希望以上的分享对大家有所帮助!
https://segmentfault.com/a/1190000044741696
https://segmentfault.com/a/1190000044675519
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。