查看: 253|回复: 0

Flutter Windows桌面窗口化与Win32注册表操作实战

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
Flutter 做桌面应用时,窗口并不是“写个页面就完事”。Windows 端由 Win32 窗口承载 Flutter engine,窗口样式、标题栏、阴影圆角、单例、注册表读写、调用系统控制台,都需要单独处理。本文基于一次从 0 到 1 的桌面项目实践,梳理 Flutter 在 Windows(以及 Android 大屏桌面设备)上做窗口化时真正会踩到的技术点。

一、Windows 端窗口从哪里来

Flutter 的 Windows 模板会在原生层创建一个 Win32Window,再由 FlutterWindow 创建 FlutterViewController 并挂载引擎视图。模板里的创建逻辑大致如下:
  1. bool Win32Window::CreateAndShow(const std::wstring& title,
  2. const Point& origin,
  3. const Size& size) {
  4. HWND window = CreateWindow(
  5. window_class, title.c_str(), WS_POPUP | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
  6. Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
  7. Scale(size.width, scale_factor), Scale(size.height, scale_factor),
  8. nullptr, nullptr, GetModuleHandle(nullptr), this);
  9. UpdateWindow(window);
  10. if (!window) {
  11. return false;
  12. }
  13. return OnCreate();
  14. }
  15. bool FlutterWindow::OnCreate() {
  16. if (!Win32Window::OnCreate()) {
  17. return false;
  18. }
  19. RECT frame = GetClientArea();
  20. flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
  21. frame.right - frame.left, frame.bottom - frame.top, project_);
  22. if (!flutter_controller_->engine() || !flutter_controller_->view()) {
  23. return false;
  24. }
  25. RegisterPlugins(flutter_controller_->engine());
  26. SetChildContent(flutter_controller_->view()->GetNativeWindow());
  27. return true;
  28. }
复制代码

也就是说,Windows 平台天然就是窗口化的,Flutter 不需要像移动端那样“造窗口”。但反过来说,Flutter 作为 UI 框架也无法直接修改 Win32 窗口的外观,标题栏、边框这些仍然要回到原生层或窗口管理插件处理。

二、去掉系统标题栏并让窗口透明居中

模板创建出来的窗口默认带系统导航栏。实际项目通常需要自定义导航栏,因此要先把原生标题栏隐藏。原文使用的是 leanFlutter 社区提供的 window_manager,版本为 ^0.2.6。在 runApp 之前完成初始化:
  1. WindowManager w = WindowManager.instance;
  2. await w.ensureInitialized();
  3. WindowOptions windowOptions = WindowOptions(
  4. size: normalWindowSize,
  5. center: true,
  6. titleBarStyle: TitleBarStyle.hidden
  7. );
  8. w.waitUntilReadyToShow(windowOptions, () async {
  9. await w.setBackgroundColor(Colors.transparent);
  10. await w.show();
  11. await w.focus();
  12. await w.setAsFrameless();
  13. });
复制代码

这里有一个容易忽略的现象:应用启动时会在左下角闪一下再居中。原因是原生 Win32 窗口默认先在左上角显示,之后才由插件完成居中。处理思路是在原生代码里先让窗口不显示,等 window_manager.show() 时再展示,避免用户看到位置跳变。

三、Android 大屏桌面设备的窗口化思路

如果目标设备是 Android 大屏桌面设备,窗口化要麻烦一些。Activity 默认全屏,而且出于安全考虑不允许点击穿透。原文的做法是把承载 Flutter 的 Activity 主题改成 Dialog,同时让窗口背景透明,并禁止点击外部关闭:
  1. <style name="Theme.DialogApp" parent="Theme.AppCompat.Light.Dialog">
  2. <item name="android:windowBackground">@drawable/launch_application</item>
  3. <item name="android:windowIsTranslucent">true</item>
  4. <item name="android:windowContentOverlay">@null</item>
  5. <item name="android:backgroundDimEnabled">false</item>
  6. <item name="windowActionBar">false</item>
  7. <item name="windowNoTitle">true</item>
  8. </style>
复制代码

Activity 清单中把 theme 指向该样式,并在 FlutterActivity 中返回透明模式、设置 setFinishOnTouchOutside(false),再把窗口宽高设为全屏:
  1. class MainActivity : FlutterActivity() {
  2. override fun getTransparencyMode(): TransparencyMode {
  3. return TransparencyMode.transparent
  4. }
  5. override fun onResume() {
  6. super.onResume()
  7. setFinishOnTouchOutside(false)
  8. var lp = window.attributes
  9. lp.width = -1
  10. lp.height = -1
  11. window.attributes = lp
  12. }
  13. }
复制代码

这样得到的是一个全屏透明窗口,真正的“窗口”由 Flutter 侧的盒子控件负责边距和阴影。原文在 MaterialApp 外层套了 GlobalBoxManager,通过 Container 设置水平和垂直留白,让应用内容看起来像悬浮窗口。

四、窗口阴影、圆角与自定义导航栏

窗口外观确定后,美化就交给 Flutter。原文在 MaterialApp 外层再包一层 Container,用 clipBehavior 裁剪、margin 留白、borderRadius 做圆角、boxShadow 做阴影;Android 和桌面平台通过 Platform.isAndroid 区分边距。

导航栏则回到 Scaffold 的 AppBar,用 PreferredSize 固定高度,并在外层套 GestureDetector 处理拖拽。Windows 平台可以调用 windowManager.startDragging() 实现拖动窗口:
  1. appBar: PreferredSize(
  2. preferredSize: const Size.fromHeight(64),
  3. child: GestureDetector(
  4. behavior: HitTestBehavior.translucent,
  5. onPanStart: (details) {
  6. if (Platform.isWindows) windowManager.startDragging();
  7. },
  8. onDoubleTap: () {},
  9. child: AppBar(
  10. title: Text(widget.title),
  11. centerTitle: true,
  12. actions: [
  13. GestureDetector(
  14. behavior: HitTestBehavior.opaque,
  15. child: const Padding(
  16. padding: EdgeInsets.symmetric(horizontal: 16),
  17. child: Icon(Icons.close, size: 24),
  18. ),
  19. ),
  20. ],
  21. ),
  22. ),
  23. ),
复制代码

这样一套代码可以在多个平台得到一致的窗口效果,这也是选择用 Flutter 处理窗口外观而不是全部写原生代码的原因:跨平台复用。

五、Windows 平台交互:注册表读写

桌面应用经常需要读写注册表。Dart 已经通过 win32 包暴露了相关 Win32 API,原文没有额外引入注册表库,而是直接用 dart:ffi 封装了一个工具类。涉及的关键 API 包括 RegOpenKeyEx、RegEnumValue、RegEnumKeyEx、RegSetKeyValue 和 RegDeleteKey。

设置注册表值使用 RegSetKeyValue,字符串类型为 REG_SZ:
  1. static setRegeditValue(String regPath, String key, String value,
  2. {int hKeyValue = HKEY_CURRENT_USER}) {
  3. final phKey = calloc<HANDLE>();
  4. final lpKeyPath = regPath.toNativeUtf16();
  5. final lpKey = key.toNativeUtf16();
  6. final lpValue = value.toNativeUtf16();
  7. try {
  8. if (RegSetKeyValue(hKeyValue, lpKeyPath, lpKey, REG_SZ, lpValue,
  9. lpValue.length * 2) != ERROR_SUCCESS) {
  10. throw Exception("Can't set registry key");
  11. }
  12. return phKey.value;
  13. } finally {
  14. free(phKey);
  15. free(lpKeyPath);
  16. free(lpKey);
  17. free(lpValue);
  18. RegCloseKey(HKEY_CURRENT_USER);
  19. }
  20. }
复制代码

读取某个键下的值则通过 RegEnumValue 逐个枚举,并根据 lpType 判断类型:REG_DWORD 转成 Uint32 再转字符串,REG_SZ 转成 Utf16 字符串。遇到 ERROR_NO_MORE_ITEMS 说明枚举结束,ERROR_MORE_DATA 则说明缓冲区不够。枚举子键使用 RegEnumKeyEx,删除子键使用 RegDeleteKey。原文特别提示:部分注册表操作需要管理员权限,应用提权逻辑要提前做好。

六、执行系统控制台指令与应用单例

Windows 上很多系统能力可以直接通过控制台命令或系统面板打开。Dart 的 io 库提供了 Process 类,例如打开网络连接面板:
  1. Process.start('ncpa.cpl', [], runInShell: true);
复制代码

另一个桌面端必须处理的问题是应用单例。Windows 默认每次点击图标都会启动新进程,而 Android 默认就是单例。原文使用 windows_single_instance 插件,在 runApp 之前执行:
  1. static setSingleInstance(List<String> args) async {
  2. await WindowsSingleInstance.ensureSingleInstance(args, "desktop_open",
  3. onSecondWindow: (args) async {
  4. if (await windowManager.isMinimized()) await windowManager.restore();
  5. windowManager.focus();
  6. });
  7. }
复制代码

再次启动时不会多开进程,而是把已有窗口恢复并置顶,符合桌面用户的操作预期。

七、桌面交互习惯的两个细节

第一是按钮点击态。移动端常见的水波纹在小屏幕上够用,但桌面显示区域大、鼠标指针小,水波纹往往来不及被用户感知。桌面应用更合适的做法是按下立即改变按钮颜色(文字和背景均可),松开恢复。第二是页面切换和启动参数:桌面窗口尺寸变化大,切换页面时需要考虑不同大小的布局;同时从系统唤起应用时要能拿到启动参数,用于打开指定文件或路由。原文提到这两点在桌面端与移动端的行为差异明显,需要在项目初期就统一处理。

整体来看,Flutter 做 Windows 桌面应用的关键不在于 UI,而在于理解 Win32 窗口的生命周期、窗口管理插件与原生代码的配合边界,以及注册表、进程、单例这些桌面系统能力如何通过 Dart FFI 和插件补齐。把这些基础能力封装好,后续页面开发才能真正复用一套代码。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-29 14:40 , Processed in 0.022011 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部