基于flutter3.19+getx+window_manager实战开发桌面OS项目正式完结了。

null

flutter-os支持自定义主题换肤、毛玻璃虚化背景、程序坞Dock菜单多级嵌套+自由拖拽排序、可拖拽路由弹窗等功能。

null

使用技术

  • 编辑器:vscode
  • 框架技术:Flutter3.19.2+Dart3.3.0
  • 窗口管理: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

null

null

项目结构

null

flutter_macos使用window_manager来进行系统窗口管理。

目前flutter比较受欢迎的两款窗口管理器是bitsdojo_windowwindow_manager插件,其中前者功能简单一些,后者则提供了更为丰富的功能。

有下面几个组件是文档没有写的:

  • DragToMoveArea 拖拽窗口
  • DragToResizeArea 缩放窗口
  • VirtualWindowFrame(仅Linux)
  • WindowCaption 自定义顶部导航栏
  • WindowCaptionButtonIcon 导航栏右上角按钮组

null

项目主入口

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

null

flutter系统托盘采用system_tray组件实现功能。

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

null

null

null

null

null

null

null

flutter-macos桌面布局模板

null

整个项目采用顶部自定义导航+桌面菜单+底部Dock菜单三大部分。

/*
 * 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(),
  ),
);

flutter3自定义Dock菜单

null

null

采用毛玻璃虚化背景、支持拖拽排序二级弹窗菜单。支持macOSwindows11两种风格

null

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'},
];

null

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: () {
                                  // ...
                                },
                              ),
                            );
                          }),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      );
    },
  );
}

null

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

该项目已经同步上架到工房,如果有需要的话,可以去了解一下哈~~
https://gf.bilibili.com/item/detail/1105670011

flutter3.x开发桌面端OS项目涉及到的知识点还是很多的,今天就先分享到这里。