这么多年了,官方终于正式把多窗口 API 单独拿出来说了,两年前 Canonical 开始负责 Flutter PC 端的开发和维护之后,多窗口也一直是它在推进,目前的完成的设计主要围绕五种窗口类型展开 regular、dialog、tooltip、 popup 和 satellite 窗口:

其中普通的 Regular 常规窗口就不用说了,基本默认的多窗口下都会用到类似的窗口能力:

其次是 Popup 窗口,Popup 主要提供类似独立窗口菜单的功能,属于子窗口,可以有输入焦点,用户可以通过方向键在下拉菜单里切换选中目标,另外还会内部还会强制要求 Popup 窗口保持可见,不会因为弹出位置导致窗口出现在屏幕之外被裁剪:

然后就是 Tooltip,它和于弹出窗口很类似,区别在于它们没有输入焦点,一般用来展示一些提示信息,比如当鼠标悬停在某个控件上时,就提示控件的作用:

Dialog 也很常见,属于子窗口,分模态和非模态,当对话框通过模态方式显示在另一个窗口里面时, 对话框窗口关闭之前,窗口会禁止获得焦点。:

Satellite 窗口也是月中很常见的辅助弹出框,它可以保持相对于父窗口的位置, 父窗口移动和调整大小时它们也会跟随变化, Satellite 还有具备对接能力,也就是它们可以从漂浮式 Satellite 窗口转到嵌入到主窗口内部:

而这里这些窗口类型目前都存在于一个窗口层级结构里,比如一个 App 可以在它 Root 里设置一个主窗口(普通窗口),然后在它下面嵌套 Pop 窗口和 Dialog ,对话框内部还可以嵌套 ToolTip。
目前这些多窗口 API 已经在 master 可以用,我自己也用了一段时间,其实用 main 分支没什么不好的:
css
flutter channel main
flutter upgrade
flutter config --enable-windowing
配置后你就可以创建原生窗口,要创建一个窗口,我们首先需要创建一个 WindowController,这个 controller 主要和底层平台交互,用来创建和更新窗口:
php
final controller = WindowController(
title: 'My Application',
size: const Size(800, 600),
);
控制器会收到窗口的初始配置信息,比如窗口大小和标题等,然后每种窗口类型都有自己的窗口控制器,比如如果要创建一个对话框窗口:
php
final dialogController = DialogWindowController(
title: 'My Dialog',
size: const Size(400, 300),
parent: parentController,
);
这里和普通窗口不同在于,
DialogWindowController有一个可选的父窗口控制器。
然后控制器创建完成后,就可以直接用于后续修改窗口,例如我们可以修改之前创建的常规窗口的标题、大小和销毁:
ini
controller.setTitle('Hello, world!');
controller.setSize(const Size.square(1000));
controller.destroy();
然后就是渲染到窗口,这里需要同样需要将控制器和要渲染的内容传递给 Window 组件:
scss
Widget build(BuildContext context) {
return Window(
controller: controller,
child: MyPage(),
);
}
每种窗口类型都有其对应的控件,比如:
- 对于普通窗口用
Window控件 - 对于对话框窗口用
DialogWindow控件
然后因为有窗口都位于同一个控件树,所以实际上可以做到跨窗口共享状态。
然后如果你需要在窗口监听事件,比如接受窗口关闭通知,目前有两种获取窗口状态的方式:通过 WindowControllerDelegate 或者 WindowScope ,比如:
scala
// Create the class first...
class MyWindowDelegate with WindowControllerDelegate {
@override
void onWindowDestroyed() {
super.onWindowDestroyed();
ServicesBinding.instance.exitApplication(AppExitType.required);
}
}
// and then pass it to the controller constructor.
final controller = WindowController(
title: 'My Application',
size: const Size(800, 600),
delegate: MyWindowDelegate(),
);
另外通过 WindowScope 也可以做到类似监听,你可以通过 WindowScope.of 访问 Scope ,然后获取对应状态:
scala
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = WindowScope.titleOf(context);
// ... do something with the window title
}
}
整个 Demo 大概类似:
scala
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: implementation_imports
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/src/widgets/_window.dart';
import 'package:flutter/widgets.dart';
/// Exits the application when the user closes the window.
class ExitOnCloseDelegate with WindowControllerDelegate {
@override
void onWindowCloseRequested(WindowController controller) {
ServicesBinding.instance.exitApplication(AppExitType.required);
}
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
runWidget(const HelloWindow());
}
/// Displays a window and owns its [WindowController].
class HelloWindow extends StatefulWidget {
const HelloWindow({super.key});
@override
State<HelloWindow> createState() => _HelloWindowState();
}
class _HelloWindowState extends State<HelloWindow> {
final WindowController _controller = WindowController(
size: const Size(600, 400),
title: 'MyApp',
delegate: ExitOnCloseDelegate(),
);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Window(
controller: _controller,
child: const Directionality(
textDirection: TextDirection.ltr,
child: ColoredBox(
color: Color(0xFFFFFFFF),
child: Center(
child: Text(
'Hello, Window',
style: TextStyle(color: Color(0xFF000000), fontSize: 24),
),
),
),
),
);
}
}
不过需要注意的是,多窗口现在用的不是 runApp,你需要用 runWidget 替代:
csharp
void main() {
WidgetsFlutterBinding.ensureInitialized();
runWidget(const MultiWindowApp());
}
总体来说,目前 Flutter 的多窗口从我的体验上已经可以直接用了,虽然有小问题,但是不会很影响主流程,至少 Window 和 macOS 上还行,Linux 我就没测试过了。