目前越来越多的人用Flutter来做桌面程序的开发,很多应用场景在Flutter开发端还不是很成熟,有些场景目前还没有很好的插件来支持,所以落地Flutter桌面版还是要慎重。
下面来说一下近期我遇到的一个问题,之前遇到一个需要双屏展示的应用场景,而且双屏还要有交互,下面就介绍这种双屏的功能怎么实现。
首先介绍需要用到的插件:
desktop_multi_window
desktop_multi_window 用于实现一个应用可以打开多个窗口的功能,主要适配macOS、Windows以及Linux系统。
window_size
window_size 是google官方提供的一个插件,用于获取系统所有屏幕的信息,其中最重要的就是可以获取屏幕的位置,这个功能的作用是在使用desktop_multi_window打开一个新窗口时,通过window_size获取副屏的坐标位置,然后直接将新窗口定位到副屏上。
下面贴代码:
import 'dart:convert';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:desktop_lifecycle/desktop_lifecycle.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_multi_window_example/event_widget.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:window_size/window_size.dart';
void main(List<String> args) {
if (args.firstOrNull == 'multi_window') {
final windowId = int.parse(args[1]);
final argument = args[2].isEmpty
? const {}
: jsonDecode(args[2]) as Map<String, dynamic>;
runApp(_ExampleSubWindow(
windowController: WindowController.fromWindowId(windowId),
args: argument,
));
} else {
runApp(const _ExampleMainWindow());
}
}
class _ExampleMainWindow extends StatefulWidget {
const _ExampleMainWindow({Key? key}) : super(key: key);
@override
State<_ExampleMainWindow> createState() => _ExampleMainWindowState();
}
class _ExampleMainWindowState extends State<_ExampleMainWindow> {
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: App(),
);
}
}
class App extends StatefulWidget{
const App({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return AppState();
}
}
class AppState extends State<App>{
List<Screen> screenList=[];
@override
void initState() {
// TODO: implement initState
super.initState();
initDevice();
}
void initDevice()async{
screenList=await getScreenList();
screenList.forEach((element) {
print(element.frame);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Column(
children: [
TextButton(
onPressed: () async {
final window =
await DesktopMultiWindow.createWindow(jsonEncode({
'args1': 'Sub window',
'args2': 100,
'args3': true,
'business': 'business_test',
}));
window
..setFrame( screenList[screenList.length-1].frame)
..setTitle('Another window')
..show();
},
child: const Text('Create a new World!'),
),
TextButton(
child: const Text('Send event to all sub windows'),
onPressed: () async {
final subWindowIds =
await DesktopMultiWindow.getAllSubWindowIds();
for (final windowId in subWindowIds) {
DesktopMultiWindow.invokeMethod(
windowId,
'broadcast',
'Broadcast from main window',
);
}
},
),
Expanded(
child: EventWidget(controller: WindowController.fromWindowId(0)),
)
],
),
);
}
}
class _ExampleSubWindow extends StatelessWidget {
const _ExampleSubWindow({
Key? key,
required this.windowController,
required this.args,
}) : super(key: key);
final WindowController windowController;
final Map? args;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Column(
children: [
if (args != null)
Text(
'Arguments: ${args.toString()}',
style: const TextStyle(fontSize: 20),
),
ValueListenableBuilder<bool>(
valueListenable: DesktopLifecycle.instance.isActive,
builder: (context, active, child) {
if (active) {
return const Text('Window Active');
} else {
return const Text('Window Inactive');
}
},
),
TextButton(
onPressed: () async {
windowController.close();
},
child: const Text('Close this window'),
),
Expanded(child: EventWidget(controller: windowController)),
],
),
),
);
}
}
event_widget.dart
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class EventWidget extends StatefulWidget {
const EventWidget({Key? key, required this.controller}) : super(key: key);
final WindowController controller;
@override
State<EventWidget> createState() => _EventWidgetState();
}
class MessageItem {
const MessageItem({this.content, required this.from, required this.method});
final int from;
final dynamic content;
final String method;
@override
String toString() {
return '$method($from): $content';
}
@override
int get hashCode => Object.hash(from, content, method);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
final MessageItem typedOther = other as MessageItem;
return typedOther.from == from && typedOther.content == content;
}
}
class _EventWidgetState extends State<EventWidget> {
final messages = <MessageItem>[];
final textInputController = TextEditingController();
final windowInputController = TextEditingController();
@override
void initState() {
super.initState();
DesktopMultiWindow.setMethodHandler(_handleMethodCallback);
}
@override
dispose() {
DesktopMultiWindow.setMethodHandler(null);
super.dispose();
}
Future<dynamic> _handleMethodCallback(
MethodCall call, int fromWindowId) async {
if (call.arguments.toString() == "ping") {
return "pong";
}
setState(() {
messages.insert(
0,
MessageItem(
from: fromWindowId,
method: call.method,
content: call.arguments,
),
);
});
}
@override
Widget build(BuildContext context) {
void submit() async {
final text = textInputController.text;
if (text.isEmpty) {
return;
}
final windowId = int.tryParse(windowInputController.text);
textInputController.clear();
final result =
await DesktopMultiWindow.invokeMethod(windowId!, "onSend", text);
debugPrint("onSend result: $result");
}
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: messages.length,
reverse: true,
itemBuilder: (context, index) =>
_MessageItemWidget(item: messages[index]),
),
),
Row(
children: [
SizedBox(
width: 100,
child: TextField(
controller: windowInputController,
decoration: const InputDecoration(
labelText: 'Window ID',
),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
),
Expanded(
child: TextField(
controller: textInputController,
decoration: const InputDecoration(
hintText: 'Enter message',
),
onSubmitted: (text) => submit(),
),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: submit,
),
],
),
],
);
}
}
class _MessageItemWidget extends StatelessWidget {
const _MessageItemWidget({Key? key, required this.item}) : super(key: key);
final MessageItem item;
@override
Widget build(BuildContext context) {
return ListTile(
title: Text("${item.method}(${item.from})"),
subtitle: Text(item.content.toString()),
);
}
}
重点代码位置:
void main(List<String> args) {
if (args.firstOrNull == 'multi_window') {
final windowId = int.parse(args[1]);
final argument = args[2].isEmpty
? const {}
: jsonDecode(args[2]) as Map<String, dynamic>;
runApp(_ExampleSubWindow(
windowController: WindowController.fromWindowId(windowId),
args: argument,
));
} else {
runApp(const _ExampleMainWindow());
}
}
这块是判断显示副屏还是主屏,副屏创建也会走main函数。
void initDevice()async{
screenList=await getScreenList();
screenList.forEach((element) {
print(element.frame);
});
}
这个是用window_size 插件中的getScreenList(),获取系统的所有屏幕信息。
TextButton(
onPressed: () async {
final window =
await DesktopMultiWindow.createWindow(jsonEncode({
'args1': 'Sub window',
'args2': 100,
'args3': true,
'business': 'business_test',
}));
window
..setFrame( screenList[screenList.length-1].frame)
..setTitle('Another window')
..show();
},
child: const Text('Create a new World!'),
),
这块是开启新窗口的代码,其中setFrame( screenList[screenList.length-1].frame)是将副屏的frame给到窗口,这样创建出来的窗口就是直接在副屏的位置,同时是全屏的状态。