【完结】Flutter3+Getx仿微信桌面端exe聊天程序
wptr33 2024-12-05 17:01 13 浏览
继上一篇基于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 sdk和dart 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知识点,限于篇幅不能一一详细分享了。
希望以上的分享内容,对小伙伴们有些帮助。
- 上一篇:Flutter——构建界面
- 下一篇:2023 年 Flutter 基础面试题
相关推荐
- C++企业级开发规范指南(c++开发gui)
-
打造高质量、可维护的C++代码标准一、前言C++作为一门功能强大的系统级编程语言,被广泛应用于操作系统、游戏引擎、高性能服务器、数据库系统等领域。知名互联网公司(如Google、Microsoft、腾...
- C++|整型的最值、上溢、下溢、截断、类型提升和转换
-
整数在计算机内以有限字长表示,当超出最值(有限字长)时,需要截断(溢出,求模)操作。不同字长的整型具有不同的值域,混合运算时,需要类型提升和转换。1整形最值在<limit.h>中有整型的...
- C++|漫谈STL细节及内部原理(c++ std stl)
-
1988年,AlexanderStepanov开始进入惠普的PaloAlto实验室工作,在随后的4年中,他从事的是有关磁盘驱动器方面的工作。直到1992年,由于参加并主持了实验室主任BillWo...
- C++11新特性总结 (二)(c++11新特性 pdf)
-
1.范围for语句C++11引入了一种更为简单的for语句,这种for语句可以很方便的遍历容器或其他序列的所有元素vector<int>vec={1,2,3,4,5,6};f...
- C++ STL 漫谈(c++中的stl到底指的什么)
-
标准模板库(StandardTemplateLibrary,STL)是惠普实验室开发的一个函数库和类库。它是由AlexanderStepanov、MengLee和DavidRMusser在...
- C++学习教程_C++语言随到随学_不耽误上班_0基础
-
C++学习教程0基础学C++也可以,空闲时间学习,不耽误上班.2019年C语言新课程已经上线,随到随学,互动性强,效果好!带你征服C++语言,让所有学过和没有学过C++语言的人,或是正准备学习C++语...
- C++遍历vector元素的四种方式(c++ 遍历vector)
-
vector是相同类型对象的集合,集合中的每个对象有个对应的索引。vector常被称为容器(container)。C++中遍历vector的所有元素是相当常用的操作,这里介绍四种方式。1、通过下标访问...
- 一起学习c++11——c++11中的新增的容器
-
c++11新增的容器1:array当时的初衷是希望提供一个在栈上分配的,定长数组,而且可以使用stl中的模板算法。array的用法如下:#include<string>#includ...
- C++编程实战基础篇:一维数组应用之投票统计
-
题目描述班上有N个同学,有五位候选人“A,B,C,D,E”,请所有的同学投票并选举出班长,现在请你编写程序来他们计算候选人的得票总数,每位同学投票将以数字的形式投票“12345”分别代表五位候选人,...
- C++20 新特性(6):new表达式也支持数组大小推导
-
new表达式也支持数组大小推导在C++17标准中,在定义并初始化静态数组时,是可以忽略数组大小,然后通过初始化数据来推导数组的大小。但使用new来定义并初始化动态数组时,并不支持这种自动推导数组大...
- C++ 结构体(struct)最全详解(c++结构体用法)
-
一、定义与声明1.先定义结构体类型再单独进行变量定义structStudent{intCode;charName[20];charSex;intA...
- 自学 C++ 第 6 课 二维数组找最值
-
键盘输入一个m×n的二维数组,通过C++编程找出元素中的最大值,并输出其所在的位置坐标。例如,输入一个4×5的二维数组,数组元素分别为{{556623749},{578964563},...
- 从缺陷中学习C/C++:聊聊 C++ 中常见的内存问题
-
在写C/C++程序时,一提到内存,大多数人会想到内存泄露。内存泄露是一个令人头疼的问题,尤其在开发大的软件系统时。一个经典的现象是,系统运行了10天、1个月都好好的,忽然有一天宕机了:OOM(Out...
- C++开发者都应该使用的十个C++11特性(上)
-
在C++11新标准中,语言本身和标准库都增加了很多新内容,本文只涉及了一些皮毛。不过我相信这些新特性当中有一些,应该成为所有C++开发者的常规装备。你也许看到过许多类似介绍各种C++11特性的文章。下...
- 深度解读C/C++指针与数组(c++指针和数组的区别)
-
指针和数组是密切相关的。事实上,指针和数组在很多情况下是可以互换的。例如,一个指向数组开头的指针,可以通过使用指针的算术运算或数组索引来访问数组。今天我们就来聊一聊数组和指针千丝万缕的关系;一维数组与...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)