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_LBUTTONDOWN、WM_COMMAND、WM_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:
- 应用程序的主窗口 HWND: 这是用户看到的顶级窗口,可以最小化、最大化、关闭。
- 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.cpp 或 windows/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.dart 或 lib/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,你需要 EnumWindows 和 GetWindowThreadProcessId 等更复杂的 FFI 调用,并进行筛选,这超出了简单获取自身 HWND 的范畴。通常,我们只在 Flutter 嵌入器中已知 HWND 时才使用平台通道进行传递。
3. 集成原生控件:挑战与策略
Flutter 的 UI 是在 Skia 画布上绘制的,它对原生控件一无所知。这意味着你不能像传统桌面应用那样,简单地在 Flutter 布局中“放置”一个原生 Win32 按钮。当你需要集成原生控件时,主要的挑战在于:
- 渲染与布局: 原生控件如何与 Flutter UI 共存?它们是独立的 Win32 窗口,有自己的绘制机制。
- 事件同步: 如何将原生控件的事件(如点击、文本输入)传递给 Flutter?如何让 Flutter 的指令(如启用/禁用控件)影响原生控件?
- Z 序管理: 如果原生控件是一个独立的 HWND,它通常会绘制在 Flutter UI 的“上方”,除非你通过复杂的区域裁剪和透明度处理来创建“孔洞”。
- 输入焦点: 当原生控件获得焦点时,Flutter 的输入处理可能会受影响,反之亦然。
最直接且相对简单的方法是原生控件的“叠加”(Overlaying):
- 在 Flutter 应用程序的主窗口中,创建一个或多个原生 Win32 子窗口作为你的原生控件。
- 使用 Win32 API (
SetWindowPos) 精确地定位这些子窗口,使它们覆盖在 Flutter UI 的特定区域。 - 在 Flutter UI 中,在该区域放置一个占位符 (
SizedBox或Opacity(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
解释:
MyNativePlugin::RegisterWithRegistrar: 在这里初始化了MethodChannel和EventChannel。EventChannel的SetStreamHandler方法接收两个 lambda 函数,分别用于流订阅和流取消。当 Dart 侧开始监听事件时,s_event_sink会被赋值,我们可以通过它发送事件。MyNativePlugin::HandleMethodCall: 处理来自 Dart 的方法调用,例如createNativeButton和createNativeTextBox。在创建原生控件时,我们使用CreateWindow或CreateWindowEx,并指定flutter_view_hwnd_作为父窗口。SetWindowLongPtr与GWLP_WNDPROC: 这是实现窗口子类化的关键。它用我们自定义的NativeButtonWndProc或NativeTextBoxWndProc替换了原生控件的默认窗口过程。original_native_button_wndproc_保存了原始的窗口过程,以便我们可以在处理完自定义逻辑后调用它,确保控件的默认行为不受影响。NativeButtonWndProc/NativeTextBoxWndProc: 这是我们自定义的窗口过程。- 对于按钮,我们拦截
WM_COMMAND消息,并检查HIWORD(wparam)是否为BN_CLICKED,表示按钮被点击。 - 对于文本框,我们拦截
WM_COMMAND消息,并检查HIWORD(wparam)是否为EN_CHANGE,表示文本内容发生改变。 - 一旦检测到相关事件,就通过
s_event_sink->Success(...)将事件数据(包括事件类型、控件 HWND、文本内容等)发送给 Dart。
- 对于按钮,我们拦截
s_instance和s_event_sink: 由于窗口过程必须是静态函数(或全局函数),它们无法直接访问MyNativePlugin类的非静态成员。为了能从WndProc中访问event_sink_,我们使用静态变量s_instance和s_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)),
),
],
),
),
),
);
}
}
解释:
nativeEventsStream: 通过_eventChannel.receiveBroadcastStream()获取事件流,并将其转换为Map<dynamic, dynamic>类型。initState: 在 Flutter widget 初始化时,调用_initNativeIntegration()来创建原生控件,并订阅nativeEventsStream。- 事件处理: 当
nativeEventsStream接收到事件时,根据eventType(例如buttonClick或textBoxChange) 更新 Flutter UI 的状态。 - 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.post 或 PlatformDispatcher.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();
}
}
解释:
setNativeButtonEnabled/setNativeTextBoxText: Dart 侧调用这些方法,通过MethodChannel传递参数(例如enable布尔值或text字符串)。- C++ 接收:
HandleMethodCall接收到方法调用后,从method_call.arguments()中解析出参数。 - Win32 API 调用:
EnableWindow(native_button_hwnd_, enable):直接调用 Win32 API 来启用或禁用指定的窗口(这里是原生按钮)。SetWindowText(native_textbox_hwnd_, ...):直接调用 Win32 API 来设置指定窗口的文本(这里是原生文本框)。
- 结果返回: 调用
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)'),
// ),
解释:
typedef定义函数签名: 必须准确匹配 Win32 API 函数的 C 语言签名,包括参数类型和返回值。DynamicLibrary.open: 加载包含所需函数的系统 DLL(如user32.dll、kernel32.dll)。lookupFunction: 根据函数名查找并绑定函数。需要提供 C 签名和 Dart 签名。- 参数转换: Dart 的
int转换为 C 的Int32或Int64。Pointer.fromAddress(hwnd)将 Dart 的整数 HWND 转换为 C 的Pointer<Void>。 - 常量: 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/free或calloc/free),避免内存泄漏。package:ffi中的arena和calloc可以简化这一过程。 - 错误处理: 原生 API 调用可能失败。需要在 C++ 侧和 Dart 侧都实现健壮的错误处理机制。
- 生命周期管理: 确保原生控件在 Flutter 应用程序关闭时被正确销毁,并恢复任何被子类化的窗口过程。
8. 展望与总结
Flutter Desktop 与原生窗口句柄(HWND)的交互,以及在此基础上的事件同步,是实现深度原生集成的关键能力。通过平台通道,我们可以安全、高效地在 Dart 和 C++ 之间传递数据和指令,实现原生控件事件向 Flutter 的流动,以及 Flutter 指令对原生控件的控制。对于更底层、高性能或批量操作的需求,FFI 提供了一条直接调用 Win32 API 的路径。
掌握这些技术,开发者能够将 Flutter 的现代化 UI 开发效率与 Windows 平台上丰富的原生生态系统相结合,集成遗留组件、利用系统级功能,从而构建出功能强大、性能卓越的混合桌面应用程序。然而,这种深度集成也伴随着额外的复杂性,需要对 Win32 API、C++ 开发以及 Flutter 平台机制有扎实的理解。在决定采用这种方法时,务必仔细权衡其带来的优势与挑战。