Flutter Desktop 的窗口管理:Win32/Cocoa API 的直接调用与窗口句柄操作

好的,现在我们开始。

Flutter Desktop 的窗口管理:Win32/Cocoa API 的直接调用与窗口句柄操作

大家好,今天我们来深入探讨 Flutter Desktop 应用的窗口管理,特别是如何通过直接调用 Win32 (Windows) 和 Cocoa (macOS) API 来进行更精细的控制,以及如何操作窗口句柄。在 Flutter Desktop 开发中,虽然 Flutter 框架提供了一些基本的窗口管理功能,但在某些高级场景下,我们需要更底层的控制能力,例如自定义窗口样式、实现特定的窗口行为等。 这时,直接调用操作系统提供的 API 就显得尤为重要。

1. 为什么需要直接调用 Win32/Cocoa API?

Flutter 框架本身对窗口管理的抽象层级较高,提供的 API 相对有限。以下是一些需要直接调用原生 API 的常见场景:

  • 自定义窗口边框和标题栏: Flutter 默认的窗口样式可能不符合设计要求,需要自定义窗口边框、标题栏按钮等。
  • 透明窗口和无边框窗口: 实现特殊效果,例如毛玻璃效果、悬浮窗口等。
  • 窗口置顶和窗口大小限制: 控制窗口的显示层级和尺寸范围。
  • 监听系统事件: 例如窗口激活/失活、窗口移动/调整大小等事件。
  • 与其他原生应用集成: 需要在 Flutter 应用和原生应用之间共享窗口句柄或进行窗口级别的通信。
  • 托盘图标和通知: 需要在系统托盘中显示图标,并在后台发送通知。

2. 如何在 Flutter 中调用原生 API?

Flutter 提供了 Platform Channels 机制,允许我们在 Dart 代码和原生代码之间进行通信。 对于桌面应用,这意味着我们可以通过 Platform Channels 调用 Win32 (Windows) 或 Cocoa (macOS) API。

2.1 Platform Channels 的基本原理

Platform Channels 允许我们在 Flutter 的 Dart 代码和原生平台代码 (例如 Java/Kotlin for Android, Objective-C/Swift for iOS/macOS, C++ for Windows/Linux) 之间传递消息。 它基于异步消息传递机制,确保 UI 线程的流畅性。

  • Dart (Flutter): 通过 MethodChannel 发送消息,并接收来自原生平台的响应。
  • Native Platform: 注册 MethodChannel,接收来自 Dart 的消息,调用相应的原生 API,并将结果返回给 Dart。

2.2 实现步骤

  1. 定义 MethodChannel: 在 Dart 代码中创建一个 MethodChannel 实例,指定一个唯一的 channel 名称。

    import 'package:flutter/services.dart';
    
    const platform = MethodChannel('my_app/window'); // 使用唯一的 channel 名称
  2. 调用原生方法: 使用 platform.invokeMethod() 方法调用原生平台的方法,并传递参数。

    try {
      final result = await platform.invokeMethod('setWindowTitle', {'title': 'My New Title'});
      print('setWindowTitle result: $result');
    } on PlatformException catch (e) {
      print("Failed to set window title: '${e.message}'.");
    }
  3. 原生平台代码实现: 在原生平台 (Windows/macOS) 的代码中,注册与 Dart 代码中相同的 MethodChannel,并实现相应的处理逻辑。

    Windows (C++):

    #include <flutter_windows.h>
    #include <windows.h>
    #include <iostream>
    
    void RegisterWindowMethods(flutter::PluginRegistrarWindows *registrar) {
      auto channel =
          std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
              registrar->messenger(), "my_app/window",
              &flutter::StandardMethodCodec::GetInstance());
    
      channel->SetMethodCallHandler(
          [registrar](const auto &call, auto result) {
            if (call.method_name().compare("setWindowTitle") == 0) {
              auto args = std::get_if<flutter::EncodableMap>(call.arguments());
              if (args) {
                auto title_arg = args->find(flutter::EncodableValue("title"));
                if (title_arg != args->end()) {
                  std::string title = std::get<std::string>(title_arg->second);
                  HWND window_handle = GetConsoleWindow(); // Or your app's window handle
                  if (window_handle) {
                    SetWindowTextA(window_handle, title.c_str());
                    result->Success(flutter::EncodableValue(true));
                  } else {
                    result->Error("WINDOW_NOT_FOUND", "Window handle not found.");
                  }
                } else {
                  result->Error("INVALID_ARGUMENT", "Missing 'title' argument.");
                }
              } else {
                result->Error("INVALID_ARGUMENT", "Invalid arguments.");
              }
            } else {
              result->NotImplemented();
            }
          });
    }
    
    // In FlutterPluginRegistrar::RegisterWithRegistrar:
    RegisterWindowMethods(registrar);

    macOS (Objective-C):

    #import <FlutterMacOS/FlutterMacOS.h>
    #import <Cocoa/Cocoa.h>
    
    @interface WindowPlugin : NSObject<FlutterPlugin>
    @end
    
    @implementation WindowPlugin
    + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
      FlutterMethodChannel* channel = [FlutterMethodChannel
          methodChannelWithName:@"my_app/window"
                binaryMessenger:[registrar messenger]];
      WindowPlugin* instance = [[WindowPlugin alloc] init];
      [registrar addMethodCallDelegate:instance channel:channel];
    }
    
    - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
      if ([@"setWindowTitle" isEqualToString:call.method]) {
        NSString *title = call.arguments[@"title"];
        if (title) {
          [[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyRegular]; // Make sure the app is active
          [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; // Bring to front
          [[[NSApplication sharedApplication] mainWindow] setTitle:title];
          result(@(YES));
        } else {
          result([FlutterError errorWithCode:@"INVALID_ARGUMENT"
                                     message:@"Missing 'title' argument."
                                     details:nil]);
        }
      } else {
        result(FlutterMethodNotImplemented);
      }
    }
    
    @end

3. 窗口句柄操作

窗口句柄 (Window Handle) 是操作系统用来标识窗口的唯一标识符。 在 Win32 中,窗口句柄是一个 HWND 类型的值;在 Cocoa 中,窗口句柄是 NSWindow 对象的指针。 通过窗口句柄,我们可以直接访问和操作窗口的属性,例如位置、大小、样式等。

3.1 获取窗口句柄

在原生平台代码中,通常可以通过以下方式获取窗口句柄:

  • Windows: 在 Flutter Windows 插件中,你可以使用 GetConsoleWindow() (如果你的应用是控制台应用) 或 GetActiveWindow() 获取当前活动窗口的句柄,或者通过查找窗口类名或窗口标题来获取特定窗口的句柄。 更可靠的方法是在 Flutter view 创建时,将窗口句柄保存下来。
  • macOS: 可以使用 [[NSApplication sharedApplication] mainWindow] 获取主窗口的 NSWindow 对象。

3.2 使用窗口句柄进行操作

3.2.1 Windows (Win32 API)

以下是一些常用的 Win32 API 函数,可以用于操作窗口:

函数名 功能
SetWindowPos 设置窗口的位置、大小、Z 顺序。
ShowWindow 显示或隐藏窗口。
GetWindowRect 获取窗口的矩形区域。
SetWindowLongPtr 设置窗口的扩展样式。
GetWindowLongPtr 获取窗口的扩展样式。
SetWindowText 设置窗口标题。
GetSystemMenu 获取系统菜单的句柄,可以用于自定义系统菜单。
DrawMenuBar 重新绘制菜单栏。
SetParent 设置窗口的父窗口。
EnableWindow 启用或禁用窗口。
MoveWindow 设置窗口的位置和大小

示例:设置窗口置顶

#include <flutter_windows.h>
#include <windows.h>

void SetWindowTopMost(HWND window_handle, bool top_most) {
  SetWindowPos(window_handle, top_most ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}

在 Dart 代码中调用:

try {
  await platform.invokeMethod('setWindowTopMost', {'topMost': true});
} on PlatformException catch (e) {
  print("Failed to set window top most: '${e.message}'.");
}

3.2.2 macOS (Cocoa API)

以下是一些常用的 Cocoa API 方法,可以用于操作窗口:

方法名 功能
setFrame:display: 设置窗口的位置和大小。
makeKeyAndOrderFront: 将窗口显示在最前面并使其成为活动窗口。
orderFront: 将窗口显示在最前面。
orderBack: 将窗口置于后面。
setStyleMask: 设置窗口的样式掩码,可以用于自定义窗口边框、标题栏等。
styleMask 获取窗口的样式掩码。
setTitle: 设置窗口标题。
contentView 获取窗口的内容视图,可以在此视图上添加 Flutter 渲染的内容。
setMovableByWindowBackground: 允许通过拖动窗口背景来移动窗口。
setOpaque: 设置窗口是否不透明

示例:设置窗口透明度

#import <FlutterMacOS/FlutterMacOS.h>
#import <Cocoa/Cocoa.h>

- (void)setWindowOpacity:(NSNumber *)opacity {
  NSWindow *window = [[NSApplication sharedApplication] mainWindow];
  [window setAlphaValue:[opacity floatValue]];
  [window setOpaque:NO];
  [window setBackgroundColor:[NSColor clearColor]];
}

在 Dart 代码中调用:

try {
  await platform.invokeMethod('setWindowOpacity', {'opacity': 0.5});
} on PlatformException catch (e) {
  print("Failed to set window opacity: '${e.message}'.");
}

4. 注意事项

  • 线程安全: 在原生平台代码中,窗口操作通常需要在主线程 (UI 线程) 中进行。 确保在 Platform Channel 的回调中将窗口操作调度到主线程。
  • 错误处理: 原生 API 调用可能会失败,例如窗口句柄无效、权限不足等。 务必进行错误处理,并将错误信息返回给 Dart 代码。
  • 内存管理: 在 Objective-C 中,需要注意内存管理,避免内存泄漏。 使用 ARC (Automatic Reference Counting) 可以简化内存管理。
  • 平台差异: Win32 和 Cocoa API 存在差异,需要编写平台特定的代码。 可以使用条件编译 (#ifdef _WIN32, #ifdef __APPLE__) 来区分平台。
  • 权限问题: 某些操作可能需要管理员权限,例如修改其他进程的窗口。

5. 窗口样式和扩展样式

Windows 的窗口样式和扩展样式是通过 SetWindowLongPtrGetWindowLongPtr 函数来设置的。 样式定义了窗口的基本外观和行为,例如是否有边框、标题栏、滚动条等。 扩展样式则定义了一些高级特性,例如透明度、阴影等。

5.1 常用窗口样式 (Windows)

样式常量 含义
WS_OVERLAPPED 创建一个重叠窗口。
WS_POPUP 创建一个弹出窗口。
WS_CHILD 创建一个子窗口。
WS_VISIBLE 使窗口可见。
WS_DISABLED 禁用窗口。
WS_BORDER 创建一个带边框的窗口。
WS_DLGFRAME 创建一个对话框风格的边框。
WS_CAPTION 创建一个带标题栏的窗口 (必须同时指定 WS_BORDER)。
WS_SYSMENU 在标题栏上显示系统菜单。
WS_THICKFRAME 允许用户通过拖动边框来调整窗口大小。
WS_MINIMIZEBOX 在标题栏上显示最小化按钮。
WS_MAXIMIZEBOX 在标题栏上显示最大化按钮。
WS_MAXIMIZE 初始时最大化窗口。
WS_MINIMIZE 初始时最小化窗口。
WS_VSCROLL 创建一个垂直滚动条。
WS_HSCROLL 创建一个水平滚动条。

5.2 常用窗口扩展样式 (Windows)

样式常量 含义
WS_EX_DLGMODALFRAME 创建一个对话框模态边框。
WS_EX_CLIENTEDGE 为窗口创建一个凹陷的边缘。
WS_EX_STATICEDGE 为窗口创建一个凸起的边缘。
WS_EX_TOOLWINDOW 创建一个工具窗口 (例如任务栏上的小窗口)。
WS_EX_TOPMOST 使窗口置顶。
WS_EX_TRANSPARENT 使窗口透明 (忽略鼠标点击)。
WS_EX_LAYERED 允许窗口使用分层窗口 API,例如设置透明度。
WS_EX_NOACTIVATE 不激活窗口 (当用户点击窗口时,不会将焦点转移到该窗口)。
WS_EX_ACCEPTFILES 允许窗口接收拖放文件。
WS_EX_APPWINDOW 强制窗口出现在任务栏上。

5.3 示例:移除窗口边框和标题栏 (Windows)

#include <flutter_windows.h>
#include <windows.h>

void RemoveWindowFrame(HWND window_handle) {
  // 获取当前窗口样式
  LONG_PTR style = GetWindowLongPtr(window_handle, GWL_STYLE);

  // 移除边框和标题栏样式
  style &= ~(WS_CAPTION | WS_BORDER | WS_THICKFRAME | WS_SYSMENU);

  // 设置新的窗口样式
  SetWindowLongPtr(window_handle, GWL_STYLE, style);

  // 重新绘制窗口
  SetWindowPos(window_handle, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
}

6. Cocoa 窗口样式掩码

macOS 的窗口样式是通过 setStyleMask: 方法来设置的。 样式掩码定义了窗口的外观和行为。

6.1 常用窗口样式掩码 (macOS)

样式掩码常量 含义
NSWindowStyleMaskBorderless 创建一个无边框窗口。
NSWindowStyleMaskTitled 创建一个带标题栏的窗口。
NSWindowStyleMaskClosable 在标题栏上显示关闭按钮。
NSWindowStyleMaskMiniaturizable 在标题栏上显示最小化按钮。
NSWindowStyleMaskResizable 允许用户通过拖动边框来调整窗口大小。
NSWindowStyleMaskUtilityWindow 创建一个工具窗口 (例如调色板)。
NSWindowStyleMaskDocModalWindow 创建一个文档模态窗口。
NSWindowStyleMaskNonactivatingPanel 创建一个非激活面板窗口 (不接收键盘焦点)。
NSWindowStyleMaskFullScreen 创建一个全屏窗口

6.2 示例:创建一个无边框窗口 (macOS)

#import <FlutterMacOS/FlutterMacOS.h>
#import <Cocoa/Cocoa.h>

- (void)setBorderlessWindow {
  NSWindow *window = [[NSApplication sharedApplication] mainWindow];
  [window setStyleMask:NSWindowStyleMaskBorderless];
  [window setBackgroundColor:[NSColor clearColor]];
  [window setOpaque:NO];
}

7. 窗口事件监听

除了控制窗口的外观和行为,我们还可以监听窗口事件,例如窗口激活/失活、窗口移动/调整大小等。

7.1 Windows 窗口事件

在 Windows 中,窗口事件通过消息机制传递。 我们需要重载窗口过程 (Window Procedure) 来处理这些消息。 Flutter Windows 插件通常会提供一个默认的窗口过程,我们可以通过 subclassing 的方式来扩展它,或者使用 SetWindowLongPtr 函数替换默认的窗口过程。

常用的窗口消息:

消息常量 含义
WM_ACTIVATE 窗口激活或失活。
WM_MOVE 窗口移动。
WM_SIZE 窗口大小调整。
WM_CLOSE 窗口关闭。
WM_DESTROY 窗口销毁。
WM_PAINT 窗口需要重绘。
WM_KEYDOWN 键盘按键按下。
WM_KEYUP 键盘按键释放。
WM_MOUSEMOVE 鼠标移动。
WM_LBUTTONDOWN 鼠标左键按下。
WM_LBUTTONUP 鼠标左键释放。

7.2 macOS 窗口事件

在 macOS 中,窗口事件通过代理 (Delegate) 机制传递。 我们可以设置窗口的代理对象,并实现相应的代理方法来处理事件. 还可以使用 Notifications 来监听事件。

常用的窗口代理方法:

代理方法 含义
- (void)windowDidBecomeKey:(NSNotification *)notification 窗口变为活动窗口。
- (void)windowDidResignKey:(NSNotification *)notification 窗口失去焦点。
- (void)windowDidMove:(NSNotification *)notification 窗口移动。
- (void)windowDidResize:(NSNotification *)notification 窗口大小调整。
- (void)windowWillClose:(NSNotification *)notification 窗口即将关闭。

8. 总结

今天我们讨论了 Flutter Desktop 应用中窗口管理的进阶技巧,包括通过 Platform Channels 直接调用 Win32/Cocoa API,以及如何操作窗口句柄。希望这些内容能帮助大家在 Flutter Desktop 开发中实现更复杂、更精细的窗口控制。掌握这些技术,你能更灵活地定制你的 Flutter 桌面应用。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注