Files
xianyan/windows/runner/flutter_window.cpp
Developer f7520b17b2 win提交
2026-06-22 03:50:59 +08:00

604 lines
24 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "flutter_window.h"
#include <optional>
#include <commctrl.h>
#include <chrono>
#include <fstream>
#include <mutex>
#include <sstream>
#include <string>
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>
#include "flutter/generated_plugin_registrant.h"
// GET_X_LPARAM / GET_Y_LPARAM from windowsx.h
// Defined inline to avoid potential macro conflicts with plugin code
#ifndef GET_X_LPARAM
#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
#endif
#ifndef GET_Y_LPARAM
#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))
#endif
// ============================================================
// debug instrumentation for window-drag-lag (flutter_window side)
// ============================================================
namespace {
std::mutex g_debug_log_mutex2;
std::wstring g_debug_log_path2;
void DebugLog(const std::string& tag, const std::string& detail) {
// 已禁用文件日志以排除 I/O 对消息循环的干扰。
(void)tag;
(void)detail;
#if 0
auto now = std::chrono::system_clock::now();
auto us = std::chrono::duration_cast<std::chrono::microseconds>(
now.time_since_epoch())
.count();
std::lock_guard<std::mutex> lock(g_debug_log_mutex2);
if (g_debug_log_path2.empty()) {
g_debug_log_path2 = L"E:\\project\\flutter\\f\\xianyan\\xianyan_drag_debug.log";
}
std::wofstream ofs(g_debug_log_path2, std::ios::app);
if (!ofs.is_open()) return;
ofs << us << L" [" << std::wstring(tag.begin(), tag.end()) << L"] "
<< std::wstring(detail.begin(), detail.end()) << L"\n";
#endif
}
std::string MsgName(UINT msg) {
switch (msg) {
case WM_NCHITTEST: return "WM_NCHITTEST";
case WM_NCLBUTTONDOWN: return "WM_NCLBUTTONDOWN";
case WM_NCLBUTTONUP: return "WM_NCLBUTTONUP";
case WM_NCMOUSEMOVE: return "WM_NCMOUSEMOVE";
case WM_LBUTTONDOWN: return "WM_LBUTTONDOWN";
case WM_LBUTTONUP: return "WM_LBUTTONUP";
case WM_MOUSEMOVE: return "WM_MOUSEMOVE";
case WM_MOVE: return "WM_MOVE";
case WM_MOVING: return "WM_MOVING";
case WM_SIZE: return "WM_SIZE";
case WM_WINDOWPOSCHANGING: return "WM_WINDOWPOSCHANGING";
case WM_WINDOWPOSCHANGED: return "WM_WINDOWPOSCHANGED";
case WM_SYSCOMMAND: return "WM_SYSCOMMAND";
case WM_ENTERSIZEMOVE: return "WM_ENTERSIZEMOVE";
case WM_EXITSIZEMOVE: return "WM_EXITSIZEMOVE";
case WM_PAINT: return "WM_PAINT";
case WM_ERASEBKGND: return "WM_ERASEBKGND";
default: return "MSG_" + std::to_string(msg);
}
}
} // namespace
// ============================================================
// 静态成员初始化
// ============================================================
bool FlutterWindow::is_in_native_drag_ = false;
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
// ============================================================
// 子类化 Flutter 子窗口:标题栏命中测试穿透
// Flutter 子窗口覆盖整个客户区,拦截了所有鼠标消息。
// 通过子类化,在标题栏可拖拽区域返回 HTTRANSPARENT让父窗口
// 的 WM_NCHITTEST 有机会返回 HTCAPTION从而触发 Windows 原生
// 模态拖拽循环。原生拖拽由 DWM 直接处理,延迟最低。
// 标题栏右侧控制按钮区域返回 HTCLIENT保证按钮可点击。
// ============================================================
HWND child_hwnd = flutter_controller_->view()->GetNativeWindow();
SetWindowSubclass(child_hwnd, ChildWndProc, kChildSubclassId,
reinterpret_cast<DWORD_PTR>(GetHandle()));
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
// Register Windows platform MethodChannel for theme control
const static std::string channel_name("apps.xy.xianyan/windows");
platform_channel_ = std::make_unique<flutter::MethodChannel<>>(
flutter_controller_->engine()->messenger(), channel_name,
&flutter::StandardMethodCodec::GetInstance());
platform_channel_->SetMethodCallHandler(
[this](const flutter::MethodCall<>& call,
std::unique_ptr<flutter::MethodResult<>> result) {
const std::string& method = call.method_name();
if (method == "setDarkMode") {
bool is_dark = false;
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
auto it = args->find(flutter::EncodableValue("isDark"));
if (it != args->end() && std::holds_alternative<bool>(it->second)) {
is_dark = std::get<bool>(it->second);
}
}
Win32Window::SetDarkMode(GetHandle(), is_dark);
result->Success();
} else if (method == "setWindowTitle") {
std::string title;
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
auto it = args->find(flutter::EncodableValue("title"));
if (it != args->end() && std::holds_alternative<std::string>(it->second)) {
title = std::get<std::string>(it->second);
}
}
std::wstring wide_title(title.begin(), title.end());
Win32Window::SetWindowTitle(GetHandle(), wide_title);
result->Success();
} else if (method == "setFullscreen") {
bool fullscreen = false;
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
auto it = args->find(flutter::EncodableValue("fullscreen"));
if (it != args->end() && std::holds_alternative<bool>(it->second)) {
fullscreen = std::get<bool>(it->second);
}
}
Win32Window::SetFullscreen(GetHandle(), fullscreen);
result->Success();
} else if (method == "isFullscreen") {
bool is_fullscreen = Win32Window::IsFullscreen(GetHandle());
result->Success(flutter::EncodableValue(is_fullscreen));
} else if (method == "setMinSize") {
unsigned int width = 0;
unsigned int height = 0;
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
auto it_w = args->find(flutter::EncodableValue("width"));
auto it_h = args->find(flutter::EncodableValue("height"));
if (it_w != args->end() && std::holds_alternative<int>(it_w->second)) {
width = static_cast<unsigned int>(std::get<int>(it_w->second));
}
if (it_h != args->end() && std::holds_alternative<int>(it_h->second)) {
height = static_cast<unsigned int>(std::get<int>(it_h->second));
}
}
Win32Window::SetMinSize(GetHandle(), width, height);
result->Success();
} else if (method == "performHapticFeedback") {
int feedback_type = 0;
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
auto it = args->find(flutter::EncodableValue("type"));
if (it != args->end() && std::holds_alternative<int>(it->second)) {
feedback_type = std::get<int>(it->second);
}
}
Win32Window::PerformHapticFeedback(GetHandle(), feedback_type);
result->Success();
} else if (method == "getSystemAppearance") {
std::string appearance = Win32Window::GetSystemAppearance();
result->Success(flutter::EncodableValue(appearance));
} else {
result->NotImplemented();
}
});
// ============================================================
// 窗口控制 MethodChannel窗口大小预设菜单等
// ============================================================
window_control_channel_ = std::make_unique<flutter::MethodChannel<>>(
flutter_controller_->engine()->messenger(), "xianyan/window_control",
&flutter::StandardMethodCodec::GetInstance());
window_control_channel_->SetMethodCallHandler(
[this](const flutter::MethodCall<>& call,
std::unique_ptr<flutter::MethodResult<>> result) {
const std::string& method = call.method_name();
if (method == "showWindowSizeMenu") {
// 使用 PostMessage 异步弹出菜单,避免 TrackPopupMenuEx
// 阻塞 MethodChannel 回调线程Flutter UI 线程)
PostMessage(GetHandle(), WM_USER + 1000, 0, 0);
result->Success(flutter::EncodableValue(true));
} else {
result->NotImplemented();
}
});
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
HWND child_hwnd = flutter_controller_->view()->GetNativeWindow();
RemoveWindowSubclass(child_hwnd, ChildWndProc, kChildSubclassId);
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
// ============================================================
// 原生窗口大小预设菜单
//
// 由于 Mica/Acrylic 导致 DefWindowProc 边框命中测试失效,
// 手动实现的边框命中测试在某些环境下仍无法调整窗口大小。
// 这里提供点击"口"按钮弹出原生菜单选择窗口大小的替代方案。
//
// 使用 Win32 TrackPopupMenuEx 弹出原生菜单,符合 Windows 原生风格。
// 菜单项包括:最大化/还原、几种预设尺寸。
// ============================================================
bool FlutterWindow::ShowWindowSizeMenu(HWND hwnd) {
if (!hwnd) return false;
// 创建弹出菜单
HMENU hMenu = CreatePopupMenu();
if (!hMenu) return false;
// 检查当前是否最大化
bool is_maximized = (::IsZoomed(hwnd) != 0);
// 菜单项 ID从 1001 开始,避免与系统命令冲突)
enum MenuId {
ID_TOGGLE_MAXIMIZE = 1001,
ID_SIZE_800x600,
ID_SIZE_1024x768,
ID_SIZE_1280x720,
ID_SIZE_1440x900,
ID_SIZE_1920x1080,
};
// 添加菜单项
AppendMenuW(hMenu, MF_STRING,
ID_TOGGLE_MAXIMIZE,
is_maximized ? L"🔽 还原窗口" : L"⬜ 最大化");
AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(hMenu, MF_STRING, ID_SIZE_800x600, L"📱 小窗 800 × 600");
AppendMenuW(hMenu, MF_STRING, ID_SIZE_1024x768, L"💻 标准 1024 × 768");
AppendMenuW(hMenu, MF_STRING, ID_SIZE_1280x720, L"🖥️ 宽屏 1280 × 720");
AppendMenuW(hMenu, MF_STRING, ID_SIZE_1440x900, L"🖥️ 大屏 1440 × 900");
AppendMenuW(hMenu, MF_STRING, ID_SIZE_1920x1080, L"📺 全高清 1920 × 1080");
// 获取鼠标位置(菜单显示在鼠标位置)
POINT cursor_pos;
GetCursorPos(&cursor_pos);
// 设置菜单为右对齐、右键选择
UINT flags = TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON |
TPM_RIGHTBUTTON | TPM_RETURNCMD;
// 弹出菜单(同步阻塞,返回选择的菜单项 ID0 表示取消)
int cmd = TrackPopupMenuEx(hMenu, flags, cursor_pos.x, cursor_pos.y, hwnd,
nullptr);
DestroyMenu(hMenu);
if (cmd == 0) return false; // 用户取消
// 处理用户选择
int width = 0, height = 0;
switch (cmd) {
case ID_TOGGLE_MAXIMIZE:
if (is_maximized) {
PostMessage(hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);
} else {
PostMessage(hwnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0);
}
return true;
case ID_SIZE_800x600: width = 800; height = 600; break;
case ID_SIZE_1024x768: width = 1024; height = 768; break;
case ID_SIZE_1280x720: width = 1280; height = 720; break;
case ID_SIZE_1440x900: width = 1440; height = 900; break;
case ID_SIZE_1920x1080: width = 1920; height = 1080; break;
default: return false;
}
// 取消最大化(如果当前是最大化状态)
if (is_maximized) {
ShowWindow(hwnd, SW_RESTORE);
}
// 获取窗口当前位置,保持左上角不变
RECT wr;
GetWindowRect(hwnd, &wr);
// 调整为指定大小(考虑 DPI 缩放)
UINT dpi = GetDpiForWindow(hwnd);
int physical_width = static_cast<int>(width * dpi / 96.0);
int physical_height = static_cast<int>(height * dpi / 96.0);
SetWindowPos(hwnd, nullptr, wr.left, wr.top,
physical_width, physical_height,
SWP_NOZORDER | SWP_NOACTIVATE);
return true;
}
// ============================================================
// Flutter 子窗口子类化回调
//
// 核心思路:
// 1. 标题栏可拖拽区域返回 HTTRANSPARENT让父窗口的 WM_NCHITTEST
// 返回 HTCAPTIONWindows 随后进入原生模态拖拽循环。
// 2. 标题栏右侧控制按钮区域返回 HTCLIENT保证 Flutter 按钮可点击。
// 3. 其他区域返回 DefSubclassProc由 Flutter 正常处理。
//
// 注意:窗口边框缩放由父窗口原生非客户区处理,不需要子窗口干预。
// ============================================================
LRESULT CALLBACK FlutterWindow::ChildWndProc(
HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam,
UINT_PTR subclass_id, DWORD_PTR ref_data) {
HWND top_hwnd = reinterpret_cast<HWND>(ref_data);
// ============================================================
// 拖拽期间:拦截 Flutter 子窗口的所有消息(除了 WM_NCHITTEST
//
// Flutter 子窗口覆盖整个客户区,在拖拽期间会持续接收消息
// WM_PAINT/WM_MOVE/WM_SIZE/WM_WINDOWPOSCHANGED 等)并触发
// Flutter 引擎重绘,阻塞 Win32 消息循环(= UI 线程),导致
// DWM 无法应用 live-drag 优化。
//
// 拖拽期间所有消息直接交给 DefWindowProc不经过 Flutter 引擎的
// WndProc阻止 Flutter 重绘。WM_NCHITTEST 仍需正常处理,
// 否则鼠标在标题栏的命中测试会失败。
// ============================================================
if (is_in_native_drag_ && message != WM_NCHITTEST) {
return DefWindowProc(hwnd, message, wparam, lparam);
}
// region debug-point child-msg
if (message == WM_NCHITTEST || message == WM_NCLBUTTONDOWN ||
message == WM_NCLBUTTONUP || message == WM_NCMOUSEMOVE ||
message == WM_LBUTTONDOWN || message == WM_LBUTTONUP ||
message == WM_MOUSEMOVE || message == WM_MOVE || message == WM_MOVING ||
message == WM_SIZE || message == WM_WINDOWPOSCHANGING ||
message == WM_WINDOWPOSCHANGED || message == WM_SYSCOMMAND ||
message == WM_ENTERSIZEMOVE || message == WM_EXITSIZEMOVE ||
message == WM_PAINT || message == WM_ERASEBKGND) {
std::ostringstream oss;
oss << "hwnd=" << hwnd << " msg=" << MsgName(message)
<< " wp=" << wparam << " lp=" << lparam;
DebugLog("child", oss.str());
}
// endregion debug-point child-msg
switch (message) {
case WM_NCHITTEST: {
POINT pt = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
ScreenToClient(hwnd, &pt);
UINT dpi = GetDpiForWindow(top_hwnd);
double scale = static_cast<double>(dpi) / 96.0;
int title_h = static_cast<int>(36 * scale);
int btn_w = static_cast<int>(138 * scale);
RECT cr;
GetClientRect(top_hwnd, &cr);
{
std::ostringstream oss;
oss << "WM_NCHITTEST raw_screen=(" << GET_X_LPARAM(lparam) << ","
<< GET_Y_LPARAM(lparam) << ") client=(" << pt.x << "," << pt.y
<< ") dpi=" << dpi << " scale=" << scale
<< " title_h=" << title_h << " btn_w=" << btn_w
<< " cr=(" << cr.right << "," << cr.bottom << ")";
DebugLog("child", oss.str());
}
if (cr.right <= 0 || cr.bottom <= 0) {
DebugLog("child", "WM_NCHITTEST fallback: empty client rect");
return DefSubclassProc(hwnd, message, wparam, lparam);
}
// 标题栏可拖拽区域:穿透到父窗口,父窗口会返回 HTCAPTION
if (pt.y >= 0 && pt.y < title_h &&
pt.x >= 0 && pt.x < cr.right - btn_w) {
std::ostringstream oss;
oss << "WM_NCHITTEST -> HTTRANSPARENT pt=(" << pt.x << "," << pt.y
<< ")";
DebugLog("child", oss.str());
return HTTRANSPARENT;
}
// 标题栏右侧控制按钮区域:保持 HTCLIENT让 Flutter 处理点击
if (pt.y >= 0 && pt.y < title_h &&
pt.x >= cr.right - btn_w && pt.x <= cr.right) {
DebugLog("child", "WM_NCHITTEST -> HTCLIENT (buttons)");
return HTCLIENT;
}
DebugLog("child", "WM_NCHITTEST -> DefSubclassProc");
return DefSubclassProc(hwnd, message, wparam, lparam);
}
}
return DefSubclassProc(hwnd, message, wparam, lparam);
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// ============================================================
// 异步弹出窗口大小预设菜单
// MethodChannel 回调中通过 PostMessage 触发,避免 TrackPopupMenuEx
// 阻塞 Flutter UI 线程
// ============================================================
if (message == WM_USER + 1000) {
ShowWindowSizeMenu(hwnd);
return 0;
}
// ============================================================
// 拖拽卡顿修复 v9WM_NCLBUTTONDOWN 时禁用 Mica + SetWindowCompositionAttribute
//
// 根因链(经 v1~v8 验证):
// 1. 手动 SetWindowPos 拖拽不触发 DWM "live drag" 优化
// 2. 完全禁用 Mica 能解决卡顿v8 验证),但临时禁用 Micav4~v7失败
// 3. v4~v7 失败的原因EnterSizeMove 遗漏了 SetWindowCompositionAttribute
// (ACCENT_DISABLED) 调用——这是 flutter_acrylic 的 setEffect(disabled)
// 能立即禁用 Mica 的关键。仅靠 DwmSetWindowAttribute 是异步的,不生效。
//
// v9 修复:
// - WM_NCLBUTTONDOWN(HTCAPTION) 时(拖拽还没开始)调用 EnterSizeMove 禁用 Mica
// (现在包含 SetWindowCompositionAttribute(ACCENT_DISABLED),能立即生效)
// - SWP_FRAMECHANGED 强制 DWM 同步应用变更
// - DefWindowProc 进入 SC_MOVE 模态循环
// - DefWindowProc 返回后(拖拽已结束)调用 ExitSizeMove 恢复 Mica
// - 模态循环期间绕过插件链 + 子窗口消息拦截v8
// ============================================================
// --- 标题栏 + 边框命中测试:直接返回,绕过插件 ---
// flutter_acrylic 的 DwmExtendFrameIntoClientArea({-1,-1,-1,-1}) 和
// window_manager 的 TitleBarStyle.hidden 会导致 DefWindowProc 的边框
// 命中测试失效。这里手动实现边框命中测试,恢复窗口边缘调整大小功能。
if (message == WM_NCHITTEST) {
POINT pt = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
UINT dpi = GetDpiForWindow(hwnd);
double scale = static_cast<double>(dpi) / 96.0;
int title_h = static_cast<int>(36.0 * scale);
int btn_w = static_cast<int>(138.0 * scale);
// 边框命中测试宽度(稍大于实际边框,方便用户操作)
int border_w = static_cast<int>(6.0 * scale);
// 使用窗口坐标(而非客户区坐标),避免客户区边距导致的负坐标问题
RECT wr;
GetWindowRect(hwnd, &wr);
bool maximized = (::IsZoomed(hwnd) != 0);
if (!maximized) {
bool on_left = pt.x >= wr.left && pt.x < wr.left + border_w;
bool on_right = pt.x >= wr.right - border_w && pt.x < wr.right;
bool on_top = pt.y >= wr.top && pt.y < wr.top + border_w;
bool on_bottom = pt.y >= wr.bottom - border_w && pt.y < wr.bottom;
if (on_top && on_left) return HTTOPLEFT;
if (on_top && on_right) return HTTOPRIGHT;
if (on_bottom && on_left) return HTBOTTOMLEFT;
if (on_bottom && on_right) return HTBOTTOMRIGHT;
if (on_left) return HTLEFT;
if (on_right) return HTRIGHT;
if (on_top) return HTTOP;
if (on_bottom) return HTBOTTOM;
}
// 标题栏拖拽区域(使用客户区坐标判断)
POINT client_pt = pt;
ScreenToClient(hwnd, &client_pt);
RECT cr;
GetClientRect(hwnd, &cr);
if (cr.right > 0 && cr.bottom > 0) {
if (client_pt.y >= 0 && client_pt.y < title_h &&
client_pt.x >= 0 && client_pt.x < cr.right - btn_w) {
return HTCAPTION;
}
}
// 非标题栏/边框区域:继续交给插件处理(返回 HTCLIENT
}
// --- 标题栏拖拽启动:提前禁用 Mica再进入原生 SC_MOVE 模态循环 ---
// 关键:在 WM_NCLBUTTONDOWN 时(拖拽还没开始)禁用 Mica。
// EnterSizeMove 现在包含 SetWindowCompositionAttribute(ACCENT_DISABLED)
// 能立即禁用 Mica与 flutter_acrylic 的 setEffect(disabled) 一致)。
if (message == WM_NCLBUTTONDOWN && wparam == HTCAPTION) {
// 1. 拖拽开始前禁用 Mica/Acrylic backdrop立即生效
EnterSizeMove(hwnd);
// 2. SWP_FRAMECHANGED 触发 WM_NCCALCSIZE强制 DWM 同步应用变更
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
SWP_NOZORDER | SWP_NOACTIVATE);
// 3. 进入原生 SC_MOVE 模态循环(同步阻塞,返回时拖拽已结束)
LRESULT drag_result = DefWindowProc(hwnd, message, wparam, lparam);
// 4. 拖拽结束后恢复 Mica/Acrylic backdrop
ExitSizeMove(hwnd);
// 5. SWP_FRAMECHANGED 强制 DWM 同步应用 Mica 恢复
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
SWP_NOZORDER | SWP_NOACTIVATE);
return drag_result;
}
// --- 原生拖拽/调整大小模态循环开始 ---
// 无论是拖拽标题栏(已在 WM_NCLBUTTONDOWN 提前禁用 Mica还是从边框调整大小
// 都会触发 WM_ENTERSIZEMOVE。对于边框调整大小的情况这里禁用 Mica。
if (message == WM_ENTERSIZEMOVE) {
is_in_native_drag_ = true;
// 如果还没有禁用 Mica边框调整大小的情况现在禁用
if (!is_in_size_move_loop_) {
EnterSizeMove(hwnd);
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
SWP_NOZORDER | SWP_NOACTIVATE);
}
return DefWindowProc(hwnd, message, wparam, lparam);
}
// --- 原生拖拽/调整大小模态循环结束 ---
if (message == WM_EXITSIZEMOVE) {
is_in_native_drag_ = false;
// 如果之前禁用了 Mica边框调整大小的情况现在恢复
if (is_in_size_move_loop_) {
ExitSizeMove(hwnd);
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
SWP_NOZORDER | SWP_NOACTIVATE);
}
return DefWindowProc(hwnd, message, wparam, lparam);
}
// ============================================================
// 原生拖拽期间:完全绕过插件链
//
// window_manager 插件在 WM_MOVING 中调用 _EmitEvent("move")
// 通过 MethodChannel 通知 Dart 侧。Dart 侧的 listener 可能触发
// setState → Flutter 重建 → 阻塞 UI 线程(= Win32 消息循环线程)。
// 22 个插件的 delegate 分发也会累积延迟。
//
// 拖拽期间所有消息直接交给 DefWindowProc让 DWM live-drag 优化
// 不受任何干扰。同时 ChildWndProc 也会拦截 Flutter 子窗口的消息,
// 阻止 Flutter 引擎重绘。此时 Mica 已被 EnterSizeMove 禁用。
// ============================================================
if (is_in_native_drag_) {
return DefWindowProc(hwnd, message, wparam, lparam);
}
// --- 非拖拽期间:正常消息流转 ---
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}