flutter3-macos:基于flutter3.19+window_manager实战桌面OS应用

最新原创研发基于flutter3.19+getx+window_manager仿macOS桌面系统实例。

flutter3_macui 使用最新跨端技术flutter3.19.2+dart3.3开发的桌面版OS平台。

整体采用模糊虚化背景,支持macoswindows11两种风格dock菜单。

技术栈

  • 编辑器:Vscode
  • 框架技术:flutter3.19.2+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

功能特性

  1. 桌面菜单支持自适应排列,一级/二级弹窗菜单
  2. 整体采用高斯毛玻璃虚化效果
  3. 经典程序坞Dock动效菜单
  4. Dock菜单可拖拽式排序、支持二级弹窗式菜单
  5. 丰富视觉效果,自定义桌面主题换肤背景
  6. 可视化多窗口路由,支持弹窗方式打开新路由页面

项目结构

入口配置

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());
}

使用system_tray插件配置桌面端系统托盘图标。

dart 复制代码
// 初始化系统托盘图标
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();
    }
  });
}

flutter自定义窗口管理window_manager

flutter_macos项目为了整体风格一致,顶部导航栏菜单自定义组件实现功能,去掉了系统原生顶部导航条,使用window_manager插件来进行窗口管理。

目前bitsdojo_windowwindow_manager这两款桌面窗口管理插件使用的比较多,其中前者功能简单一些,后者则提供了更为丰富的功能。

官方有下面几个组件是文档没有列出的:

  • DragToMoveArea 拖拽窗口
  • DragToResizeArea 缩放窗口
  • VirtualWindowFrame(仅Linux)
  • WindowCaption 自定义顶部导航栏
  • WindowCaptionButtonIcon 导航栏右上角按钮组
dart 复制代码
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),
    ],
  ),
);

导航条支持自定义标题(居中)、背景色、头部、尾部、导航条高度等功能。

dart 复制代码
// 标题
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;

flutter-os布局模板

如上图:分为顶部导航栏+桌面菜单+底部Dock栏。

less 复制代码
/*
 * 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菜单

原创自研flutter实现类似macOS程序坞Dock菜单。鼠标滑过图标带动画效果放大图标。

dart 复制代码
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配置,支持二级菜单功能。

dart 复制代码
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'},
];

flutter3桌面菜单

桌面菜单使用Wrap自适应垂直排列。支持二级弹窗菜单。

dart 复制代码
@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配置其实和dock菜单配置差不多。其中component参数用于弹窗打开页面,dialog用于配置弹窗参数。

dart 复制代码
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参数,点击菜单项,会以弹窗方式打开新页面。

dart 复制代码
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, // 鼠标滑出弹窗是否销毁关闭
    ),
  ));
}

整个项目开发下来,涉及到的知识点还是蛮多的,限于篇幅,就先分享到这里。

juejin.cn/post/731918...

juejin.cn/post/734153...

相关推荐
C语言魔术师几秒前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳1 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?1 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二7 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
九酒9 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔10 小时前
HTML5 新表单属性详解
前端·html·html5
lee57610 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm