Flutter 多窗口支持类型和 API 介绍

这么多年了,官方终于正式把多窗口 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 我就没测试过了。

链接

flutter.dev/to/windowin...

flutter.dev/blog/deskto...

相关推荐
liangshanbo12156 分钟前
面试题:React 中 Lane 和 Scheduler 是怎么配合的?
前端·react.js·前端框架
liangshanbo121515 分钟前
请求拦截器相关面试题
java·前端·javascript
ShawnLiaoking17 分钟前
ADS305 Big Data Lecuture4 Web Scraping
大数据·前端
liangshanbo121517 分钟前
React面试题:Hooks 为什么不能在分支和循环里面写?
前端·javascript·react.js
开开心心就好22 分钟前
视频压缩工具推荐,绿色版免安装双击即用
java·前端·人工智能·spring·智能手机·intellij-idea·excel
西红柿炖牛腩35425 分钟前
GLB 转 OBJ/PLY 后体积没变?3D 模型不减面变小靠什么
前端·javascript·3d
欣欣之王来了32 分钟前
前端开发学习路线图:2026年Vue方向最新版
前端·学习·架构·项目·vue3教程
夏幻灵34 分钟前
前端八股:Vue2 与 Vue3 的核心区别:响应式、Composition API 与性能优化
前端
m0_7381858240 分钟前
Flutter 鸿蒙化实战:media_info 适配 OpenHarmony,媒体信息与缩略图
flutter·华为·harmonyos·鸿蒙·媒体