Flutter Desktop 的窗口句柄(HWND)交互:与原生控件的事件同步

Flutter Desktop 的窗口句柄(HWND)交互:与原生控件的事件同步

Flutter 凭借其卓越的跨平台能力和高效的开发体验,在移动和 Web 领域取得了巨大成功。随着 Flutter 桌面版本的日益成熟,开发者们开始探索更深层次的原生集成,尤其是在 Windows 平台上,与传统 Win32 API 交互、操作窗口句柄(HWND)以及同步原生控件事件的需求逐渐浮现。尽管 Flutter 的设计哲学是抽象化底层平台,通过 Skia 引擎在自己的画布上绘制所有 UI,但在某些特定场景下,我们仍然需要直接与操作系统的窗口系统进行交互。

这些场景可能包括:

  • 集成遗留或高性能原生控件: 例如,需要嵌入一个由 Win32、MFC 或 WPF 实现的复杂图表库、视频播放器、CAD 渲染器,或者需要调用特定于硬件的原生界面。
  • 利用操作系统级功能: 如自定义窗口行为(无边框窗口拖动、自定义最大化/最小化逻辑)、任务栏交互、系统托盘图标等,这些往往需要直接操作窗口句柄。
  • 性能优化: 对于某些图形密集型或计算密集型任务,原生组件可能提供更好的性能,而 Flutter 仅作为其宿主和协调者。
  • 系统级交互: 当需要监听全局键盘事件、鼠标事件或与其他应用程序进行 DDE/COM 通信时。

本文将深入探讨 Flutter Desktop (Windows) 环境下,如何获取和操作窗口句柄(HWND),以及如何实现 Flutter UI 与原生 Win32 控件之间的双向事件同步。我们将从 Win32 API 的基础概念讲起,逐步深入到 Flutter 的平台通道和 FFI 机制,并通过详细的代码示例,展示如何构建一个能够无缝集成原生控件的混合应用。

1. 理解 Windows 窗口句柄(HWND)与 Win32 API 基础

在 Windows 操作系统中,几乎所有可视元素,无论是应用程序主窗口、对话框、按钮、文本框,还是菜单、滚动条,都被视为一个“窗口”。每个窗口在创建时都会被赋予一个唯一的标识符,称为窗口句柄(Handle to Window, HWND)。HWND 是一个不透明的指针,由系统管理,用于在 Win32 API 调用中引用特定的窗口对象。

1.1 HWND 的本质与作用

HWND 本质上是一个 void* 类型,但在 Win32 API 中通常被定义为 typedef void* HWND;typedef struct HWND__* HWND;,其具体数值是系统内部的一个索引或指针。通过 HWND,我们可以:

  • 识别窗口: 区分不同的窗口或控件。
  • 操作窗口: 移动、改变大小、显示、隐藏、设置标题、修改样式等。
  • 发送消息: 向窗口发送消息,触发其内部事件处理逻辑。
  • 接收消息: 窗口通过其窗口过程(Window Procedure, WndProc)接收和处理系统发送的消息。
  • 建立父子关系: 一个窗口可以是另一个窗口的子窗口,从而实现 UI 元素的组织和管理。

1.2 Win32 API 中的核心概念

为了与 HWND 交互,我们离不开 Win32 API。以下是一些与 HWND 密切相关的核心函数和概念:

  • CreateWindowEx / CreateWindow 创建一个新的窗口或控件。
  • FindWindow / FindWindowEx 根据窗口类名和窗口标题查找现有窗口的 HWND。
  • GetParent / SetParent 获取或设置一个窗口的父窗口。
  • GetWindowRect / GetClientRect 获取窗口在屏幕坐标或客户区坐标中的矩形区域。
  • SetWindowPos / MoveWindow 改变窗口的位置、大小和 Z 序。
  • SendMessage / PostMessage 向指定窗口发送消息。
    • SendMessage 是同步的,直到消息被处理才返回。
    • PostMessage 是异步的,将消息放入目标窗口的消息队列后立即返回。
  • WndProc (Window Procedure): 每个窗口都有一个与之关联的函数,用于处理系统发送给该窗口的所有消息。消息包括鼠标点击、键盘输入、窗口重绘、窗口大小改变等。
  • 消息(Messages):WM_ 开头的常量(如 WM_LBUTTONDOWNWM_COMMANDWM_PAINT)表示不同类型的事件或请求。
  • SetWindowLongPtr / GetWindowLongPtr 用于获取或设置与窗口关联的各种属性,最常用的是获取或修改窗口的 WndProc(即“窗口子类化”)。

表 1.1:常用 Win32 API 函数及其功能

函数名称 功能描述
CreateWindowEx 创建一个重叠式、弹出式或子窗口。
FindWindow 检索顶级窗口的句柄,其类名和窗口名称与指定字符串匹配。
SetParent 更改指定子窗口的父窗口。
GetWindowRect 检索指定窗口的尺寸,包括其边框、标题栏和滚动条。
SetWindowPos 更改子窗口、弹出窗口或顶级窗口的大小、位置和 Z 序。
SendMessage 将指定消息发送到窗口或窗口。
PostMessage 将消息发布到指定窗口的消息队列,并立即返回。
SetWindowLongPtr 更改指定窗口的属性。该函数还可以在窗口的额外内存中设置值。
CallWindowProc 调用默认的窗口过程。
EnableWindow 启用或禁用指定的窗口或控件。
GetWindowText 复制指定窗口的标题栏文本(如果它有)到缓冲区。
SetWindowText 更改指定窗口的标题栏文本(如果它有)。

2. Flutter Desktop (Windows) 架构与 HWND

Flutter 在桌面平台上,特别是 Windows,采用了一种嵌入式(embedding)架构。这意味着 Flutter 应用程序本身并不是一个原生 Win32 应用程序,而是运行在一个由 C++ 编写的宿主(host)应用程序内部。这个宿主应用程序负责创建原生的顶级窗口,并提供一个渲染表面(通常是 DirectX 或 OpenGL 纹理),Flutter 引擎将 UI 渲染到这个表面上。

2.1 Flutter Windows 嵌入器

当你创建一个 Flutter Desktop 项目时,windows/runner 目录下包含了 C++ 嵌入器的代码。这个嵌入器负责:

  • 创建主窗口: 使用 Win32 API (CreateWindowEx) 创建应用程序的主窗口。
  • 初始化 Flutter 引擎: 加载 Dart VM 和 Flutter 引擎。
  • 创建 FlutterView 在主窗口内部创建一个子窗口,作为 Flutter UI 的渲染目标。这个子窗口通常是一个 HWND,Flutter 引擎会在此绘制其所有内容。
  • 处理 Win32 消息: 嵌入器的主消息循环会接收并分发 Win32 消息,其中一部分会转发给 Flutter 引擎进行处理(例如键盘、鼠标事件)。

因此,对于一个 Flutter Windows 应用程序,存在至少两个重要的 HWND:

  1. 应用程序的主窗口 HWND: 这是用户看到的顶级窗口,可以最小化、最大化、关闭。
  2. Flutter 渲染视图的 HWND: 这是主窗口内部的一个子窗口,Flutter UI 实际上在此绘制。通常,这个 HWND 是主窗口的直接子窗口。

在进行原生交互时,我们通常需要获取这两个 HWND,特别是 Flutter 渲染视图的 HWND,因为它代表了 Flutter UI 的实际物理位置和输入焦点区域。

2.2 如何在 Flutter 中获取 HWND

由于 HWND 是一个原生概念,Dart 代码无法直接访问。我们需要通过平台通道(Platform Channels)FFI (Foreign Function Interface) 来实现 C++ 层与 Dart 层的数据交换。

2.2.1 通过平台通道获取 Flutter 窗口 HWND

这是最常见且推荐的方式。Dart 代码通过 MethodChannel 向 C++ 嵌入器发送请求,C++ 嵌入器获取 HWND 后将其作为整数返回给 Dart。

C++ 侧(windows/runner/main.cppwindows/your_plugin/your_plugin.cpp):

首先,我们需要在 C++ 侧获取到 Flutter 窗口的 HWND。通常,FlutterView 对象会持有这个 HWND。

// 假设这是在你的插件或嵌入器初始化逻辑中
// windows/my_native_plugin/my_native_plugin.cpp

#include "my_native_plugin.h"

#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>

#include <windows.h> // 包含 Win32 API 头文件

#include <memory>

namespace MyNativePlugin {

// 用于存储 Flutter 视图的 HWND,以便后续使用
static HWND flutter_view_hwnd = nullptr;

// 存储 plugin_registrar,以便在需要时创建 MethodChannel
static flutter::PluginRegistrarWindows* s_plugin_registrar = nullptr;

void MyNativePlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar) {
    s_plugin_registrar = registrar; // 保存 registrar

    // 获取 Flutter 视图的 HWND
    // 在 Flutter 引擎初始化完成后,可以通过 registrar->Get                 
    // CoreWindow()->GetNativeWindow() 获取。
    // 然而,GetNativeWindow() 返回的通常是顶级窗口的句柄。
    // 如果需要 Flutter 渲染视图的 HWND,可能需要更深入地查找。
    // 一个简化的方法是,在 Runner 的 Win32Window 类中获取并保存。
    // 或者,在 FlutterView 的内部实现中,通常有一个 HWND 成员。
    // 为了简化,我们假设可以通过某种方式(例如,从一个已知的父窗口查找)
    // 或者在 Runner 的 Win32Window 构造时传递给插件。
    // 实际项目中,通常在 Runner 的 Win32Window::OnCreate 中保存 HWND。

    // 假设我们可以在这里获取到 Flutter 视图的 HWND
    // 这个 HWND 实际上是 Flutter 引擎用来渲染内容的子窗口。
    // 对于 Flutter 0.25+ 版本,可以通过 registrar->GetView()->GetNativeWindow() 获取到
    // Flutter 渲染视图的 HWND。
    flutter_view_hwnd = registrar->GetView()->GetNativeWindow();

    auto channel =
        std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
            registrar->messenger(), "my_native_plugin",
            &flutter::StandardMethodCodec::GetInstance());

    channel->SetMethodCallHandler(
        [](const flutter::MethodCall<flutter::EncodableValue>& call,
           std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
            if (call.method_name().compare("getFlutterWindowHandle") == 0) {
                if (flutter_view_hwnd) {
                    result->Success(flutter::EncodableValue(reinterpret_cast<int64_t>(flutter_view_hwnd)));
                } else {
                    result->Error("UNAVAILABLE", "Flutter window handle not found.");
                }
            } else {
                result->NotImplemented();
            }
        });
}

} // namespace MyNativePlugin

Dart 侧(lib/main.dartlib/my_native_plugin.dart):

import 'package:flutter/services.dart';

// 定义平台通道名称
const MethodChannel _channel = MethodChannel('my_native_plugin');

/// 获取 Flutter 渲染视图的 HWND
Future<int?> getFlutterWindowHandle() async {
  try {
    final int? hwnd = await _channel.invokeMethod('getFlutterWindowHandle');
    return hwnd;
  } on PlatformException catch (e) {
    print("Failed to get Flutter window handle: '${e.message}'.");
    return null;
  }
}

// 示例用法
void _getHandleAndPrint() async {
  final int? hwnd = await getFlutterWindowHandle();
  if (hwnd != null) {
    print('Flutter Window HWND: 0x${hwnd.toRadixString(16)}');
  }
}
2.2.2 通过 FFI 直接获取 Flutter 窗口 HWND (更复杂且不推荐直接用于此目的)

理论上,可以通过 FFI 直接调用 Win32 API 来枚举当前进程的窗口并找到 Flutter 窗口。但这通常比通过嵌入器提供的接口更复杂,因为需要知道窗口的类名或标题,而且存在多个窗口的可能性。对于获取 Flutter 自身窗口,不如直接让嵌入器传递。不过,FFI 在调用其他 Win32 API 时非常有用。

示例:通过 FFI 调用 GetDesktopWindow

import 'dart:ffi';
import 'package:ffi/ffi.dart'; // 用于内存管理

// 定义 Win32 API 函数签名
typedef GetDesktopWindowC = Pointer<Void> Function();
typedef GetDesktopWindowDart = Pointer<Void> Function();

// 加载 user32.dll
final DynamicLibrary user32 = DynamicLibrary.open('user32.dll');

// 查找 GetDesktopWindow 函数
final GetDesktopWindowDart getDesktopWindow =
    user32.lookupFunction<GetDesktopWindowC, GetDesktopWindowDart>('GetDesktopWindow');

void _getDesktopHandleAndPrint() {
  final Pointer<Void> desktopHwnd = getDesktopWindow();
  print('Desktop Window HWND: 0x${desktopHwnd.address.toRadixString(16)}');
}

这只是一个 FFI 示例,用于展示如何调用 Win32 API。要找到 Flutter 自己的 HWND,你需要 EnumWindowsGetWindowThreadProcessId 等更复杂的 FFI 调用,并进行筛选,这超出了简单获取自身 HWND 的范畴。通常,我们只在 Flutter 嵌入器中已知 HWND 时才使用平台通道进行传递。

3. 集成原生控件:挑战与策略

Flutter 的 UI 是在 Skia 画布上绘制的,它对原生控件一无所知。这意味着你不能像传统桌面应用那样,简单地在 Flutter 布局中“放置”一个原生 Win32 按钮。当你需要集成原生控件时,主要的挑战在于:

  1. 渲染与布局: 原生控件如何与 Flutter UI 共存?它们是独立的 Win32 窗口,有自己的绘制机制。
  2. 事件同步: 如何将原生控件的事件(如点击、文本输入)传递给 Flutter?如何让 Flutter 的指令(如启用/禁用控件)影响原生控件?
  3. Z 序管理: 如果原生控件是一个独立的 HWND,它通常会绘制在 Flutter UI 的“上方”,除非你通过复杂的区域裁剪和透明度处理来创建“孔洞”。
  4. 输入焦点: 当原生控件获得焦点时,Flutter 的输入处理可能会受影响,反之亦然。

最直接且相对简单的方法是原生控件的“叠加”(Overlaying)

  • 在 Flutter 应用程序的主窗口中,创建一个或多个原生 Win32 子窗口作为你的原生控件。
  • 使用 Win32 API (SetWindowPos) 精确地定位这些子窗口,使它们覆盖在 Flutter UI 的特定区域。
  • 在 Flutter UI 中,在该区域放置一个占位符 (SizedBoxOpacity(opacity: 0.0)),以确保 Flutter 不在该区域绘制,并为原生控件留出空间。

这种方法虽然简单,但存在固有的 Z 序问题:原生 Win32 子窗口总是绘制在 Flutter 内容之上。如果 Flutter UI 需要在原生控件之上绘制(例如一个叠加的菜单或对话框),这将变得非常复杂,需要高级的窗口分层和区域裁剪技术。然而,对于大多数嵌入式原生控件,如视频播放器或外部渲染器,这种叠加方式是可行的。

本节的重点将放在事件同步上,假设我们已经通过叠加的方式创建并定位了原生控件。

4. 事件同步策略:原生到 Flutter

当原生控件发生事件时(例如按钮点击、文本输入、滑动),我们需要将这些信息传递给 Dart 层的 Flutter 应用。这可以通过以下两种主要机制实现:

4.1 通过平台通道(EventChannel)

EventChannel 是 Flutter 提供的标准机制,用于从原生平台向 Dart 代码发送持续的事件流。这是推荐的、高层次的解决方案。

场景示例: Flutter 应用中有一个原生 Win32 按钮。当用户点击这个原生按钮时,Flutter UI 上的一个 Text 组件会更新。

4.1.1 C++ 侧:创建原生按钮并发送事件
// windows/my_native_plugin/my_native_plugin.h
#pragma once

#include <flutter/encodable_value.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <flutter/event_channel.h> // 引入 EventChannel

#include <memory>
#include <optional>

namespace MyNativePlugin {

class MyNativePlugin : public flutter::Plugin {
public:
    static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);

    MyNativePlugin(flutter::PluginRegistrarWindows* registrar);
    virtual ~MyNativePlugin();

private:
    // 回调函数,用于处理来自 Flutter 的方法调用
    void HandleMethodCall(
        const flutter::MethodCall<flutter::EncodableValue>& method_call,
        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

    // EventChannel 的事件处理器
    std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink_;

    // 用于存储原生按钮的 HWND
    HWND native_button_hwnd_ = nullptr;
    HWND native_textbox_hwnd_ = nullptr;

    HWND flutter_view_hwnd_ = nullptr; // Flutter 渲染视图的 HWND

    // 原始的窗口过程,用于子类化
    WNDPROC original_native_button_wndproc_ = nullptr;
    WNDPROC original_native_textbox_wndproc_ = nullptr;

    // 静态成员,用于子类化后的 WndProc
    static LRESULT CALLBACK NativeButtonWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
    static LRESULT CALLBACK NativeTextBoxWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);

    flutter::PluginRegistrarWindows* registrar_;
};

} // namespace MyNativePlugin
// windows/my_native_plugin/my_native_plugin.cpp
#include "my_native_plugin.h"

#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <flutter/event_channel.h>

#include <windows.h>
#include <CommCtrl.h> // For WC_EDIT for text box

#include <memory>
#include <string>

namespace MyNativePlugin {

// 静态成员用于 EventChannel 的事件发送
static std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> s_event_sink = nullptr;

// 静态成员用于存储 MyNativePlugin 实例的指针,以便在静态 WndProc 中访问成员
static MyNativePlugin* s_instance = nullptr;

void MyNativePlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar) {
    s_instance = new MyNativePlugin(registrar); // 创建实例并保存

    auto method_channel =
        std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
            registrar->messenger(), "my_native_plugin",
            &flutter::StandardMethodCodec::GetInstance());

    method_channel->SetMethodCallHandler(
        [plugin_pointer = s_instance](const auto& call, auto result) {
            plugin_pointer->HandleMethodCall(call, std::move(result));
        });

    // 创建 EventChannel
    auto event_channel =
        std::make_unique<flutter::EventChannel<flutter::EncodableValue>>(
            registrar->messenger(), "my_native_plugin_events",
            &flutter::StandardMethodCodec::GetInstance());

    event_channel->SetStreamHandler(std::make_unique<flutter::StreamHandler<flutter::EncodableValue>>(
        [](const flutter::EncodableValue* arguments, std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& events) -> std::unique_ptr<flutter::StreamHandlerError<flutter::EncodableValue>> {
            s_event_sink = std::move(events); // 保存 EventSink
            return nullptr;
        },
        [](const flutter::EncodableValue* arguments) -> std::unique_ptr<flutter::StreamHandlerError<flutter::EncodableValue>> {
            s_event_sink.reset(); // 清除 EventSink
            return nullptr;
        }
    ));
}

MyNativePlugin::MyNativePlugin(flutter::PluginRegistrarWindows* registrar) : registrar_(registrar) {
    flutter_view_hwnd_ = registrar->GetView()->GetNativeWindow();
    // 确保 Common Controls 库被加载,以便使用标准控件
    InitCommonControls();
}

MyNativePlugin::~MyNativePlugin() {
    // 恢复原始窗口过程并销毁原生控件
    if (native_button_hwnd_ && original_native_button_wndproc_) {
        SetWindowLongPtr(native_button_hwnd_, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(original_native_button_wndproc_));
        DestroyWindow(native_button_hwnd_);
    }
    if (native_textbox_hwnd_ && original_native_textbox_wndproc_) {
        SetWindowLongPtr(native_textbox_hwnd_, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(original_native_textbox_wndproc_));
        DestroyWindow(native_textbox_hwnd_);
    }
    s_instance = nullptr; // 清除静态实例指针
}

// 静态的窗口过程,用于处理原生按钮的事件
LRESULT CALLBACK MyNativePlugin::NativeButtonWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
    if (message == WM_COMMAND) {
        // 检查是否是按钮点击事件
        if (HIWORD(wparam) == BN_CLICKED) {
            if (s_event_sink) {
                // 发送事件到 Dart
                s_event_sink->Success(flutter::EncodableValue(flutter::EncodableMap{
                    {flutter::EncodableValue("eventType"), flutter::EncodableValue("buttonClick")},
                    {flutter::EncodableValue("hwnd"), flutter::EncodableValue(reinterpret_cast<int64_t>(hwnd))}
                }));
            }
        }
    }
    // 调用原始的窗口过程以确保默认处理
    return CallWindowProc(s_instance->original_native_button_wndproc_, hwnd, message, wparam, lparam);
}

// 静态的窗口过程,用于处理原生文本框的事件
LRESULT CALLBACK MyNativePlugin::NativeTextBoxWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
    if (message == WM_COMMAND) {
        // 检查是否是文本框内容改变事件
        if (HIWORD(wparam) == EN_CHANGE) {
            if (s_event_sink) {
                // 获取文本框内容
                int length = GetWindowTextLength(hwnd);
                if (length > 0) {
                    std::wstring buffer(length + 1, L'');
                    GetWindowText(hwnd, &buffer[0], length + 1);
                    std::string text_str(buffer.begin(), buffer.end()); // 转换为窄字符 for EncodableValue
                    s_event_sink->Success(flutter::EncodableValue(flutter::EncodableMap{
                        {flutter::EncodableValue("eventType"), flutter::EncodableValue("textBoxChange")},
                        {flutter::EncodableValue("hwnd"), flutter::EncodableValue(reinterpret_cast<int64_t>(hwnd))},
                        {flutter::EncodableValue("text"), flutter::EncodableValue(text_str)}
                    }));
                }
            }
        }
    }
    // 调用原始的窗口过程以确保默认处理
    return CallWindowProc(s_instance->original_native_textbox_wndproc_, hwnd, message, wparam, lparam);
}

void MyNativePlugin::HandleMethodCall(
    const flutter::MethodCall<flutter::EncodableValue>& method_call,
    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {

    if (method_call.method_name().compare("getFlutterWindowHandle") == 0) {
        if (flutter_view_hwnd_) {
            result->Success(flutter::EncodableValue(reinterpret_cast<int64_t>(flutter_view_hwnd_)));
        } else {
            result->Error("UNAVAILABLE", "Flutter window handle not found.");
        }
    } else if (method_call.method_name().compare("createNativeButton") == 0) {
        if (!flutter_view_hwnd_) {
            result->Error("UNAVAILABLE", "Flutter window handle not available.");
            return;
        }

        // 创建原生按钮
        native_button_hwnd_ = CreateWindow(
            L"BUTTON",                      // Window class
            L"Native Button",               // Window text
            WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON, // Styles
            10, 10, 150, 40,                // Size and position (relative to parent)
            flutter_view_hwnd_,             // Parent window
            (HMENU)1001,                    // Control ID
            GetModuleHandle(nullptr),       // Instance handle
            nullptr                         // Additional creation data
        );

        if (native_button_hwnd_) {
            // 子类化按钮的窗口过程,以拦截 WM_COMMAND 消息
            original_native_button_wndproc_ = reinterpret_cast<WNDPROC>(
                SetWindowLongPtr(native_button_hwnd_, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(NativeButtonWndProc))
            );
            result->Success(flutter::EncodableValue(reinterpret_cast<int64_t>(native_button_hwnd_)));
        } else {
            result->Error("FAILED_CREATION", "Failed to create native button.");
        }
    } else if (method_call.method_name().compare("createNativeTextBox") == 0) {
        if (!flutter_view_hwnd_) {
            result->Error("UNAVAILABLE", "Flutter window handle not available.");
            return;
        }

        native_textbox_hwnd_ = CreateWindowEx(
            WS_EX_CLIENTEDGE,               // Extended window style
            L"EDIT",                        // Window class (WC_EDIT)
            L"Type here...",                // Default text
            WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL, // Styles
            10, 60, 200, 80,                // Size and position
            flutter_view_hwnd_,             // Parent window
            (HMENU)1002,                    // Control ID
            GetModuleHandle(nullptr),       // Instance handle
            nullptr
        );

        if (native_textbox_hwnd_) {
            // 子类化文本框的窗口过程
            original_native_textbox_wndproc_ = reinterpret_cast<WNDPROC>(
                SetWindowLongPtr(native_textbox_hwnd_, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(NativeTextBoxWndProc))
            );
            result->Success(flutter::EncodableValue(reinterpret_cast<int64_t>(native_textbox_hwnd_)));
        } else {
            result->Error("FAILED_CREATION", "Failed to create native text box.");
        }
    } else if (method_call.method_name().compare("setNativeButtonEnabled") == 0) {
        if (!native_button_hwnd_) {
            result->Error("NOT_CREATED", "Native button not created.");
            return;
        }
        const auto& args = method_call.arguments()->MapValue();
        bool enable = args.at(flutter::EncodableValue("enable")).BoolValue();
        EnableWindow(native_button_hwnd_, enable);
        result->Success();
    } else if (method_call.method_name().compare("setNativeTextBoxText") == 0) {
        if (!native_textbox_hwnd_) {
            result->Error("NOT_CREATED", "Native text box not created.");
            return;
        }
        const auto& args = method_call.arguments()->MapValue();
        std::string text = args.at(flutter::EncodableValue("text")).StringValue();
        SetWindowText(native_textbox_hwnd_, std::wstring(text.begin(), text.end()).c_str());
        result->Success();
    } else {
        result->NotImplemented();
    }
}

} // namespace MyNativePlugin

解释:

  1. MyNativePlugin::RegisterWithRegistrar 在这里初始化了 MethodChannelEventChannelEventChannelSetStreamHandler 方法接收两个 lambda 函数,分别用于流订阅和流取消。当 Dart 侧开始监听事件时,s_event_sink 会被赋值,我们可以通过它发送事件。
  2. MyNativePlugin::HandleMethodCall 处理来自 Dart 的方法调用,例如 createNativeButtoncreateNativeTextBox。在创建原生控件时,我们使用 CreateWindowCreateWindowEx,并指定 flutter_view_hwnd_ 作为父窗口。
  3. SetWindowLongPtrGWLP_WNDPROC 这是实现窗口子类化的关键。它用我们自定义的 NativeButtonWndProcNativeTextBoxWndProc 替换了原生控件的默认窗口过程。original_native_button_wndproc_ 保存了原始的窗口过程,以便我们可以在处理完自定义逻辑后调用它,确保控件的默认行为不受影响。
  4. NativeButtonWndProc / NativeTextBoxWndProc 这是我们自定义的窗口过程。
    • 对于按钮,我们拦截 WM_COMMAND 消息,并检查 HIWORD(wparam) 是否为 BN_CLICKED,表示按钮被点击。
    • 对于文本框,我们拦截 WM_COMMAND 消息,并检查 HIWORD(wparam) 是否为 EN_CHANGE,表示文本内容发生改变。
    • 一旦检测到相关事件,就通过 s_event_sink->Success(...) 将事件数据(包括事件类型、控件 HWND、文本内容等)发送给 Dart。
  5. s_instances_event_sink 由于窗口过程必须是静态函数(或全局函数),它们无法直接访问 MyNativePlugin 类的非静态成员。为了能从 WndProc 中访问 event_sink_,我们使用静态变量 s_instances_event_sink 来保存指向 MyNativePlugin 实例和 EventSink 的指针。
4.1.2 Dart 侧:监听事件
// lib/my_native_plugin.dart
import 'dart:ffi'; // 导入 ffi
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

// 定义平台通道名称
const MethodChannel _methodChannel = MethodChannel('my_native_plugin');
const EventChannel _eventChannel = EventChannel('my_native_plugin_events');

/// 获取 Flutter 渲染视图的 HWND
Future<int?> getFlutterWindowHandle() async {
  try {
    final int? hwnd = await _methodChannel.invokeMethod('getFlutterWindowHandle');
    return hwnd;
  } on PlatformException catch (e) {
    print("Failed to get Flutter window handle: '${e.message}'.");
    return null;
  }
}

/// 创建原生按钮
Future<int?> createNativeButton() async {
  try {
    final int? hwnd = await _methodChannel.invokeMethod('createNativeButton');
    return hwnd;
  } on PlatformException catch (e) {
    print("Failed to create native button: '${e.message}'.");
    return null;
  }
}

/// 创建原生文本框
Future<int?> createNativeTextBox() async {
  try {
    final int? hwnd = await _methodChannel.invokeMethod('createNativeTextBox');
    return hwnd;
  } on PlatformException catch (e) {
    print("Failed to create native text box: '${e.message}'.");
    return null;
  }
}

/// 设置原生按钮的启用状态
Future<void> setNativeButtonEnabled(bool enable) async {
  try {
    await _methodChannel.invokeMethod('setNativeButtonEnabled', {'enable': enable});
  } on PlatformException catch (e) {
    print("Failed to set native button enabled state: '${e.message}'.");
  }
}

/// 设置原生文本框的文本
Future<void> setNativeTextBoxText(String text) async {
  try {
    await _methodChannel.invokeMethod('setNativeTextBoxText', {'text': text});
  } on PlatformException catch (e) {
    print("Failed to set native text box text: '${e.message}'.");
  }
}

// 事件流
Stream<Map<dynamic, dynamic>> get nativeEventsStream {
  return _eventChannel.receiveBroadcastStream().map((event) => event as Map<dynamic, dynamic>);
}

// lib/main.dart
void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int? _flutterHwnd;
  int? _nativeButtonHwnd;
  int? _nativeTextBoxHwnd;
  String _nativeButtonStatus = 'Not clicked yet';
  String _nativeTextBoxContent = 'No input yet';
  bool _isNativeButtonEnabled = true;

  @override
  void initState() {
    super.initState();
    _initNativeIntegration();
    nativeEventsStream.listen((event) {
      final eventType = event['eventType'];
      if (eventType == 'buttonClick') {
        setState(() {
          _nativeButtonStatus = 'Native button clicked at ${DateTime.now().second}s';
        });
      } else if (eventType == 'textBoxChange') {
        setState(() {
          _nativeTextBoxContent = event['text'] ?? 'No text';
        });
      }
    });
  }

  Future<void> _initNativeIntegration() async {
    final flutterHwnd = await getFlutterWindowHandle();
    setState(() {
      _flutterHwnd = flutterHwnd;
    });

    final buttonHwnd = await createNativeButton();
    setState(() {
      _nativeButtonHwnd = buttonHwnd;
    });

    final textBoxHwnd = await createNativeTextBox();
    setState(() {
      _nativeTextBoxHwnd = textBoxHwnd;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Native HWND Interaction'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Flutter HWND: 0x${_flutterHwnd?.toRadixString(16) ?? 'N/A'}'),
              Text('Native Button HWND: 0x${_nativeButtonHwnd?.toRadixString(16) ?? 'N/A'}'),
              Text('Native Text Box HWND: 0x${_nativeTextBoxHwnd?.toRadixString(16) ?? 'N/A'}'),
              const SizedBox(height: 20),
              Text('Native Button Status: $_nativeButtonStatus'),
              Text('Native Text Box Content: $_nativeTextBoxContent'),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  setState(() {
                    _isNativeButtonEnabled = !_isNativeButtonEnabled;
                  });
                  setNativeButtonEnabled(_isNativeButtonEnabled);
                },
                child: Text(_isNativeButtonEnabled ? 'Disable Native Button' : 'Enable Native Button'),
              ),
              const SizedBox(height: 10),
              ElevatedButton(
                onPressed: () {
                  setNativeTextBoxText('Hello from Flutter! ${DateTime.now().second}');
                },
                child: const Text('Set Native Text Box Text'),
              ),
              const SizedBox(height: 10),
              // Flutter UI 占位符,模拟原生控件的位置
              // 注意:这里只是一个占位符,实际的原生控件会覆盖在上面
              Container(
                width: 150,
                height: 40,
                color: Colors.transparent, // 透明背景,让原生控件显现
                margin: const EdgeInsets.only(left: 10, top: 10),
                alignment: Alignment.topLeft,
                child: const Text('Native Button Placeholder', style: TextStyle(color: Colors.red)), // 提示用户这里有原生控件
              ),
              Container(
                width: 200,
                height: 80,
                color: Colors.transparent, // 透明背景
                margin: const EdgeInsets.only(left: 10, top: 10),
                alignment: Alignment.topLeft,
                child: const Text('Native Text Box Placeholder', style: TextStyle(color: Colors.blue)),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

解释:

  1. nativeEventsStream 通过 _eventChannel.receiveBroadcastStream() 获取事件流,并将其转换为 Map<dynamic, dynamic> 类型。
  2. initState 在 Flutter widget 初始化时,调用 _initNativeIntegration() 来创建原生控件,并订阅 nativeEventsStream
  3. 事件处理:nativeEventsStream 接收到事件时,根据 eventType (例如 buttonClicktextBoxChange) 更新 Flutter UI 的状态。
  4. Flutter UI 占位符: 为了让原生控件正确显示在 Flutter 界面上,我们使用 Container 创建透明的占位符,并设置其大小和位置。请注意,原生控件的位置是相对于其父窗口(flutter_view_hwnd_)的,因此你需要协调 Flutter 占位符和 C++ 中 CreateWindow 的坐标。

4.2 通过 FFI 回调(更底层,复杂)

FFI 回调允许 C 代码直接调用 Dart 函数。这种方法在事件非常频繁且对性能要求极高时可能有用,但管理 Dart 回调的生命周期和内存安全性更为复杂。

C++ 侧:

// 假设你有一个 Dart 函数指针
typedef void (*DartNativeEventCallback)(int32_t event_type, int64_t hwnd);
DartNativeEventCallback s_dart_callback = nullptr;

// ... 在某个 MethodChannel 调用中接收 Dart 函数指针
void MyNativePlugin::HandleMethodCall(...) {
    if (call.method_name().compare("registerNativeEventCallback") == 0) {
        s_dart_callback = reinterpret_cast<DartNativeEventCallback>(
            call.arguments()->Int64Value() // Dart 会传递一个 Pointer.address
        );
        result->Success();
    }
}

// 在 NativeButtonWndProc 中调用 Dart 回调
LRESULT CALLBACK MyNativePlugin::NativeButtonWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
    if (message == WM_COMMAND && HIWORD(wparam) == BN_CLICKED) {
        if (s_dart_callback) {
            s_dart_callback(1, reinterpret_cast<int64_t>(hwnd)); // 1 代表按钮点击事件
        }
    }
    return CallWindowProc(s_instance->original_native_button_wndproc_, hwnd, message, wparam, lparam);
}

Dart 侧:

import 'dart:ffi';
import 'package:ffi/ffi.dart';

// 定义 Dart 回调类型
typedef NativeEventCallback = void Function(int eventType, int hwnd);
typedef NativeEventCallbackC = Void Function(Int32 eventType, Int64 hwnd);

// 实现 Dart 回调函数
@pragma('vm:entry-point') // 必须添加,否则在 AOT 编译时会被优化掉
void _handleNativeEvent(int eventType, int hwnd) {
  print('Received native event: type=$eventType, hwnd=0x${hwnd.toRadixString(16)}');
  // 在这里更新 Flutter UI,可能需要使用 Isolate.post or PlatformDispatcher.sendPlatformMessage
  // 因为 FFI 回调可能不在 Flutter UI 线程上
}

// 获取 Dart 回调函数的指针并发送给 C++
Future<void> registerNativeEventCallback() async {
  final Pointer<NativeFunction<NativeEventCallbackC>> nativeCallbackPtr =
      Pointer.fromFunction<NativeEventCallbackC>(_handleNativeEvent, 0, 0); // 0,0 是默认的错误值

  await _methodChannel.invokeMethod(
      'registerNativeEventCallback', nativeCallbackPtr.address);
}

// 使用示例
void _setupFFIEventHandling() async {
  await registerNativeEventCallback();
}

注意: FFI 回调在 Dart 侧默认是在一个新的 Isolate 上执行的,因此直接在回调中调用 setState 会导致错误。你需要使用 Isolate.postPlatformDispatcher.sendPlatformMessage 等机制将事件传递回主 UI Isolate。这增加了复杂性,因此对于大多数事件同步,EventChannel 是更简单的选择。

5. 事件同步策略:Flutter 到原生

当 Flutter UI 中的事件(例如 Flutter 按钮点击)需要影响原生控件时(例如启用/禁用原生按钮、设置原生文本框内容),我们通常通过 MethodChannel 从 Dart 向 C++ 发送指令。

场景示例: Flutter UI 有一个开关,用于启用或禁用上面创建的原生 Win32 按钮。Flutter UI 还有一个按钮,点击后设置原生文本框的内容。

5.1 Dart 侧:发送指令

Dart 侧的代码已经在 4.1.2 节中展示,通过 _methodChannel.invokeMethod 调用 C++ 插件中的方法。

// lib/main.dart (部分)
ElevatedButton(
  onPressed: () {
    setState(() {
      _isNativeButtonEnabled = !_isNativeButtonEnabled;
    });
    setNativeButtonEnabled(_isNativeButtonEnabled); // 调用 C++ 方法
  },
  child: Text(_isNativeButtonEnabled ? 'Disable Native Button' : 'Enable Native Button'),
),
const SizedBox(height: 10),
ElevatedButton(
  onPressed: () {
    setNativeTextBoxText('Hello from Flutter! ${DateTime.now().second}'); // 调用 C++ 方法
  },
  child: const Text('Set Native Text Box Text'),
),
5.2 C++ 侧:接收指令并操作原生控件

C++ 侧的 MyNativePlugin::HandleMethodCall 方法已经实现了对这些指令的响应。

// windows/my_native_plugin/my_native_plugin.cpp (部分)

void MyNativePlugin::HandleMethodCall(...) {
    // ... 其他方法处理

    else if (method_call.method_name().compare("setNativeButtonEnabled") == 0) {
        if (!native_button_hwnd_) {
            result->Error("NOT_CREATED", "Native button not created.");
            return;
        }
        const auto& args = method_call.arguments()->MapValue();
        bool enable = args.at(flutter::EncodableValue("enable")).BoolValue();
        EnableWindow(native_button_hwnd_, enable); // 调用 Win32 API 启用/禁用窗口
        result->Success();
    } else if (method_call.method_name().compare("setNativeTextBoxText") == 0) {
        if (!native_textbox_hwnd_) {
            result->Error("NOT_CREATED", "Native text box not created.");
            return;
        }
        const auto& args = method_call.arguments()->MapValue();
        std::string text = args.at(flutter::EncodableValue("text")).StringValue();
        SetWindowText(native_textbox_hwnd_, std::wstring(text.begin(), text.end()).c_str()); // 调用 Win32 API 设置文本
        result->Success();
    } else {
        result->NotImplemented();
    }
}

解释:

  1. setNativeButtonEnabled / setNativeTextBoxText Dart 侧调用这些方法,通过 MethodChannel 传递参数(例如 enable 布尔值或 text 字符串)。
  2. C++ 接收: HandleMethodCall 接收到方法调用后,从 method_call.arguments() 中解析出参数。
  3. Win32 API 调用:
    • EnableWindow(native_button_hwnd_, enable):直接调用 Win32 API 来启用或禁用指定的窗口(这里是原生按钮)。
    • SetWindowText(native_textbox_hwnd_, ...):直接调用 Win32 API 来设置指定窗口的文本(这里是原生文本框)。
  4. 结果返回: 调用 result->Success()result->Error() 返回操作结果给 Dart。

6. 复杂交互:FFI 直接调用 Win32 API

对于一些简单的、不需要 C++ 逻辑封装的 Win32 API 调用,或者为了避免平台通道的少量开销,Dart 可以通过 FFI 直接调用 user32.dll 或其他系统 DLL 中的函数。

示例:Dart 直接调用 SetWindowPos 来移动原生控件

import 'dart:ffi';
import 'package:ffi/ffi.dart'; // 用于内存管理

// 定义 Win32 API 函数签名
typedef SetWindowPosC = Int32 Function(
  Pointer<Void> hWnd,
  Pointer<Void> hWndInsertAfter,
  Int32 X,
  Int32 Y,
  Int32 cx,
  Int32 cy,
  Uint32 uFlags,
);
typedef SetWindowPosDart = int Function(
  Pointer<Void> hWnd,
  Pointer<Void> hWndInsertAfter,
  int X,
  int Y,
  int cx,
  int cy,
  int uFlags,
);

// 加载 user32.dll
final DynamicLibrary user32 = DynamicLibrary.open('user32.dll');

// 查找 SetWindowPos 函数
final SetWindowPosDart setWindowPos =
    user32.lookupFunction<SetWindowPosC, SetWindowPosDart>('SetWindowPos');

// Win32 常量
const int SWP_NOSIZE = 0x0001;
const int SWP_NOMOVE = 0x0002;
const int SWP_NOZORDER = 0x0004;
const int SWP_NOACTIVATE = 0x0010;

/// 移动原生控件到指定位置
void moveNativeControl(int hwnd, int x, int y) {
  final Pointer<Void> nativeHwnd = Pointer.fromAddress(hwnd);
  setWindowPos(
    nativeHwnd,
    nullptr, // HWND_TOP (nullptr) 或其他 Z 序句柄
    x,
    y,
    0, // SWP_NOSIZE flag means width/height ignored
    0, // SWP_NOSIZE flag means width/height ignored
    SWP_NOSIZE | SWP_NOACTIVATE, // 组合标志,只移动,不改变大小,不激活
  );
}

// 在 Flutter UI 中使用
// ElevatedButton(
//   onPressed: () {
//     if (_nativeButtonHwnd != null) {
//       moveNativeControl(_nativeButtonHwnd!, 200, 100); // 移动到 (200, 100)
//     }
//   },
//   child: const Text('Move Native Button (FFI)'),
// ),

解释:

  1. typedef 定义函数签名: 必须准确匹配 Win32 API 函数的 C 语言签名,包括参数类型和返回值。
  2. DynamicLibrary.open 加载包含所需函数的系统 DLL(如 user32.dllkernel32.dll)。
  3. lookupFunction 根据函数名查找并绑定函数。需要提供 C 签名和 Dart 签名。
  4. 参数转换: Dart 的 int 转换为 C 的 Int32Int64Pointer.fromAddress(hwnd) 将 Dart 的整数 HWND 转换为 C 的 Pointer<Void>
  5. 常量: Win32 API 经常使用各种常量作为标志,这些常量需要在 Dart 中重新定义。

FFI 直接调用 Win32 API 提供最大的灵活性和最小的开销,但它也要求 Dart 开发者对 Win32 API 有深入的理解,并处理好内存管理和错误处理。

7. 挑战与考量

虽然通过 HWND 交互和事件同步为 Flutter Desktop 提供了强大的原生集成能力,但也带来了一系列挑战:

  • Z 序管理和裁剪: 原生控件是独立的窗口,它们总是绘制在 Flutter 的 Skia 画布之上。要在原生控件上方显示 Flutter 内容,你需要复杂的 Win32 窗口分层 (SetLayeredWindowAttributes)、区域裁剪 (SetWindowRgn) 和透明度处理,或者确保 Flutter 在原生控件区域留出“空洞”。
  • 输入焦点: 当原生控件获得焦点时,键盘和鼠标事件将直接发送给原生控件,Flutter 将不再接收这些事件。你需要手动协调 Flutter 和原生控件之间的焦点管理。
  • 样式和主题: 原生控件将使用操作系统的默认样式和主题,这可能与 Flutter 的 Material 或 Cupertino 设计语言不一致。要实现视觉上的统一,可能需要对原生控件进行自定义绘制或主题化,这会增加复杂性。
  • 可访问性: Flutter 有自己的可访问性树,而原生控件有自己的可访问性接口。将两者桥接起来以提供一致的用户体验是一个复杂的问题。
  • 跨平台兼容性: 本文讨论的 HWND 交互是 Windows 平台特有的。在 macOS 和 Linux 上,你需要使用不同的原生 API(例如 macOS 上的 NSWindow / NSView,Linux 上的 GTK 或 Qt 窗口系统)。这意味着原生集成代码将是平台特定的。
  • 性能考量: 频繁的平台通道调用或 FFI 调用可能会带来一定的开销。对于高频率的事件(例如鼠标移动),需要仔细权衡。
  • 内存管理: 在 FFI 场景下,需要手动管理原生内存(例如 malloc / freecalloc / free),避免内存泄漏。package:ffi 中的 arenacalloc 可以简化这一过程。
  • 错误处理: 原生 API 调用可能失败。需要在 C++ 侧和 Dart 侧都实现健壮的错误处理机制。
  • 生命周期管理: 确保原生控件在 Flutter 应用程序关闭时被正确销毁,并恢复任何被子类化的窗口过程。

8. 展望与总结

Flutter Desktop 与原生窗口句柄(HWND)的交互,以及在此基础上的事件同步,是实现深度原生集成的关键能力。通过平台通道,我们可以安全、高效地在 Dart 和 C++ 之间传递数据和指令,实现原生控件事件向 Flutter 的流动,以及 Flutter 指令对原生控件的控制。对于更底层、高性能或批量操作的需求,FFI 提供了一条直接调用 Win32 API 的路径。

掌握这些技术,开发者能够将 Flutter 的现代化 UI 开发效率与 Windows 平台上丰富的原生生态系统相结合,集成遗留组件、利用系统级功能,从而构建出功能强大、性能卓越的混合桌面应用程序。然而,这种深度集成也伴随着额外的复杂性,需要对 Win32 API、C++ 开发以及 Flutter 平台机制有扎实的理解。在决定采用这种方法时,务必仔细权衡其带来的优势与挑战。

发表回复

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