百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 博客教程 > 正文

【完结】Flutter3+Getx仿微信桌面端exe聊天程序

connygpt 2024-12-12 11:30 9 浏览

#精品长文创作季#

继上一篇基于Flutter3.x桌面端仿微信exe实例一经发表,被赞爆了。

今天再来分享一些实现的技术知识,希望以下分享对大家有些许的帮助~~

技术框架

  • 开发工具:vscode
  • 窗口管理:bitsdojo_window: ^0.1.6
  • 托盘管理:system_tray: ^2.0.3
  • 路由/状态管理:get: ^4.6.6
  • 本地存储:get_storage: ^2.1.1
  • 图片预览插件:photo_view: ^0.14.0
  • 网址预览:url_launcher: ^6.2.4
  • 视频组件:media_kit: ^1.1.10+1
  • 文件选择器:file_picker: ^6.1.1

已经实现了聊天消息、通讯录、收藏、朋友圈、短视频、我的等页面模块。

项目结构目录

// 创建flutter项目模板
flutter create win_proj
// 运行到桌面
flutter run -d windows

注意:开发之前需要自行配置好flutter sdkdart sdk环境。

https://flutter.dev/
https://www.dartcn.com/

整体采用 bitsdojo_window 插件进行窗口管理。支持自定义窗口尺寸及系统操作按钮(最大化/最小化/关闭)。

https://pub-web.flutter-io.cn/packages/bitsdojo_window

flutter桌面端通过 system_tray 插件,生成系统托盘图标。

https://pub-web.flutter-io.cn/packages/system_tray

main.dart入口配置

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:media_kit/media_kit.dart';
import 'package:system_tray/system_tray.dart';

import 'utils/index.dart';

// 引入公共样式
import 'styles/index.dart';

// 引入公共布局模板
import 'layouts/index.dart';

// 引入路由配置
import 'router/index.dart';

void main() async {
  // 初始化get_storage存储类
  await GetStorage.init();

  // 初始化media_kit视频套件
  WidgetsFlutterBinding.ensureInitialized();
  MediaKit.ensureInitialized();

  initSystemTray();

  runApp(const MyApp());

  // 初始化bitsdojo_window窗口
  doWhenWindowReady(() {
    appWindow.size = const Size(850, 620);
    appWindow.minSize = const Size(700, 500);
    appWindow.alignment = Alignment.center;
    appWindow.title = 'Flutter3-WinChat';
    appWindow.show();
  });
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'FLUTTER3 WINCHAT',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primaryColor: FStyle.primaryColor,
        useMaterial3: true,
        // 修正windows端字体粗细不一致
        fontFamily: Platform.isWindows ? 'Microsoft YaHei' : null,
      ),
      home: const Layout(),
      // 初始路由
      initialRoute: Utils.isLogin() ? '/index' :'/login',
      // 路由页面
      getPages: routes,
      onInit: () {},
      onReady: () {},
    );
  }
}

// 创建系统托盘图标
Future<void> initSystemTray() async {
  String trayIco = 'assets/images/tray.ico';
  SystemTray systemTray = SystemTray();

  // 初始化系统托盘
  await systemTray.initSystemTray(
    title: 'system-tray',
    iconPath: trayIco,
  );

  // 右键菜单
  final Menu menu = Menu();
  await menu.buildFrom([
    MenuItemLabel(label: 'show', onClicked: (menuItem) => appWindow.show()),
    MenuItemLabel(label: 'hide', onClicked: (menuItem) => appWindow.hide()),
    MenuItemLabel(label: 'close', onClicked: (menuItem) => appWindow.close()),
  ]);
  await systemTray.setContextMenu(menu);

  // 右键事件
  systemTray.registerSystemTrayEventHandler((eventName) {
    debugPrint('eventName: $eventName');
    if (eventName == kSystemTrayEventClick) {
      Platform.isWindows ? appWindow.show() : systemTray.popUpContextMenu();
    } else if (eventName == kSystemTrayEventRightClick) {
      Platform.isWindows ? systemTray.popUpContextMenu() : appWindow.show();
    }
  });
}

flutter自定义右上角操作按钮组

@override
Widget build(BuildContext context){
  return Row(
    children: [
      Container(
        child: widget.leading,
      ),
      Visibility(
        visible: widget.minimizable,
        child: MouseRegion(
          cursor: SystemMouseCursors.click,
          child: SizedBox(
            width: 32.0,
            height: 36.0,
            child: MinimizeWindowButton(colors: buttonColors, onPressed: handleMinimize,),
          )
        ),
      ),
      Visibility(
        visible: widget.maximizable,
        child: MouseRegion(
          cursor: SystemMouseCursors.click,
          child: SizedBox(
            width: 32.0,
            height: 36.0,
            child: isMaximized ? 
            RestoreWindowButton(colors: buttonColors, onPressed: handleMaxRestore,)
            : 
            MaximizeWindowButton(colors: buttonColors, onPressed: handleMaxRestore,),
          ),
        ),
      ),
      Visibility(
        visible: widget.closable,
        child: MouseRegion(
          cursor: SystemMouseCursors.click,
          child: SizedBox(
            width: 32.0,
            height: 36.0,
            child: CloseWindowButton(colors: closeButtonColors, onPressed: handleExit,),
          ),
        ),
      ),
      Container(
        child: widget.trailing,
      ),
    ],
  );
}
// 最小化
void handleMinimize() {
  appWindow.minimize();
}
// 设置最大化/恢复
void handleMaxRestore() {
  appWindow.maximizeOrRestore();
}
// 关闭
void handleExit() {
  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        content: const Text('是否最小化至托盘,不退出程序?', style: TextStyle(fontSize: 16.0),),
        backgroundColor: Colors.white,
        surfaceTintColor: Colors.white,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3.0)),
        elevation: 3.0,
        actionsPadding: const EdgeInsets.all(15.0),
        actions: [
          TextButton(
            onPressed: () {
              Get.back();
              appWindow.close();
            },
            child: const Text('退出', style: TextStyle(color: Colors.red),)
          ),
          TextButton(
            onPressed: () {
              Get.back();
              appWindow.hide();
            },
            child: const Text('最小化至托盘', style: TextStyle(color: Colors.deepPurple),)
          ),
        ],
      );
    }
  );
}

通过flutter内置的WidgetsBindingObserver来监测窗口变化。

class _WinbtnState extends State<Winbtn> with WidgetsBindingObserver {
  // 是否最大化
  bool isMaximized = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  // 监听窗口尺寸变化
  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() {
        isMaximized = appWindow.isMaximized;
      });
    });
  }

  // ...
}

flutter布局模板

项目整体参照微信客户端,分为如下几个大模块。

class Layout extends StatefulWidget {
  const Layout({
    super.key,
    this.activitybar = const Activitybar(),
    this.sidebar,
    this.workbench,
    this.showSidebar = true,
  });

  final Widget? activitybar; // 左侧操作栏
  final Widget? sidebar; // 侧边栏
  final Widget? workbench; // 右侧工作面板
  final bool showSidebar; // 是否显示侧边栏

  @override
  State<Layout> createState() => _LayoutState();
}
return Scaffold(
  backgroundColor: Colors.grey[100],
  body: Flex(
    direction: Axis.horizontal,
    children: [
      // 左侧操作栏
      MoveWindow(
        child: widget.activitybar,
        onDoubleTap: () => {},
      ),
      // 侧边栏
      Visibility(
        visible: widget.showSidebar,
        child: SizedBox(
          width: 270.0,
          child: Container(
            decoration: const BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
                colors: [
                  Color(0xFFEEEBE7), Color(0xFFEEEEEE)
                ]
              ),
            ),
            child: widget.sidebar,
          ),
        ),
      ),
      // 主体容器
      Expanded(
        child: Column(
          children: [
            WindowTitleBarBox(
              child: Row(
                children: [
                  Expanded(
                    child: MoveWindow(),
                  ),
                  // 右上角操作按钮组
                  Winbtn(
                    leading: Row(
                      children: [
                        IconButton(onPressed: () {}, icon: const Icon(Icons.auto_fix_high), iconSize: 14.0,),
                        IconButton(
                          onPressed: () {
                            setState(() {
                              winTopMost = !winTopMost;
                            });
                          },
                          tooltip: winTopMost ? '取消置顶' : '置顶',
                          icon: const Icon(Icons.push_pin_outlined),
                          iconSize: 14.0,
                          highlightColor: Colors.transparent, // 点击水波纹颜色
                          isSelected: winTopMost ? true : false, // 是否选中
                          style: ButtonStyle(
                            visualDensity: VisualDensity.compact,
                            backgroundColor: MaterialStateProperty.all(winTopMost ? Colors.grey[300] : Colors.transparent),
                            shape: MaterialStatePropertyAll(
                              RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0))
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            // 右侧工作面板
            Expanded(
              child: Container(
                child: widget.workbench,
              ),
            ),
          ],
        ),
      ),
    ],
  ),
);
@override
Widget build(BuildContext context) {
  return Container(
    width: 54.0,
    decoration: const BoxDecoration(
      color: Color(0xFF2E2E2E),
    ),
    child: NavigationRail(
      backgroundColor: Colors.transparent,
      labelType: NavigationRailLabelType.none, // all 显示图标+标签 selected 只显示激活图标+标签 none 不显示标签
      indicatorColor: Colors.transparent, // 去掉选中椭圆背景
      indicatorShape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(0.0),
      ),
      unselectedIconTheme: const IconThemeData(color: Color(0xFF979797), size: 24.0),
      selectedIconTheme: const IconThemeData(color: Color(0xFF07C160), size: 24.0,),
      unselectedLabelTextStyle: const TextStyle(color: Color(0xFF979797),),
      selectedLabelTextStyle: const TextStyle(color: Color(0xFF07C160),),
      // 头部(图像)
      leading: GestureDetector(
        onPanStart: (details) => {},
        child: Container(
          margin: const EdgeInsets.only(top: 30.0, bottom: 10.0),
          child: InkWell(
            child: Image.asset('assets/images/avatar/uimg1.jpg', height: 36.0, width: 36.0,),
            onTapDown: (TapDownDetails details) {
              cardDX = details.globalPosition.dx;
              cardDY = details.globalPosition.dy;
            },
            onTap: () {
              showCardDialog(context);
            },
          ),
        ),
      ),
      // 尾部(链接)
      trailing: Expanded(
        child: Container(
          margin: const EdgeInsets.only(bottom: 10.0),
          child: GestureDetector(
            onPanStart: (details) => {},
            child: Column(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                IconButton(icon: Icon(Icons.info_outline, color: Color(0xFF979797), size: 24.0), onPressed:(){showAboutDialog(context);}),
                PopupMenuButton(
                  icon: const Icon(Icons.menu, color: Color(0xFF979797), size: 24.0,),
                  offset: const Offset(54.0, 0.0),
                  tooltip: '',
                  color: const Color(0xFF353535),
                  surfaceTintColor: Colors.transparent,
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
                  padding: EdgeInsets.zero,
                  itemBuilder: (BuildContext context) {
                    return <PopupMenuItem>[
                      popupMenuItem('我的私密空间', 0),
                      popupMenuItem('锁定', 1),
                      popupMenuItem('意见反馈', 2),
                      popupMenuItem('设置', 3),
                    ];
                  },
                  onSelected: (value) {
                    switch(value) {
                      case 0:
                        Get.toNamed('/my');
                        break;
                      case 3:
                        Get.toNamed('/setting');
                        break;
                    }
                  },
                ),
              ],
            ),
          ),
        ),
      ),
      selectedIndex: tabCur,
      destinations: [
        ...tabNavs
      ],
      onDestinationSelected: (index) {
        setState(() {
          tabCur = index;
          if(tabRoute[index] != null && tabRoute[index]?['path'] != null) {
            Get.toNamed(tabRoute[index]['path']);
          }
        });
      },
    ),
  );
}

flutter3短视频模板

// flutter3短视频模板
Container(
  width: MediaQuery.of(context).size.height * 9 / 16,
  decoration: const BoxDecoration(
    color: Colors.black,
  ),
  child: Stack(
    children: [
      // Swiper垂直滚动区域
      PageView(
        // 自定义滚动行为(支持桌面端滑动、去掉滚动条槽)
        scrollBehavior: SwiperScrollBehavior().copyWith(scrollbars: false),
        scrollDirection: Axis.vertical,
        controller: pageController,
        onPageChanged: (index) {
          // 暂停(垂直滑动)
          controller.player.pause();
        },
        children: [
          Stack(
            children: [
              // 视频区域
              Positioned(
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                child: GestureDetector(
                  child: Stack(
                    children: [
                      // 短视频插件
                      Video(
                        controller: controller,
                        fit: BoxFit.cover,
                        // 无控制条
                        controls: NoVideoControls,
                      ),
                      // 播放/暂停按钮
                      Center(
                        child: IconButton(
                          onPressed: () {
                            controller.player.playOrPause();
                          },
                          icon: StreamBuilder(
                            stream: controller.player.stream.playing,
                            builder: (context, playing) {
                              return Visibility(
                                visible: playing.data == false,
                                child: Icon(
                                  playing.data == true ? Icons.pause : Icons.play_arrow_rounded,
                                  color: Colors.white70,
                                  size: 50,
                                ),
                              );
                            },
                          ),
                        ),
                      ),
                    ],
                  ),
                  onTap: () {
                    controller.player.playOrPause();
                  },
                ),
              ),
              // 右侧操作栏
              Positioned(
                bottom: 70.0,
                right: 10.0,
                child: Column(
                  children: [
                    // ...
                  ],
                ),
              ),
              // 底部信息区域
              Positioned(
                bottom: 30.0,
                left: 15.0,
                right: 80.0,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // ...
                  ],
                ),
              ),
              // 播放mini进度条
              Positioned(
                bottom: 15.0,
                left: 15.0,
                right: 15.0,
                child: Container(
                  // ...
                ),
              ),
            ],
          ),
          Container(
            color: Colors.black,
            child: const Center(child: Text('1', style: TextStyle(color: Colors.white, fontSize: 60),),)
          ),
          Container(
            color: Colors.black,
            child: const Center(child: Text('2', style: TextStyle(color: Colors.white, fontSize: 60),),)
          ),
          Container(
            color: Colors.black,
            child: const Center(child: Text('3', style: TextStyle(color: Colors.white, fontSize: 60),),)
          ),
        ],
      ),
      // 固定tab菜单
      Align(
        alignment: Alignment.topCenter,
        child: DefaultTabController(
          length: 3,
          child: TabBar(
            tabs: const [
              Tab(text: '推荐'),
              Tab(text: '关注'),
              Tab(text: '同城'),
            ],
            tabAlignment: TabAlignment.center,
            overlayColor: MaterialStateProperty.all(Colors.transparent),
            unselectedLabelColor: Colors.white70,
            labelColor: const Color(0xff0091ea),
            indicatorColor: const Color(0xff0091ea),
            indicatorSize: TabBarIndicatorSize.label,
            dividerHeight: 0,
            indicatorPadding: const EdgeInsets.all(5),
          ),
        ),
      ),
    ],
  ),
),

flutter_winchat项目涵盖了很多flutter知识点,限于篇幅不能一一详细分享了。

希望以上的分享内容,对小伙伴们有些帮助。

相关推荐

自学Python,写一个挨打的游戏代码来初识While循环

自学Python的第11天。旋转~跳跃~,我~闭着眼!学完循环,沐浴着while的光芒,闲来无事和同事一起扯皮,我说:“编程语言好神奇,一个小小的循环,竟然在生活中也可以找到原理和例子”,同事也...

常用的 Python 工具与资源,你知道几个?

最近几年你会发现,越来越多的人开始学习Python,工欲善其事必先利其器,今天纬软小编就跟大家分享一些常用的Python工具与资源,记得收藏哦!不然下次就找不到我了。1、PycharmPychar...

一张思维导图概括Python的基本语法, 一周的学习成果都在里面了

一周总结不知不觉已经自学Python一周的时间了,这一周,从认识Python到安装Python,再到基本语法和基本数据类型,对于小白的我来说无比艰辛的,充满坎坷。最主要的是每天学习时间有限。只...

三日速成python?打工人,小心钱包,别当韭菜

随着人工智能的热度越来越高,许多非计算机专业的同学们也都纷纷投入到学习编程的道路上来。而Python,作为一种相对比较容易上手的语言,也越来越受欢迎。网络上各类网课层出不穷,各式广告令人眼花缭乱。某些...

Python自动化软件测试怎么学?路线和方法都在这里了

Python自动化测试是指使用Python编程语言和相关工具,对软件系统进行自动化测试的过程。学习Python自动化测试需要掌握以下技术:Python编程语言:学习Python自动化测试需要先掌握Py...

Python从放弃到入门:公众号历史文章爬取为例谈快速学习技能

这篇文章不谈江流所专研的营销与运营,而聊一聊技能学习之路,聊一聊Python这门最简单的编程语言该如何学习,我完成的第一个Python项目,将任意公众号的所有历史文章导出成PDF电子书。或许我这个Py...

【黑客必会】python学习计划

阅读Python文档从Python官方网站上下载并阅读Python最新版本的文档(中文版),这是学习Python的最好方式。对于每个新概念和想法,请尝试运行一些代码片段,并检查生成的输出。这将帮助您更...

公布了!2025CDA考试安排

CDA数据分析师报考流程数据分析师是指在不同行业中专门从事行业数据搜集、整理、分析依据数据作出行业研究评估的专业人员CDA证书分为1-3级,中英文双证就业面广,含金量高!!?报考条件:满18...

一文搞懂全排列、组合、子集问题(经典回溯递归)

原创公众号:【bigsai】头条号:程序员bigsai前言Hello,大家好,我是bigsai,longtimenosee!在刷题和面试过程中,我们经常遇到一些排列组合类的问题,而全排列、组合...

「西法带你学算法」一次搞定前缀和

我花了几天时间,从力扣中精选了五道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。467.环绕字符串中唯一的子字符串[1](中等)795.区...

平均数的5种方法,你用过几种方法?

平均数,看似很简单的东西,其实里面包含着很多学问。今天,分享5种经常会用到的平均数方法。1.算术平均法用到最多的莫过于算术平均法,考试平均分、平均工资等等,都是用到这个。=AVERAGE(B2:B11...

【干货收藏】如何最简单、通俗地理解决策树分类算法?

决策树(Decisiontree)是基于已知各种情况(特征取值)的基础上,通过构建树型决策结构来进行分析的一种方式,是常用的有监督的分类算法。决策树算法是机器学习中的一种经典算法,它通过一系列的规则...

面试必备:回溯算法详解

我们刷leetcode的时候,经常会遇到回溯算法类型题目。回溯算法是五大基本算法之一,一般大厂也喜欢问。今天跟大家一起来学习回溯算法的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~什么是回溯...

「机器学习」决策树——ID3、C4.5、CART(非常详细)

决策树是一个非常常见并且优秀的机器学习算法,它易于理解、可解释性强,其可作为分类算法,也可用于回归模型。本文将分三篇介绍决策树,第一篇介绍基本树(包括ID3、C4.5、CART),第二篇介绍Ran...

大话AI算法: 决策树

所谓的决策树算法,通俗的说就是建立一个树形的结构,通过这个结构去一层一层的筛选判断问题是否好坏的算法。比如判断一个西瓜是否好瓜,有20条西瓜的样本提供给你,让你根据这20条(通过机器学习)建立起...